From 160eb5770c5f161a5cdfbd9d11ade7d1e263d266 Mon Sep 17 00:00:00 2001
From: AnrokX <192667251+AnrokX@users.noreply.github.com>
Date: Wed, 4 Mar 2026 15:34:49 -0600
Subject: [PATCH 01/15] feat: cleanly port windows onboarding flow onto main
---
CODEBASE_DOCUMENTATION.md | 11 +
client/app.js | 2277 +++++++++++---
client/index.html | 106 +-
client/notifications.js | 6 +-
client/styles.css | 5772 +++++++++++++++++++++-------------
client/styles/tabs.css | 19 +-
server/diagnosticsService.js | 201 +-
server/index.js | 124 +
server/setupActionService.js | 542 ++++
src-tauri/src/main.rs | 15 +
10 files changed, 6318 insertions(+), 2755 deletions(-)
create mode 100644 server/setupActionService.js
diff --git a/CODEBASE_DOCUMENTATION.md b/CODEBASE_DOCUMENTATION.md
index eadc9144..41397f7b 100644
--- a/CODEBASE_DOCUMENTATION.md
+++ b/CODEBASE_DOCUMENTATION.md
@@ -538,5 +538,16 @@ LOGGING: Winston-based structured logging with rotation
9. **Mixed-repo workspaces**: Terminal naming must avoid conflicts between repos
10. **Template validation**: Always validate workspace templates against schemas
+
+## First-Run Dependency Onboarding (Windows)
+
+```
+server/setupActionService.js - Defines setup actions and launches PowerShell installers
+server/index.js - Routes: GET /api/setup-actions, POST /api/setup-actions/run
+client/app.js - Guided dependency onboarding steps + diagnostics integration
+client/index.html - Dependency onboarding modal markup + launch button
+client/styles.css - Dependency onboarding progress/step styling
+```
+
---
๐จ **END OF FILE - ENSURE YOU READ EVERYTHING ABOVE** ๐จ
diff --git a/client/app.js b/client/app.js
index c9936ba9..e4f8dec4 100644
--- a/client/app.js
+++ b/client/app.js
@@ -845,7 +845,7 @@ class ClaudeOrchestrator {
if (!expiresAt || expiresAt <= now) this.pendingWorktreeReservations.delete(key);
}
}
-
+
async init() {
try {
// Initialize managers
@@ -984,21 +984,21 @@ class ClaudeOrchestrator {
if (this.settings.notifications) {
this.notificationManager.requestPermission();
}
-
+
// Set up UI
this.setupEventListeners();
this.applyTheme();
this.syncSettingsUI();
this.applySimpleModeConfig();
this.installAuthFetchShim();
-
+
// Connect to server
await this.connectToServer();
await this.ensureProjectTypeTaxonomy();
// Hook panels that depend on socket events
this.activityFeedPanel?.onSocketConnected?.(this.socket);
-
+
// Load user settings from server
await this.loadUserSettings();
this.applySidebarDesktopCollapsedFromPrefs();
@@ -1016,22 +1016,22 @@ class ClaudeOrchestrator {
// WIP / Queue banner (process status)
this.startProcessStatusBanner();
-
+
// Check for updates on startup
this.checkForSettingsUpdates();
-
+
// Hide loading message if it exists
const loadingMessage = document.getElementById('loading-message');
if (loadingMessage) {
loadingMessage.classList.add('hidden');
}
-
+
} catch (error) {
console.error('Failed to initialize:', error);
this.showError('Failed to initialize application');
}
}
-
+
async connectToServer() {
return new Promise((resolve, reject) => {
console.log('Attempting to connect to server...');
@@ -1044,29 +1044,29 @@ class ClaudeOrchestrator {
const serverUrl = window.location.origin;
this.socket = io(serverUrl, socketOptions);
console.log(`Socket connecting to ${serverUrl}...`);
-
+
// Connection events
this.socket.on('connect', () => {
console.log('Connected to server');
this.updateConnectionStatus(true);
resolve();
});
-
+
this.socket.on('connect_error', (error) => {
console.error('Connection error:', error);
this.updateConnectionStatus(false);
-
+
if (error.message === 'Authentication failed') {
this.showError('Authentication failed. Please check your token.');
}
reject(error);
});
-
+
this.socket.on('disconnect', () => {
console.log('Disconnected from server');
this.updateConnectionStatus(false);
});
-
+
// Session events
this.socket.on('sessions', async (sessionStates) => {
console.log('Received sessions event:', sessionStates);
@@ -1085,7 +1085,7 @@ class ClaudeOrchestrator {
this.worktreeTags.set(worktreePath, tag || {});
this.buildSidebar();
});
-
+
this.socket.on('terminal-output', ({ sessionId, data }) => {
this.terminalManager.handleOutput(sessionId, data);
@@ -1133,7 +1133,7 @@ class ClaudeOrchestrator {
this.sessionActivity.set(sessionId, 'active');
}
});
-
+
this.socket.on('autosuggest-response', ({ sessionId, suggestion, prefix }) => {
this.terminalManager.handleAutosuggestResponse(sessionId, suggestion, prefix);
});
@@ -1142,15 +1142,15 @@ class ClaudeOrchestrator {
this.updateSessionStatus(sessionId, status);
this.maybeAutoSendPrompt(sessionId, status);
});
-
+
this.socket.on('branch-update', ({ sessionId, branch, remoteUrl, defaultBranch, existingPR }) => {
this.updateSessionBranch(sessionId, branch, remoteUrl, defaultBranch, existingPR);
});
-
+
this.socket.on('notification-trigger', (notification) => {
this.notificationManager.handleNotification(notification);
});
-
+
this.socket.on('session-exited', ({ sessionId, exitCode }) => {
this.handleSessionExit(sessionId, exitCode);
});
@@ -1343,7 +1343,7 @@ class ClaudeOrchestrator {
// Hide + persist dismissal so it doesn't resurrect on refresh/worktree-add
this.hideStartupUI(sessionId);
this.scheduleAutoPromptFallback(sessionId, 'claude');
-
+
// Enable the start button now that Claude has started
const startBtn = document.getElementById(`claude-start-btn-${sessionId}`);
if (startBtn) {
@@ -1361,7 +1361,7 @@ class ClaudeOrchestrator {
this.socket.on('claude-update-required', (updateInfo) => {
this.showClaudeUpdateRequired(updateInfo);
});
-
+
this.socket.on('user-settings-updated', (settings) => {
console.log('User settings updated:', settings);
this.userSettings = settings;
@@ -1589,7 +1589,7 @@ class ClaudeOrchestrator {
this.socket.on('git-updated', (result) => {
console.log('Git updated:', result);
this.showTemporaryMessage(`Repository updated successfully! ${result.wasUpToDate ? 'Already up to date.' : 'Changes pulled.'}`, 'success');
-
+
// Refresh the page after successful update
if (!result.wasUpToDate) {
setTimeout(() => {
@@ -1600,45 +1600,45 @@ class ClaudeOrchestrator {
}, 3000);
}
});
-
+
// Build production events
this.socket.on('build-started', ({ sessionId, worktreeNum }) => {
console.log(`Build started for worktree ${worktreeNum}`);
});
-
+
this.socket.on('build-completed', ({ sessionId, worktreeNum, zipPath }) => {
console.log(`Build completed for worktree ${worktreeNum}: ${zipPath}`);
-
+
// Restore the build button (use work{num} pattern to find buttons)
this.restoreBuildButton(`work${worktreeNum}`);
-
+
// Request to reveal the file in explorer
this.socket.emit('reveal-in-explorer', { path: zipPath });
});
-
+
this.socket.on('build-failed', ({ sessionId, worktreeNum, error }) => {
console.error(`Build failed for worktree ${worktreeNum}:`, error);
this.showError(`โ Build failed for Worktree ${worktreeNum}: ${error}`);
-
+
// Restore the build button (use work{num} pattern to find buttons)
this.restoreBuildButton(`work${worktreeNum}`);
});
-
+
// Periodic heartbeat to keep sessions alive while UI is open
this.startHeartbeats();
-
+
this.socket.on('server-started', ({ sessionId, port }) => {
console.log(`[SERVER-STARTED EVENT] Session: ${sessionId}, Port: ${port}`);
this.serverPorts.set(sessionId, port);
console.log(`Server ${sessionId} started on port ${port}`);
console.log('Current serverPorts:', Array.from(this.serverPorts.entries()));
-
+
// Only open localhost automatically - Hytopia needs manual click due to popup blockers
setTimeout(() => {
const localhostUrl = `https://localhost:${port}`;
console.log(`Opening localhost for initialization: ${localhostUrl}`);
window.open(localhostUrl, '_blank');
-
+
// Show notification that server is ready
if (this.settings.notifications) {
this.showNotification('Server Ready', `Server ${sessionId.replace('-server', '')} is running on port ${port}. Click ๐ฎ to play!`);
@@ -1657,14 +1657,14 @@ class ClaudeOrchestrator {
reject(new Error('Connection timeout'));
}
}, 10000);
-
+
// Clear timeout on successful connection
this.socket.on('connect', () => {
clearTimeout(timeoutId);
});
});
}
-
+
startHeartbeats() {
if (this._heartbeatInterval) {
clearInterval(this._heartbeatInterval);
@@ -1676,7 +1676,7 @@ class ClaudeOrchestrator {
}
}, 30000);
}
-
+
setupEventListeners() {
// Check if elements exist before adding listeners
const elements = {
@@ -1739,7 +1739,7 @@ class ClaudeOrchestrator {
'start-claude',
'cancel-claude-startup'
]);
-
+
// Check all elements exist
for (const id in elements) {
elements[id] = document.getElementById(id);
@@ -1747,7 +1747,7 @@ class ClaudeOrchestrator {
console.warn(`Element not found: ${id}`);
}
}
-
+
// Sidebar worktree clicks - use toggle instead of show
if (elements['worktree-list']) {
elements['worktree-list'].addEventListener('click', (e) => {
@@ -1856,7 +1856,7 @@ class ClaudeOrchestrator {
if (!document.body.classList.contains('sidebar-open')) return;
this.closeSidebar();
});
-
+
// View buttons
const dashboardBtn = document.getElementById('dashboard-btn');
if (dashboardBtn) {
@@ -1878,26 +1878,26 @@ class ClaudeOrchestrator {
this.setViewMode('all');
if (this.isMobileLayout()) this.closeSidebar();
});
-
+
document.getElementById('view-claude-only').addEventListener('click', () => {
this.setViewMode('claude');
if (this.isMobileLayout()) this.closeSidebar();
});
-
+
document.getElementById('view-servers-only').addEventListener('click', () => {
this.setViewMode('server');
if (this.isMobileLayout()) this.closeSidebar();
});
-
+
// Presets
document.getElementById('view-presets').addEventListener('click', () => {
document.getElementById('presets-modal').classList.remove('hidden');
});
-
+
document.getElementById('close-presets').addEventListener('click', () => {
document.getElementById('presets-modal').classList.add('hidden');
});
-
+
// Preset buttons
document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', () => {
@@ -1906,9 +1906,9 @@ class ClaudeOrchestrator {
document.getElementById('presets-modal').classList.add('hidden');
});
});
-
+
// Grid layout dropdown removed - using dynamic layout now
-
+
// Settings
const settingsToggle = document.getElementById('settings-toggle');
const closeSettingsPanel = () => {
@@ -1936,7 +1936,7 @@ class ClaudeOrchestrator {
} else {
console.error('Settings toggle button not found!');
}
-
+
document.getElementById('close-settings').addEventListener('click', () => {
closeSettingsPanel();
});
@@ -1962,7 +1962,7 @@ class ClaudeOrchestrator {
if (panel.contains(e.target)) return;
closeSettingsPanel();
});
-
+
// Settings inputs
document.getElementById('enable-notifications').addEventListener('change', (e) => {
this.settings.notifications = e.target.checked;
@@ -1971,12 +1971,12 @@ class ClaudeOrchestrator {
this.notificationManager.requestPermission();
}
});
-
+
document.getElementById('enable-sounds').addEventListener('change', (e) => {
this.settings.sounds = e.target.checked;
this.saveSettings();
});
-
+
document.getElementById('auto-scroll').addEventListener('change', (e) => {
this.settings.autoScroll = e.target.checked;
this.saveSettings();
@@ -1994,7 +1994,7 @@ class ClaudeOrchestrator {
}
}
});
-
+
document.getElementById('theme-select').addEventListener('change', (e) => {
this.settings.theme = e.target.value;
this.saveSettings();
@@ -2048,6 +2048,7 @@ class ClaudeOrchestrator {
// Settings UI helpers: search + section jump so the panel doesnโt feel like an endless scroll.
this.setupSettingsPanelNavigation();
this.setupDiagnosticsPanel();
+ this.setupDependencySetupWizard();
const tasksThemeSelect = document.getElementById('tasks-theme-select');
if (tasksThemeSelect) {
@@ -2306,7 +2307,7 @@ class ClaudeOrchestrator {
document.getElementById('dismiss-git-notification').addEventListener('click', () => {
document.getElementById('git-update-notification').classList.add('hidden');
});
-
+
// Workflow notification settings (server-persisted)
const workflowNotifyMode = document.getElementById('workflow-notify-mode');
if (workflowNotifyMode) {
@@ -2727,33 +2728,33 @@ class ClaudeOrchestrator {
if (markReadBtn) {
markReadBtn.addEventListener('click', () => this.notificationManager?.markAllAsRead?.());
}
-
+
// Claude startup modal handlers (simplified)
const cancelClaudeBtn = document.getElementById('cancel-claude-startup');
-
+
if (cancelClaudeBtn) {
cancelClaudeBtn.addEventListener('click', () => {
this.hideClaudeStartupModal();
});
}
-
+
// Handle startup option button clicks
document.addEventListener('click', (e) => {
if (e.target.closest('.startup-option-btn')) {
const btn = e.target.closest('.startup-option-btn');
const mode = btn.dataset.mode;
-
+
// Check if modal YOLO is checked
const modalYolo = document.getElementById('modal-yolo');
const skipPermissions = modalYolo ? modalYolo.checked : false;
-
+
if (this.pendingClaudeSession) {
this.startClaudeWithOptions(this.pendingClaudeSession, mode, skipPermissions);
this.hideClaudeStartupModal();
}
}
});
-
+
// Handle window resize to fix blank terminals
let resizeTimeout;
window.addEventListener('resize', () => {
@@ -2899,7 +2900,7 @@ class ClaudeOrchestrator {
});
});
}
-
+
setViewMode(mode, { persist = true } = {}) {
const normalized = String(mode || '').toLowerCase();
if (!['all', 'claude', 'server'].includes(normalized)) return;
@@ -2914,7 +2915,7 @@ class ClaudeOrchestrator {
this.updateGlobalUserSetting('ui.terminals.viewMode', normalized);
}
}
-
+
updateViewModeButtons() {
const allBtn = document.getElementById('view-all');
const claudeBtn = document.getElementById('view-claude-only');
@@ -3544,21 +3545,21 @@ class ClaudeOrchestrator {
return null;
}
-
+
matchesViewMode(sessionId) {
if (this.viewMode === 'all') return true;
-
+
const session = this.sessions.get(sessionId);
const type = session?.type;
-
+
if (this.viewMode === 'claude') {
return type === 'claude' || type === 'codex' || /-(claude|codex)$/.test(String(sessionId || ''));
}
-
+
if (this.viewMode === 'server') {
return type === 'server' || sessionId.includes('-server');
}
-
+
return true;
}
@@ -3571,7 +3572,7 @@ class ClaudeOrchestrator {
&& this.matchesTierFilter(sessionId)
&& this.matchesWorkflowMode(sessionId);
}
-
+
handleInitialSessions(sessionStates) {
console.log('Received initial sessions:', sessionStates);
@@ -3588,7 +3589,7 @@ class ClaudeOrchestrator {
this.sessions.clear();
this.sessionActivity.clear();
this.visibleTerminals.clear();
-
+
// Process sessions
for (const [sessionId, state] of Object.entries(sessionStates)) {
const sessionData = {
@@ -3634,13 +3635,13 @@ class ClaudeOrchestrator {
this.pruneIntentHaikuState(new Set(Object.keys(sessionStates)));
this.lastSessionsWorkspaceId = currentWorkspaceId;
-
+
// Hide loading message FIRST
const loadingMessage = document.getElementById('loading-message');
if (loadingMessage) {
loadingMessage.style.display = 'none';
}
-
+
// Build sidebar
this.buildSidebar();
@@ -4237,10 +4238,10 @@ class ClaudeOrchestrator {
// Always ensure filter toggle exists and is updated FIRST
this.ensureFilterToggleExists();
-
+
// Clear and rebuild the worktree list
worktreeList.innerHTML = '';
-
+
// Group sessions by worktree and repository for mixed-repo support
const worktrees = new Map();
@@ -4274,22 +4275,22 @@ class ClaudeOrchestrator {
worktree.server = session;
}
}
-
+
// Create sidebar items
for (const [worktreeId, worktree] of worktrees) {
// Check if worktree is active (has any session marked as active)
const isActive = this.isWorktreeActive(worktreeId);
-
+
// Skip inactive worktrees if filter is enabled
if (this.showActiveOnly && !isActive) {
continue;
}
-
+
// Check if any session in this worktree is visible.
const claudeVisible = !!(worktree.claude && this.isSessionVisibleByWorktreeSelection(worktree.claude.sessionId, worktree.claude));
const serverVisible = !!(worktree.server && this.isSessionVisibleByWorktreeSelection(worktree.server.sessionId, worktree.server));
const isVisible = claudeVisible || serverVisible;
-
+
const item = document.createElement('div');
// Only show visibility state, not activity state (activity filtering is handled separately)
item.className = `worktree-item ${!isVisible ? 'hidden-terminal' : ''}`;
@@ -4408,9 +4409,9 @@ class ClaudeOrchestrator {
`;
-
+
// Click handler is already attached via event delegation in setupEventListeners
-
+
worktreeList.appendChild(item);
}
@@ -4791,21 +4792,21 @@ class ClaudeOrchestrator {
const current = this.worktreeTags.get(worktreePath)?.readyForReview;
return this.setWorktreeReadyForReview(worktreePath, !current);
}
-
+
ensureFilterToggleExists() {
let filterToggle = document.getElementById('filter-toggle');
-
+
if (!filterToggle) {
// Create the filter toggle element
filterToggle = document.createElement('div');
filterToggle.className = 'filter-toggle';
filterToggle.id = 'filter-toggle';
-
+
// Insert it right before the worktree list
const worktreeList = document.getElementById('worktree-list');
worktreeList.parentNode.insertBefore(filterToggle, worktreeList);
}
-
+
// Always update the button content
const visibility = this.getUiVisibilityConfig().sidebar || {};
const showActiveFilter = visibility.activeFilter !== false;
@@ -4838,7 +4839,7 @@ class ClaudeOrchestrator {
if (status === 'error') return 'error';
return 'idle';
}
-
+
isWorktreeActive(worktreeIdOrKey) {
// Check if any session for this worktree has been marked as active.
// For mixed-repo workspaces we may receive:
@@ -4867,11 +4868,11 @@ class ClaudeOrchestrator {
return false;
}
-
+
toggleActivityFilter() {
this.showActiveOnly = !this.showActiveOnly;
this.buildSidebar();
-
+
// Also update the main grid view to match the filter
if (this.showActiveOnly) {
this.showActiveWorktreesOnly();
@@ -4879,11 +4880,11 @@ class ClaudeOrchestrator {
this.showAllTerminals();
}
}
-
+
showActiveWorktreesOnly() {
// Clear visible terminals first
this.visibleTerminals.clear();
-
+
// Add only active worktree sessions to visible set
for (const [sessionId, session] of this.sessions) {
const sessionWorktreeId = session.worktreeId || sessionId.split('-')[0];
@@ -4894,7 +4895,7 @@ class ClaudeOrchestrator {
this.visibleTerminals.add(sessionId);
}
}
-
+
// If no active sessions, show all
if (this.visibleTerminals.size === 0) {
this.showAllTerminals();
@@ -4903,7 +4904,7 @@ class ClaudeOrchestrator {
this.buildSidebar();
}
}
-
+
resizeAllVisibleTerminals() {
// Force resize all visible terminals to fit their containers
this.activeView.forEach(sessionId => {
@@ -5037,7 +5038,7 @@ class ClaudeOrchestrator {
this.updateTerminalGrid();
this.buildSidebar();
}
-
+
showWorktree(worktreeIdOrKey) {
// Show terminals for this EXACT worktree key
const claudeId = `${worktreeIdOrKey}-claude`;
@@ -5049,17 +5050,17 @@ class ClaudeOrchestrator {
this.updateTerminalGrid();
this.buildSidebar();
}
-
+
showAllTerminals() {
// Add all sessions to visible set
for (const sessionId of this.sessions.keys()) {
this.visibleTerminals.add(sessionId);
}
-
+
this.updateTerminalGrid();
this.buildSidebar();
}
-
+
/**
* Get the terminal grid container for the current tab
*/
@@ -5088,7 +5089,7 @@ class ClaudeOrchestrator {
const allSessions = Array.from(this.sessions.keys());
this.renderTerminalsWithVisibility(allSessions);
}
-
+
renderTerminalsWithVisibility(sessionIds) {
// Render all terminals but apply visibility using CSS (don't destroy DOM)
this.activeView = sessionIds.filter(id => this.isSessionVisibleInCurrentView(id));
@@ -5232,18 +5233,18 @@ class ClaudeOrchestrator {
this.resizeAllVisibleTerminals();
}, 200);
}
-
+
showClaudeOnly() {
this.setViewMode('claude');
}
-
+
showServersOnly() {
this.setViewMode('server');
}
-
+
applyPreset(preset) {
this.visibleTerminals.clear();
-
+
switch (preset) {
case 'all':
this.showAllTerminals();
@@ -5277,9 +5278,9 @@ class ClaudeOrchestrator {
break;
}
}
-
+
// changeLayout method removed - using dynamic layout based on visible terminal count
-
+
showTerminals(sessionIds) {
// Legacy function - update visible set and refresh everything
this.visibleTerminals.clear();
@@ -5291,31 +5292,31 @@ class ClaudeOrchestrator {
this.updateTerminalGrid();
this.buildSidebar();
}
-
+
renderTerminals(sessionIds) {
// Core rendering function - just displays terminals without updating state
this.activeView = sessionIds;
const grid = this.getTerminalGrid();
-
+
// Sort sessionIds to ensure proper ordering: work1-claude, work1-server, work2-claude, work2-server, etc.
const sortedSessionIds = sessionIds.slice().sort((a, b) => {
// Extract worktree number
const getWorkNum = (id) => parseInt(id.match(/work(\d+)/)?.[1] || 0);
const numA = getWorkNum(a);
const numB = getWorkNum(b);
-
+
// First sort by worktree number
if (numA !== numB) return numA - numB;
-
+
// Then claude before server
if (a.includes('claude') && b.includes('server')) return -1;
if (a.includes('server') && b.includes('claude')) return 1;
return 0;
});
-
+
// Clear grid but don't destroy terminals
grid.innerHTML = '';
-
+
// Create terminal elements for active view
sortedSessionIds.forEach((sessionId) => {
const session = this.sessions.get(sessionId);
@@ -5324,7 +5325,7 @@ class ClaudeOrchestrator {
grid.appendChild(wrapper);
}
});
-
+
// Now handle terminal instances
sortedSessionIds.forEach((sessionId, index) => {
const session = this.sessions.get(sessionId);
@@ -5332,25 +5333,25 @@ class ClaudeOrchestrator {
setTimeout(() => {
const terminalEl = document.getElementById(this.getSessionDomId('terminal', sessionId));
if (!terminalEl) return;
-
+
if (this.terminalManager.terminals.has(sessionId)) {
// Re-attach existing terminal to the new element
const term = this.terminalManager.terminals.get(sessionId);
-
+
// Clear and re-open the terminal in the new element
terminalEl.innerHTML = '';
term.open(terminalEl);
-
+
// Force a resize and refresh
this.terminalManager.fitTerminal(sessionId);
-
+
// Force a screen refresh to show content
term.refresh(0, term.rows - 1);
} else {
// Create new terminal only if it doesn't exist
this.terminalManager.createTerminal(sessionId, session);
}
-
+
// Don't auto-start Claude - let user choose via modal or button
}, 50 + (index * 25)); // Reduced stagger time
}
@@ -5576,41 +5577,41 @@ class ClaudeOrchestrator {
getTicketMetaForSession(sessionId, sessionOverride = null) {
const sid = String(sessionId || '').trim();
if (!sid) return null;
-
+
const session = sessionOverride || this.sessions.get(sid);
const recordIds = [];
recordIds.push(`session:${sid}`);
-
+
const worktreePath = session?.config?.cwd || session?.cwd || session?.worktreePath || null;
if (worktreePath) recordIds.push(`worktree:${worktreePath}`);
-
+
const prUrl = this.githubLinks.get(sid)?.pr || null;
const prTaskId = prUrl ? this.getPRTaskIdFromUrl(prUrl) : null;
if (prTaskId) recordIds.push(prTaskId);
-
+
for (const id of recordIds) {
const rec = this.taskRecords.get(id);
if (!rec) continue;
-
+
const ticketProvider = String(rec.ticketProvider || '').trim().toLowerCase();
const ticketCardId = String(rec.ticketCardId || '').trim();
const ticketCardUrl = String(rec.ticketCardUrl || '').trim();
const ticketTitle = String(rec.ticketTitle || '').trim();
-
+
if (!ticketTitle && !ticketCardId && !ticketCardUrl) continue;
-
+
const rawUrl =
ticketCardUrl
|| ((ticketProvider === 'trello' || !ticketProvider) && ticketCardId ? `https://trello.com/c/${ticketCardId}` : '');
const url = (rawUrl && /^https?:\/\//i.test(rawUrl)) ? rawUrl : '';
-
+
const label = ticketTitle || (ticketProvider && ticketCardId ? `${ticketProvider}:${ticketCardId}` : (ticketCardId ? ticketCardId : url));
const tooltipParts = [
ticketTitle || null,
ticketProvider && ticketCardId ? `${ticketProvider}:${ticketCardId}` : null,
url || null
].filter(Boolean);
-
+
return {
provider: ticketProvider || null,
cardId: ticketCardId || null,
@@ -5620,36 +5621,36 @@ class ClaudeOrchestrator {
tooltip: tooltipParts.join(' โข ')
};
}
-
+
return null;
}
-
+
updateTerminalTicketLabel(sessionId) {
const sid = String(sessionId || '').trim();
if (!sid) return;
-
+
const wrapper = this.getSessionWrapperElement(sid);
if (!wrapper) return;
-
+
const titleRow = wrapper.querySelector('.terminal-title');
if (!titleRow) return;
-
+
const existing = titleRow.querySelector('.terminal-ticket');
const meta = this.getTicketMetaForSession(sid);
-
+
if (!meta || !meta.label) {
existing?.remove();
return;
}
-
+
const label = `๐งพ ${meta.label}`;
const tooltip = meta.tooltip || meta.title || meta.label;
const url = meta.url || '';
-
+
const wantsLink = !!url;
const isLink = existing && existing.tagName && existing.tagName.toLowerCase() === 'a';
const isSpan = existing && existing.tagName && existing.tagName.toLowerCase() === 'span';
-
+
if (wantsLink) {
let el = existing;
if (!isLink) {
@@ -5658,7 +5659,7 @@ class ClaudeOrchestrator {
el.className = 'terminal-ticket';
el.target = '_blank';
el.rel = 'noopener noreferrer';
-
+
const branchEl = titleRow.querySelector('.terminal-branch');
if (branchEl && branchEl.parentElement === titleRow) {
branchEl.insertAdjacentElement('afterend', el);
@@ -5671,7 +5672,7 @@ class ClaudeOrchestrator {
el.title = tooltip;
return;
}
-
+
let el = existing;
if (!isSpan) {
existing?.remove();
@@ -5687,7 +5688,7 @@ class ClaudeOrchestrator {
el.textContent = label;
el.title = tooltip;
}
-
+
refreshBranchLabels() {
try {
for (const [sessionId, session] of this.sessions) {
@@ -5699,7 +5700,7 @@ class ClaudeOrchestrator {
}
this.buildSidebar();
}
-
+
createTerminalElement(sessionId, session) {
const wrapper = document.createElement('div');
wrapper.className = 'terminal-wrapper';
@@ -5709,7 +5710,7 @@ class ClaudeOrchestrator {
wrapper.addEventListener('mousedown', () => {
this.lastInteractedSessionId = sessionId;
});
-
+
const sessionType = String(session?.type || '').trim().toLowerCase();
const isAgentSession = sessionType === 'claude' || sessionType === 'codex';
const isServerSession = sessionType === 'server';
@@ -5819,7 +5820,7 @@ class ClaudeOrchestrator {
return wrapper;
}
-
+
updateSessionStatus(sessionId, status) {
const statusElement = document.getElementById(this.getSessionDomId('status', sessionId));
// Update session data
@@ -5903,7 +5904,7 @@ class ClaudeOrchestrator {
apply(status);
}
}
-
+
// Update quick actions for agent sessions
if (/-claude$|-codex$/.test(sessionId)) {
// Clear any pending notification timer if agent goes busy again
@@ -5935,11 +5936,11 @@ class ClaudeOrchestrator {
}, 3000);
}
}
-
+
// Update sidebar
this.updateSidebarStatus(sessionId, status);
}
-
+
updateSidebarStatus(sessionId, status) {
const session = this.sessions.get(sessionId);
const key = this.getSessionWorktreeKey(sessionId, session);
@@ -5996,7 +5997,7 @@ class ClaudeOrchestrator {
}
}
}
-
+
updateSessionBranch(sessionId, branch, remoteUrl, defaultBranch, existingPR) {
const session = this.sessions.get(sessionId);
if (session) {
@@ -6007,9 +6008,9 @@ class ClaudeOrchestrator {
if (defaultBranch) {
session.defaultBranch = defaultBranch;
}
-
+
console.log(`Branch updated for ${sessionId}: ${branch}`, existingPR ? `(existing PR: ${existingPR})` : '');
-
+
// If there's an existing PR, add it to GitHub links automatically
if (existingPR) {
const links = this.githubLinks.get(sessionId) || {};
@@ -6019,17 +6020,17 @@ class ClaudeOrchestrator {
this.maybeSchedulePrIntentRefresh(sessionId, existingPR);
}
}
-
+
// Update terminal branch display
this.updateTerminalBranchLabel(sessionId, branch || '');
-
+
// Update sidebar
this.buildSidebar();
-
+
// Update GitHub buttons with new remote URL
this.updateTerminalControls(sessionId);
}
-
+
// Server control methods
toggleServer(sessionId, environment = 'development') {
const status = this.serverStatuses.get(sessionId);
@@ -6056,22 +6057,22 @@ class ClaudeOrchestrator {
});
}
}
-
+
killServer(sessionId) {
// Send force kill
this.socket.emit('server-control', { sessionId, action: 'kill' });
this.serverStatuses.set(sessionId, 'idle');
-
+
// Update UI
const button = document.getElementById(`server-toggle-${sessionId}`);
if (button) {
button.textContent = 'โถ';
}
-
+
this.updateSidebarStatus(sessionId, 'idle');
this.updateServerControls(sessionId);
}
-
+
playInHytopia(sessionId) {
console.log(`[PLAY IN HYTOPIA] Session: ${sessionId}`);
console.log('Available ports:', Array.from(this.serverPorts.entries()));
@@ -6089,21 +6090,21 @@ class ClaudeOrchestrator {
}
return;
}
-
+
const serverUrl = `localhost:${port}`;
const hytopiaUrl = `https://hytopia.com/play/?${serverUrl}`;
-
+
console.log(`Opening Hytopia for ${sessionId} at ${hytopiaUrl}`);
window.open(hytopiaUrl, '_blank');
}
-
+
restoreBuildButton(sessionId) {
// Find any button that might be building for this worktree
const worktreeMatch = sessionId.match(/work(\d+)/);
if (!worktreeMatch) return;
-
+
const worktreeNum = worktreeMatch[1];
-
+
// Check both claude and server buttons for this worktree
[`work${worktreeNum}-claude`, `work${worktreeNum}-server`].forEach(id => {
const btn = this.buildingButtons?.get(id);
@@ -6115,7 +6116,7 @@ class ClaudeOrchestrator {
}
});
}
-
+
buildProduction(sessionId) {
// Extract worktree number from sessionId (e.g., 'work1-claude' -> 1)
const worktreeMatch = sessionId.match(/work(\d+)/);
@@ -6124,10 +6125,10 @@ class ClaudeOrchestrator {
this.showError('Failed to identify worktree for build');
return;
}
-
+
const worktreeNum = worktreeMatch[1];
console.log(`Building production ZIP for worktree ${worktreeNum}`);
-
+
// Disable the build button and show loading state
const wrapper = document.getElementById(this.getSessionDomId('wrapper', sessionId));
const buildBtn = wrapper ? wrapper.querySelector('button[onclick*="buildProduction"]') : null;
@@ -6136,26 +6137,26 @@ class ClaudeOrchestrator {
buildBtn.innerHTML = '';
buildBtn.classList.add('building');
}
-
+
// Store the button reference for later
this.buildingButtons = this.buildingButtons || new Map();
this.buildingButtons.set(sessionId, buildBtn);
-
+
// Emit socket event to trigger build on backend
- this.socket.emit('build-production', {
+ this.socket.emit('build-production', {
sessionId,
- worktreeNum
+ worktreeNum
});
}
-
+
detectGitHubLinks(sessionId, data) {
// Look for GitHub URLs with improved pattern matching
const githubUrlPattern = /https:\/\/github\.com\/[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+(?:\/(?!https:\/\/github\.com\/)[^\s\)\]\}\>\"\'\`]*)?/g;
const matches = data.match(githubUrlPattern);
-
+
if (matches) {
const links = this.githubLinks.get(sessionId) || {};
-
+
matches.forEach(originalUrl => {
// Clean up ANSI escape codes and other terminal artifacts
let url = originalUrl
@@ -6164,7 +6165,7 @@ class ClaudeOrchestrator {
.replace(/\u001b\[[0-9;]*m/g, '') // Remove Unicode ANSI codes
.replace(/[\x00-\x1F\x7F-\x9F]/g, '') // Remove other control characters
.trim();
-
+
// Remove common trailing punctuation that might be captured
url = url.replace(/[,;.!?)\]}>'"`]*$/, '');
@@ -6173,7 +6174,7 @@ class ClaudeOrchestrator {
if (secondUrlIndex > 0) {
url = url.slice(0, secondUrlIndex);
}
-
+
// Validate URL format
try {
new URL(url);
@@ -6181,7 +6182,7 @@ class ClaudeOrchestrator {
console.warn('Invalid GitHub URL detected:', url);
return;
}
-
+
// Categorize the URL
if (url.includes('/pull/') && url.match(/\/pull\/\d+\/?$/)) {
if (links.pr !== url) {
@@ -6201,36 +6202,36 @@ class ClaudeOrchestrator {
}
}
});
-
+
this.githubLinks.set(sessionId, links);
this.updateTerminalControls(sessionId);
}
}
-
+
clearGitHubLinks(sessionId) {
this.githubLinks.delete(sessionId);
this.githubLinkLogs.delete(sessionId);
this.updateTerminalControls(sessionId);
}
-
+
copyLocalhostUrl(sessionId) {
const port = this.serverPorts.get(sessionId);
if (!port) {
console.error('No port found for server', sessionId);
return;
}
-
+
const url = `https://localhost:${port}`;
navigator.clipboard.writeText(url).then(() => {
console.log(`Copied ${url} to clipboard`);
this.showNotification('Copied!', `${url} copied to clipboard`);
});
}
-
+
openHytopiaWebsite() {
window.open('https://hytopia.com', '_blank');
}
-
+
openPRLink(url) {
try {
// Validate the URL
@@ -6242,17 +6243,17 @@ class ClaudeOrchestrator {
this.showToast('Invalid PR URL', 'error');
}
}
-
+
getGitHubButtons(sessionId) {
const links = this.githubLinks.get(sessionId) || {};
let buttons = '';
const visibility = this.getTerminalVisibilityConfig();
-
+
// Always show branch button (uses current session's git info)
const session = this.sessions.get(sessionId);
if (session && session.branch && session.branch !== 'master' && session.branch !== 'main') {
const worktreeId = sessionId.split('-')[0];
-
+
// Use dynamic remote URL if available
if (session.remoteUrl) {
const encodeRef = (ref) => encodeURIComponent(String(ref || '').trim());
@@ -6261,7 +6262,7 @@ class ClaudeOrchestrator {
// Use the actual default branch from git, fallback to 'main' if not available
const defaultBranch = session.defaultBranch || 'main';
const compareUrl = `${session.remoteUrl}/compare/${encodeRef(defaultBranch)}...${branchRef}`;
-
+
if (visibility.viewBranchOnGithub !== false) {
buttons += ``;
}
@@ -6273,7 +6274,7 @@ class ClaudeOrchestrator {
}
}
}
-
+
// Show PR button if PR link detected
if (links.pr) {
const lastLogged = this.githubLinkLogs.get(sessionId);
@@ -6288,17 +6289,17 @@ class ClaudeOrchestrator {
buttons += ``;
}
}
-
+
// Check for commit URLs
if (links.commit) {
if (visibility.advancedDiff !== false) {
buttons += ``;
}
}
-
+
return buttons;
}
-
+
updateTerminalControls(sessionId) {
const wrapper = document.getElementById(this.getSessionDomId('wrapper', sessionId));
if (!wrapper) return;
@@ -6378,43 +6379,43 @@ class ClaudeOrchestrator {
: '';
return [stopBtn, removeBtn].filter(Boolean).join('\n');
}
-
+
updateServerStatus(sessionId, output) {
// Check if server started - look for various startup messages
- if (output.includes('Server started') ||
- output.includes('Listening on') ||
+ if (output.includes('Server started') ||
+ output.includes('Listening on') ||
output.includes('Server running') ||
output.includes('Started server') ||
output.includes('๐')) {
this.serverStatuses.set(sessionId, 'running');
this.updateSidebarStatus(sessionId, 'running');
-
+
const button = document.getElementById(`server-toggle-${sessionId}`);
if (button) {
button.textContent = 'โน';
}
-
+
this.updateServerControls(sessionId);
const linkedClaude = this.getLinkedClaudeSessionIdForServer(sessionId);
if (linkedClaude) this.updateTerminalControls(linkedClaude);
}
-
+
// Check if server stopped
if (output.includes('Server stopped') || output.includes('exit')) {
this.serverStatuses.set(sessionId, 'idle');
this.updateSidebarStatus(sessionId, 'idle');
-
+
const button = document.getElementById(`server-toggle-${sessionId}`);
if (button) {
button.textContent = 'โถ';
}
-
+
this.updateServerControls(sessionId);
const linkedClaude = this.getLinkedClaudeSessionIdForServer(sessionId);
if (linkedClaude) this.updateTerminalControls(linkedClaude);
}
}
-
+
/**
* Get dynamic launch options based on current workspace
*/
@@ -6485,14 +6486,14 @@ class ClaudeOrchestrator {
// Use dynamic button system
controlsDiv.innerHTML = this.getServerControlsHTML(sessionId);
}
-
+
handleServerError(sessionId, output) {
const worktreeId = sessionId.split('-')[0];
-
+
// Update status
this.serverStatuses.set(sessionId, 'error');
this.updateSidebarStatus(sessionId, 'error');
-
+
// Show notification
this.notificationManager.handleNotification({
sessionId,
@@ -6518,13 +6519,13 @@ class ClaudeOrchestrator {
session.hasUserInput = false;
this.updateSidebarStatus(sid, String(session.status || 'idle').trim().toLowerCase() || 'idle');
}
-
+
sendTerminalInput(sessionId, data) {
if (!this.socket || !this.socket.connected) {
console.error('Not connected to server');
return;
}
-
+
// Mark session as active when user first provides input
// But only for meaningful input (not just arrow keys, etc.)
if (data.length > 0 && !data.match(/^[\x1b\x7f\r\n]/) && data.trim().length > 0) {
@@ -6547,7 +6548,7 @@ class ClaudeOrchestrator {
console.error('Failed to interrupt session', e);
}
}
-
+
resizeTerminal(sessionId, cols, rows) {
if (this.socket && this.socket.connected) {
this.socket.emit('terminal-resize', { sessionId, cols, rows });
@@ -7522,7 +7523,7 @@ class ClaudeOrchestrator {
}
}
}
-
+
handleSessionRestart(sessionId) {
console.log(`Session ${sessionId} restarted`);
// Terminal will automatically reconnect and show new content
@@ -7551,20 +7552,20 @@ class ClaudeOrchestrator {
}
}
}
-
+
restartClaudeSession(sessionId) {
console.log(`Restarting Claude session: ${sessionId}`);
-
+
if (this.socket && this.socket.connected) {
this.socket.emit('restart-session', { sessionId });
-
+
// Update UI to show restarting
this.updateSessionStatus(sessionId, 'restarting');
} else {
this.showError('Not connected to server');
}
}
-
+
refreshTerminal(sessionId) {
console.log('Refreshing terminal:', sessionId);
const term = this.terminalManager.terminals.get(sessionId);
@@ -7572,10 +7573,10 @@ class ClaudeOrchestrator {
// Force fit and refresh
this.terminalManager.fitTerminal(sessionId);
term.refresh(0, term.rows - 1);
-
+
// Also try scrolling to bottom to trigger redraw
term.scrollToBottom();
-
+
// If still blank, re-attach to DOM
const terminalEl = document.getElementById(this.getSessionDomId('terminal', sessionId));
if (terminalEl && terminalEl.children.length === 0) {
@@ -7583,13 +7584,13 @@ class ClaudeOrchestrator {
}
}
}
-
+
updateConnectionStatus(connected) {
const statusElement = document.getElementById('connection-status');
if (statusElement) {
const dot = statusElement.querySelector('.status-dot');
const text = statusElement.querySelector('span:last-child');
-
+
if (connected) {
dot.classList.remove('disconnected');
dot.classList.add('connected');
@@ -7601,12 +7602,12 @@ class ClaudeOrchestrator {
}
}
}
-
+
showError(message) {
// For now, use alert. Could be improved with a toast notification
alert(`Error: ${message}`);
}
-
+
showClaudeUpdateRequired(updateInfo) {
// Create update banner
const banner = document.createElement('div');
@@ -7621,10 +7622,10 @@ class ClaudeOrchestrator {
`;
-
+
// Add to top of page
document.body.insertBefore(banner, document.body.firstChild);
-
+
// Also show in console
console.warn('Claude Update Required:', updateInfo);
}
@@ -7661,36 +7662,18 @@ class ClaudeOrchestrator {
return;
}
this.lastNotificationTime[sessionId] = now;
-
+
const worktreeId = sessionId.replace('-claude', '');
const session = this.sessions.get(sessionId);
const branch = session ? session.branch : '';
-
- // Create small toast notification
- const toast = document.createElement('div');
- toast.className = 'ready-toast';
- toast.innerHTML = `
-
- โ
- Claude ${worktreeId} ready ${branch ? `(${branch})` : ''}
-
- `;
-
- // Add to page
- document.body.appendChild(toast);
-
- // Remove after 3 seconds
- setTimeout(() => {
- if (toast.parentNode) {
- toast.remove();
- }
- }, 3000);
-
+
+ this.showToast(`Claude ${worktreeId} ready ${branch ? `(${branch})` : ''}`, 'success', { durationMs: 3000 });
+
// Play notification sound if enabled
if (this.settings.sounds) {
this.playNotificationSound();
}
-
+
// Browser notification if enabled
if (this.settings.notifications && 'Notification' in window && Notification.permission === 'granted') {
new Notification(`Claude ${worktreeId} Ready`, {
@@ -7709,26 +7692,26 @@ class ClaudeOrchestrator {
});
}
}
-
+
playNotificationSound() {
// Create a simple notification sound
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
-
+
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
-
+
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
oscillator.frequency.setValueAtTime(600, audioContext.currentTime + 0.1);
-
+
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
-
+
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3);
}
-
+
loadSettings() {
const stored = localStorage.getItem('claude-orchestrator-settings');
const defaults = {
@@ -8501,11 +8484,11 @@ class ClaudeOrchestrator {
globalNodeInput.value = nodeOptions.join(' ');
globalArgsInput.value = gameArgs.join(' ');
}
-
+
saveSettings() {
localStorage.setItem('claude-orchestrator-settings', JSON.stringify(this.settings));
}
-
+
applyTheme() {
const mode = this.settings.theme === 'light' ? 'light' : 'dark';
document.body.classList.toggle('light-theme', mode === 'light');
@@ -8683,7 +8666,7 @@ class ClaudeOrchestrator {
}
return normalized;
}
-
+
syncSettingsUI() {
// Sync checkbox states with settings
document.getElementById('enable-notifications').checked = this.settings.notifications;
@@ -8702,29 +8685,29 @@ class ClaudeOrchestrator {
const label = document.getElementById('skin-intensity-value');
if (label) label.textContent = `${v}%`;
}
-
+
// Sync user settings UI if loaded
if (this.userSettings) {
this.syncUserSettingsUI();
}
}
-
+
showCodeReviewDropdown(sessionId) {
// Close any existing dropdowns
document.querySelectorAll('.review-dropdown').forEach(dropdown => dropdown.remove());
-
+
// Get the terminal controls container
const terminalWrapper = document.getElementById(this.getSessionDomId('wrapper', sessionId));
const controlsContainer = terminalWrapper.querySelector('.terminal-controls');
-
+
// Create dropdown
const dropdown = document.createElement('div');
dropdown.className = 'review-dropdown';
dropdown.innerHTML = this.buildReviewerDropdownHTML(sessionId);
-
+
// Position and add to DOM
controlsContainer.appendChild(dropdown);
-
+
// Close dropdown when clicking outside
const closeDropdown = (e) => {
if (!dropdown.contains(e.target)) {
@@ -8732,22 +8715,22 @@ class ClaudeOrchestrator {
document.removeEventListener('click', closeDropdown);
}
};
-
+
// Add close listener after a short delay to prevent immediate closure
setTimeout(() => {
document.addEventListener('click', closeDropdown);
}, 100);
}
-
+
buildReviewerDropdownHTML(requestingSessionId) {
const availableReviewers = this.getAvailableReviewers(requestingSessionId);
-
+
let html = `
`;
-
+
if (availableReviewers.length === 0) {
html += `
@@ -8767,19 +8750,19 @@ class ClaudeOrchestrator {
`;
});
}
-
+
return html;
}
-
+
getAvailableReviewers(requestingSessionId) {
const reviewers = [];
-
+
for (const [sessionId, session] of this.sessions) {
// Only include Claude sessions that are not the requesting session
if (sessionId.includes('-claude') && sessionId !== requestingSessionId) {
const worktreeNumber = sessionId.replace('-claude', '').replace('work', '');
const isActive = this.sessionActivity.get(sessionId) === 'active';
-
+
// Prefer active sessions, but include inactive ones as backup
if (isActive || session.status === 'waiting') {
reviewers.push({
@@ -8792,7 +8775,7 @@ class ClaudeOrchestrator {
}
}
}
-
+
// Sort by preference: active + ready first, then active + busy, then inactive
reviewers.sort((a, b) => {
if (a.isActive && !b.isActive) return -1;
@@ -8801,48 +8784,48 @@ class ClaudeOrchestrator {
if (a.status !== 'waiting' && b.status === 'waiting') return 1;
return 0;
});
-
+
return reviewers;
}
-
+
async assignCodeReview(requestingSessionId, reviewerSessionId) {
// Close dropdown
document.querySelectorAll('.review-dropdown').forEach(dropdown => dropdown.remove());
-
+
try {
// Extract code/PR information from the requesting session
const codeInfo = await this.extractCodeForReview(requestingSessionId);
-
+
if (!codeInfo.hasContent) {
this.showToast(`No code changes detected in Claude ${requestingSessionId.replace('work', '').replace('-claude', '')}`, 'warning');
return;
}
-
+
// Format review request
const reviewRequest = this.formatReviewRequest(codeInfo, requestingSessionId);
-
+
// Send to reviewer Claude
this.sendTerminalInput(reviewerSessionId, reviewRequest);
-
+
// Mark both sessions as active
this.sessionActivity.set(reviewerSessionId, 'active');
this.buildSidebar();
-
+
// Show success message
const requestingWorktree = requestingSessionId.replace('work', '').replace('-claude', '');
const reviewerWorktree = reviewerSessionId.replace('work', '').replace('-claude', '');
this.showToast(`Code review assigned: Claude ${requestingWorktree} โ Claude ${reviewerWorktree}`, 'success');
-
+
} catch (error) {
console.error('Error assigning code review:', error);
this.showToast('Failed to assign code review', 'error');
}
}
-
+
async extractCodeForReview(sessionId) {
// Get terminal content
const terminalContent = this.terminalManager.getTerminalContent(sessionId);
-
+
// Look for various types of code content
const codePatterns = {
prUrl: /https:\/\/github\.com\/[^\s]+\/pull\/\d+/g,
@@ -8851,7 +8834,7 @@ class ClaudeOrchestrator {
codeBlocks: /```[\s\S]*?```/g,
bashCommands: /(?:git\s+(?:diff|log|show)|gh\s+pr)/g
};
-
+
const extracted = {
prUrls: [...(terminalContent.match(codePatterns.prUrl) || [])],
gitDiffs: [...(terminalContent.match(codePatterns.gitDiff) || [])],
@@ -8859,20 +8842,20 @@ class ClaudeOrchestrator {
recentCommands: this.extractRecentCommands(terminalContent),
hasContent: false
};
-
+
// Determine if there's reviewable content
- extracted.hasContent = extracted.prUrls.length > 0 ||
- extracted.gitDiffs.length > 0 ||
+ extracted.hasContent = extracted.prUrls.length > 0 ||
+ extracted.gitDiffs.length > 0 ||
extracted.codeBlocks.length > 0 ||
extracted.recentCommands.some(cmd => cmd.includes('git') || cmd.includes('gh pr'));
-
+
return extracted;
}
-
+
extractRecentCommands(terminalContent) {
const lines = terminalContent.split('\n');
const commands = [];
-
+
// Look for command patterns (simple approach)
for (let i = lines.length - 1; i >= 0 && commands.length < 10; i--) {
const line = lines[i].trim();
@@ -8880,15 +8863,15 @@ class ClaudeOrchestrator {
commands.unshift(line);
}
}
-
+
return commands;
}
-
+
formatReviewRequest(codeInfo, requestingSessionId) {
const requestingWorktree = requestingSessionId.replace('work', '').replace('-claude', '');
-
+
let request = `Please review the code from Claude ${requestingWorktree}:\n\n`;
-
+
if (codeInfo.prUrls.length > 0) {
request += `**Pull Request(s):**\n`;
codeInfo.prUrls.forEach(url => {
@@ -8900,19 +8883,19 @@ class ClaudeOrchestrator {
request += `- Suggestions for improvement\n`;
request += `- Architecture and design patterns\n\n`;
}
-
+
if (codeInfo.gitDiffs.length > 0) {
request += `**Git Diff:**\n\`\`\`diff\n`;
request += codeInfo.gitDiffs.slice(0, 2).join('\n'); // Limit to first 2 diffs
request += `\n\`\`\`\n\n`;
}
-
+
if (codeInfo.codeBlocks.length > 0) {
request += `**Code Changes:**\n`;
request += codeInfo.codeBlocks.slice(0, 3).join('\n\n'); // Limit to first 3 blocks
request += `\n\n`;
}
-
+
if (codeInfo.recentCommands.length > 0) {
request += `**Recent Commands:**\n`;
codeInfo.recentCommands.forEach(cmd => {
@@ -8920,9 +8903,9 @@ class ClaudeOrchestrator {
});
request += `\n`;
}
-
+
request += `Please provide a thorough code review with specific feedback and suggestions.\n`;
-
+
return request;
}
@@ -9250,75 +9233,1426 @@ class ClaudeOrchestrator {
throw new Error(String(data?.error || data?.message || `HTTP ${res.status}`));
}
- const diagnostics = data?.diagnostics;
- if (diagnostics && typeof diagnostics === 'object') {
- state.firstRun = diagnostics;
- renderRepairActions(state.firstRun);
- } else {
- await refreshFirstRun();
+ const diagnostics = data?.diagnostics;
+ if (diagnostics && typeof diagnostics === 'object') {
+ state.firstRun = diagnostics;
+ renderRepairActions(state.firstRun);
+ } else {
+ await refreshFirstRun();
+ }
+ if (!state.base) await refreshBase();
+ await refreshInstallWizard().catch(() => {});
+ render(state.base, state.firstRun, state.wizard);
+
+ const appliedCount = Number(data?.appliedCount || 0);
+ const failedCount = Number(data?.failedCount || 0);
+ const skippedManualCount = Number(data?.skippedManualCount || 0);
+ if (failedCount > 0) {
+ this.showToast?.(`Auto-fix applied ${appliedCount}, failed ${failedCount}`, 'warning');
+ } else {
+ const tail = skippedManualCount > 0 ? `, ${skippedManualCount} manual step(s) left` : '';
+ this.showToast?.(`Auto-fix applied ${appliedCount}${tail}`, 'success');
+ }
+ if (statusEl) statusEl.textContent = 'Safe auto-fix completed';
+ } catch (error) {
+ this.showToast?.(`Safe auto-fix failed: ${String(error?.message || error)}`, 'error');
+ if (statusEl) statusEl.textContent = '';
+ } finally {
+ btnRepairSafe.disabled = false;
+ }
+ });
+ repairEl?.addEventListener('click', async (event) => {
+ const target = event.target.closest('[data-diagnostics-repair]');
+ if (!target) return;
+ const action = String(target.getAttribute('data-diagnostics-repair') || '').trim();
+ if (!action) return;
+ target.disabled = true;
+ if (statusEl) statusEl.textContent = `Running repair: ${action}โฆ`;
+ try {
+ const res = await fetch('/api/diagnostics/first-run/repair', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ action })
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || data?.ok === false) {
+ throw new Error(String(data?.error || data?.message || `HTTP ${res.status}`));
+ }
+ const repair = data?.repair || {};
+ if (repair.manual) {
+ this.showToast?.(String(repair?.message || 'Manual action required'), 'warning');
+ } else {
+ this.showToast?.(String(repair?.message || 'Repair completed'), 'success');
+ }
+ if (data?.diagnostics) {
+ state.firstRun = data.diagnostics;
+ renderRepairActions(state.firstRun);
+ } else {
+ await refreshFirstRun();
+ }
+ if (!state.base) await refreshBase();
+ await refreshInstallWizard().catch(() => {});
+ render(state.base, state.firstRun, state.wizard);
+ if (statusEl) statusEl.textContent = `Repair completed: ${action}`;
+ } catch (error) {
+ this.showToast?.(`Repair failed: ${String(error?.message || error)}`, 'error');
+ if (statusEl) statusEl.textContent = '';
+ } finally {
+ target.disabled = false;
+ }
+ });
+ }
+
+ openDiagnosticsPanel({ refresh = true } = {}) {
+ try {
+ document.getElementById('settings-panel')?.classList?.remove?.('hidden');
+ setTimeout(() => {
+ try {
+ document.getElementById('diagnostics-output')?.scrollIntoView?.({ behavior: 'smooth', block: 'start' });
+ } catch {
+ // ignore
+ }
+ if (!refresh) return;
+ if (typeof this.refreshDiagnosticsPanel === 'function') {
+ this.refreshDiagnosticsPanel();
+ return;
+ }
+ try {
+ document.getElementById('diagnostics-refresh')?.click?.();
+ } catch {
+ // ignore
+ }
+ }, 50);
+ } catch {
+ // ignore
+ }
+ }
+
+ setupDependencySetupWizard() {
+ const modal = document.getElementById('dependency-setup-modal');
+ const openBtn = document.getElementById('dependency-setup-open');
+ const summaryEl = document.getElementById('dependency-setup-summary');
+ const listEl = document.getElementById('dependency-setup-list');
+ const closeBtn = document.getElementById('dependency-setup-close');
+ if (!modal || !summaryEl || !listEl) return;
+ const body = document.body;
+ const isWindowsHost = (() => {
+ try {
+ const platform = String(navigator?.platform || '').toLowerCase();
+ const userAgent = String(navigator?.userAgent || '').toLowerCase();
+ return platform.includes('win') || userAgent.includes('windows');
+ } catch {
+ return false;
+ }
+ })();
+
+ const setBootstrapPending = (pending) => {
+ if (!isWindowsHost) return;
+ if (pending) {
+ body?.classList?.add?.('dependency-onboarding-booting');
+ body?.classList?.remove?.('dependency-onboarding-active');
+ return;
+ }
+ body?.classList?.remove?.('dependency-onboarding-booting');
+ };
+ setBootstrapPending(true);
+
+ const dismissKey = 'orchestrator-dependency-setup-dismissed-v3';
+ const completedKey = 'orchestrator-dependency-onboarding-completed-v2';
+ const progressKey = 'orchestrator-dependency-onboarding-progress-v2';
+ const skippedStepsKey = 'orchestrator-dependency-onboarding-skipped-v1';
+ const state = {
+ loading: false,
+ diagnostics: null,
+ actions: [],
+ currentStep: 0,
+ showWelcome: true,
+ skippedActionIds: new Set(),
+ actionRuns: new Map(),
+ actionRunPollers: new Map(),
+ gitIdentity: {
+ name: '',
+ email: ''
+ },
+ gitIdentityHelpVisible: false
+ };
+
+ const readDismissed = () => {
+ try {
+ return localStorage.getItem(dismissKey) === 'true';
+ } catch {
+ return false;
+ }
+ };
+
+ const writeDismissed = (value) => {
+ try {
+ if (value) localStorage.setItem(dismissKey, 'true');
+ else localStorage.removeItem(dismissKey);
+ } catch {
+ // ignore
+ }
+ };
+
+ const readCompleted = () => {
+ try {
+ return localStorage.getItem(completedKey) === 'true';
+ } catch {
+ return false;
+ }
+ };
+
+ const writeCompleted = (value) => {
+ try {
+ if (value) localStorage.setItem(completedKey, 'true');
+ else localStorage.removeItem(completedKey);
+ } catch {
+ // ignore
+ }
+ };
+
+ const readSavedStep = () => {
+ try {
+ const raw = Number.parseInt(String(localStorage.getItem(progressKey) || ''), 10);
+ if (Number.isFinite(raw) && raw >= 0) return raw;
+ return 0;
+ } catch {
+ return 0;
+ }
+ };
+
+ const writeSavedStep = (step) => {
+ try {
+ localStorage.setItem(progressKey, String(Math.max(0, Number(step) || 0)));
+ } catch {
+ // ignore
+ }
+ };
+
+ const readSkippedStepIds = () => {
+ try {
+ const raw = localStorage.getItem(skippedStepsKey);
+ if (!raw) return new Set();
+ const parsed = JSON.parse(raw);
+ if (!Array.isArray(parsed)) return new Set();
+ const ids = parsed
+ .map((value) => String(value || '').trim())
+ .filter(Boolean);
+ return new Set(ids);
+ } catch {
+ return new Set();
+ }
+ };
+
+ const writeSkippedStepIds = () => {
+ try {
+ if (!(state.skippedActionIds instanceof Set) || state.skippedActionIds.size === 0) {
+ localStorage.removeItem(skippedStepsKey);
+ return;
+ }
+ localStorage.setItem(skippedStepsKey, JSON.stringify(Array.from(state.skippedActionIds)));
+ } catch {
+ // ignore
+ }
+ };
+
+ const setStepSkipped = (actionId, skipped) => {
+ const id = String(actionId || '').trim();
+ if (!id) return;
+ if (skipped) state.skippedActionIds.add(id);
+ else state.skippedActionIds.delete(id);
+ writeSkippedStepIds();
+ };
+
+ const toToolMap = (diagnostics) => {
+ const map = new Map();
+ const tools = Array.isArray(diagnostics?.tools) ? diagnostics.tools : [];
+ tools.forEach((tool) => {
+ const id = String(tool?.id || '').trim();
+ if (!id) return;
+ map.set(id, !!tool?.ok);
+ });
+ return map;
+ };
+
+ const getToolResult = (diagnostics, toolId) => {
+ const id = String(toolId || '').trim();
+ if (!id) return null;
+ const tools = Array.isArray(diagnostics?.tools) ? diagnostics.tools : [];
+ return tools.find((tool) => String(tool?.id || '').trim() === id) || null;
+ };
+
+ const parseGitIdentityVersion = (value) => {
+ const raw = String(value || '').trim();
+ if (!raw) return { name: '', email: '' };
+ const pair = raw.match(/^(.*)\s<([^<>]+)>$/);
+ if (pair?.[1] && pair?.[2]) {
+ return {
+ name: String(pair[1] || '').trim(),
+ email: String(pair[2] || '').trim()
+ };
+ }
+ if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw)) {
+ return { name: '', email: raw };
+ }
+ return { name: raw, email: '' };
+ };
+
+ const stripAnsiText = (value) => String(value || '').replace(/\u001b\[[0-9;]*m/g, '');
+
+ const collectRunOutputLines = (runInfo, { limit = 25 } = {}) => {
+ const lines = Array.isArray(runInfo?.output)
+ ? runInfo.output
+ .map((entry) => stripAnsiText(String(entry?.line || '')).trim())
+ .filter(Boolean)
+ : [];
+ if (!Number.isFinite(limit) || limit <= 0) return lines;
+ return lines.slice(-Math.max(1, Number(limit) || 1));
+ };
+
+ const extractGithubLoginInfo = (lines = []) => {
+ const fallbackUrl = 'https://github.com/login/device';
+ let link = fallbackUrl;
+ let code = '';
+ let sawDeviceHint = false;
+
+ (Array.isArray(lines) ? lines : []).forEach((lineRaw) => {
+ const line = String(lineRaw || '').trim();
+ if (!line) return;
+
+ if (/one[-\s]?time code|login\/device|authenticate in your web browser|copied to your clipboard|open this url/i.test(line)) {
+ sawDeviceHint = true;
+ }
+
+ const linkMatch = line.match(/https:\/\/github\.com\/login\/device(?:\S*)?/i);
+ if (linkMatch?.[0]) link = linkMatch[0].trim();
+
+ const codeMatch = line.match(/\b([A-Z0-9]{4}-[A-Z0-9]{4})\b/i);
+ if (codeMatch?.[1]) code = String(codeMatch[1]).toUpperCase();
+ });
+
+ return {
+ link,
+ code,
+ sawDeviceHint
+ };
+ };
+
+ const hydrateGitIdentityDraft = (diagnostics) => {
+ const gitIdentityTool = getToolResult(diagnostics, 'gitIdentity');
+ const parsed = parseGitIdentityVersion(String(gitIdentityTool?.version || ''));
+ if (!state.gitIdentity.name && parsed.name) {
+ state.gitIdentity.name = parsed.name;
+ }
+ if (!state.gitIdentity.email && parsed.email) {
+ state.gitIdentity.email = parsed.email;
+ }
+ };
+
+ const getRequirementState = (toolsMap) => {
+ const gitOk = !!toolsMap.get('git');
+ const claudeOk = !!toolsMap.get('claude');
+ const codexOk = !!toolsMap.get('codex');
+ const hasAgentCli = claudeOk || codexOk;
+ const coreReady = gitOk && hasAgentCli;
+ const missingCore = [];
+ if (!gitOk) missingCore.push('git');
+ if (!hasAgentCli) missingCore.push('agent-cli');
+ return {
+ gitOk,
+ claudeOk,
+ codexOk,
+ hasAgentCli,
+ coreReady,
+ missingCore
+ };
+ };
+
+ const isActionComplete = (actionId, toolsMap) => {
+ switch (String(actionId || '').trim()) {
+ case 'install-git':
+ return !!toolsMap.get('git');
+ case 'configure-git-identity':
+ return !!toolsMap.get('gitIdentity');
+ case 'install-node':
+ return !!toolsMap.get('node') && !!toolsMap.get('npm');
+ case 'install-gh':
+ return !!toolsMap.get('gh');
+ case 'gh-login':
+ return !!toolsMap.get('ghAuth');
+ case 'install-claude':
+ return !!toolsMap.get('claude');
+ case 'install-codex':
+ return !!toolsMap.get('codex');
+ default:
+ return false;
+ }
+ };
+
+ const getActionLevelText = (level) => {
+ if (level === 'required') return 'Required';
+ if (level === 'optional') return 'Optional';
+ if (level === 'core-option') return 'Core option';
+ return 'Recommended';
+ };
+
+ const getActionLevelClass = (level) => {
+ if (level === 'optional') return 'level-optional';
+ return level === 'recommended' ? 'level-recommended' : 'level-required';
+ };
+
+ const getActionStatusText = (actionId, done) => {
+ const id = String(actionId || '').trim();
+ if (id === 'gh-login') return done ? 'Logged in' : 'Not logged in';
+ if (id === 'configure-git-identity') return done ? 'Configured' : 'Not configured';
+ return done ? 'Installed' : 'Missing';
+ };
+
+ const getResolvedSteps = () => {
+ const toolsMap = toToolMap(state.diagnostics);
+ const actions = Array.isArray(state.actions) ? state.actions : [];
+ return actions.map((action) => {
+ const id = String(action?.id || '').trim();
+ const level = getActionLevel(id);
+ const done = isActionComplete(id, toolsMap);
+ return {
+ ...action,
+ id,
+ level,
+ optional: action?.optional === true || level === 'optional',
+ done,
+ levelText: getActionLevelText(level),
+ levelClass: getActionLevelClass(level),
+ statusText: getActionStatusText(id, done),
+ statusClass: done ? 'status-ok' : 'status-missing',
+ runSupported: action?.runSupported !== false
+ };
+ });
+ };
+
+ const syncSkippedSteps = (steps) => {
+ if (!(state.skippedActionIds instanceof Set)) {
+ state.skippedActionIds = new Set();
+ }
+ const validSkippedIds = new Set(
+ (Array.isArray(steps) ? steps : [])
+ .filter((step) => {
+ const id = String(step?.id || '').trim();
+ return !!id && step?.optional && !step?.done;
+ })
+ .map((step) => String(step?.id || '').trim())
+ );
+ let changed = false;
+ for (const id of Array.from(state.skippedActionIds)) {
+ if (!validSkippedIds.has(id)) {
+ state.skippedActionIds.delete(id);
+ changed = true;
+ }
+ }
+ if (changed) writeSkippedStepIds();
+ };
+
+ const isOnboardingLocked = () => {
+ 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();
+ if (!force && locked) {
+ openModal();
+ return false;
+ }
+ modal.classList.add('hidden');
+ body?.classList?.remove?.('dependency-onboarding-active');
+ setBootstrapPending(false);
+ return true;
+ };
+ const openModal = ({ showWelcome = null } = {}) => {
+ const wasHidden = modal.classList.contains('hidden');
+ modal.classList.remove('hidden');
+ setBootstrapPending(false);
+ body?.classList?.add?.('dependency-onboarding-active');
+ if (typeof showWelcome === 'boolean') {
+ state.showWelcome = showWelcome;
+ } else if (wasHidden) {
+ state.showWelcome = true;
+ }
+ if (state.diagnostics && Array.isArray(state.actions) && state.actions.length > 0) {
+ render();
+ }
+ applyOnboardingLockUI();
+ };
+
+ const setLoading = (loading) => {
+ state.loading = !!loading;
+ if (openBtn) openBtn.disabled = state.loading;
+ if (state.loading) {
+ summaryEl.textContent = '';
+ }
+ };
+
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
+
+ const loadAndRender = async ({ open = false, forceAutoShow = false, bootstrap = false } = {}) => {
+ if (state.loading) return false;
+ setLoading(true);
+ try {
+ const [diagRes, actionsRes] = await Promise.all([
+ fetch('/api/diagnostics'),
+ fetch('/api/setup-actions')
+ ]);
+ const diagData = await diagRes.json().catch(() => ({}));
+ const actionsData = await actionsRes.json().catch(() => ({}));
+
+ if (!diagRes.ok || diagData?.ok === false) {
+ throw new Error(String(diagData?.error || `Diagnostics HTTP ${diagRes.status}`));
+ }
+ if (!actionsRes.ok || actionsData?.ok === false) {
+ throw new Error(String(actionsData?.error || `Setup actions HTTP ${actionsRes.status}`));
+ }
+
+ state.diagnostics = diagData;
+ hydrateGitIdentityDraft(diagData);
+ const allActions = Array.isArray(actionsData?.actions) ? actionsData.actions : [];
+ const toolsMap = toToolMap(diagData);
+ state.actions = allActions.filter((action) => String(action?.id || '').trim() !== 'gh-login');
+ const allowedActionIds = new Set(
+ state.actions
+ .map((action) => String(action?.id || '').trim())
+ .filter(Boolean)
+ );
+ const persistedSkippedIds = readSkippedStepIds();
+ state.skippedActionIds = new Set(
+ Array.from(persistedSkippedIds).filter((id) => allowedActionIds.has(id))
+ );
+ if (state.actions.length > 0) {
+ const savedStep = readSavedStep();
+ setCurrentStep(savedStep, { persist: false });
+ }
+ const view = render();
+ applyOnboardingLockUI();
+ if (view.req?.coreReady) writeDismissed(false);
+
+ const shouldAutoShow = forceAutoShow || (!readDismissed() && (!readCompleted() || !(view.req?.coreReady)));
+ if (open || shouldAutoShow) {
+ openModal();
+ } else {
+ setBootstrapPending(false);
+ }
+ return true;
+ } catch (err) {
+ summaryEl.textContent = `Dependency check failed: ${String(err?.message || err)}`;
+ listEl.innerHTML = '
Unable to load setup actions right now.
';
+ if (open) openModal();
+ else if (!bootstrap) setBootstrapPending(false);
+ return false;
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const stopRunPolling = (actionId) => {
+ const id = String(actionId || '').trim();
+ if (!id) return;
+ const poller = state.actionRunPollers.get(id);
+ if (poller?.timer) clearTimeout(poller.timer);
+ state.actionRunPollers.delete(id);
+ };
+
+ const updateActionRunState = (actionId, patch = {}, { rerender = true } = {}) => {
+ const id = String(actionId || '').trim();
+ if (!id) return null;
+ const prev = state.actionRuns.get(id) || { actionId: id };
+ const next = {
+ ...prev,
+ ...patch,
+ actionId: id
+ };
+ state.actionRuns.set(id, next);
+ if (rerender) render();
+ return next;
+ };
+
+ const fetchSetupActionRunStatus = async ({ runId = '', actionId = '' } = {}) => {
+ const params = new URLSearchParams();
+ if (runId) params.set('runId', String(runId));
+ if (actionId) params.set('actionId', String(actionId));
+ const res = await fetch(`/api/setup-actions/run-status?${params.toString()}`);
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || data?.ok === false || !data?.run) {
+ throw new Error(String(data?.error || `HTTP ${res.status}`));
+ }
+ return data.run;
+ };
+
+ const getVerifyPolicyForAction = (actionId) => {
+ const id = String(actionId || '').trim();
+ if (id === 'gh-login') {
+ return { attempts: 14, delayMs: 900 };
+ }
+ if (id === 'install-git' || id === 'install-node' || id === 'install-gh') {
+ return { attempts: 10, delayMs: 650 };
+ }
+ return { attempts: 8, delayMs: 650 };
+ };
+
+ const verifyActionInstalled = async (actionId, runId, options = {}) => {
+ const id = String(actionId || '').trim();
+ if (!id) return false;
+ const policy = {
+ ...getVerifyPolicyForAction(id),
+ ...(options && typeof options === 'object' ? options : {})
+ };
+ const attempts = Math.max(1, Number(policy.attempts) || 1);
+ const delayMs = Math.max(250, Number(policy.delayMs) || 650);
+
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
+ const runState = state.actionRuns.get(id);
+ if (!runState || String(runState?.runId || '') !== String(runId || '')) return false;
+
+ updateActionRunState(id, {
+ status: 'verifying',
+ verifyAttempt: attempt,
+ verifyMax: attempts,
+ updatedAt: new Date().toISOString()
+ });
+
+ await loadAndRender({ open: true, forceAutoShow: true });
+ const toolsMap = toToolMap(state.diagnostics);
+ if (isActionComplete(id, toolsMap)) {
+ updateActionRunState(id, {
+ status: 'verified',
+ verifyAttempt: attempts,
+ verifyMax: attempts,
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast('Dependency detected automatically.', 'success');
+ return true;
+ }
+ await sleep(delayMs);
+ }
+
+ updateActionRunState(id, {
+ status: 'needs-attention',
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast(
+ id === 'gh-login'
+ ? 'GitHub login is not detected yet. Complete sign-in in browser and try again.'
+ : 'Install finished but dependency is still missing. Review output and run again if needed.',
+ 'warning'
+ );
+ return false;
+ };
+
+ const verifyActionWithoutRun = async (actionId, options = {}) => {
+ const id = String(actionId || '').trim();
+ if (!id) return false;
+ const policy = {
+ ...getVerifyPolicyForAction(id),
+ ...(options && typeof options === 'object' ? options : {})
+ };
+ const attempts = Math.max(1, Number(policy.attempts) || 1);
+ const delayMs = Math.max(250, Number(policy.delayMs) || 650);
+
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
+ updateActionRunState(id, {
+ status: 'verifying',
+ verifyAttempt: attempt,
+ verifyMax: attempts,
+ updatedAt: new Date().toISOString()
+ });
+
+ await loadAndRender({ open: true, forceAutoShow: true });
+ const toolsMap = toToolMap(state.diagnostics);
+ if (isActionComplete(id, toolsMap)) {
+ updateActionRunState(id, {
+ status: 'verified',
+ verifyAttempt: attempts,
+ verifyMax: attempts,
+ updatedAt: new Date().toISOString()
+ });
+ return true;
+ }
+ if (attempt < attempts) {
+ await sleep(delayMs);
+ }
+ }
+
+ updateActionRunState(id, {
+ status: 'needs-attention',
+ updatedAt: new Date().toISOString()
+ });
+ return false;
+ };
+
+ const pollRunUntilDone = async (actionId, runId) => {
+ const id = String(actionId || '').trim();
+ const rid = String(runId || '').trim();
+ if (!id || !rid) return;
+
+ stopRunPolling(id);
+ const pollLoop = async () => {
+ try {
+ const run = await fetchSetupActionRunStatus({ runId: rid, actionId: id });
+ updateActionRunState(id, run);
+
+ if (String(run?.status || '').toLowerCase() === 'running') {
+ const timer = setTimeout(pollLoop, 850);
+ state.actionRunPollers.set(id, { runId: rid, timer });
+ return;
+ }
+
+ stopRunPolling(id);
+ await loadAndRender({ open: true, forceAutoShow: true });
+
+ if (String(run?.status || '').toLowerCase() === 'success') {
+ await verifyActionInstalled(id, rid);
+ return;
+ }
+
+ if (String(run?.status || '').toLowerCase() === 'failed') {
+ if (id === 'gh-login') {
+ const ghRunLines = collectRunOutputLines(run, { limit: 160 });
+ const ghRunInfo = extractGithubLoginInfo(ghRunLines);
+ const hasDeviceSignal = ghRunInfo.sawDeviceHint || !!ghRunInfo.code || /login\/device/i.test(ghRunLines.join('\n'));
+ const verifyOptions = hasDeviceSignal
+ ? { attempts: 14, delayMs: 900 }
+ : { attempts: 5, delayMs: 700 };
+ const detected = await verifyActionWithoutRun(id, verifyOptions);
+ if (detected) {
+ this.showToast('GitHub login detected automatically.', 'success');
+ return;
+ }
+
+ const runError = String(run?.error || '').trim();
+ const exitCode = Number(run?.exitCode);
+ const interrupted = exitCode === 1 || /code\s*1/i.test(runError) || /cancel|not completed/i.test(runError);
+ const missingDeviceCode = !hasDeviceSignal;
+ updateActionRunState(id, {
+ status: 'needs-attention',
+ error: interrupted
+ ? 'Login was not completed in browser.'
+ : (missingDeviceCode
+ ? 'GitHub CLI did not return a one-time code.'
+ : (runError || 'Login was not completed.')),
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast(
+ missingDeviceCode
+ ? 'GitHub CLI did not return a one-time code. Click Start login again.'
+ : (interrupted
+ ? 'GitHub login is still not detected. If browser sign-in just finished, click Start login again.'
+ : `GitHub login failed: ${runError || 'Unknown error'}`),
+ (interrupted || missingDeviceCode) ? 'warning' : 'error'
+ );
+ return;
+ }
+ this.showToast(`Install failed: ${String(run?.error || 'Unknown error')}`, 'error');
+ }
+ } catch (err) {
+ stopRunPolling(id);
+ updateActionRunState(id, {
+ status: 'failed',
+ error: String(err?.message || err || 'Failed to fetch setup status'),
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast(`Install monitoring failed: ${String(err?.message || err)}`, 'error');
+ }
+ };
+
+ const timer = setTimeout(pollLoop, 250);
+ state.actionRunPollers.set(id, { runId: rid, timer });
+ };
+
+ const runBootstrapLoad = async () => {
+ const delaysMs = [0, 240, 420, 700, 1050, 1450, 1900];
+ for (let attempt = 0; attempt < delaysMs.length; attempt += 1) {
+ if (attempt > 0) {
+ await sleep(delaysMs[attempt]);
+ }
+ const ok = await loadAndRender({ open: false, forceAutoShow: false, bootstrap: true });
+ if (ok) return;
+ }
+ setBootstrapPending(false);
+ };
+
+ const runSetupAction = async (actionId, btnEl) => {
+ const id = String(actionId || '').trim();
+ if (!id) return;
+ const button = btnEl || null;
+ if (button) button.disabled = true;
+ try {
+ const existingRunStatus = String(state.actionRuns.get(id)?.status || '').trim().toLowerCase();
+ if (existingRunStatus === 'running' || existingRunStatus === 'verifying' || existingRunStatus === 'success' || existingRunStatus === 'completed') {
+ this.showToast(
+ id === 'gh-login'
+ ? 'Login is still in progress. Please wait while we finish checking.'
+ : 'Install is still in progress. Please wait while we finish checking.',
+ 'info'
+ );
+ return;
+ }
+
+ const existingStep = getResolvedSteps().find((step) => String(step?.id || '').trim() === id);
+ const toolsMap = toToolMap(state.diagnostics);
+ if (id === 'gh-login' && !toToolMap(state.diagnostics).get('gh')) {
+ updateActionRunState(id, {
+ status: 'needs-attention',
+ error: 'Install GitHub CLI before starting login.',
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast('Install GitHub CLI first. Login is optional and only available after installation.', 'warning');
+ await loadAndRender({ open: true, forceAutoShow: true });
+ return;
+ }
+ if (id === 'install-codex' && !(toolsMap.get('node') && toolsMap.get('npm'))) {
+ updateActionRunState(id, {
+ status: 'needs-attention',
+ error: 'Install Node.js LTS first. Codex requires npm.',
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast('Install Node.js LTS first. Codex depends on npm.', 'warning');
+ await loadAndRender({ open: true, forceAutoShow: true });
+ return;
+ }
+ if (existingStep?.done) {
+ updateActionRunState(id, {
+ status: 'verified',
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast('Dependency already detected.', 'success');
+ await loadAndRender({ open: true, forceAutoShow: true });
+ return;
+ }
+
+ updateActionRunState(id, {
+ runId: null,
+ status: 'running',
+ error: null,
+ output: [],
+ verifyAttempt: 0,
+ verifyMax: 0,
+ updatedAt: new Date().toISOString()
+ });
+ const res = await fetch('/api/setup-actions/run', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ actionId: id })
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || data?.ok === false) {
+ throw new Error(String(data?.error || `HTTP ${res.status}`));
}
- if (!state.base) await refreshBase();
- await refreshInstallWizard().catch(() => {});
- render(state.base, state.firstRun, state.wizard);
-
- const appliedCount = Number(data?.appliedCount || 0);
- const failedCount = Number(data?.failedCount || 0);
- const skippedManualCount = Number(data?.skippedManualCount || 0);
- if (failedCount > 0) {
- this.showToast?.(`Auto-fix applied ${appliedCount}, failed ${failedCount}`, 'warning');
+ const run = (data?.run && typeof data.run === 'object') ? data.run : null;
+ if (run) {
+ updateActionRunState(id, {
+ ...run,
+ verifyAttempt: 0,
+ verifyMax: 0
+ });
+ if (run?.runId) {
+ await pollRunUntilDone(id, run.runId);
+ } else {
+ await loadAndRender({ open: true, forceAutoShow: true });
+ }
} else {
- const tail = skippedManualCount > 0 ? `, ${skippedManualCount} manual step(s) left` : '';
- this.showToast?.(`Auto-fix applied ${appliedCount}${tail}`, 'success');
+ await loadAndRender({ open: true, forceAutoShow: true });
}
- if (statusEl) statusEl.textContent = 'Safe auto-fix completed';
- } catch (error) {
- this.showToast?.(`Safe auto-fix failed: ${String(error?.message || error)}`, 'error');
- if (statusEl) statusEl.textContent = '';
+ const defaultMessage = data?.alreadyRunning
+ ? (id === 'gh-login'
+ ? 'GitHub login is already running. Complete it in your browser.'
+ : 'Install is already running. Watching for completion...')
+ : (id === 'gh-login'
+ ? 'GitHub login started. Complete sign-in in your browser.'
+ : 'Install started. We will check this step automatically.');
+ this.showToast(String(data?.message || defaultMessage), 'info');
+ } catch (err) {
+ updateActionRunState(id, {
+ status: 'failed',
+ error: String(err?.message || err),
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast(`Failed to start action: ${String(err?.message || err)}`, 'error');
} finally {
- btnRepairSafe.disabled = false;
+ if (button) button.disabled = false;
}
- });
- repairEl?.addEventListener('click', async (event) => {
- const target = event.target.closest('[data-diagnostics-repair]');
- if (!target) return;
- const action = String(target.getAttribute('data-diagnostics-repair') || '').trim();
- if (!action) return;
- target.disabled = true;
- if (statusEl) statusEl.textContent = `Running repair: ${action}โฆ`;
+ };
+
+ const saveGitIdentity = async (btnEl) => {
+ const button = btnEl || null;
+ const id = 'configure-git-identity';
+ const nameInput = listEl.querySelector('[data-setup-git-name]');
+ const emailInput = listEl.querySelector('[data-setup-git-email]');
+ const name = String(nameInput?.value || state.gitIdentity?.name || '').trim();
+ const email = String(emailInput?.value || state.gitIdentity?.email || '').trim();
+
+ state.gitIdentity.name = name;
+ state.gitIdentity.email = email;
+
+ if (!name || !email) {
+ this.showToast('Enter both Git name and email.', 'warning');
+ return;
+ }
+
+ if (button) button.disabled = true;
try {
- const res = await fetch('/api/diagnostics/first-run/repair', {
+ updateActionRunState(id, {
+ runId: 'manual-git-identity',
+ status: 'running',
+ error: null,
+ output: [],
+ verifyAttempt: 0,
+ verifyMax: 0,
+ updatedAt: new Date().toISOString()
+ });
+
+ const res = await fetch('/api/setup-actions/configure-git-identity', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ action })
+ body: JSON.stringify({ name, email })
});
const data = await res.json().catch(() => ({}));
if (!res.ok || data?.ok === false) {
- throw new Error(String(data?.error || data?.message || `HTTP ${res.status}`));
+ throw new Error(String(data?.error || `HTTP ${res.status}`));
}
- const repair = data?.repair || {};
- if (repair.manual) {
- this.showToast?.(String(repair?.message || 'Manual action required'), 'warning');
+
+ state.gitIdentity.name = String(data?.name || name).trim();
+ state.gitIdentity.email = String(data?.email || email).trim();
+
+ const detected = await verifyActionWithoutRun(id, { attempts: 6, delayMs: 350 });
+ if (detected) {
+ this.showToast('Git identity saved and detected automatically.', 'success');
} else {
- this.showToast?.(String(repair?.message || 'Repair completed'), 'success');
+ this.showToast('Git identity saved, but detection is delayed. Try saving again in a few seconds.', 'warning');
}
- if (data?.diagnostics) {
- state.firstRun = data.diagnostics;
- renderRepairActions(state.firstRun);
- } else {
- await refreshFirstRun();
- }
- if (!state.base) await refreshBase();
- await refreshInstallWizard().catch(() => {});
- render(state.base, state.firstRun, state.wizard);
- if (statusEl) statusEl.textContent = `Repair completed: ${action}`;
- } catch (error) {
- this.showToast?.(`Repair failed: ${String(error?.message || error)}`, 'error');
- if (statusEl) statusEl.textContent = '';
+ } catch (err) {
+ updateActionRunState(id, {
+ status: 'failed',
+ error: String(err?.message || err),
+ updatedAt: new Date().toISOString()
+ });
+ this.showToast(`Failed to save Git identity: ${String(err?.message || err)}`, 'error');
} finally {
- target.disabled = false;
+ if (button) button.disabled = false;
+ await loadAndRender({ open: true, forceAutoShow: true });
+ }
+ };
+
+ listEl.addEventListener('click', async (event) => {
+ const runBtn = event.target.closest('[data-setup-run]');
+ if (runBtn) {
+ await runSetupAction(runBtn.getAttribute('data-setup-run'), runBtn);
+ return;
+ }
+
+ const saveGitBtn = event.target.closest('[data-setup-git-save]');
+ if (saveGitBtn) {
+ await saveGitIdentity(saveGitBtn);
+ return;
+ }
+
+ const prevBtn = event.target.closest('[data-setup-prev]');
+ if (prevBtn) {
+ setCurrentStep(state.currentStep - 1);
+ render();
+ return;
+ }
+
+ const nextBtn = event.target.closest('[data-setup-next]');
+ if (nextBtn) {
+ const total = Array.isArray(state.actions) ? state.actions.length : 0;
+ const steps = getResolvedSteps();
+ syncSkippedSteps(steps);
+ const currentStep = steps[state.currentStep];
+ if (!currentStep?.done) {
+ if (!currentStep?.optional) {
+ this.showToast('Install this dependency before continuing.', 'warning');
+ return;
+ }
+ setStepSkipped(currentStep?.id, true);
+ this.showToast('Skipping optional setup for now. You can configure it later.', 'warning');
+ } else {
+ setStepSkipped(currentStep?.id, false);
+ }
+ if (state.currentStep >= (total - 1)) {
+ writeCompleted(true);
+ writeDismissed(false);
+ closeModal({ force: true });
+ this.showToast('Dependency onboarding complete.', 'success');
+ return;
+ }
+ setCurrentStep(state.currentStep + 1);
+ render();
+ return;
+ }
+ const beginBtn = event.target.closest('[data-setup-begin]');
+ if (beginBtn) {
+ state.showWelcome = false;
+ render();
+ return;
+ }
+
+ const jumpBtn = event.target.closest('[data-setup-jump]');
+ if (jumpBtn) {
+ const idx = Number.parseInt(String(jumpBtn.getAttribute('data-setup-jump') || ''), 10);
+ if (Number.isFinite(idx)) {
+ setCurrentStep(idx);
+ render();
+ }
+ return;
+ }
+
+ const copyBtn = event.target.closest('[data-setup-copy-id]');
+ if (copyBtn) {
+ const actionId = String(copyBtn.getAttribute('data-setup-copy-id') || '').trim();
+ const action = (Array.isArray(state.actions) ? state.actions : []).find((item) => String(item?.id || '').trim() === actionId);
+ const command = String(action?.command || '').trim();
+ if (!command) return;
+ try {
+ await navigator.clipboard.writeText(command);
+ this.showToast('Command copied to clipboard.', 'success');
+ } catch (err) {
+ this.showToast(`Copy failed: ${String(err?.message || err)}`, 'error');
+ }
+ return;
+ }
+
+ const copyGhCodeBtn = event.target.closest('[data-setup-copy-gh-code]');
+ if (copyGhCodeBtn) {
+ event.preventDefault();
+ event.stopPropagation();
+ const code = String(copyGhCodeBtn.getAttribute('data-setup-copy-gh-code') || '').trim();
+ if (!code) return;
+ try {
+ await navigator.clipboard.writeText(code);
+ this.showToast('GitHub one-time code copied.', 'success');
+ } catch (err) {
+ this.showToast(`Copy failed: ${String(err?.message || err)}`, 'error');
+ }
+ return;
+ }
+
+ const openGhLoginBtn = event.target.closest('[data-setup-open-gh-login]');
+ if (openGhLoginBtn) {
+ event.preventDefault();
+ event.stopPropagation();
+ const link = String(openGhLoginBtn.getAttribute('data-setup-open-gh-login') || '').trim();
+ if (!link) return;
+ try {
+ const res = await fetch('/api/setup-actions/open-url', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ url: link })
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || data?.ok === false) {
+ throw new Error(String(data?.error || `HTTP ${res.status}`));
+ }
+ this.showToast('Opened GitHub login in your browser.', 'info');
+ } catch (err) {
+ this.showToast(`Could not open login link: ${String(err?.message || err)}`, 'error');
+ }
+ return;
+ }
+
+ const toggleGitHelpBtn = event.target.closest('[data-setup-toggle-git-help]');
+ if (toggleGitHelpBtn) {
+ event.preventDefault();
+ event.stopPropagation();
+ state.gitIdentityHelpVisible = !state.gitIdentityHelpVisible;
+ render();
+ return;
}
});
- }
+
+ if (openBtn) {
+ openBtn.addEventListener('click', () => {
+ writeDismissed(false);
+ setCurrentStep(0);
+ loadAndRender({ open: true, forceAutoShow: true });
+ });
+ }
+ if (closeBtn) {
+ closeBtn.addEventListener('click', () => closeModal());
+ }
+
+ modal.addEventListener('click', (event) => {
+ if (event.target === modal) closeModal();
+ });
+
+ document.addEventListener('keydown', (event) => {
+ if (event.key !== 'Escape') return;
+ if (modal.classList.contains('hidden')) return;
+ closeModal();
+ });
+
+ runBootstrapLoad();
+ }
notifyWorkflow({ type = 'info', message = '', sessionId = null, metadata = null } = {}) {
const msg = String(message || '').trim();
@@ -9353,46 +10687,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 +10747,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 +10777,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 +14090,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 +14234,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 +14242,7 @@ class ClaudeOrchestrator {
window.history.replaceState({}, document.title, window.location.pathname);
return tokenFromUrl;
}
-
+
// Check localStorage
return localStorage.getItem('claude-orchestrator-token');
}
@@ -13733,7 +15068,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 +15112,7 @@ class ClaudeOrchestrator {
window.__claudeOrchestratorFetchAuthInstalled = true;
}
-
+
// Terminal Focus Feature - Now shows only that worktree
focusTerminal(sessionId) {
// Extract worktree ID from session ID
@@ -13815,14 +15150,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 +15169,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 +15211,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 +15254,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 +15301,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 +15327,10 @@ class ClaudeOrchestrator {
}, 100);
}
}
-
+
// Clean up
this.focusedTerminalInfo = null;
-
+
// Remove ESC key listener
if (this.handleEscKey) {
document.removeEventListener('keydown', this.handleEscKey);
@@ -14005,14 +15340,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 +15484,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 +15513,7 @@ class ClaudeOrchestrator {
}
}
}
-
+
hideClaudeStartupModal() {
const modal = document.getElementById('claude-startup-modal');
if (modal) {
@@ -14186,7 +15521,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 +15651,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 +16152,7 @@ class ClaudeOrchestrator {
document.addEventListener('keydown', handleEsc);
});
}
-
+
updateYoloState(sessionId, checked) {
// Update button styles to show YOLO is active
const buttons = [
@@ -14825,7 +16160,7 @@ class ClaudeOrchestrator {
document.getElementById(`btn-continue-${sessionId}`),
document.getElementById(`btn-resume-${sessionId}`)
];
-
+
buttons.forEach(btn => {
if (btn) {
if (checked) {
@@ -14836,25 +16171,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 +16198,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 +16218,10 @@ class ClaudeOrchestrator {
restartClaudeSession(sessionId) {
console.log(`Restarting Claude session: ${sessionId}`);
-
+
if (this.socket && this.socket.connected) {
this.socket.emit('restart-session', { sessionId });
-
+
// Update UI to show restarting
this.updateSessionStatus(sessionId, 'restarting');
} else {
@@ -14922,16 +16257,16 @@ class ClaudeOrchestrator {
if (!this.userSettings) {
console.warn('User settings not loaded, attempting to load...');
await this.loadUserSettings();
-
+
if (!this.userSettings) {
console.error('Failed to load user settings');
return;
}
}
-
+
const pathParts = path.split('.');
const newGlobal = JSON.parse(JSON.stringify(this.userSettings.global));
-
+
// Navigate to the correct nested property
let current = newGlobal;
for (let i = 0; i < pathParts.length - 1; i++) {
@@ -15556,7 +16891,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 +16917,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 +16935,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 +16951,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 +16977,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 +16990,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 +17022,7 @@ class ClaudeOrchestrator {
setTimeout(checkAndStart, 500); // Check again in 500ms
}
};
-
+
setTimeout(checkAndStart, 1000); // Initial delay for terminal setup
}
@@ -15696,7 +17031,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 +17137,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 +17170,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 +17178,14 @@ class ClaudeOrchestrator {
if (response.ok) {
const result = await response.json();
-
+
if (result.success) {
// Success message will be handled by socket event
const notification = document.getElementById('git-update-notification');
notification.classList.add('hidden');
} else {
this.showTemporaryMessage(result.error || 'Failed to pull changes', 'error');
-
+
// Show specific error details if available
if (result.changes && result.changes.length > 0) {
console.log('Uncommitted changes:', result.changes);
@@ -19679,7 +21014,7 @@ class ClaudeOrchestrator {
applyView();
return;
}
-
+
state.selectedCardId = card.id || null;
applyView();
@@ -21029,7 +22364,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 +22382,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 +22427,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 +31659,7 @@ class ClaudeOrchestrator {
// While worktree sessions are spinning up, we reserve the worktree so it isn't recommended again.
this.cleanupExpiredWorktreeReservations();
if (this.isWorktreeReserved(repoPathNorm, worktreeId)) return true;
-
+
// Extract repo name from path for session matching
const repoName = (repoNameOverride || repoPathNorm.split('/').pop() || '').toLowerCase();
diff --git a/client/index.html b/client/index.html
index 0605696e..c27ae39b 100644
--- a/client/index.html
+++ b/client/index.html
@@ -1,5 +1,6 @@
+
@@ -7,15 +8,17 @@
-
+
-
+
+
-
+
@@ -131,7 +134,7 @@
Claude Orchestrator
-
+
@@ -148,7 +151,7 @@
View Presets
-
+
@@ -191,7 +194,7 @@
Start AI Agent -
-
+
@@ -302,7 +306,8 @@
Workflow Notifications
-
Quiet: log only. Normal: toast for key events. Aggressive: toasts + optional browser/sound alerts.
+
Quiet: log only. Normal: toast for key events. Aggressive: toasts +
+ optional browser/sound alerts.
-
Controls the initial layout preset when opening the Review Console.
+
Controls the initial layout preset when opening the Review Console.
+
-
When enabled, Review Console includes the paired server tile next to the agent tile (when available).
+
When enabled, Review Console includes the paired server tile next to
+ the agent tile (when available).
@@ -546,22 +553,28 @@
Glossary
Workspace
- A saved configuration that groups worktrees/terminals for a project (or many repos in a mixed workspace). Opening a workspace starts its sessions.
+ A saved configuration that groups worktrees/terminals for a project
+ (or many repos in a mixed workspace). Opening a workspace starts its sessions.
Worktree
- A git worktree folder (e.g. work6) on disk. One worktree usually has an Agent terminal and (optionally) a paired Server terminal.
+ A git worktree folder (e.g. work6) on disk. One
+ worktree usually has an Agent terminal and (optionally) a paired Server
+ terminal.
Terminal / Session
- A running process (PTY) owned by the Orchestrator. It can be recovered after restart unless you explicitly close it.
+ A running process (PTY) owned by the Orchestrator. It can be
+ recovered after restart unless you explicitly close it.
Agent vs Server
- Agent runs Claude/Codex work. Server runs your dev server (game/app). Theyโre paired per worktree and should generally live/die together.
+ Agent runs Claude/Codex work.
+ Server runs your dev server (game/app). Theyโre paired per worktree and should
+ generally live/die together.
@@ -666,6 +679,7 @@ Scheduler
Click โRefresh schedulerโ.
+
Pager / Pollcat
@@ -785,7 +799,8 @@
PR Merge Automation
Enable PR-merge automation (Trello)
-
When a PR merges, auto-move/comment on the linked Trello card (when a Trello card is linked to the PR task).
+
When a PR merges, auto-move/comment on the linked Trello card (when a
+ Trello card is linked to the PR task).
-
Configure board-specific Done/For Test lists in Tasks โ Board Settings โ Conventions.
+
Configure board-specific Done/For Test lists in Tasks โ Board
+ Settings โ Conventions.
-
+
Terminal Settings
-
+
@@ -866,7 +885,7 @@ Terminal Settings
Apply --dangerously-skip-permissions to all new Claude sessions
-
+
@@ -894,7 +913,8 @@ Terminal Settings
Start Delay (ms):
-
+
@@ -939,10 +959,11 @@
Terminal Settings
Per-Terminal Overrides
-
Override global settings for specific terminals (requires terminal restart)
+
Override global settings for specific terminals (requires terminal
+ restart)
-
+
Default Template Management
Manage the default settings template committed to the repository
@@ -962,10 +983,11 @@
Default Template Management
-
+
Repository Updates
-
Web/dev mode: Git pull updates. Tauri desktop mode: app updater check/install.
+
Web/dev mode: Git pull updates. Tauri desktop mode: app updater
+ check/install.
+
+
+
-
+
-
+
@@ -1044,10 +1085,12 @@ Notifications
-
+
-
+
@@ -1062,4 +1105,5 @@
Notifications
+
diff --git a/client/notifications.js b/client/notifications.js
index 6fa218c3..7ec3ea9d 100644
--- a/client/notifications.js
+++ b/client/notifications.js
@@ -389,21 +389,21 @@ class NotificationManager {
// Add notification styles
const notificationStyles = document.createElement('style');
notificationStyles.textContent = `
- .empty-message {
+ .notifications-panel .empty-message {
padding: var(--space-xl);
text-align: center;
color: var(--text-secondary);
font-size: 0.875rem;
}
- .notification-header {
+ .notifications-panel .notification-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-xs);
}
- .notification-meta {
+ .notifications-panel .notification-meta {
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: var(--space-xs);
diff --git a/client/styles.css b/client/styles.css
index eab6a28d..62b1bf22 100644
--- a/client/styles.css
+++ b/client/styles.css
@@ -79,6 +79,15 @@
--shadow-float: 0 18px 40px rgba(0, 0, 0, 0.45);
--shadow-soft: 0 4px 12px rgba(0, 0, 0, 0.15);
--focus-terminal-bg: color-mix(in srgb, var(--bg-primary) 72%, #000000 28%);
+
+ /* Scrollbars */
+ --scrollbar-size: 10px;
+ --scrollbar-radius: 999px;
+ --scrollbar-track: #1b2230;
+ --scrollbar-thumb: #3f5f8f;
+ --scrollbar-thumb-hover: #4f76ae;
+ --scrollbar-thumb-active: #5a84bf;
+ --scrollbar-thumb-border: rgba(13, 17, 23, 0.72);
}
/* Light Theme */
@@ -107,6 +116,30 @@ body.light-theme {
--shadow-float: 0 18px 40px rgba(0, 0, 0, 0.22);
--shadow-soft: 0 4px 12px rgba(0, 0, 0, 0.12);
--focus-terminal-bg: color-mix(in srgb, var(--bg-primary) 94%, #000000 6%);
+
+ --scrollbar-track: #dbe1e8;
+ --scrollbar-thumb: #7f91aa;
+ --scrollbar-thumb-hover: #6f86a8;
+ --scrollbar-thumb-active: #5d789d;
+ --scrollbar-thumb-border: rgba(255, 255, 255, 0.76);
+}
+
+@supports (color: color-mix(in srgb, #000000 50%, #ffffff 50%)) {
+ :root {
+ --scrollbar-track: color-mix(in srgb, var(--bg-secondary) 78%, #000000 22%);
+ --scrollbar-thumb: color-mix(in srgb, var(--accent-primary) 46%, var(--bg-tertiary) 54%);
+ --scrollbar-thumb-hover: color-mix(in srgb, var(--accent-primary-hover) 60%, var(--bg-tertiary) 40%);
+ --scrollbar-thumb-active: color-mix(in srgb, var(--accent-primary) 72%, var(--bg-tertiary) 28%);
+ --scrollbar-thumb-border: color-mix(in srgb, var(--bg-primary) 76%, transparent 24%);
+ }
+
+ body.light-theme {
+ --scrollbar-track: color-mix(in srgb, var(--bg-tertiary) 70%, #ffffff 30%);
+ --scrollbar-thumb: color-mix(in srgb, var(--accent-primary) 44%, #7f8a98 56%);
+ --scrollbar-thumb-hover: color-mix(in srgb, var(--accent-primary) 58%, #6f7c8f 42%);
+ --scrollbar-thumb-active: color-mix(in srgb, var(--accent-primary) 68%, #617187 32%);
+ --scrollbar-thumb-border: color-mix(in srgb, #ffffff 68%, transparent 32%);
+ }
}
/* Skins */
@@ -178,6 +211,133 @@ body.light-theme.skin-high-contrast {
margin: 0;
padding: 0;
box-sizing: border-box;
+ scrollbar-width: thin;
+ scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
+}
+
+*::-webkit-scrollbar {
+ width: var(--scrollbar-size);
+ height: var(--scrollbar-size);
+}
+
+*::-webkit-scrollbar-track {
+ background: var(--scrollbar-track);
+ border-radius: var(--scrollbar-radius);
+}
+
+*::-webkit-scrollbar-thumb {
+ background: var(--scrollbar-thumb);
+ border-radius: var(--scrollbar-radius);
+ border: 2px solid var(--scrollbar-thumb-border);
+ background-clip: padding-box;
+}
+
+*::-webkit-scrollbar-thumb:hover {
+ background: var(--scrollbar-thumb-hover);
+}
+
+*::-webkit-scrollbar-thumb:active {
+ background: var(--scrollbar-thumb-active);
+}
+
+*::-webkit-scrollbar-corner {
+ background: var(--scrollbar-track);
+}
+
+:where(
+ .onboarding-overlay,
+ .onboarding-container,
+ .dependency-setup-body,
+ .dependency-setup-item-output,
+ .diagnostics-output,
+ .worktree-list,
+ .workspace-tabs-container,
+ .header-actions,
+ .terminal .xterm-viewport,
+ .focused-terminal-body .xterm-viewport
+) {
+ scrollbar-width: thin;
+ scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
+}
+
+:where(
+ .onboarding-overlay,
+ .onboarding-container,
+ .dependency-setup-body,
+ .dependency-setup-item-output,
+ .diagnostics-output,
+ .worktree-list,
+ .workspace-tabs-container,
+ .header-actions,
+ .terminal .xterm-viewport,
+ .focused-terminal-body .xterm-viewport
+)::-webkit-scrollbar {
+ width: var(--scrollbar-size);
+ height: var(--scrollbar-size);
+}
+
+:where(
+ .onboarding-overlay,
+ .onboarding-container,
+ .dependency-setup-body,
+ .dependency-setup-item-output,
+ .diagnostics-output,
+ .worktree-list,
+ .workspace-tabs-container,
+ .header-actions,
+ .terminal .xterm-viewport,
+ .focused-terminal-body .xterm-viewport
+)::-webkit-scrollbar-track {
+ background: var(--scrollbar-track);
+ border-radius: var(--scrollbar-radius);
+}
+
+:where(
+ .onboarding-overlay,
+ .onboarding-container,
+ .dependency-setup-body,
+ .dependency-setup-item-output,
+ .diagnostics-output,
+ .worktree-list,
+ .workspace-tabs-container,
+ .header-actions,
+ .terminal .xterm-viewport,
+ .focused-terminal-body .xterm-viewport
+)::-webkit-scrollbar-thumb {
+ background: var(--scrollbar-thumb);
+ border-radius: var(--scrollbar-radius);
+ border: 2px solid var(--scrollbar-thumb-border);
+ background-clip: padding-box;
+}
+
+:where(
+ .onboarding-overlay,
+ .onboarding-container,
+ .dependency-setup-body,
+ .dependency-setup-item-output,
+ .diagnostics-output,
+ .worktree-list,
+ .workspace-tabs-container,
+ .header-actions,
+ .terminal .xterm-viewport,
+ .focused-terminal-body .xterm-viewport
+)::-webkit-scrollbar-thumb:hover {
+ background: var(--scrollbar-thumb-hover);
+}
+
+:where(
+ .onboarding-overlay,
+ .onboarding-container,
+ .dependency-setup-body,
+ .dependency-setup-item-output,
+ .diagnostics-output,
+ .worktree-list,
+ .workspace-tabs-container,
+ .header-actions,
+ .terminal .xterm-viewport,
+ .focused-terminal-body .xterm-viewport
+)::-webkit-scrollbar-thumb:active {
+ background: var(--scrollbar-thumb-active);
}
body {
@@ -2143,12 +2303,19 @@ header h1 {
}
.worktree-inspector-header.review-console-header::-webkit-scrollbar {
- height: 6px;
+ height: 8px;
+}
+
+.worktree-inspector-header.review-console-header::-webkit-scrollbar-track {
+ background: var(--scrollbar-track);
+ border-radius: var(--scrollbar-radius);
}
.worktree-inspector-header.review-console-header::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.16);
- border-radius: 999px;
+ background: var(--scrollbar-thumb);
+ border-radius: var(--scrollbar-radius);
+ border: 2px solid var(--scrollbar-thumb-border);
+ background-clip: padding-box;
}
.review-console-route-bar {
@@ -4354,19 +4521,23 @@ header h1 {
line-height: 1.35;
}
-.settings-toolbar input {
- flex: 1 1 auto;
- min-width: 0;
- padding: 0 10px;
+body.dependency-onboarding-active {
+ overflow: hidden;
+ background:
+ radial-gradient(circle at 12% 18%, rgba(56, 139, 253, 0.16), transparent 48%),
+ radial-gradient(circle at 88% 8%, rgba(34, 197, 94, 0.14), transparent 44%),
+ var(--bg-primary);
}
-.settings-toolbar select {
- flex: 0 0 auto;
- min-width: 120px;
- padding: 0 8px;
+body.dependency-onboarding-booting {
+ overflow: hidden;
+ background:
+ radial-gradient(circle at 14% 22%, rgba(56, 139, 253, 0.14), transparent 46%),
+ radial-gradient(circle at 82% 10%, rgba(34, 197, 94, 0.12), transparent 42%),
+ var(--bg-primary);
}
-.settings-filter-hidden {
+body.dependency-onboarding-booting > :not(#dependency-setup-modal):not(#toast-stack):not(.toast):not(.ready-toast):not(#notifications-panel):not(.cross-workspace-notifications-area) {
display: none !important;
}
@@ -4416,57 +4587,45 @@ header h1 {
border-radius: 10px;
}
-.setting-group {
- margin-bottom: var(--space-md);
+body.dependency-onboarding-active > :not(#dependency-setup-modal):not(#toast-stack):not(.toast):not(.ready-toast):not(#notifications-panel):not(.cross-workspace-notifications-area) {
+ display: none !important;
}
-.setting-group label {
+body.dependency-onboarding-active #dependency-setup-modal {
display: flex;
align-items: center;
- gap: var(--space-sm);
- cursor: pointer;
-}
-
-.setting-group select {
- background: var(--bg-tertiary);
- color: var(--text-primary);
- border: 1px solid var(--border-color);
- padding: var(--space-xs) var(--space-sm);
- border-radius: var(--radius-sm);
- margin-left: var(--space-sm);
+ justify-content: center;
+ padding: 24px;
+ z-index: 4500;
}
-.setting-section {
- margin: var(--space-lg) 0;
- padding-top: var(--space-lg);
- border-top: 1px solid var(--border-color);
+.dependency-setup-content {
+ max-width: min(760px, 94vw);
+ width: min(760px, 94vw);
+ border: 1px solid rgba(56, 139, 253, 0.2);
+ box-shadow: 0 24px 54px rgba(0, 0, 0, 0.35);
}
-.setting-section h4 {
- color: var(--text-primary);
- margin-bottom: var(--space-md);
- font-size: 1.1em;
+.dependency-setup-content .modal-header {
+ margin-bottom: 10px;
}
-.setting-section h5 {
- color: var(--text-secondary);
- margin-bottom: var(--space-sm);
- font-size: 0.9em;
- font-weight: 600;
+.dependency-setup-content .modal-header h3 {
+ font-size: 1.2rem;
+ letter-spacing: 0.01em;
}
-.setting-group label small {
- display: block;
- color: var(--text-tertiary);
- font-size: 0.8em;
- margin-top: var(--space-xs);
- margin-left: var(--space-lg);
+.dependency-setup-body {
+ max-height: 68vh;
+ overflow: auto;
+ padding-right: 4px;
}
-.setting-description {
- color: var(--text-tertiary);
- font-size: 0.85em;
- margin-bottom: var(--space-md);
+.dependency-setup-list {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ margin-top: 12px;
}
.skin-gallery {
@@ -4539,612 +4698,1216 @@ header h1 {
.settings-glossary details {
border: 1px solid var(--border-color);
- border-radius: var(--radius-sm);
- padding: 8px 10px;
+ border-radius: var(--radius-lg);
background: var(--bg-tertiary);
- margin: 10px 0;
-}
-
-.settings-glossary details[open] {
- background: rgba(255, 255, 255, 0.02);
+ padding: 14px;
}
-.settings-glossary summary {
- cursor: pointer;
+.dependency-setup-item-header {
display: flex;
align-items: center;
- gap: 8px;
- list-style: none;
-}
-
-.settings-glossary summary::-webkit-details-marker {
- display: none;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 6px;
+ flex-wrap: wrap;
}
-.settings-glossary details > .setting-description {
- margin: 10px 0 0;
- opacity: 0.95;
+.dependency-setup-item-title {
+ font-weight: 650;
+ font-size: 1.03rem;
+ color: var(--text-primary);
}
-.identity-saved-list {
+.dependency-setup-badges {
display: flex;
- flex-wrap: wrap;
+ align-items: center;
gap: 6px;
+ flex-wrap: wrap;
}
-.identity-chip {
+.dependency-setup-badge {
display: inline-flex;
align-items: center;
- gap: 6px;
- padding: 2px 8px;
- border-radius: var(--radius-sm);
+ border-radius: 999px;
border: 1px solid var(--border-color);
- background: rgba(255, 255, 255, 0.03);
- font-family: var(--font-mono);
- font-size: 0.78rem;
- max-width: 100%;
+ padding: 2px 8px;
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ background: var(--bg-secondary);
}
-.identity-chip button {
- border: none;
- background: transparent;
- color: var(--text-tertiary);
- cursor: pointer;
- padding: 0;
- line-height: 1;
+.dependency-setup-badge.status-ok {
+ color: var(--accent-success);
+ border-color: rgba(34, 197, 94, 0.45);
}
-.identity-chip button:hover {
- color: var(--text-secondary);
+.dependency-setup-badge.status-missing {
+ color: var(--accent-warning);
+ border-color: rgba(245, 158, 11, 0.45);
}
-.per-terminal-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: var(--space-sm);
- margin-bottom: var(--space-xs);
- background: var(--bg-tertiary);
- border-radius: var(--radius-sm);
+.dependency-setup-badge.status-pending {
+ color: var(--accent-primary);
+ border-color: rgba(56, 139, 253, 0.45);
}
-.per-terminal-item .terminal-name {
- color: var(--text-primary);
- font-weight: 500;
- font-family: var(--font-mono);
+.dependency-setup-badge.level-required {
+ color: var(--accent-danger);
+ border-color: rgba(239, 68, 68, 0.45);
}
-.per-terminal-item .terminal-override {
- color: var(--text-secondary);
- font-size: 0.8em;
+.dependency-setup-badge.level-recommended {
+ color: var(--accent-primary);
+ border-color: rgba(56, 139, 253, 0.45);
}
-.per-terminal-items {
- margin-top: var(--space-sm);
+.dependency-setup-badge.level-optional {
+ color: var(--accent-warning);
+ border-color: rgba(245, 158, 11, 0.45);
}
-/* Notifications Panel */
-.notifications-panel {
- position: fixed;
- top: var(--header-height);
- right: 0;
- bottom: 0;
- width: 380px;
+.dependency-setup-item-desc {
+ color: var(--text-secondary);
+ margin-bottom: 10px;
+ line-height: 1.45;
+}
+
+.dependency-setup-item-command {
+ margin: 0;
+ padding: 8px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
background: var(--bg-secondary);
- border-left: 1px solid var(--border-color);
- box-shadow: -4px 0 12px rgba(0, 0, 0, 0.15);
- transform: translateX(100%);
- transition: transform 0.3s;
- z-index: 101;
- display: flex;
- flex-direction: column;
+ color: var(--text-secondary);
+ white-space: pre-wrap;
+ font-size: 0.85rem;
}
-.notifications-panel:not(.hidden) {
- transform: translateX(0);
+.dependency-onboarding-command-wrap {
+ margin-bottom: 10px;
}
-.notifications-content {
- padding: var(--space-md);
- overflow-y: auto;
- flex: 1;
+.dependency-onboarding-command-label {
+ margin: 0 0 6px;
+ font-size: 0.76rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--text-tertiary);
}
-.notification-list {
+.dependency-setup-item-actions {
display: flex;
- flex-direction: column;
- gap: var(--space-sm);
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
}
-.notification-item {
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-tertiary);
- padding: var(--space-sm);
- cursor: pointer;
- transition: border-color 0.15s, background 0.15s;
+.dependency-setup-item-actions .btn-secondary {
+ width: auto;
+ min-height: 32px;
+ flex: 0 0 auto;
}
-.notification-item:hover {
- border-color: rgba(56, 139, 253, 0.55);
- background: rgba(56, 139, 253, 0.06);
+.dependency-setup-item-actions a.btn-secondary {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ text-decoration: none;
}
-.notification-item.unread {
- border-left: 4px solid var(--accent-primary);
+.dependency-setup-actions {
+ justify-content: flex-start;
+ flex-wrap: wrap;
+ gap: 8px;
}
-.notification-type {
- font-size: 0.75rem;
- padding: 2px 6px;
- border-radius: 999px;
- border: 1px solid var(--border-color);
+.dependency-setup-actions .btn-secondary {
+ flex: 0 0 auto;
+}
+
+.dependency-setup-empty {
+ padding: 12px;
+ border: 1px dashed var(--border-color);
+ border-radius: var(--radius-sm);
color: var(--text-secondary);
}
-.notification-type.waiting {
- border-color: rgba(245, 158, 11, 0.45);
- color: var(--accent-warning);
+.dependency-onboarding-progress {
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-tertiary);
+ padding: 12px;
}
-.notification-type.completed {
- border-color: rgba(34, 197, 94, 0.45);
- color: var(--accent-success);
+.dependency-onboarding-progress-meta {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 10px;
+ color: var(--text-secondary);
}
-.notification-type.error {
- border-color: rgba(239, 68, 68, 0.45);
- color: var(--accent-danger);
+.dependency-onboarding-progress-track {
+ width: 100%;
+ height: 8px;
+ border-radius: 999px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ overflow: hidden;
}
-.notification-message {
- color: var(--text-primary);
- line-height: 1.35;
+.dependency-onboarding-progress-bar {
+ height: 100%;
+ background: var(--accent-primary);
+ transition: width 180ms ease-in-out;
}
-.notification-actions {
- display: flex;
- gap: var(--space-xs);
- margin-top: var(--space-sm);
- justify-content: flex-end;
+.dependency-onboarding-step {
+ margin-top: 12px;
}
-.notification-action {
- padding: 4px 8px;
- font-size: 0.8rem;
- width: auto;
+.dependency-onboarding-step-card {
+ background:
+ linear-gradient(150deg, rgba(56, 139, 253, 0.09), rgba(34, 197, 94, 0.03)),
+ var(--bg-tertiary);
}
-.terminal-controls {
- display: flex;
- align-items: center;
- gap: var(--space-sm);
+.dependency-onboarding-step-kicker {
+ margin-bottom: 6px;
+ font-size: 0.74rem;
+ font-weight: 600;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ color: var(--text-tertiary);
}
-.terminal-controls label {
- font-size: 0.9em;
- margin: 0;
+.dependency-onboarding-state {
+ margin: 0 0 8px;
+ color: var(--text-secondary);
+ font-weight: 500;
}
-/* Loading spinner for build button */
-@keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
+.dependency-onboarding-state.status-ok {
+ color: var(--accent-success);
}
-/* Server Launch Settings */
-.server-launch-group {
- display: inline-flex;
- gap: 4px;
- align-items: center;
+.dependency-onboarding-state.status-missing {
+ color: var(--accent-warning);
}
-#launch-settings-modal {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: var(--overlay-backdrop);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 2000;
+.dependency-onboarding-state.status-pending {
+ color: var(--accent-primary);
}
-#launch-settings-modal .modal-content {
- background: var(--bg-primary);
- border-radius: var(--radius-lg);
- max-width: 800px;
- width: 90%;
- max-height: 90vh;
- overflow-y: auto;
- box-shadow: var(--shadow-modal);
+.dependency-gh-login-helper {
+ margin: 0 0 10px;
+ padding: 10px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ background:
+ linear-gradient(145deg, rgba(56, 139, 253, 0.1), rgba(56, 139, 253, 0.02)),
+ var(--bg-tertiary);
}
-#launch-settings-modal .modal-header {
- padding: var(--space-lg);
- border-bottom: 1px solid var(--border-color);
- display: flex;
- justify-content: space-between;
- align-items: center;
+.dependency-git-identity-helper {
+ margin: 0 0 10px;
+ padding: 10px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ background:
+ linear-gradient(145deg, rgba(34, 197, 94, 0.08), rgba(34, 197, 94, 0.02)),
+ var(--bg-tertiary);
}
-#launch-settings-modal .modal-header h2 {
- margin: 0;
- font-size: 1.5rem;
- color: var(--text-primary);
+.dependency-git-identity-fields {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
+ gap: 8px;
+ margin-bottom: 8px;
}
-#launch-settings-modal .close-btn {
- background: transparent;
- border: none;
- color: var(--text-secondary);
- font-size: 1.5rem;
- cursor: pointer;
- padding: 0;
- width: 32px;
- height: 32px;
+.dependency-git-identity-field {
display: flex;
- align-items: center;
- justify-content: center;
- border-radius: var(--radius-sm);
+ flex-direction: column;
+ gap: 4px;
+ color: var(--text-secondary);
+ font-size: 0.86rem;
}
-#launch-settings-modal .close-btn:hover {
+.dependency-git-identity-field input {
+ width: 100%;
+ min-height: 34px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
background: var(--bg-secondary);
+ color: var(--text-primary);
+ padding: 0 10px;
+ font-size: 0.9rem;
}
-#launch-settings-modal .modal-body {
- padding: var(--space-lg);
-}
-
-#launch-settings-modal .settings-section {
- margin-bottom: var(--space-xl);
-}
-
-#launch-settings-modal .settings-section:last-child {
- margin-bottom: 0;
-}
-
-#launch-settings-modal .settings-section h3 {
- color: var(--accent-primary);
- font-size: 1.1rem;
- margin-bottom: var(--space-md);
+.dependency-git-identity-field input:focus {
+ outline: none;
+ border-color: var(--accent-primary);
}
-#launch-settings-modal .setting-group {
- margin-bottom: var(--space-md);
+.dependency-git-identity-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
}
-#launch-settings-modal .setting-group label {
- display: block;
- color: var(--text-secondary);
- font-size: 0.9rem;
- margin-bottom: var(--space-xs);
+.dependency-git-help-btn {
+ min-width: 32px;
+ width: 32px;
+ padding: 0;
+ font-weight: 700;
}
-#launch-settings-modal .setting-group input {
- width: 100%;
- padding: var(--space-sm) var(--space-md);
- background: var(--bg-secondary);
+.dependency-git-help-inline {
+ margin-top: 8px;
+ padding: 10px;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
- color: var(--text-primary);
- font-family: monospace;
+ background: var(--bg-secondary);
}
-#launch-settings-modal .setting-group input:focus {
- outline: none;
- border-color: var(--accent-primary);
+.dependency-git-help-line {
+ color: var(--text-secondary);
+ line-height: 1.45;
}
-#launch-settings-modal .setting-group small {
- display: block;
- color: var(--text-tertiary);
- font-size: 0.8rem;
- margin-top: var(--space-xs);
+.dependency-git-help-line + .dependency-git-help-line {
+ margin-top: 6px;
}
-#launch-settings-modal .preset-checkboxes {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
- gap: var(--space-md);
+.dependency-gh-login-helper-text {
+ color: var(--text-secondary);
+ margin-bottom: 8px;
}
-#launch-settings-modal .preset-checkbox {
+.dependency-gh-login-code-wrap {
display: flex;
align-items: center;
- padding: var(--space-md);
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: all 0.2s;
- user-select: none;
-}
-
-#launch-settings-modal .preset-checkbox:hover {
- background: var(--bg-tertiary);
- border-color: var(--accent-primary);
+ gap: 8px;
+ flex-wrap: wrap;
}
-#launch-settings-modal .preset-checkbox input[type="checkbox"] {
- margin-right: var(--space-sm);
- width: 18px;
- height: 18px;
- cursor: pointer;
+.dependency-gh-login-helper-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ margin-top: 8px;
}
-#launch-settings-modal .preset-checkbox input[type="checkbox"]:checked + span {
- color: var(--accent-primary);
+.dependency-gh-login-code {
+ display: inline-block;
+ padding: 7px 10px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ letter-spacing: 0.06em;
font-weight: 600;
}
-#launch-settings-modal .preset-checkbox span {
- font-size: 0.9rem;
- transition: all 0.2s;
+.dependency-setup-item-output {
+ max-height: 150px;
+ overflow: auto;
}
-#launch-settings-modal .modal-footer {
- padding: var(--space-lg);
- border-top: 1px solid var(--border-color);
+.dependency-onboarding-nav {
display: flex;
+ align-items: center;
justify-content: flex-end;
- gap: var(--space-md);
+ gap: 8px;
+ margin-top: 12px;
}
-#launch-settings-modal .btn-save,
-#launch-settings-modal .btn-cancel {
- padding: var(--space-sm) var(--space-lg);
- border-radius: var(--radius-sm);
- cursor: pointer;
- font-size: 0.9rem;
- transition: all 0.2s;
+.dependency-onboarding-nav .btn-secondary,
+.dependency-onboarding-nav .btn-primary {
+ width: auto;
+ min-height: 34px;
+ flex: 0 0 auto;
}
-#launch-settings-modal .btn-save {
- background: var(--accent-primary);
- color: white;
- border: none;
-}
+@media (max-width: 700px) {
+ body.dependency-onboarding-active #dependency-setup-modal {
+ padding: 14px;
+ }
-#launch-settings-modal .btn-save:hover {
- background: var(--accent-secondary);
-}
+ .dependency-setup-content {
+ width: min(760px, 96vw);
+ }
-#launch-settings-modal .btn-cancel {
- background: var(--bg-secondary);
- color: var(--text-secondary);
- border: 1px solid var(--border-color);
+ .dependency-setup-actions {
+ justify-content: flex-start;
+ }
+
+ .dependency-onboarding-nav {
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ }
}
-#launch-settings-modal .btn-cancel:hover {
- background: var(--bg-tertiary);
+.settings-toolbar input {
+ flex: 1 1 auto;
+ min-width: 0;
+ padding: 0 10px;
}
-.loading-spinner {
- display: inline-block;
- width: 14px;
- height: 14px;
- border: 2px solid rgba(255, 255, 255, 0.3);
- border-top: 2px solid #fff;
- border-radius: 50%;
- animation: spin 1s linear infinite;
+.settings-toolbar select {
+ flex: 0 0 auto;
+ min-width: 120px;
+ padding: 0 8px;
}
-.control-btn.building {
- background: var(--color-warning);
- cursor: not-allowed;
- opacity: 0.8;
+.settings-filter-hidden {
+ display: none !important;
}
-.clear-override-btn {
- background: var(--bg-primary);
- color: var(--text-secondary);
+/* Review Console */
+.review-console-warning {
+ margin: 10px 12px 14px;
+ padding: 10px 12px;
border: 1px solid var(--border-color);
- border-radius: var(--radius-sm);
- padding: var(--space-xs);
- cursor: pointer;
- font-size: 0.8em;
- transition: background-color 0.2s;
+ border-left: 4px solid var(--accent-warning);
+ background: rgba(210, 153, 34, 0.10);
+ border-radius: 10px;
}
-.clear-override-btn:hover {
- background: var(--bg-tertiary);
- color: var(--text-primary);
+.setting-group {
+ margin-bottom: var(--space-md);
}
-.template-actions {
+.setting-group label {
display: flex;
+ align-items: center;
gap: var(--space-sm);
- margin-top: var(--space-sm);
+ cursor: pointer;
}
-.template-btn {
- padding: var(--space-sm) var(--space-md);
+.setting-group select {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
border: 1px solid var(--border-color);
+ padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-sm);
- cursor: pointer;
- font-size: 0.9em;
- transition: all 0.2s;
+ margin-left: var(--space-sm);
}
-.template-btn.primary {
- background: var(--accent-primary);
- color: white;
- border-color: var(--accent-primary);
+.setting-section {
+ margin: var(--space-lg) 0;
+ padding-top: var(--space-lg);
+ border-top: 1px solid var(--border-color);
}
-.template-btn.primary:hover {
- background: var(--accent-primary-hover);
- border-color: var(--accent-primary-hover);
+.setting-section h4 {
+ color: var(--text-primary);
+ margin-bottom: var(--space-md);
+ font-size: 1.1em;
}
-.template-btn.secondary {
- background: var(--bg-primary);
+.setting-section h5 {
color: var(--text-secondary);
- border-color: var(--border-color);
+ margin-bottom: var(--space-sm);
+ font-size: 0.9em;
+ font-weight: 600;
}
-.template-btn.secondary:hover {
- background: var(--bg-tertiary);
- color: var(--text-primary);
+.setting-group label small {
+ display: block;
+ color: var(--text-tertiary);
+ font-size: 0.8em;
+ margin-top: var(--space-xs);
+ margin-left: var(--space-lg);
}
-.update-notification {
- margin-top: var(--space-sm);
- padding: var(--space-sm);
- background: var(--bg-tertiary);
- border: 1px solid var(--accent-warning);
+.setting-description {
+ color: var(--text-tertiary);
+ font-size: 0.85em;
+ margin-bottom: var(--space-md);
+}
+
+.settings-glossary details {
+ border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
- transition: all 0.3s;
+ padding: 8px 10px;
+ background: var(--bg-tertiary);
+ margin: 10px 0;
}
-.update-notification.hidden {
- display: none;
+.settings-glossary details[open] {
+ background: rgba(255, 255, 255, 0.02);
}
-.notification-content {
+.settings-glossary summary {
+ cursor: pointer;
display: flex;
align-items: center;
- gap: var(--space-sm);
+ gap: 8px;
+ list-style: none;
}
-.notification-icon {
- font-size: 1.1em;
+.settings-glossary summary::-webkit-details-marker {
+ display: none;
}
-.notification-text {
- flex: 1;
- color: var(--text-primary);
- font-size: 0.9em;
+.settings-glossary details > .setting-description {
+ margin: 10px 0 0;
+ opacity: 0.95;
}
-.dismiss-btn {
- background: none;
+.identity-saved-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.identity-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 8px;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--border-color);
+ background: rgba(255, 255, 255, 0.03);
+ font-family: var(--font-mono);
+ font-size: 0.78rem;
+ max-width: 100%;
+}
+
+.identity-chip button {
border: none;
- color: var(--text-secondary);
+ background: transparent;
+ color: var(--text-tertiary);
cursor: pointer;
- font-size: 1.2em;
padding: 0;
- width: 24px;
- height: 24px;
+ line-height: 1;
+}
+
+.identity-chip button:hover {
+ color: var(--text-secondary);
+}
+
+.per-terminal-item {
display: flex;
align-items: center;
- justify-content: center;
+ justify-content: space-between;
+ padding: var(--space-sm);
+ margin-bottom: var(--space-xs);
+ background: var(--bg-tertiary);
border-radius: var(--radius-sm);
- transition: all 0.2s;
}
-.dismiss-btn:hover {
- background: var(--bg-primary);
+.per-terminal-item .terminal-name {
color: var(--text-primary);
+ font-weight: 500;
+ font-family: var(--font-mono);
}
-/* Loading */
-.loading-message {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- text-align: center;
+.per-terminal-item .terminal-override {
+ color: var(--text-secondary);
+ font-size: 0.8em;
}
-.spinner {
- width: 40px;
- height: 40px;
- border: 3px solid var(--bg-tertiary);
- border-top-color: var(--accent-primary);
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin: 0 auto var(--space-md);
+.per-terminal-items {
+ margin-top: var(--space-sm);
}
-@keyframes spin {
- to { transform: rotate(360deg); }
+/* Notifications Panel */
+.notifications-panel {
+ position: fixed;
+ top: var(--header-height);
+ right: 0;
+ bottom: 0;
+ width: min(420px, 96vw);
+ background:
+ linear-gradient(180deg, rgba(18, 24, 38, 0.94), rgba(10, 15, 27, 0.94));
+ border-left: 1px solid rgba(147, 197, 253, 0.22);
+ box-shadow: -20px 0 44px rgba(2, 6, 16, 0.58);
+ backdrop-filter: blur(12px);
+ transform: translateX(100%);
+ transition: transform 0.24s ease;
+ z-index: 12040;
+ display: flex;
+ flex-direction: column;
}
-@keyframes pulse {
- from { opacity: 0.5; }
- to { opacity: 1; }
+.notifications-panel:not(.hidden) {
+ transform: translateX(0);
}
-/* Toast Notifications */
-.ready-toast {
- position: fixed;
- top: calc(var(--header-height) + 20px);
- right: 20px;
- background: var(--accent-success);
- color: white;
- padding: var(--space-sm) var(--space-md);
- border-radius: var(--radius-md);
- box-shadow: var(--shadow-soft);
- z-index: 1000;
- animation: slideInRight 0.3s ease-out, fadeOutRight 0.3s ease-in 2.7s forwards;
+.notifications-panel .panel-header {
+ padding: 14px 16px;
+ border-bottom: 1px solid rgba(147, 197, 253, 0.2);
+ background:
+ linear-gradient(180deg, rgba(35, 51, 74, 0.64), rgba(26, 38, 58, 0.38));
}
-.toast-content {
- display: flex;
- align-items: center;
- gap: var(--space-sm);
+.notifications-panel .panel-header h3 {
+ margin: 0;
+ font-size: 1rem;
+ font-weight: 640;
+ letter-spacing: 0.01em;
+ color: #f4f8ff;
}
-.toast-icon {
- font-size: 1.2rem;
+.notifications-panel .panel-actions .icon-button {
+ width: 28px;
+ height: 28px;
+ border-radius: 8px;
+ border: 1px solid rgba(147, 197, 253, 0.24);
+ background: rgba(7, 12, 22, 0.56);
+ color: rgba(244, 248, 255, 0.9);
}
-.toast-text {
- font-weight: 500;
- font-size: 0.875rem;
+.notifications-panel .panel-actions .icon-button:hover {
+ border-color: rgba(147, 197, 253, 0.44);
+ background: rgba(29, 78, 216, 0.2);
}
-@keyframes slideInRight {
- from {
- transform: translateX(100%);
- opacity: 0;
- }
- to {
- transform: translateX(0);
- opacity: 1;
- }
+.notifications-content {
+ padding: 12px 14px 18px;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ flex: 1;
}
-@keyframes fadeOutRight {
- from {
- transform: translateX(0);
- opacity: 1;
- }
- to {
- transform: translateX(100%);
- opacity: 0;
- }
+.notification-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
}
-/* Responsive - ONLY apply if data-visible-count is not set */
-@media (max-width: 1200px) {
- .terminal-grid:not([data-visible-count]) {
- grid-template-columns: repeat(2, 1fr);
- }
+.notifications-panel .empty-message {
+ border: 1px dashed rgba(147, 197, 253, 0.24);
+ border-radius: 12px;
+ background: rgba(12, 19, 33, 0.62);
+ color: rgba(223, 231, 245, 0.76);
}
-@media (max-width: 768px) {
- .projects-chats-shell {
- grid-template-columns: 1fr;
- min-height: 70vh;
- }
+.notifications-panel .notification-item {
+ border: 1px solid rgba(147, 197, 253, 0.2);
+ border-radius: 14px;
+ background:
+ linear-gradient(180deg, rgba(17, 25, 40, 0.92), rgba(10, 16, 28, 0.92));
+ padding: 12px 12px 10px;
+ cursor: pointer;
+ transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
+ box-shadow: 0 8px 18px rgba(2, 6, 16, 0.34);
+}
- .sidebar-toggle {
- display: inline-flex;
- }
+.notifications-panel .notification-item:hover {
+ transform: translateY(-1px);
+ border-color: rgba(96, 165, 250, 0.5);
+ box-shadow: 0 12px 24px rgba(9, 30, 66, 0.42);
+}
- header {
- padding: 0 var(--space-md);
- }
+.notifications-panel .notification-item.unread {
+ border-left: 3px solid #60a5fa;
+ box-shadow: 0 0 0 1px rgba(96, 165, 250, 0.22), 0 12px 24px rgba(9, 30, 66, 0.38);
+}
- .header-content {
- gap: var(--space-md);
- }
+.notifications-panel .notification-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 7px;
+ gap: 8px;
+}
+
+.notifications-panel .notification-time {
+ font-size: 0.75rem;
+ letter-spacing: 0.01em;
+ color: rgba(223, 231, 245, 0.66);
+}
+
+.notifications-panel .notification-type {
+ font-size: 0.69rem;
+ font-weight: 620;
+ letter-spacing: 0.055em;
+ text-transform: uppercase;
+ padding: 3px 8px;
+ border-radius: 999px;
+ border: 1px solid rgba(148, 163, 184, 0.3);
+ background: rgba(15, 23, 42, 0.65);
+ color: rgba(226, 232, 240, 0.9);
+}
+
+.notifications-panel .notification-type.waiting {
+ border-color: rgba(245, 158, 11, 0.45);
+ background: rgba(245, 158, 11, 0.14);
+ color: #fcd34d;
+}
+
+.notifications-panel .notification-type.completed {
+ border-color: rgba(34, 197, 94, 0.45);
+ background: rgba(34, 197, 94, 0.14);
+ color: #86efac;
+}
+
+.notifications-panel .notification-type.error {
+ border-color: rgba(239, 68, 68, 0.45);
+ background: rgba(239, 68, 68, 0.12);
+ color: #fca5a5;
+}
+
+.notifications-panel .notification-message {
+ color: rgba(239, 245, 255, 0.94);
+ line-height: 1.42;
+ font-size: 0.89rem;
+ margin: 0;
+}
+
+.notifications-panel .notification-meta {
+ font-size: 0.75rem;
+ color: rgba(190, 206, 230, 0.72);
+ margin-top: 6px;
+}
+
+.notifications-panel .notification-actions {
+ display: flex;
+ gap: 8px;
+ margin-top: 10px;
+ justify-content: flex-end;
+}
+
+.notifications-panel .notification-action {
+ min-width: 0;
+ width: auto;
+ border-radius: 8px;
+ border: 1px solid rgba(147, 197, 253, 0.24);
+ background: rgba(13, 20, 35, 0.6);
+ color: rgba(239, 245, 255, 0.9);
+ padding: 4px 10px;
+ font-size: 0.78rem;
+}
+
+.notifications-panel .notification-action:hover {
+ border-color: rgba(147, 197, 253, 0.45);
+ background: rgba(37, 99, 235, 0.24);
+}
+
+.terminal-controls {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+}
+
+.terminal-controls label {
+ font-size: 0.9em;
+ margin: 0;
+}
+
+/* Loading spinner for build button */
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* Server Launch Settings */
+.server-launch-group {
+ display: inline-flex;
+ gap: 4px;
+ align-items: center;
+}
+
+#launch-settings-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: var(--overlay-backdrop);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 2000;
+}
+
+#launch-settings-modal .modal-content {
+ background: var(--bg-primary);
+ border-radius: var(--radius-lg);
+ max-width: 800px;
+ width: 90%;
+ max-height: 90vh;
+ overflow-y: auto;
+ box-shadow: var(--shadow-modal);
+}
+
+#launch-settings-modal .modal-header {
+ padding: var(--space-lg);
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+#launch-settings-modal .modal-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ color: var(--text-primary);
+}
+
+#launch-settings-modal .close-btn {
+ background: transparent;
+ border: none;
+ color: var(--text-secondary);
+ font-size: 1.5rem;
+ cursor: pointer;
+ padding: 0;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-sm);
+}
+
+#launch-settings-modal .close-btn:hover {
+ background: var(--bg-secondary);
+}
+
+#launch-settings-modal .modal-body {
+ padding: var(--space-lg);
+}
+
+#launch-settings-modal .settings-section {
+ margin-bottom: var(--space-xl);
+}
+
+#launch-settings-modal .settings-section:last-child {
+ margin-bottom: 0;
+}
+
+#launch-settings-modal .settings-section h3 {
+ color: var(--accent-primary);
+ font-size: 1.1rem;
+ margin-bottom: var(--space-md);
+}
+
+#launch-settings-modal .setting-group {
+ margin-bottom: var(--space-md);
+}
+
+#launch-settings-modal .setting-group label {
+ display: block;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ margin-bottom: var(--space-xs);
+}
+
+#launch-settings-modal .setting-group input {
+ width: 100%;
+ padding: var(--space-sm) var(--space-md);
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ font-family: monospace;
+}
+
+#launch-settings-modal .setting-group input:focus {
+ outline: none;
+ border-color: var(--accent-primary);
+}
+
+#launch-settings-modal .setting-group small {
+ display: block;
+ color: var(--text-tertiary);
+ font-size: 0.8rem;
+ margin-top: var(--space-xs);
+}
+
+#launch-settings-modal .preset-checkboxes {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: var(--space-md);
+}
+
+#launch-settings-modal .preset-checkbox {
+ display: flex;
+ align-items: center;
+ padding: var(--space-md);
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ transition: all 0.2s;
+ user-select: none;
+}
+
+#launch-settings-modal .preset-checkbox:hover {
+ background: var(--bg-tertiary);
+ border-color: var(--accent-primary);
+}
+
+#launch-settings-modal .preset-checkbox input[type="checkbox"] {
+ margin-right: var(--space-sm);
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+}
+
+#launch-settings-modal .preset-checkbox input[type="checkbox"]:checked + span {
+ color: var(--accent-primary);
+ font-weight: 600;
+}
+
+#launch-settings-modal .preset-checkbox span {
+ font-size: 0.9rem;
+ transition: all 0.2s;
+}
+
+#launch-settings-modal .modal-footer {
+ padding: var(--space-lg);
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--space-md);
+}
+
+#launch-settings-modal .btn-save,
+#launch-settings-modal .btn-cancel {
+ padding: var(--space-sm) var(--space-lg);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ font-size: 0.9rem;
+ transition: all 0.2s;
+}
+
+#launch-settings-modal .btn-save {
+ background: var(--accent-primary);
+ color: white;
+ border: none;
+}
+
+#launch-settings-modal .btn-save:hover {
+ background: var(--accent-secondary);
+}
+
+#launch-settings-modal .btn-cancel {
+ background: var(--bg-secondary);
+ color: var(--text-secondary);
+ border: 1px solid var(--border-color);
+}
+
+#launch-settings-modal .btn-cancel:hover {
+ background: var(--bg-tertiary);
+}
+
+.loading-spinner {
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ border-top: 2px solid #fff;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+.control-btn.building {
+ background: var(--color-warning);
+ cursor: not-allowed;
+ opacity: 0.8;
+}
+
+.clear-override-btn {
+ background: var(--bg-primary);
+ color: var(--text-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ padding: var(--space-xs);
+ cursor: pointer;
+ font-size: 0.8em;
+ transition: background-color 0.2s;
+}
+
+.clear-override-btn:hover {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+}
+
+.template-actions {
+ display: flex;
+ gap: var(--space-sm);
+ margin-top: var(--space-sm);
+}
+
+.template-btn {
+ padding: var(--space-sm) var(--space-md);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ font-size: 0.9em;
+ transition: all 0.2s;
+}
+
+.template-btn.primary {
+ background: var(--accent-primary);
+ color: white;
+ border-color: var(--accent-primary);
+}
+
+.template-btn.primary:hover {
+ background: var(--accent-primary-hover);
+ border-color: var(--accent-primary-hover);
+}
+
+.template-btn.secondary {
+ background: var(--bg-primary);
+ color: var(--text-secondary);
+ border-color: var(--border-color);
+}
+
+.template-btn.secondary:hover {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+}
+
+.update-notification {
+ margin-top: var(--space-sm);
+ padding: var(--space-sm);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--accent-warning);
+ border-radius: var(--radius-sm);
+ transition: all 0.3s;
+}
+
+.update-notification.hidden {
+ display: none;
+}
+
+.notification-content {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+}
+
+.notification-icon {
+ font-size: 1.1em;
+}
+
+.notification-text {
+ flex: 1;
+ color: var(--text-primary);
+ font-size: 0.9em;
+}
+
+.dismiss-btn {
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ cursor: pointer;
+ font-size: 1.2em;
+ padding: 0;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-sm);
+ transition: all 0.2s;
+}
+
+.dismiss-btn:hover {
+ background: var(--bg-primary);
+ color: var(--text-primary);
+}
+
+/* Loading */
+.loading-message {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ text-align: center;
+}
+
+.spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid var(--bg-tertiary);
+ border-top-color: var(--accent-primary);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin: 0 auto var(--space-md);
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+@keyframes pulse {
+ from { opacity: 0.5; }
+ to { opacity: 1; }
+}
+
+/* Toast Notifications */
+.toast-stack {
+ position: fixed;
+ top: calc(var(--header-height) + 20px);
+ right: 20px;
+ width: min(420px, calc(100vw - 24px));
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ z-index: 12060;
+ pointer-events: none;
+}
+
+.toast,
+.ready-toast {
+ --toast-accent: 96, 165, 250;
+ pointer-events: auto;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+ border-radius: 14px;
+ border: 1px solid rgba(var(--toast-accent), 0.42);
+ background:
+ linear-gradient(180deg, rgba(17, 25, 40, 0.95), rgba(9, 14, 24, 0.95));
+ box-shadow: 0 14px 32px rgba(2, 6, 16, 0.5);
+ color: rgba(244, 248, 255, 0.96);
+ padding: 11px 12px;
+ transform: translateX(20px);
+ opacity: 0;
+ transition: opacity 0.22s ease, transform 0.22s ease;
+}
+
+.toast.is-visible,
+.ready-toast.is-visible {
+ transform: translateX(0);
+ opacity: 1;
+}
+
+.toast.is-leaving,
+.ready-toast.is-leaving {
+ transform: translateX(16px);
+ opacity: 0;
+}
+
+.toast.toast-success,
+.ready-toast {
+ --toast-accent: 74, 222, 128;
+}
+
+.toast.toast-warning {
+ --toast-accent: 251, 191, 36;
+}
+
+.toast.toast-error {
+ --toast-accent: 248, 113, 113;
+}
+
+.toast-content {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ min-width: 0;
+ flex: 1;
+}
+
+.toast-icon {
+ width: 24px;
+ height: 24px;
+ border-radius: 999px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(var(--toast-accent), 0.2);
+ color: rgb(var(--toast-accent));
+ flex: 0 0 auto;
+}
+
+.toast-icon svg {
+ width: 14px;
+ height: 14px;
+ display: block;
+}
+
+.toast-text {
+ font-weight: 500;
+ font-size: 0.88rem;
+ line-height: 1.38;
+ color: rgba(242, 247, 255, 0.95);
+ word-break: break-word;
+}
+
+.toast-close {
+ width: 22px;
+ height: 22px;
+ border-radius: 7px;
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ background: rgba(15, 23, 42, 0.58);
+ color: rgba(230, 237, 249, 0.84);
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.72rem;
+ line-height: 1;
+ padding: 0;
+ flex: 0 0 auto;
+}
+
+.toast-close:hover {
+ background: rgba(51, 65, 85, 0.62);
+ color: rgba(255, 255, 255, 0.97);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+@keyframes slideInRight {
+ from {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+@keyframes fadeOutRight {
+ from {
+ transform: translateX(0);
+ opacity: 1;
+ }
+ to {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+}
+
+/* Responsive - ONLY apply if data-visible-count is not set */
+@media (max-width: 1200px) {
+ .terminal-grid:not([data-visible-count]) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media (max-width: 768px) {
+ .projects-chats-shell {
+ grid-template-columns: 1fr;
+ min-height: 70vh;
+ }
+
+ .sidebar-toggle {
+ display: inline-flex;
+ }
+
+ header {
+ padding: 0 var(--space-md);
+ }
+
+ .header-content {
+ gap: var(--space-md);
+ }
header h1 {
font-size: 1rem;
@@ -7438,25 +8201,26 @@ header h1 {
top: calc(var(--header-height) + 20px);
right: 20px;
width: 300px;
- z-index: 1001;
+ z-index: 12050;
pointer-events: none;
}
.cross-workspace-notification {
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-left: 4px solid var(--accent-warning);
- border-radius: var(--radius-md);
+ background:
+ linear-gradient(180deg, rgba(18, 24, 38, 0.94), rgba(10, 15, 27, 0.94));
+ border: 1px solid rgba(245, 158, 11, 0.28);
+ border-left: 3px solid rgba(245, 158, 11, 0.85);
+ border-radius: 12px;
margin-bottom: var(--space-md);
- box-shadow: var(--shadow-soft);
+ box-shadow: 0 14px 28px rgba(2, 6, 16, 0.42);
pointer-events: auto;
- animation: slideInRight 0.3s ease-out;
+ animation: slideInRight 0.24s ease-out;
}
-.notification-header {
+.cross-workspace-notification .notification-header {
padding: var(--space-sm) var(--space-md);
- background: var(--bg-tertiary);
- border-bottom: 1px solid var(--border-color);
+ background: rgba(245, 158, 11, 0.1);
+ border-bottom: 1px solid rgba(245, 158, 11, 0.24);
display: flex;
justify-content: space-between;
align-items: center;
@@ -7468,7 +8232,7 @@ header h1 {
color: var(--text-primary);
}
-.notification-close {
+.cross-workspace-notification .notification-close {
background: none;
border: none;
color: var(--text-secondary);
@@ -7481,17 +8245,17 @@ header h1 {
justify-content: center;
}
-.notification-body {
+.cross-workspace-notification .notification-body {
padding: var(--space-md);
}
-.notification-message {
+.cross-workspace-notification .notification-message {
font-size: 0.9rem;
color: var(--text-primary);
margin-bottom: var(--space-md);
}
-.notification-actions {
+.cross-workspace-notification .notification-actions {
display: flex;
gap: var(--space-sm);
}
@@ -9260,395 +10024,661 @@ header h1 {
font-size: 0.75rem;
}
-.detail-row-full {
+.detail-row-full {
+ display: flex;
+ gap: var(--space-sm);
+ align-items: flex-start;
+}
+
+.detail-row-full .detail-label {
+ color: var(--text-muted);
+ flex-shrink: 0;
+ min-width: 80px;
+}
+
+.folder-path-full {
+ color: #90cdf4;
+ word-break: break-all;
+ font-family: monospace;
+ font-size: 0.75rem;
+ background: rgba(99, 179, 237, 0.1);
+ padding: 4px 8px;
+ border-radius: 4px;
+}
+
+/* Conversation details grid */
+.conv-details-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: var(--space-xs) var(--space-md);
+ margin: var(--space-sm) 0;
+ padding: var(--space-sm);
+ background: var(--bg-primary);
+ border-radius: var(--radius-xs);
+ font-size: 0.75rem;
+}
+
+.detail-row {
+ display: flex;
+ gap: var(--space-xs);
+ align-items: baseline;
+}
+
+.detail-label {
+ color: var(--text-muted);
+ min-width: 60px;
+ flex-shrink: 0;
+}
+
+.detail-value {
+ color: var(--text-secondary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.detail-value.folder-path {
+ color: #63b3ed;
+ cursor: help;
+}
+
+.detail-value.repo-name {
+ color: #9f7aea;
+}
+
+.detail-value.branch-name {
+ color: #48bb78;
+}
+
+.detail-value.model-name {
+ color: #f6ad55;
+}
+
+.conv-actions {
+ display: flex;
+ gap: var(--space-sm);
+ flex-wrap: wrap;
+ margin-top: var(--space-sm);
+}
+
+.conv-actions .btn-small {
+ padding: 6px 12px;
+ font-size: 0.8rem;
+}
+
+.conv-actions .btn-small.primary {
+ background: var(--accent-primary);
+ color: var(--text-on-accent);
+ border: none;
+}
+
+.conv-actions .btn-small.primary:hover {
+ background: var(--accent-primary-hover);
+}
+
+.conv-actions .btn-small.secondary {
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ color: var(--text-secondary);
+}
+
+.conv-actions .btn-small.secondary:hover {
+ background: var(--bg-tertiary);
+}
+
+.browser-footer {
+ padding: var(--space-sm) var(--space-lg);
+ border-top: 1px solid var(--border-color);
+ font-size: 0.8rem;
+ color: var(--text-muted);
+}
+
+.no-results, .loading, .error {
+ text-align: center;
+ padding: var(--space-xl);
+ color: var(--text-muted);
+}
+
+/* Conversation Details Modal */
+.conversation-details-modal .details-content {
+ max-width: 700px;
+ width: 90vw;
+ max-height: 85vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.details-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-md) var(--space-lg);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.details-meta {
+ padding: var(--space-md) var(--space-lg);
+ background: var(--bg-tertiary);
+ font-size: 0.85rem;
+}
+
+.details-meta p {
+ margin: var(--space-xs) 0;
+}
+
+.details-summary {
+ padding: var(--space-md) var(--space-lg);
+ border-bottom: 1px solid var(--border-color);
+ font-style: italic;
+ color: var(--text-secondary);
+}
+
+.details-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: var(--space-md) var(--space-lg);
+}
+
+.details-messages h4 {
+ margin-bottom: var(--space-md);
+}
+
+.messages-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+}
+
+.messages-list .message {
+ padding: var(--space-sm) var(--space-md);
+ border-radius: var(--radius-sm);
+ font-size: 0.85rem;
+}
+
+.messages-list .message.user {
+ background: var(--bg-tertiary);
+ margin-left: var(--space-lg);
+}
+
+.messages-list .message.assistant {
+ background: var(--accent-primary);
+ background: rgba(59, 130, 246, 0.2);
+ margin-right: var(--space-lg);
+}
+
+.message-header {
+ display: flex;
+ justify-content: space-between;
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ margin-bottom: var(--space-xs);
+}
+
+.message-content {
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.tool-uses {
+ margin-top: var(--space-xs);
+ font-size: 0.7rem;
+ color: var(--text-muted);
+}
+
+.more-messages {
+ text-align: center;
+ padding: var(--space-md);
+ color: var(--text-muted);
+ font-style: italic;
+}
+
+.details-actions {
+ padding: var(--space-md) var(--space-lg);
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: flex-end;
+}
+
+/* ============================================
+ Ports Panel Styles
+ ============================================ */
+
+.ports-modal .ports-content {
+ max-width: 1100px;
+ width: 96vw;
+ max-height: 92vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.ports-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-md) var(--space-lg);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.ports-header h2 {
+ margin: 0;
+ font-size: 1.2rem;
+}
+
+.ports-info {
+ padding: var(--space-sm) var(--space-lg);
+ background: var(--bg-tertiary);
+ font-size: 0.85rem;
+ color: var(--text-muted);
+}
+
+.ports-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: var(--space-lg);
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
+ gap: var(--space-md);
+ align-content: start;
+}
+
+.port-item {
display: flex;
+ flex-direction: column;
gap: var(--space-sm);
- align-items: flex-start;
+ padding: var(--space-md);
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-sm);
+ border-left: 3px solid var(--border-color);
+ transition: all 0.15s;
}
-.detail-row-full .detail-label {
- color: var(--text-muted);
- flex-shrink: 0;
- min-width: 80px;
+.port-item:hover {
+ background: var(--bg-primary);
}
-.folder-path-full {
- color: #90cdf4;
- word-break: break-all;
- font-family: monospace;
- font-size: 0.75rem;
- background: rgba(99, 179, 237, 0.1);
- padding: 4px 8px;
- border-radius: 4px;
+.port-item.orchestrator,
+.port-item.orchestrator-dev {
+ border-left-color: #9f7aea;
}
-/* Conversation details grid */
-.conv-details-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
- gap: var(--space-xs) var(--space-md);
- margin: var(--space-sm) 0;
- padding: var(--space-sm);
- background: var(--bg-primary);
- border-radius: var(--radius-xs);
- font-size: 0.75rem;
+.port-item.client,
+.port-item.client-dev {
+ border-left-color: #63b3ed;
}
-.detail-row {
- display: flex;
- gap: var(--space-xs);
- align-items: baseline;
+.port-item.vite,
+.port-item.react {
+ border-left-color: #48bb78;
}
-.detail-label {
- color: var(--text-muted);
- min-width: 60px;
- flex-shrink: 0;
+.port-item.game-server {
+ border-left-color: #f6ad55;
}
-.detail-value {
- color: var(--text-secondary);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+.port-item.python,
+.port-item.flask {
+ border-left-color: #ffd93d;
}
-.detail-value.folder-path {
- color: #63b3ed;
- cursor: help;
+.port-item.node {
+ border-left-color: #68d391;
}
-.detail-value.repo-name {
- color: #9f7aea;
+.port-icon {
+ font-size: 1.2rem;
+ flex-shrink: 0;
}
-.detail-value.branch-name {
- color: #48bb78;
+.port-card-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: var(--space-md);
+ min-width: 0;
}
-.detail-value.model-name {
- color: #f6ad55;
+.port-main {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-md);
+ min-width: 0;
+ flex: 1;
}
-.conv-actions {
- display: flex;
- gap: var(--space-sm);
- flex-wrap: wrap;
- margin-top: var(--space-sm);
+.port-details {
+ flex: 1;
+ min-width: 0;
}
-.conv-actions .btn-small {
- padding: 6px 12px;
- font-size: 0.8rem;
+.port-name {
+ display: block;
+ font-weight: 500;
+ color: var(--text-primary);
+ font-size: 0.9rem;
}
-.conv-actions .btn-small.primary {
- background: var(--accent-primary);
- color: var(--text-on-accent);
- border: none;
+.port-process {
+ font-size: 0.75rem;
+ color: var(--text-muted);
}
-.conv-actions .btn-small.primary:hover {
- background: var(--accent-primary-hover);
+.port-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: var(--space-xs);
+ flex-wrap: wrap;
}
-.conv-actions .btn-small.secondary {
- background: var(--bg-primary);
+.port-action-btn {
border: 1px solid var(--border-color);
- color: var(--text-secondary);
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ border-radius: var(--radius-xs);
+ padding: 4px 8px;
+ font-size: 0.75rem;
+ cursor: pointer;
+ line-height: 1.1;
+ transition: background 0.15s, border-color 0.15s;
}
-.conv-actions .btn-small.secondary:hover {
- background: var(--bg-tertiary);
+.port-action-btn:hover {
+ background: var(--bg-secondary);
+ border-color: var(--accent-primary);
}
-.browser-footer {
- padding: var(--space-sm) var(--space-lg);
+.ports-footer {
+ padding: var(--space-md) var(--space-lg);
border-top: 1px solid var(--border-color);
- font-size: 0.8rem;
- color: var(--text-muted);
+ display: flex;
+ justify-content: flex-end;
}
-.no-results, .loading, .error {
+.no-ports {
text-align: center;
- padding: var(--space-xl);
+ padding: var(--space-lg);
color: var(--text-muted);
}
-/* Conversation Details Modal */
-.conversation-details-modal .details-content {
- max-width: 700px;
- width: 90vw;
- max-height: 85vh;
+/* ============================================
+ PRs Panel Styles
+ ============================================ */
+
+.prs-modal .prs-content {
+ max-width: 1100px;
+ width: 96vw;
+ max-height: 92vh;
display: flex;
flex-direction: column;
}
-.details-header {
+.prs-toolbar {
+ padding: var(--space-sm) var(--space-lg);
+ border-bottom: 1px solid var(--border-color);
+ background: var(--bg-tertiary);
display: flex;
- justify-content: space-between;
+ gap: var(--space-md);
align-items: center;
- padding: var(--space-md) var(--space-lg);
- border-bottom: 1px solid var(--border-color);
+ flex-wrap: wrap;
}
-.details-meta {
- padding: var(--space-md) var(--space-lg);
- background: var(--bg-tertiary);
- font-size: 0.85rem;
+.prs-toolbar-group {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ flex-wrap: wrap;
}
-.details-meta p {
- margin: var(--space-xs) 0;
+.prs-label {
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
}
-.details-summary {
- padding: var(--space-md) var(--space-lg);
- border-bottom: 1px solid var(--border-color);
- font-style: italic;
- color: var(--text-secondary);
+.prs-search {
+ min-width: 220px;
+ flex: 1;
}
-.details-messages {
+.prs-input {
+ min-width: 220px;
+ flex: 0 1 320px;
+}
+
+.prs-list {
flex: 1;
overflow-y: auto;
- padding: var(--space-md) var(--space-lg);
+ padding: var(--space-lg);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
}
-.details-messages h4 {
- margin-bottom: var(--space-md);
+.pr-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-md);
+ padding: var(--space-md);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-secondary);
}
-.messages-list {
+.pr-main {
display: flex;
flex-direction: column;
- gap: var(--space-sm);
+ gap: 6px;
+ min-width: 0;
+ flex: 1;
}
-.messages-list .message {
- padding: var(--space-sm) var(--space-md);
- border-radius: var(--radius-sm);
- font-size: 0.85rem;
+.pr-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
}
-.messages-list .message.user {
- background: var(--bg-tertiary);
- margin-left: var(--space-lg);
+.pr-repo {
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+ color: var(--text-secondary);
}
-.messages-list .message.assistant {
- background: var(--accent-primary);
- background: rgba(59, 130, 246, 0.2);
- margin-right: var(--space-lg);
+.pr-number {
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+ color: var(--text-secondary);
}
-.message-header {
- display: flex;
- justify-content: space-between;
+.pr-badge {
+ font-family: var(--font-mono);
font-size: 0.75rem;
- color: var(--text-muted);
- margin-bottom: var(--space-xs);
+ padding: 3px 6px;
+ border-radius: 999px;
+ border: 1px solid var(--border-color);
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
}
-.message-content {
- white-space: pre-wrap;
- word-break: break-word;
+.pr-badge.draft {
+ border-color: var(--accent-warning);
}
-.tool-uses {
- margin-top: var(--space-xs);
- font-size: 0.7rem;
- color: var(--text-muted);
+.pr-subtitle {
+ font-weight: 600;
+ color: var(--text-primary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.more-messages {
- text-align: center;
- padding: var(--space-md);
- color: var(--text-muted);
- font-style: italic;
+.pr-meta {
+ font-size: 0.75rem;
+ color: var(--text-tertiary);
}
-.details-actions {
- padding: var(--space-md) var(--space-lg);
- border-top: 1px solid var(--border-color);
+.pr-actions {
display: flex;
- justify-content: flex-end;
+ gap: var(--space-sm);
+ flex-shrink: 0;
}
/* ============================================
- Ports Panel Styles
+ Tasks Panel Styles
============================================ */
-.ports-modal .ports-content {
- max-width: 1100px;
- width: 96vw;
- max-height: 92vh;
- display: flex;
- flex-direction: column;
-}
-
-.ports-header {
+.queue-summary {
display: flex;
- justify-content: space-between;
- align-items: center;
- padding: var(--space-md) var(--space-lg);
- border-bottom: 1px solid var(--border-color);
-}
-
-.ports-header h2 {
- margin: 0;
- font-size: 1.2rem;
-}
-
-.ports-info {
- padding: var(--space-sm) var(--space-lg);
- background: var(--bg-tertiary);
- font-size: 0.85rem;
- color: var(--text-muted);
-}
-
-.ports-list {
- flex: 1;
- overflow-y: auto;
- padding: var(--space-lg);
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
- gap: var(--space-md);
- align-content: start;
+ gap: 8px;
+ flex-wrap: wrap;
+ padding: 0 0 10px 0;
}
-.port-item {
+.tasks-modal .tasks-content {
+ /* Tasks is a primary workflow: prefer a robust, full-viewport panel. */
+ max-width: none;
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 100vw;
+ max-height: 100vh;
+ height: 100%;
display: flex;
flex-direction: column;
- gap: var(--space-sm);
+ overflow: hidden;
+ position: relative;
+ border-radius: 0;
padding: var(--space-md);
- background: var(--bg-tertiary);
- border-radius: var(--radius-sm);
- border-left: 3px solid var(--border-color);
- transition: all 0.15s;
+ --tasks-board-accent: var(--accent-primary);
+ direction: ltr;
+ text-align: left;
}
-.port-item:hover {
- background: var(--bg-primary);
+.tasks-modal .tasks-content.tasks-has-board-accent .tasks-toolbar {
+ border-bottom-color: color-mix(in srgb, var(--border-color) 55%, var(--tasks-board-accent) 45%);
}
-.port-item.orchestrator,
-.port-item.orchestrator-dev {
- border-left-color: #9f7aea;
+.tasks-modal .tasks-content.tasks-has-board-accent .tasks-column-header {
+ background: color-mix(in srgb, var(--bg-tertiary) 86%, var(--tasks-board-accent) 14%);
}
-.port-item.client,
-.port-item.client-dev {
- border-left-color: #63b3ed;
+.tasks-modal .tasks-content.tasks-has-board-accent .tasks-column-header:hover {
+ background: color-mix(in srgb, var(--bg-primary) 86%, var(--tasks-board-accent) 14%);
}
-.port-item.vite,
-.port-item.react {
- border-left-color: #48bb78;
+.tasks-modal .tasks-body {
+ direction: ltr;
+ text-align: left;
}
-.port-item.game-server {
- border-left-color: #f6ad55;
+.tasks-modal {
+ background: transparent;
}
-.port-item.python,
-.port-item.flask {
- border-left-color: #ffd93d;
+.tasks-modal.tasks-theme-light {
+ --bg-primary: #ffffff;
+ --bg-secondary: #f6f8fa;
+ --bg-tertiary: #e6e8eb;
+ --text-primary: #24292f;
+ --text-secondary: #57606a;
+ --text-tertiary: #6e7781;
+ --border-color: #d0d7de;
}
-.port-item.node {
- border-left-color: #68d391;
+.tasks-modal .modal-header {
+ position: sticky;
+ top: 0;
+ z-index: 5;
+ background: var(--bg-secondary);
+ padding-bottom: var(--space-sm);
}
-.port-icon {
- font-size: 1.2rem;
- flex-shrink: 0;
+.tasks-modal .tasks-close-btn {
+ width: 44px;
+ height: 44px;
+ font-size: 1.6rem;
+ color: var(--accent-danger);
+ border: 1px solid rgba(248, 81, 73, 0.35);
+ background: rgba(248, 81, 73, 0.12);
}
-.port-card-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: var(--space-md);
- min-width: 0;
+.tasks-modal .tasks-close-btn:hover {
+ background: rgba(248, 81, 73, 0.2);
+ border-color: rgba(248, 81, 73, 0.55);
+ color: var(--accent-danger);
}
-.port-main {
- display: flex;
- align-items: flex-start;
- gap: var(--space-md);
- min-width: 0;
- flex: 1;
+.tasks-modal .tasks-close-btn:focus-visible {
+ outline: 2px solid var(--accent-danger);
+ outline-offset: 2px;
}
-.port-details {
- flex: 1;
- min-width: 0;
+.tasks-view-toggle {
+ display: flex;
+ gap: 6px;
+ align-items: center;
}
-.port-name {
- display: block;
- font-weight: 500;
- color: var(--text-primary);
- font-size: 0.9rem;
+.tasks-view-btn.active {
+ border-color: var(--accent-primary);
+ box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.15);
}
-.port-process {
- font-size: 0.75rem;
- color: var(--text-muted);
+.tasks-filter {
+ position: relative;
}
-.port-actions {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- gap: var(--space-xs);
- flex-wrap: wrap;
+.tasks-filter summary {
+ list-style: none;
}
-.port-action-btn {
- border: 1px solid var(--border-color);
- background: var(--bg-primary);
- color: var(--text-primary);
- border-radius: var(--radius-xs);
- padding: 4px 8px;
- font-size: 0.75rem;
- cursor: pointer;
- line-height: 1.1;
- transition: background 0.15s, border-color 0.15s;
+.tasks-filter summary::-webkit-details-marker {
+ display: none;
}
-.port-action-btn:hover {
+.tasks-filter-popover {
+ position: absolute;
+ top: calc(100% + 8px);
+ left: 0;
+ min-width: 240px;
+ max-height: 320px;
+ overflow: auto;
+ z-index: 5;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
background: var(--bg-secondary);
- border-color: var(--accent-primary);
+ box-shadow: 0 10px 24px rgba(0,0,0,0.12);
+ padding: 10px;
}
-.ports-footer {
- padding: var(--space-md) var(--space-lg);
- border-top: 1px solid var(--border-color);
+.tasks-filter-actions {
display: flex;
- justify-content: flex-end;
+ gap: 8px;
+ margin-bottom: 10px;
}
-.no-ports {
- text-align: center;
- padding: var(--space-lg);
- color: var(--text-muted);
+.tasks-filter-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
}
-/* ============================================
- PRs Panel Styles
- ============================================ */
-
-.prs-modal .prs-content {
- max-width: 1100px;
- width: 96vw;
- max-height: 92vh;
+.tasks-filter-item {
display: flex;
- flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ font-size: 0.85rem;
+ color: var(--text-primary);
+}
+
+.tasks-filter-item input {
+ accent-color: var(--accent-primary);
}
-.prs-toolbar {
+.tasks-toolbar {
padding: var(--space-sm) var(--space-lg);
border-bottom: 1px solid var(--border-color);
background: var(--bg-tertiary);
@@ -9658,201 +10688,255 @@ header h1 {
flex-wrap: wrap;
}
-.prs-toolbar-group {
- display: flex;
- gap: 8px;
+.tasks-launch-defaults {
+ display: inline-flex;
align-items: center;
- flex-wrap: wrap;
+ gap: 6px;
+ padding: 4px 8px;
+ border: 1px solid var(--border-color);
+ border-radius: 999px;
+ background: var(--bg-secondary);
}
-.prs-label {
- font-size: 0.75rem;
- color: var(--text-secondary);
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.04em;
+.tasks-launch-default-tier-group {
+ padding: 0;
+ border: none;
+ background: transparent;
}
-.prs-search {
- min-width: 220px;
- flex: 1;
+.tasks-launch-default-agent-group {
+ padding: 0;
+ border: none;
+ background: transparent;
}
-.prs-input {
- min-width: 220px;
- flex: 0 1 320px;
+.tasks-launch-default-mode-group {
+ padding: 0;
+ border: none;
+ background: transparent;
}
-.prs-list {
- flex: 1;
- overflow-y: auto;
- padding: var(--space-lg);
- display: flex;
- flex-direction: column;
- gap: var(--space-sm);
+.tasks-launch-defaults-label {
+ font-weight: 900;
+ font-size: 0.85rem;
+ color: var(--text-secondary);
}
-.pr-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--space-md);
- padding: var(--space-md);
+.tasks-toggle.tasks-toggle-mini {
+ font-size: 0.75rem;
+ gap: 6px;
+}
+
+.tasks-toggle.tasks-toggle-mini span {
+ opacity: 0.9;
+}
+
+.tasks-board-accent {
+ width: 12px;
+ height: 12px;
+ border-radius: 999px;
+ background: var(--tasks-board-accent);
border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
+ box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.12);
}
-.pr-main {
- display: flex;
- flex-direction: column;
- gap: 6px;
- min-width: 0;
- flex: 1;
+.tasks-board-accent.is-hidden {
+ display: none;
}
-.pr-title {
- display: flex;
+.tasks-board-picker {
+ position: relative;
+ display: inline-flex;
align-items: center;
gap: 8px;
- flex-wrap: wrap;
}
-.pr-repo {
- font-family: var(--font-mono);
- font-size: 0.8rem;
- color: var(--text-secondary);
+.tasks-board-btn {
+ min-width: 200px;
+ text-align: left;
+ justify-content: flex-start;
}
-.pr-number {
- font-family: var(--font-mono);
- font-size: 0.8rem;
- color: var(--text-secondary);
+.tasks-select-hidden {
+ position: absolute;
+ left: -9999px;
+ width: 1px;
+ height: 1px;
+ opacity: 0;
+ pointer-events: none;
}
-.pr-badge {
- font-family: var(--font-mono);
- font-size: 0.75rem;
- padding: 3px 6px;
- border-radius: 999px;
- border: 1px solid var(--border-color);
+.tasks-board-menu {
+ position: absolute;
+ top: calc(100% + 6px);
+ left: 0;
+ z-index: 10;
+ min-width: 320px;
+ max-width: 420px;
+ max-height: 60vh;
+ overflow: auto;
background: var(--bg-tertiary);
- color: var(--text-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ box-shadow: 0 14px 32px rgba(0, 0, 0, 0.35);
+ padding: 6px;
}
-.pr-badge.draft {
- border-color: var(--accent-warning);
+.tasks-board-menu-search-wrap {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ background: var(--bg-tertiary);
+ padding: 6px;
+ border-bottom: 1px solid var(--border-color);
+ margin: -6px -6px 6px;
}
-.pr-subtitle {
- font-weight: 600;
+.tasks-board-menu-search {
+ width: 100%;
+ background: var(--bg-secondary);
color: var(--text-primary);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 8px 10px;
+ font-size: 0.85rem;
}
-.pr-meta {
- font-size: 0.75rem;
- color: var(--text-tertiary);
+.tasks-board-menu-search:focus-visible {
+ outline: 2px solid var(--accent-primary);
+ outline-offset: 2px;
}
-.pr-actions {
+.tasks-board-menu.hidden {
+ display: none;
+}
+
+.tasks-board-menu-item {
+ width: 100%;
display: flex;
- gap: var(--space-sm);
- flex-shrink: 0;
+ align-items: center;
+ gap: 10px;
+ background: transparent;
+ border: 1px solid transparent;
+ color: var(--text-primary);
+ border-radius: var(--radius-md);
+ padding: 8px 10px;
+ cursor: pointer;
+ text-align: left;
}
-/* ============================================
- Tasks Panel Styles
- ============================================ */
+.tasks-board-menu-item:hover {
+ background: rgba(31, 111, 235, 0.12);
+ border-color: rgba(31, 111, 235, 0.25);
+}
-.queue-summary {
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
- padding: 0 0 10px 0;
+.tasks-board-menu-item.is-active {
+ border-color: rgba(31, 111, 235, 0.5);
}
-.tasks-modal .tasks-content {
- /* Tasks is a primary workflow: prefer a robust, full-viewport panel. */
- max-width: none;
- box-sizing: border-box;
- width: 100%;
- max-width: 100vw;
- max-height: 100vh;
- height: 100%;
- display: flex;
- flex-direction: column;
+.tasks-board-menu-item.is-selected {
+ background: rgba(31, 111, 235, 0.18);
+ border-color: rgba(31, 111, 235, 0.35);
+}
+
+.tasks-board-menu-dot {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
+ border: 1px solid var(--border-color);
+ flex: 0 0 auto;
+}
+
+.tasks-board-menu-dot.is-hidden {
+ visibility: hidden;
+}
+
+.tasks-board-menu-label {
overflow: hidden;
- position: relative;
- border-radius: 0;
- padding: var(--space-md);
- --tasks-board-accent: var(--accent-primary);
- direction: ltr;
- text-align: left;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.tasks-modal .tasks-content.tasks-has-board-accent .tasks-toolbar {
- border-bottom-color: color-mix(in srgb, var(--border-color) 55%, var(--tasks-board-accent) 45%);
+.tasks-board-menu-empty {
+ padding: 10px 12px;
+ color: var(--text-tertiary);
+ font-size: 0.85rem;
}
-.tasks-modal .tasks-content.tasks-has-board-accent .tasks-column-header {
- background: color-mix(in srgb, var(--bg-tertiary) 86%, var(--tasks-board-accent) 14%);
+.tasks-hotkeys-overlay {
+ position: absolute;
+ inset: 0;
+ background: var(--overlay-backdrop);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 16px;
+ z-index: 20;
}
-.tasks-modal .tasks-content.tasks-has-board-accent .tasks-column-header:hover {
- background: color-mix(in srgb, var(--bg-primary) 86%, var(--tasks-board-accent) 14%);
+.tasks-launch-popover-overlay {
+ position: absolute;
+ inset: 0;
+ background: var(--overlay-backdrop-faint);
+ z-index: 19;
+}
+
+.tasks-launch-popover {
+ position: absolute;
+ width: min(460px, 92vw);
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-float);
+ padding: 12px;
}
-.tasks-modal .tasks-body {
- direction: ltr;
- text-align: left;
+.tasks-launch-popover-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 8px;
}
-.tasks-modal {
- background: transparent;
+.tasks-launch-popover-title {
+ font-weight: 800;
+ color: var(--text-primary);
}
-.tasks-modal.tasks-theme-light {
- --bg-primary: #ffffff;
- --bg-secondary: #f6f8fa;
- --bg-tertiary: #e6e8eb;
- --text-primary: #24292f;
- --text-secondary: #57606a;
- --text-tertiary: #6e7781;
- --border-color: #d0d7de;
+.tasks-launch-popover-meta {
+ font-size: 0.8rem;
+ color: var(--text-tertiary);
+ margin-bottom: 10px;
}
-.tasks-modal .modal-header {
- position: sticky;
- top: 0;
- z-index: 5;
+.tasks-launch-popover-warn {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 10px 12px;
background: var(--bg-secondary);
- padding-bottom: var(--space-sm);
-}
-
-.tasks-modal .tasks-close-btn {
- width: 44px;
- height: 44px;
- font-size: 1.6rem;
- color: var(--accent-danger);
- border: 1px solid rgba(248, 81, 73, 0.35);
- background: rgba(248, 81, 73, 0.12);
+ margin-bottom: 10px;
}
-.tasks-modal .tasks-close-btn:hover {
- background: rgba(248, 81, 73, 0.2);
- border-color: rgba(248, 81, 73, 0.55);
- color: var(--accent-danger);
+.tasks-launch-popover-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 10px;
+ align-items: end;
}
-.tasks-modal .tasks-close-btn:focus-visible {
- outline: 2px solid var(--accent-danger);
- outline-offset: 2px;
+.tasks-launch-popover-field {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ font-size: 0.8rem;
+ color: var(--text-secondary);
}
-.tasks-view-toggle {
+.tasks-launch-popover-actions {
display: flex;
gap: 6px;
align-items: center;
@@ -9860,9 +10944,15 @@ header h1 {
max-width: 100%;
}
-.tasks-view-btn.active {
- border-color: var(--accent-primary);
- box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.15);
+.tasks-hotkeys-card {
+ width: min(860px, 96vw);
+ max-height: min(80vh, 760px);
+ overflow: auto;
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
+ padding: 14px;
}
.tasks-view-btn {
@@ -9873,50 +10963,58 @@ header h1 {
position: relative;
}
-.tasks-filter summary {
- list-style: none;
+.tasks-hotkeys-title {
+ font-weight: 800;
+ font-size: 1rem;
+ color: var(--text-primary);
}
-.tasks-filter summary::-webkit-details-marker {
- display: none;
+.tasks-hotkeys-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 12px;
}
-.tasks-filter-popover {
- position: absolute;
- top: calc(100% + 8px);
- left: 0;
- min-width: 240px;
- max-height: 320px;
- overflow: auto;
- z-index: 5;
+.tasks-hotkeys-group {
border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
background: var(--bg-secondary);
- box-shadow: 0 10px 24px rgba(0,0,0,0.12);
+ border-radius: var(--radius-md);
padding: 10px;
}
-.tasks-filter-actions {
- display: flex;
- gap: 8px;
- margin-bottom: 10px;
+.tasks-hotkeys-group-title {
+ font-weight: 800;
+ font-size: 0.85rem;
+ color: var(--text-primary);
+ margin-bottom: 8px;
}
-.tasks-filter-list {
- display: flex;
- flex-direction: column;
- gap: 6px;
+.tasks-hotkeys-row {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ line-height: 1.3;
+ margin-bottom: 6px;
}
-.tasks-filter-item {
- display: flex;
+.tasks-hotkeys-row code {
+ font-size: 0.8rem;
+ background: rgba(31, 111, 235, 0.12);
+ border: 1px solid rgba(31, 111, 235, 0.2);
+ border-radius: 6px;
+ padding: 2px 6px;
+ color: var(--text-primary);
+}
+
+.tasks-toggle {
+ display: inline-flex;
align-items: center;
gap: 8px;
- font-size: 0.85rem;
- color: var(--text-primary);
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ user-select: none;
}
-.tasks-filter-item input {
+.tasks-toggle input {
accent-color: var(--accent-primary);
}
@@ -9927,7 +11025,11 @@ header h1 {
display: flex;
gap: var(--space-sm);
align-items: center;
- flex-wrap: wrap;
+ gap: 6px;
+ padding: 4px 6px;
+ border: 1px solid var(--border-color);
+ border-radius: 999px;
+ background: var(--bg-secondary);
}
#queue-panel .tasks-toolbar {
@@ -9999,389 +11101,460 @@ header h1 {
align-items: center;
gap: 6px;
padding: 4px 8px;
- border: 1px solid var(--border-color);
border-radius: 999px;
- background: var(--bg-secondary);
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ cursor: pointer;
+ user-select: none;
}
-.tasks-launch-default-tier-group {
- padding: 0;
- border: none;
- background: transparent;
+.tasks-radio-option input {
+ margin: 0;
+ accent-color: var(--accent-primary);
}
-.tasks-launch-default-agent-group {
- padding: 0;
- border: none;
- background: transparent;
+.tasks-radio-option:has(input:checked) {
+ background: rgba(31, 111, 235, 0.15);
+ color: var(--text-primary);
}
-.tasks-launch-default-mode-group {
- padding: 0;
- border: none;
- background: transparent;
+.tasks-radio-option:has(input:focus-visible) {
+ outline: 2px solid var(--accent-primary);
+ outline-offset: 2px;
}
-.tasks-launch-defaults-label {
- font-weight: 900;
+.tasks-select {
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 6px 10px;
font-size: 0.85rem;
- color: var(--text-secondary);
+ min-width: 160px;
}
-.tasks-toggle.tasks-toggle-mini {
- font-size: 0.75rem;
- gap: 6px;
+.tasks-search {
+ min-width: 220px;
+ flex: 1;
}
-.tasks-toggle.tasks-toggle-mini span {
- opacity: 0.9;
+.tasks-body {
+ flex: 1;
+ min-height: 0;
+ display: grid;
+ grid-template-columns: 420px 1fr;
+ grid-template-rows: 1fr;
+ grid-template-areas: "cards detail";
}
-.tasks-board-accent {
- width: 12px;
- height: 12px;
- border-radius: 999px;
- background: var(--tasks-board-accent);
- border: 1px solid var(--border-color);
- box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.12);
+.tasks-body.tasks-body-board {
+ /* Board view: show full-width kanban until a card is selected. */
+ grid-template-columns: 1fr;
+ grid-template-areas: "cards";
+ position: relative;
+ overflow: hidden;
+}
+
+.tasks-body.tasks-body-board .tasks-detail {
+ display: none;
+}
+
+.tasks-body.tasks-body-board .tasks-cards {
+ grid-column: 1;
+ border-left: none;
+}
+
+.tasks-body.tasks-body-board.tasks-kanban-wrap .tasks-cards {
+ overflow: auto;
+}
+
+.tasks-body.tasks-body-board.tasks-has-detail {
+ /* When a card is selected, show details as an overlay on the right. */
+ grid-template-columns: 1fr;
+}
+
+.tasks-body.tasks-body-board.tasks-has-detail .tasks-detail {
+ display: block;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 520px;
+ max-width: min(520px, 65vw);
+ border-left: 1px solid var(--border-color);
+ box-shadow: -10px 0 24px rgba(0, 0, 0, 0.25);
+ z-index: 5;
+}
+
+.tasks-body.tasks-body-board.tasks-has-detail .tasks-cards {
+ grid-column: 1;
+ border-left: none;
+}
+
+.tasks-cards {
+ grid-area: cards;
+ grid-column: 1;
+ grid-row: 1;
+ overflow-y: auto;
+ padding: var(--space-md);
+ border-right: 1px solid var(--border-color);
+ background: var(--bg-secondary);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+ min-width: 0;
}
-.tasks-board-accent.is-hidden {
- display: none;
+.tasks-body.tasks-body-board .tasks-cards {
+ border-right: none;
+ overflow: hidden;
+ padding: var(--space-sm);
+ background: var(--bg-primary);
}
-.tasks-board-picker {
- position: relative;
- display: inline-flex;
- align-items: center;
- gap: 8px;
+.tasks-board {
+ height: 100%;
+ display: flex;
+ gap: var(--space-md);
+ justify-content: flex-start;
+ overflow-x: auto;
+ overflow-y: hidden;
+ padding-bottom: var(--space-sm);
+ scroll-snap-type: x mandatory;
}
-.tasks-board-btn {
- min-width: 200px;
- text-align: left;
- justify-content: flex-start;
+.tasks-board.tasks-board-wrap {
+ flex-wrap: wrap;
+ overflow-x: hidden;
+ overflow-y: auto;
+ align-content: flex-start;
+ scroll-snap-type: none;
}
-.tasks-select-hidden {
- position: absolute;
- left: -9999px;
- width: 1px;
- height: 1px;
- opacity: 0;
- pointer-events: none;
+.tasks-board.tasks-board-wrap.tasks-board-grid .tasks-column-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+ align-content: start;
}
-.tasks-board-menu {
- position: absolute;
- top: calc(100% + 6px);
- left: 0;
- z-index: 10;
- min-width: 320px;
- max-width: 420px;
- max-height: 60vh;
- overflow: auto;
- background: var(--bg-tertiary);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- box-shadow: 0 14px 32px rgba(0, 0, 0, 0.35);
- padding: 6px;
+.tasks-board.tasks-board-expand .tasks-column-cards {
+ display: grid;
+ grid-auto-flow: column;
+ grid-template-rows: repeat(var(--tasks-card-rows, 1), min-content);
+ grid-template-columns: repeat(var(--tasks-card-columns, 1), minmax(180px, 1fr));
+ align-content: start;
+ gap: var(--space-sm);
+ overflow: hidden;
+ flex: 1;
+ min-height: 0;
}
-.tasks-board-menu-search-wrap {
- position: sticky;
- top: 0;
- z-index: 1;
- background: var(--bg-tertiary);
- padding: 6px;
- border-bottom: 1px solid var(--border-color);
- margin: -6px -6px 6px;
+.tasks-board.tasks-board-grid .task-card-board {
+ height: fit-content;
}
-.tasks-board-menu-search {
- width: 100%;
+.tasks-column {
+ --tasks-col-expanded: clamp(240px, 22vw, 360px);
+ --tasks-col-collapsed: 56px;
+ --tasks-card-columns: 1;
+ --tasks-card-rows: 1;
+ width: var(--tasks-col-expanded);
+ min-width: var(--tasks-col-expanded);
background: var(--bg-secondary);
- color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
- padding: 8px 10px;
- font-size: 0.85rem;
-}
-
-.tasks-board-menu-search:focus-visible {
- outline: 2px solid var(--accent-primary);
- outline-offset: 2px;
+ display: flex;
+ flex-direction: column;
+ max-height: 100%;
+ scroll-snap-align: start;
+ position: relative;
}
-.tasks-board-menu.hidden {
- display: none;
+.tasks-column.hover {
+ border-color: var(--tasks-board-accent);
+ box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.15);
}
-.tasks-board-menu-item {
- width: 100%;
+.tasks-column-header {
+ padding: var(--space-sm) var(--space-md);
display: flex;
- align-items: center;
- gap: 10px;
- background: transparent;
- border: 1px solid transparent;
- color: var(--text-primary);
- border-radius: var(--radius-md);
- padding: 8px 10px;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: var(--space-sm);
+ width: 100%;
+ background: var(--bg-tertiary);
+ border: none;
+ border-bottom: 1px solid var(--border-color);
cursor: pointer;
text-align: left;
+ color: var(--text-primary);
}
-.tasks-board-menu-item:hover {
- background: rgba(31, 111, 235, 0.12);
- border-color: rgba(31, 111, 235, 0.25);
-}
-
-.tasks-board-menu-item.is-active {
- border-color: rgba(31, 111, 235, 0.5);
-}
-
-.tasks-board-menu-item.is-selected {
- background: rgba(31, 111, 235, 0.18);
- border-color: rgba(31, 111, 235, 0.35);
-}
-
-.tasks-board-menu-dot {
- display: inline-block;
- width: 10px;
- height: 10px;
- border-radius: 999px;
- border: 1px solid var(--border-color);
- flex: 0 0 auto;
+.tasks-column-header:hover {
+ background: var(--bg-primary);
}
-.tasks-board-menu-dot.is-hidden {
- visibility: hidden;
+.tasks-column-header:focus-visible {
+ outline: 2px solid var(--accent-primary);
+ outline-offset: 2px;
}
-.tasks-board-menu-label {
+.tasks-column-title {
+ font-weight: 700;
+ font-size: 0.9rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+ color: var(--text-primary);
}
-.tasks-board-menu-empty {
- padding: 10px 12px;
+.tasks-column-count {
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
color: var(--text-tertiary);
- font-size: 0.85rem;
}
-.tasks-hotkeys-overlay {
- position: absolute;
- inset: 0;
- background: var(--overlay-backdrop);
+.tasks-column-cards {
+ overflow-y: auto;
+ padding: var(--space-sm);
display: flex;
- align-items: center;
- justify-content: center;
- padding: 16px;
- z-index: 20;
-}
-
-.tasks-launch-popover-overlay {
- position: absolute;
- inset: 0;
- background: var(--overlay-backdrop-faint);
- z-index: 19;
+ flex-direction: column;
+ gap: var(--space-sm);
}
-.tasks-launch-popover {
- position: absolute;
- width: min(460px, 92vw);
- background: var(--bg-primary);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- box-shadow: var(--shadow-float);
- padding: 12px;
+.tasks-column.is-collapsed {
+ width: var(--tasks-col-collapsed);
+ min-width: var(--tasks-col-collapsed);
}
-.tasks-launch-popover-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 8px;
+.tasks-column.is-collapsed .tasks-column-cards {
+ display: none;
}
-.tasks-launch-popover-title {
+.tasks-column.is-collapsed .tasks-column-title {
+ writing-mode: vertical-rl;
+ transform: rotate(180deg);
+ white-space: nowrap;
+ overflow: visible;
+ text-overflow: unset;
+ font-size: 0.9rem;
font-weight: 800;
- color: var(--text-primary);
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
}
-.tasks-launch-popover-meta {
- font-size: 0.8rem;
- color: var(--text-tertiary);
- margin-bottom: 10px;
+.tasks-column.is-collapsed .tasks-column-header {
+ justify-content: flex-start;
+ align-items: center;
+ flex-direction: column;
+ height: 100%;
+ padding: 10px 8px;
+ gap: 14px;
}
-.tasks-launch-popover-warn {
- font-size: 0.85rem;
- color: var(--text-secondary);
+.tasks-column.is-collapsed .tasks-column-count {
+ display: inline-flex;
+ min-width: 30px;
+ height: 30px;
+ padding: 0 10px;
+ border-radius: 999px;
border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- padding: 10px 12px;
background: var(--bg-secondary);
- margin-bottom: 10px;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+ color: var(--text-primary);
+ order: -1;
}
-.tasks-launch-popover-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 10px;
- align-items: end;
+.task-card-top {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 6px;
}
-.tasks-launch-popover-field {
- display: flex;
- flex-direction: column;
- gap: 6px;
- font-size: 0.8rem;
- color: var(--text-secondary);
+.task-card-top-right {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
}
-.tasks-launch-popover-actions {
- display: flex;
- gap: 10px;
- justify-content: flex-end;
- margin-top: 12px;
+.task-card-quick-actions {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
}
-.tasks-hotkeys-card {
- width: min(860px, 96vw);
- max-height: min(80vh, 760px);
- overflow: auto;
- background: var(--bg-primary);
+.tasks-quick-tier-group {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px;
border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
- padding: 14px;
+ border-radius: 999px;
+ background: var(--bg-secondary);
}
-.tasks-hotkeys-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 12px;
+.tasks-quick-tier-btn {
+ padding: 3px 7px;
+ font-size: 0.72rem;
+ font-weight: 900;
+ line-height: 1;
+ border-radius: 999px;
}
-.tasks-hotkeys-title {
- font-weight: 800;
- font-size: 1rem;
+.tasks-quick-tier-btn.is-selected {
+ border-color: var(--accent-primary);
+ background: rgba(31, 111, 235, 0.18);
color: var(--text-primary);
}
-.tasks-hotkeys-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
- gap: 12px;
+.tasks-select.tasks-select-mini {
+ min-width: 64px;
+ padding: 4px 6px;
+ font-size: 0.75rem;
+ font-weight: 800;
}
-.tasks-hotkeys-group {
+.tasks-quick-launch-btn {
+ padding: 4px 8px;
+ font-size: 0.8rem;
+ line-height: 1;
+}
+
+.task-card-labels {
+ display: inline-flex;
+ gap: 6px;
+ flex-wrap: nowrap;
+ overflow: hidden;
+ min-width: 0;
+}
+
+.tasks-label {
+ display: inline-flex;
+ align-items: center;
+ padding: 2px 6px;
+ border-radius: 999px;
+ font-size: 0.7rem;
+ line-height: 1.2;
border: 1px solid var(--border-color);
background: var(--bg-secondary);
- border-radius: var(--radius-md);
- padding: 10px;
+ color: var(--text-secondary);
+ max-width: 120px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.tasks-hotkeys-group-title {
- font-weight: 800;
- font-size: 0.85rem;
- color: var(--text-primary);
- margin-bottom: 8px;
+.tasks-label-editor {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
}
-.tasks-hotkeys-row {
- font-size: 0.85rem;
- color: var(--text-secondary);
- line-height: 1.3;
- margin-bottom: 6px;
+.tasks-label-toggle {
+ appearance: none;
+ cursor: pointer;
}
-.tasks-hotkeys-row code {
- font-size: 0.8rem;
- background: rgba(31, 111, 235, 0.12);
- border: 1px solid rgba(31, 111, 235, 0.2);
- border-radius: 6px;
- padding: 2px 6px;
- color: var(--text-primary);
+.tasks-label-toggle.is-selected {
+ box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.25);
}
-.tasks-toggle {
+.tasks-checkbox {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.8rem;
color: var(--text-secondary);
- user-select: none;
}
-.tasks-toggle input {
+.tasks-checkbox input {
+ margin: 0;
accent-color: var(--accent-primary);
}
-.tasks-radio {
+.tasks-label--green { border-color: #2ea043; background: rgba(46, 160, 67, 0.12); color: var(--text-primary); }
+.tasks-label--yellow { border-color: #d29922; background: rgba(210, 153, 34, 0.14); color: var(--text-primary); }
+.tasks-label--orange { border-color: #f78166; background: rgba(247, 129, 102, 0.14); color: var(--text-primary); }
+.tasks-label--red { border-color: #f85149; background: rgba(248, 81, 73, 0.14); color: var(--text-primary); }
+.tasks-label--purple { border-color: #a371f7; background: rgba(163, 113, 247, 0.14); color: var(--text-primary); }
+.tasks-label--blue { border-color: #1f6feb; background: rgba(31, 111, 235, 0.14); color: var(--text-primary); }
+.tasks-label--sky { border-color: #79c0ff; background: rgba(121, 192, 255, 0.14); color: var(--text-primary); }
+.tasks-label--lime { border-color: #7ee787; background: rgba(126, 231, 135, 0.14); color: var(--text-primary); }
+.tasks-label--pink { border-color: #ff80c8; background: rgba(255, 128, 200, 0.14); color: var(--text-primary); }
+.tasks-label--black { border-color: #30363d; background: rgba(48, 54, 61, 0.6); color: var(--text-primary); }
+.tasks-label--more { border-color: var(--border-color); background: var(--bg-tertiary); color: var(--text-primary); font-family: var(--font-mono); }
+
+.task-card-assignees {
display: inline-flex;
- align-items: center;
gap: 6px;
- padding: 4px 6px;
- border: 1px solid var(--border-color);
- border-radius: 999px;
- background: var(--bg-secondary);
+ flex-shrink: 0;
}
-.tasks-radio-option {
+.tasks-avatar {
+ width: 22px;
+ height: 22px;
+ border-radius: 999px;
+ border: 1px solid var(--border-color);
+ background: var(--bg-secondary);
display: inline-flex;
align-items: center;
- gap: 6px;
- padding: 4px 8px;
- border-radius: 999px;
- font-size: 0.8rem;
+ justify-content: center;
+ overflow: hidden;
color: var(--text-secondary);
- cursor: pointer;
- user-select: none;
+ font-size: 0.75rem;
+ text-decoration: none;
}
-.tasks-radio-option input {
- margin: 0;
- accent-color: var(--accent-primary);
+.tasks-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
}
-.tasks-radio-option:has(input:checked) {
- background: rgba(31, 111, 235, 0.15);
- color: var(--text-primary);
+.tasks-avatar-more {
+ font-family: var(--font-mono);
+ font-size: 0.7rem;
}
-.tasks-radio-option:has(input:focus-visible) {
- outline: 2px solid var(--accent-primary);
- outline-offset: 2px;
+.task-card-due {
+ font-family: var(--font-mono);
+ font-size: 0.72rem;
+ color: var(--text-secondary);
}
-.tasks-select {
- background: var(--bg-secondary);
- color: var(--text-primary);
+.tasks-kv {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.tasks-kv-row {
+ display: grid;
+ grid-template-columns: 140px 1fr;
+ gap: 10px;
+ padding: 8px 10px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
- padding: 6px 10px;
- font-size: 0.85rem;
- min-width: 160px;
+ background: var(--bg-secondary);
}
-.tasks-search {
- min-width: 220px;
- flex: 1;
+.tasks-kv-row-edit {
+ align-items: center;
}
-.tasks-body {
- flex: 1;
- min-height: 0;
- display: grid;
- grid-template-columns: 420px 1fr;
- grid-template-rows: 1fr;
- grid-template-areas: "cards detail";
+.tasks-kv-key {
+ color: var(--text-secondary);
+ font-size: 0.8rem;
+ font-weight: 700;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
#queue-panel .tasks-body {
@@ -10407,106 +11580,139 @@ header h1 {
margin-top: var(--space-sm);
}
-.tasks-body.tasks-body-board {
- /* Board view: show full-width kanban until a card is selected. */
- grid-template-columns: 1fr;
- grid-template-areas: "cards";
- position: relative;
- overflow: hidden;
+.tasks-body.tasks-body-board {
+ /* Board view: show full-width kanban until a card is selected. */
+ grid-template-columns: 1fr;
+ grid-template-areas: "cards";
+ position: relative;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.tasks-kv-val-edit {
+ overflow: visible;
+ text-overflow: unset;
+ white-space: normal;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.task-card-board {
+ cursor: grab;
+}
+
+.task-card-board.dragging {
+ opacity: 0.6;
+ cursor: grabbing;
+}
+
+.task-card-row {
+ padding: var(--space-sm) var(--space-md);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-tertiary);
+ cursor: pointer;
+ transition: border-color 0.15s, transform 0.15s;
+}
+
+.task-card-list {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.task-card-list-main {
+ min-width: 0;
+ flex: 1;
}
-.tasks-body.tasks-body-board .tasks-detail {
- display: none;
+.task-card-list-actions {
+ flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
}
-.tasks-body.tasks-body-board .tasks-cards {
- grid-column: 1;
- border-left: none;
+.task-card-row:hover {
+ border-color: var(--accent-primary);
+ transform: translateY(-1px);
}
-.tasks-body.tasks-body-board.tasks-kanban-wrap .tasks-cards {
- overflow: auto;
+.task-card-row.active {
+ border-color: var(--accent-primary);
+ box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.2);
}
-.tasks-body.tasks-body-board.tasks-has-detail {
- /* When a card is selected, show details as an overlay on the right. */
- grid-template-columns: 1fr;
+.task-card-title {
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 4px;
}
-.tasks-body.tasks-body-board.tasks-has-detail .tasks-detail {
- display: block;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- width: 520px;
- max-width: min(520px, 65vw);
- border-left: 1px solid var(--border-color);
- box-shadow: -10px 0 24px rgba(0, 0, 0, 0.25);
- z-index: 5;
+.tasks-card-board-dot {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
+ border: 1px solid var(--border-color);
+ margin-right: 8px;
+ transform: translateY(1px);
}
-.tasks-body.tasks-body-board.tasks-has-detail .tasks-cards {
- grid-column: 1;
- border-left: none;
+.task-card-meta {
+ font-size: 0.75rem;
+ color: var(--text-tertiary);
}
-.tasks-cards {
- grid-area: cards;
- grid-column: 1;
+.tasks-detail {
+ grid-area: detail;
+ grid-column: 2;
grid-row: 1;
overflow-y: auto;
- padding: var(--space-md);
- border-right: 1px solid var(--border-color);
- background: var(--bg-secondary);
- display: flex;
- flex-direction: column;
- gap: var(--space-sm);
+ padding: var(--space-lg);
+ background: var(--bg-primary);
min-width: 0;
}
-.tasks-body.tasks-body-board .tasks-cards {
- border-right: none;
- overflow: hidden;
- padding: var(--space-sm);
- background: var(--bg-primary);
+.tasks-detail-empty {
+ color: var(--text-secondary);
}
-.tasks-board {
- height: 100%;
+.tasks-detail-header {
display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
gap: var(--space-md);
- justify-content: flex-start;
- overflow-x: auto;
- overflow-y: hidden;
- padding-bottom: var(--space-sm);
- scroll-snap-type: x mandatory;
+ margin-bottom: var(--space-sm);
}
-.tasks-board.tasks-board-wrap {
- flex-wrap: wrap;
- overflow-x: hidden;
- overflow-y: auto;
- align-content: flex-start;
- scroll-snap-type: none;
+.tasks-detail-title {
+ font-weight: 700;
+ font-size: 1rem;
+ line-height: 1.2;
}
-.tasks-board.tasks-board-wrap.tasks-board-grid .tasks-column-cards {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
- align-content: start;
+.tasks-detail-actions {
+ display: flex;
+ gap: var(--space-sm);
+ align-items: center;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ flex-shrink: 0;
}
-.tasks-board.tasks-board-expand .tasks-column-cards {
- display: grid;
- grid-auto-flow: column;
- grid-template-rows: repeat(var(--tasks-card-rows, 1), min-content);
- grid-template-columns: repeat(var(--tasks-card-columns, 1), minmax(180px, 1fr));
- align-content: start;
- gap: var(--space-sm);
- overflow: hidden;
- flex: 1;
- min-height: 0;
+.tasks-input {
+ width: 100%;
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 8px 10px;
+ font-size: 0.95rem;
+ font-weight: 700;
}
.tasks-board.tasks-board-expand .task-card-title {
@@ -10522,1143 +11728,1163 @@ header h1 {
height: fit-content;
}
-.tasks-column {
- --tasks-col-expanded: clamp(240px, 22vw, 360px);
- --tasks-col-collapsed: 56px;
- --tasks-card-columns: 1;
- --tasks-card-rows: 1;
- width: var(--tasks-col-expanded);
- min-width: var(--tasks-col-expanded);
+.tasks-textarea {
+ width: 100%;
background: var(--bg-secondary);
+ color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
- display: flex;
- flex-direction: column;
- max-height: 100%;
- scroll-snap-align: start;
- position: relative;
-}
-
-.tasks-column.hover {
- border-color: var(--tasks-board-accent);
- box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.15);
+ padding: 8px 10px;
+ font-size: 0.85rem;
+ font-family: var(--font-mono);
+ resize: vertical;
}
-.tasks-column-header {
- padding: var(--space-sm) var(--space-md);
+.tasks-inline-row {
display: flex;
- align-items: baseline;
- justify-content: space-between;
gap: var(--space-sm);
- width: 100%;
- background: var(--bg-tertiary);
- border: none;
- border-bottom: 1px solid var(--border-color);
- cursor: pointer;
- text-align: left;
- color: var(--text-primary);
-}
-
-.tasks-column-header:hover {
- background: var(--bg-primary);
-}
-
-.tasks-column-header:focus-visible {
- outline: 2px solid var(--accent-primary);
- outline-offset: 2px;
-}
-
-.tasks-column-title {
- font-weight: 700;
- font-size: 0.9rem;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: var(--text-primary);
-}
-
-.tasks-column-count {
- font-family: var(--font-mono);
- font-size: 0.8rem;
- color: var(--text-tertiary);
+ align-items: center;
+ flex-wrap: wrap;
}
-.tasks-column-cards {
- overflow-y: auto;
- padding: var(--space-sm);
+.tasks-combined-list {
display: flex;
flex-direction: column;
- gap: var(--space-sm);
-}
-
-.tasks-column.is-collapsed {
- width: var(--tasks-col-collapsed);
- min-width: var(--tasks-col-collapsed);
-}
-
-.tasks-column.is-collapsed .tasks-column-cards {
- display: none;
-}
-
-.tasks-column.is-collapsed .tasks-column-title {
- writing-mode: vertical-rl;
- transform: rotate(180deg);
- white-space: nowrap;
- overflow: visible;
- text-overflow: unset;
- font-size: 0.9rem;
- font-weight: 800;
- letter-spacing: 0.04em;
- text-transform: uppercase;
+ gap: 8px;
}
-.tasks-column.is-collapsed .tasks-column-header {
- justify-content: flex-start;
+.tasks-combined-item {
+ display: flex;
align-items: center;
- flex-direction: column;
- height: 100%;
- padding: 10px 8px;
- gap: 14px;
-}
-
-.tasks-column.is-collapsed .tasks-column-count {
- display: inline-flex;
- min-width: 30px;
- height: 30px;
- padding: 0 10px;
- border-radius: 999px;
+ justify-content: space-between;
+ gap: 10px;
+ padding: 10px 12px;
border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
background: var(--bg-secondary);
- align-items: center;
- justify-content: center;
- font-weight: 700;
- color: var(--text-primary);
- order: -1;
}
-.task-card-top {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- margin-bottom: 6px;
+.tasks-combined-label {
+ font-size: 0.85rem;
+ color: var(--text-primary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.task-card-top-right {
+.tasks-combined-actions {
display: inline-flex;
- align-items: center;
- gap: 8px;
+ gap: 6px;
flex-shrink: 0;
}
-.task-card-quick-actions {
+.tasks-chips {
display: inline-flex;
- align-items: center;
gap: 6px;
+ flex-wrap: wrap;
+ margin-left: 6px;
}
-.tasks-quick-tier-group {
+.tasks-chip {
display: inline-flex;
align-items: center;
- gap: 4px;
- padding: 2px;
- border: 1px solid var(--border-color);
+ gap: 6px;
+ padding: 4px 8px;
border-radius: 999px;
+ border: 1px solid var(--border-color);
background: var(--bg-secondary);
+ font-size: 0.75rem;
+ color: var(--text-secondary);
}
-.tasks-quick-tier-btn {
- padding: 3px 7px;
- font-size: 0.72rem;
- font-weight: 900;
- line-height: 1;
+.tasks-chip-avatar {
+ width: 16px;
+ height: 16px;
border-radius: 999px;
+ object-fit: cover;
+ display: inline-block;
}
-.tasks-quick-tier-btn.is-selected {
- border-color: var(--accent-primary);
- background: rgba(31, 111, 235, 0.18);
+.tasks-chip-link {
+ color: var(--text-secondary);
+ text-decoration: none;
+}
+
+.tasks-chip-link:hover {
color: var(--text-primary);
+ text-decoration: underline;
}
-.tasks-select.tasks-select-mini {
- min-width: 64px;
- padding: 4px 6px;
- font-size: 0.75rem;
- font-weight: 800;
+.tasks-chip-muted {
+ opacity: 0.7;
}
-.tasks-quick-launch-btn {
- padding: 4px 8px;
- font-size: 0.8rem;
+.tasks-chip-x {
+ appearance: none;
+ border: none;
+ background: transparent;
+ color: var(--text-tertiary);
+ cursor: pointer;
+ font-size: 0.9rem;
line-height: 1;
+ padding: 0 2px;
}
-.task-card-labels {
- display: inline-flex;
- gap: 6px;
- flex-wrap: nowrap;
- overflow: hidden;
- min-width: 0;
+.tasks-chip-x:hover {
+ color: var(--text-primary);
}
-.tasks-label {
- display: inline-flex;
+.tasks-detail-block {
+ margin-top: var(--space-md);
+}
+
+.tasks-detail-block.tasks-dropzone-hover {
+ outline: 2px dashed var(--accent-color);
+ outline-offset: 6px;
+ border-radius: var(--radius-md);
+}
+
+.tasks-detail-block-title {
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ margin-bottom: 6px;
+}
+
+.tasks-move-row {
+ display: flex;
+ gap: var(--space-sm);
align-items: center;
- padding: 2px 6px;
- border-radius: 999px;
- font-size: 0.7rem;
- line-height: 1.2;
+}
+
+.tasks-select-inline {
+ min-width: 240px;
+}
+
+.tasks-comment-row {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+}
+
+.tasks-comments {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+}
+
+.tasks-comment {
+ padding: var(--space-sm) var(--space-md);
border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
background: var(--bg-secondary);
- color: var(--text-secondary);
- max-width: 120px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
}
-.tasks-label-editor {
+.tasks-comment-meta {
+ font-size: 0.75rem;
+ color: var(--text-tertiary);
+ margin-bottom: 6px;
+}
+
+.tasks-comment-text {
+ white-space: pre-wrap;
+ color: var(--text-primary);
+ line-height: 1.35;
+ font-size: 0.85rem;
+}
+
+.tasks-deps {
display: flex;
- flex-wrap: wrap;
+ flex-direction: column;
gap: 6px;
}
-.tasks-label-toggle {
- appearance: none;
- cursor: pointer;
+.tasks-dep-row {
+ display: grid;
+ grid-template-columns: 18px 1fr auto;
+ gap: 10px;
+ align-items: center;
+ padding: 8px 10px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-secondary);
}
-.tasks-label-toggle.is-selected {
- box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.25);
+.tasks-dep-row.done {
+ opacity: 0.75;
}
-.tasks-checkbox {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- font-size: 0.8rem;
- color: var(--text-secondary);
+.tasks-cover {
+ display: block;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-secondary);
+ overflow: hidden;
}
-.tasks-checkbox input {
- margin: 0;
- accent-color: var(--accent-primary);
+.tasks-cover img {
+ display: block;
+ width: 100%;
+ max-height: 220px;
+ object-fit: cover;
}
-.tasks-label--green { border-color: #2ea043; background: rgba(46, 160, 67, 0.12); color: var(--text-primary); }
-.tasks-label--yellow { border-color: #d29922; background: rgba(210, 153, 34, 0.14); color: var(--text-primary); }
-.tasks-label--orange { border-color: #f78166; background: rgba(247, 129, 102, 0.14); color: var(--text-primary); }
-.tasks-label--red { border-color: #f85149; background: rgba(248, 81, 73, 0.14); color: var(--text-primary); }
-.tasks-label--purple { border-color: #a371f7; background: rgba(163, 113, 247, 0.14); color: var(--text-primary); }
-.tasks-label--blue { border-color: #1f6feb; background: rgba(31, 111, 235, 0.14); color: var(--text-primary); }
-.tasks-label--sky { border-color: #79c0ff; background: rgba(121, 192, 255, 0.14); color: var(--text-primary); }
-.tasks-label--lime { border-color: #7ee787; background: rgba(126, 231, 135, 0.14); color: var(--text-primary); }
-.tasks-label--pink { border-color: #ff80c8; background: rgba(255, 128, 200, 0.14); color: var(--text-primary); }
-.tasks-label--black { border-color: #30363d; background: rgba(48, 54, 61, 0.6); color: var(--text-primary); }
-.tasks-label--more { border-color: var(--border-color); background: var(--bg-tertiary); color: var(--text-primary); font-family: var(--font-mono); }
+.tasks-cover.tasks-cover-color {
+ height: 110px;
+}
-.task-card-assignees {
- display: inline-flex;
+.tasks-attachments {
+ display: flex;
+ flex-direction: column;
gap: 6px;
- flex-shrink: 0;
}
-.tasks-avatar {
- width: 22px;
- height: 22px;
- border-radius: 999px;
+.tasks-attachment {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ padding: 8px 10px;
border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
background: var(--bg-secondary);
+}
+
+.tasks-attachment-thumb {
display: inline-flex;
+ width: 44px;
+ height: 44px;
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+ border: 1px solid var(--border-color);
+ background: rgba(0, 0, 0, 0.08);
+ flex: 0 0 auto;
align-items: center;
justify-content: center;
- overflow: hidden;
- color: var(--text-secondary);
- font-size: 0.75rem;
- text-decoration: none;
}
-.tasks-avatar img {
+.tasks-attachment-thumb.is-empty {
+ opacity: 0.5;
+}
+
+.tasks-attachment-thumb img {
+ display: block;
width: 100%;
height: 100%;
object-fit: cover;
- display: block;
}
-.tasks-avatar-more {
- font-family: var(--font-mono);
- font-size: 0.7rem;
+.tasks-attachment-body {
+ min-width: 0;
+ flex: 1;
}
-.task-card-due {
- font-family: var(--font-mono);
- font-size: 0.72rem;
- color: var(--text-secondary);
+.tasks-attachment-name {
+ color: var(--text-primary);
+ font-size: 0.85rem;
+ line-height: 1.25;
+ text-decoration: none;
+ word-break: break-word;
+}
+
+.tasks-attachment-name:hover {
+ text-decoration: underline;
+}
+
+.tasks-attachment-meta {
+ margin-top: 2px;
+ color: var(--text-tertiary);
+ font-size: 0.75rem;
+ line-height: 1.2;
+ word-break: break-word;
}
-.tasks-kv {
+.tasks-checklists {
display: flex;
flex-direction: column;
- gap: 6px;
+ gap: var(--space-sm);
}
-.tasks-kv-row {
- display: grid;
- grid-template-columns: 140px 1fr;
- gap: 10px;
- padding: 8px 10px;
+.tasks-checklist {
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
background: var(--bg-secondary);
+ padding: 10px;
}
-.tasks-kv-row-edit {
+.tasks-checklist-header {
+ display: flex;
+ gap: 10px;
align-items: center;
+ justify-content: space-between;
}
-.tasks-kv-key {
- color: var(--text-secondary);
- font-size: 0.8rem;
+.tasks-checklist-title {
font-weight: 700;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.tasks-kv-val {
color: var(--text-primary);
- font-size: 0.85rem;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+ font-size: 0.9rem;
+ min-width: 0;
+ word-break: break-word;
}
-.tasks-kv-val-edit {
- overflow: visible;
- text-overflow: unset;
- white-space: normal;
+.tasks-checklist-actions {
display: flex;
+ gap: 6px;
align-items: center;
- gap: 8px;
}
-.task-card-board {
- cursor: grab;
-}
-
-.task-card-board.dragging {
- opacity: 0.6;
- cursor: grabbing;
+.tasks-checkitems {
+ margin-top: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
}
-.task-card-row {
- padding: var(--space-sm) var(--space-md);
+.tasks-checkitem-row {
+ display: grid;
+ grid-template-columns: 18px 1fr auto;
+ gap: 10px;
+ align-items: center;
+ padding: 8px 10px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
- background: var(--bg-tertiary);
+ background: var(--bg-primary);
+}
+
+.tasks-checkitem-row.done {
+ opacity: 0.75;
+}
+
+.tasks-checkitem-text {
+ min-width: 0;
+ color: var(--text-primary);
+ font-size: 0.85rem;
cursor: pointer;
- transition: border-color 0.15s, transform 0.15s;
+ word-break: break-word;
}
-.task-card-list {
+.tasks-checkitem-add,
+.tasks-checklist-add {
+ margin-top: 8px;
+}
+
+.tasks-list-manager-row {
display: flex;
- align-items: flex-start;
+ align-items: center;
justify-content: space-between;
gap: 10px;
+ padding: 10px 12px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-secondary);
+ margin-bottom: 8px;
}
-.task-card-list-main {
+.tasks-list-manager-name {
min-width: 0;
flex: 1;
+ color: var(--text-primary);
+ font-weight: 600;
+ word-break: break-word;
}
-.task-card-list-actions {
- flex-shrink: 0;
- display: inline-flex;
- align-items: center;
+.tasks-list-manager-actions {
+ display: flex;
gap: 6px;
+ align-items: center;
}
-.task-card-row:hover {
- border-color: var(--accent-primary);
- transform: translateY(-1px);
+.tasks-dep-row.done .tasks-dep-text {
+ text-decoration: line-through;
}
-.task-card-row.active {
- border-color: var(--accent-primary);
- box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.2);
+.tasks-dep-text a {
+ color: var(--accent-primary);
+ text-decoration: none;
}
-.task-card-title {
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 4px;
+.tasks-dep-text a:hover {
+ text-decoration: underline;
}
-.tasks-card-board-dot {
- display: inline-block;
- width: 10px;
- height: 10px;
- border-radius: 999px;
- border: 1px solid var(--border-color);
- margin-right: 8px;
- transform: translateY(1px);
+.tasks-dep-remove {
+ padding: 4px 8px;
+ font-weight: 700;
}
-.task-card-meta {
- font-size: 0.75rem;
+.tasks-dep-add {
+ margin-top: var(--space-sm);
+}
+
+.tasks-detail-meta {
+ font-size: 0.8rem;
color: var(--text-tertiary);
+ margin-bottom: var(--space-md);
}
-.tasks-detail {
- grid-area: detail;
- grid-column: 2;
- grid-row: 1;
- overflow-y: auto;
- padding: var(--space-lg);
- background: var(--bg-primary);
- min-width: 0;
+.tasks-detail-desc {
+ white-space: pre-wrap;
+ font-family: var(--font-mono);
+ font-size: 0.85rem;
+ padding: var(--space-md);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-secondary);
+ color: var(--text-primary);
}
-.tasks-detail-empty {
- color: var(--text-secondary);
+.tasks-config-hint {
+ padding: var(--space-lg);
+ border: 1px dashed var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-tertiary);
}
-.tasks-detail-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: var(--space-md);
+.tasks-config-title {
+ font-weight: 700;
margin-bottom: var(--space-sm);
}
-.tasks-detail-title {
- font-weight: 700;
- font-size: 1rem;
- line-height: 1.2;
+.tasks-config-text {
+ color: var(--text-secondary);
+ line-height: 1.4;
}
-.tasks-detail-actions {
- display: flex;
- gap: var(--space-sm);
- align-items: center;
- flex-wrap: wrap;
- justify-content: flex-end;
- flex-shrink: 0;
+/* Port panel - project detection styles */
+.port-name {
+ cursor: pointer;
+ transition: color 0.15s;
}
-.tasks-input {
- width: 100%;
- background: var(--bg-secondary);
- color: var(--text-primary);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- padding: 8px 10px;
- font-size: 0.95rem;
- font-weight: 700;
+.port-name:hover {
+ color: var(--accent-primary);
}
-.tasks-input-inline {
- width: auto;
- min-width: 220px;
- display: inline-flex;
- margin: 0 6px;
- font-weight: 600;
- font-size: 0.85rem;
- padding: 6px 10px;
+.port-name.custom-label {
+ color: #9f7aea;
}
-.tasks-textarea {
- width: 100%;
- background: var(--bg-secondary);
- color: var(--text-primary);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- padding: 8px 10px;
- font-size: 0.85rem;
- font-family: var(--font-mono);
- resize: vertical;
+.port-path {
+ display: block;
+ font-size: 0.7rem;
+ color: var(--accent-primary);
+ opacity: 0.8;
+ max-width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.tasks-inline-row {
+/* Port context - project/worktree info */
+.port-context {
display: flex;
- gap: var(--space-sm);
align-items: center;
- flex-wrap: wrap;
+ gap: 0.3rem;
+ font-size: 0.75rem;
+ margin-top: 2px;
+}
+
+.port-project {
+ color: #9f7aea;
+ font-weight: 500;
+}
+
+.port-worktree {
+ background: var(--accent-primary);
+ color: white;
+ padding: 1px 6px;
+ border-radius: 3px;
+ font-size: 0.7rem;
+ font-weight: 500;
+}
+
+.port-subpath {
+ color: var(--text-muted);
+ font-size: 0.7rem;
}
-.tasks-combined-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
+/* ============================================
+ Sidebar Ports Section
+ ============================================ */
+
+.sidebar-section {
+ border-top: 1px solid var(--border-color);
+ margin-top: auto;
}
-.tasks-combined-item {
+.sidebar-section-header {
display: flex;
align-items: center;
- justify-content: space-between;
- gap: 10px;
- padding: 10px 12px;
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
+ gap: var(--space-sm);
+ padding: var(--space-sm) var(--space-md);
+ cursor: pointer;
+ user-select: none;
+ font-size: 0.85rem;
+ font-weight: 500;
+ color: var(--text-secondary);
+ transition: background 0.15s;
}
-.tasks-combined-label {
- font-size: 0.85rem;
- color: var(--text-primary);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+.sidebar-section-header:hover {
+ background: var(--bg-tertiary);
}
-.tasks-combined-actions {
- display: inline-flex;
- gap: 6px;
- flex-shrink: 0;
+.ports-count {
+ background: var(--accent-primary);
+ color: white;
+ padding: 1px 6px;
+ border-radius: 10px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ min-width: 18px;
+ text-align: center;
}
-.tasks-chips {
- display: inline-flex;
- gap: 6px;
- flex-wrap: wrap;
- margin-left: 6px;
+.collapse-icon {
+ margin-left: auto;
+ font-size: 0.7rem;
+ transition: transform 0.2s;
}
-.tasks-chip {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 4px 8px;
- border-radius: 999px;
- border: 1px solid var(--border-color);
- background: var(--bg-secondary);
- font-size: 0.75rem;
- color: var(--text-secondary);
+.sidebar-section.collapsed .collapse-icon {
+ transform: rotate(-90deg);
}
-.tasks-chip-avatar {
- width: 16px;
- height: 16px;
- border-radius: 999px;
- object-fit: cover;
- display: inline-block;
+.sidebar-section.collapsed .ports-sidebar-list {
+ display: none;
}
-.tasks-chip-link {
- color: var(--text-secondary);
- text-decoration: none;
+.ports-sidebar-list {
+ max-height: 200px;
+ overflow-y: auto;
+ padding: var(--space-xs) 0;
}
-.tasks-chip-link:hover {
- color: var(--text-primary);
- text-decoration: underline;
+.port-sidebar-item {
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+ padding: var(--space-xs) var(--space-md);
+ font-size: 0.75rem;
+ cursor: pointer;
+ transition: background 0.15s;
+ border-left: 2px solid transparent;
}
-.tasks-chip-muted {
- opacity: 0.7;
+.port-sidebar-item:hover {
+ background: var(--bg-tertiary);
}
-.tasks-chip-x {
- appearance: none;
- border: none;
- background: transparent;
- color: var(--text-tertiary);
- cursor: pointer;
+.port-sidebar-item.orchestrator { border-left-color: #9f7aea; }
+.port-sidebar-item.node { border-left-color: #68d391; }
+.port-sidebar-item.rails { border-left-color: #f56565; }
+.port-sidebar-item.python { border-left-color: #ffd93d; }
+.port-sidebar-item.vite { border-left-color: #48bb78; }
+
+.port-sidebar-icon {
font-size: 0.9rem;
- line-height: 1;
- padding: 0 2px;
+ flex-shrink: 0;
}
-.tasks-chip-x:hover {
+.port-sidebar-info {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+}
+
+.port-sidebar-name {
+ display: block;
+ font-weight: 500;
color: var(--text-primary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.tasks-detail-block {
- margin-top: var(--space-md);
+.port-sidebar-context {
+ display: block;
+ font-size: 0.65rem;
+ color: var(--text-muted);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.tasks-detail-block.tasks-dropzone-hover {
- outline: 2px dashed var(--accent-color);
- outline-offset: 6px;
- border-radius: var(--radius-md);
+.port-sidebar-port {
+ color: var(--accent-primary);
+ font-weight: 500;
+ font-size: 0.7rem;
+ flex-shrink: 0;
}
-.tasks-detail-block-title {
- font-size: 0.8rem;
- color: var(--text-secondary);
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.04em;
- margin-bottom: 6px;
+.ports-sidebar-empty {
+ padding: var(--space-sm) var(--space-md);
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ font-style: italic;
}
-.tasks-move-row {
- display: flex;
- gap: var(--space-sm);
- align-items: center;
+/* ============================================
+ Dashboard Ports Section
+ ============================================ */
+
+.ports-dashboard-section {
+ margin-top: var(--space-lg);
}
-.tasks-select-inline {
- min-width: 240px;
+.ports-dashboard-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+ gap: var(--space-md);
}
-.tasks-comment-row {
+.port-dashboard-card {
display: flex;
- flex-direction: column;
+ align-items: center;
gap: var(--space-sm);
+ padding: var(--space-md);
+ background: var(--bg-secondary);
+ border-radius: var(--radius-md);
+ border-left: 3px solid var(--border-color);
+ cursor: pointer;
+ transition: all 0.15s;
}
-.tasks-comments {
- display: flex;
- flex-direction: column;
- gap: var(--space-sm);
+.port-dashboard-card:hover {
+ background: var(--bg-tertiary);
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-soft);
}
-.tasks-comment {
- padding: var(--space-sm) var(--space-md);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
+.port-dashboard-card.orchestrator { border-left-color: #9f7aea; }
+.port-dashboard-card.node { border-left-color: #68d391; }
+.port-dashboard-card.rails { border-left-color: #f56565; }
+.port-dashboard-card.ruby { border-left-color: #f56565; }
+.port-dashboard-card.python { border-left-color: #ffd93d; }
+.port-dashboard-card.vite { border-left-color: #48bb78; }
+
+.port-card-icon {
+ font-size: 1.5rem;
+ flex-shrink: 0;
}
-.tasks-comment-meta {
- font-size: 0.75rem;
- color: var(--text-tertiary);
- margin-bottom: 6px;
+.port-card-info {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
}
-.tasks-comment-text {
- white-space: pre-wrap;
+.port-card-name {
+ display: block;
+ font-weight: 600;
color: var(--text-primary);
- line-height: 1.35;
- font-size: 0.85rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.tasks-deps {
- display: flex;
- flex-direction: column;
- gap: 6px;
+.port-card-context {
+ display: block;
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.tasks-dep-row {
- display: grid;
- grid-template-columns: 18px 1fr auto;
- gap: 10px;
- align-items: center;
- padding: 8px 10px;
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
+.port-card-port {
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--accent-primary);
+ flex-shrink: 0;
}
-.tasks-dep-row.done {
- opacity: 0.75;
+.ports-empty,
+.ports-loading {
+ grid-column: 1 / -1;
+ text-align: center;
+ padding: var(--space-lg);
+ color: var(--text-muted);
+ font-style: italic;
}
-.tasks-cover {
- display: block;
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
- overflow: hidden;
-}
+/* ============================================
+ Dashboard Split Row Layout
+ ============================================ */
-.tasks-cover img {
- display: block;
- width: 100%;
- max-height: 220px;
- object-fit: cover;
+.dashboard-split-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--space-lg);
+ margin-top: var(--space-lg);
}
-.tasks-cover.tasks-cover-color {
- height: 110px;
+.dashboard-half {
+ background: var(--bg-secondary);
+ border-radius: var(--radius-lg);
+ padding: var(--space-md);
+ max-height: 400px;
+ overflow-y: auto;
}
-.tasks-attachments {
- display: flex;
- flex-direction: column;
- gap: 6px;
+.dashboard-half h2 {
+ margin: 0 0 var(--space-sm) 0;
+ font-size: 1rem;
+ position: sticky;
+ top: 0;
+ background: var(--bg-secondary);
+ padding-bottom: var(--space-xs);
}
-.tasks-attachment {
- display: flex;
- gap: 10px;
- align-items: center;
- padding: 8px 10px;
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
+/* Grid layout for ports in dashboard half */
+.dashboard-half .ports-dashboard-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ gap: var(--space-xs);
}
-.tasks-attachment-thumb {
- display: inline-flex;
- width: 44px;
- height: 44px;
- border-radius: var(--radius-sm);
- overflow: hidden;
- border: 1px solid var(--border-color);
- background: rgba(0, 0, 0, 0.08);
- flex: 0 0 auto;
+.dashboard-half .port-dashboard-card {
+ padding: var(--space-sm);
+ flex-direction: column;
align-items: center;
- justify-content: center;
+ text-align: center;
+ gap: var(--space-xs);
+ min-height: 70px;
}
-.tasks-attachment-thumb.is-empty {
- opacity: 0.5;
+.dashboard-half .port-card-icon {
+ font-size: 1.3rem;
}
-.tasks-attachment-thumb img {
- display: block;
+.dashboard-half .port-card-info {
width: 100%;
- height: 100%;
- object-fit: cover;
}
-.tasks-attachment-body {
- min-width: 0;
- flex: 1;
+.dashboard-half .port-card-name {
+ font-size: 0.75rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.tasks-attachment-name {
- color: var(--text-primary);
- font-size: 0.85rem;
- line-height: 1.25;
- text-decoration: none;
- word-break: break-word;
+.dashboard-half .port-card-context {
+ font-size: 0.65rem;
}
-.tasks-attachment-name:hover {
- text-decoration: underline;
+.dashboard-half .port-card-port {
+ font-size: 0.8rem;
+ margin-top: auto;
}
-.tasks-attachment-meta {
- margin-top: 2px;
- color: var(--text-tertiary);
- font-size: 0.75rem;
- line-height: 1.2;
- word-break: break-word;
+/* Grid layout for quick links in dashboard half */
+.dashboard-half .quick-links-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ gap: var(--space-xs);
}
-.tasks-checklists {
- display: flex;
+.dashboard-half .quick-link-item {
flex-direction: column;
- gap: var(--space-sm);
+ align-items: center;
+ text-align: center;
+ padding: var(--space-sm);
+ min-height: 60px;
+ gap: var(--space-xs);
+ border-left: none;
+ border-bottom: 2px solid var(--accent-primary);
}
-.tasks-checklist {
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
- padding: 10px;
+.dashboard-half .quick-link-item .quick-link-icon {
+ font-size: 1.2rem;
}
-.tasks-checklist-header {
- display: flex;
- gap: 10px;
- align-items: center;
- justify-content: space-between;
+.dashboard-half .quick-link-item .quick-link-label {
+ font-size: 0.7rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
}
-.tasks-checklist-title {
- font-weight: 700;
- color: var(--text-primary);
- font-size: 0.9rem;
- min-width: 0;
- word-break: break-word;
+/* Quick links container inside dashboard */
+.dashboard-half .quick-links-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ gap: var(--space-xs);
}
-.tasks-checklist-actions {
- display: flex;
- gap: 6px;
- align-items: center;
+.dashboard-half .quick-links-section {
+ display: contents;
}
-.tasks-checkitems {
- margin-top: 8px;
- display: flex;
- flex-direction: column;
- gap: 6px;
+.dashboard-half .quick-links-section h3 {
+ display: none;
}
-.tasks-checkitem-row {
- display: grid;
- grid-template-columns: 18px 1fr auto;
- gap: 10px;
- align-items: center;
- padding: 8px 10px;
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-primary);
+@media (max-width: 900px) {
+ .dashboard-split-row {
+ grid-template-columns: 1fr;
+ }
}
-.tasks-checkitem-row.done {
- opacity: 0.75;
-}
+/* ============================================
+ Quick Link Items (for dashboard half)
+ ============================================ */
-.tasks-checkitem-text {
- min-width: 0;
- color: var(--text-primary);
- font-size: 0.85rem;
+.quick-link-item {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+ padding: var(--space-sm) var(--space-md);
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-sm);
+ border-left: 3px solid var(--accent-primary);
cursor: pointer;
- word-break: break-word;
+ transition: all 0.15s;
+ text-decoration: none;
+ color: inherit;
+ border: none;
+ width: 100%;
+ text-align: left;
+ font-size: inherit;
+ font-family: inherit;
}
-.tasks-checkitem-add,
-.tasks-checklist-add {
- margin-top: 8px;
+.quick-link-item:hover {
+ background: var(--bg-primary);
+ transform: translateX(2px);
}
-.tasks-list-manager-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
- padding: 10px 12px;
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
- margin-bottom: 8px;
+.quick-link-icon {
+ font-size: 1rem;
+ flex-shrink: 0;
}
-.tasks-list-manager-name {
- min-width: 0;
+.quick-link-label {
flex: 1;
+ font-weight: 500;
color: var(--text-primary);
- font-weight: 600;
- word-break: break-word;
}
-.tasks-list-manager-actions {
- display: flex;
- gap: 6px;
- align-items: center;
-}
-
-.tasks-dep-row.done .tasks-dep-text {
- text-decoration: line-through;
+.quick-links-empty {
+ padding: var(--space-md);
+ text-align: center;
+ color: var(--text-muted);
+ font-style: italic;
+ grid-column: 1 / -1;
}
-.tasks-dep-text a {
- color: var(--accent-primary);
- text-decoration: none;
-}
+/* ============================================
+ Session Recovery Dialog
+ ============================================ */
-.tasks-dep-text a:hover {
- text-decoration: underline;
+.recovery-modal .modal-content {
+ max-width: 600px;
+ width: 90vw;
}
-.tasks-dep-remove {
- padding: 4px 8px;
- font-weight: 700;
+.recovery-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-md) var(--space-lg);
+ border-bottom: 1px solid var(--border-color);
+ background: var(--bg-tertiary);
}
-.tasks-dep-add {
- margin-top: var(--space-sm);
+.recovery-header h2 {
+ margin: 0;
+ font-size: 1.1rem;
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
}
-.tasks-detail-meta {
+.recovery-info {
+ padding: var(--space-sm) var(--space-lg);
+ background: var(--bg-secondary);
font-size: 0.8rem;
- color: var(--text-tertiary);
- margin-bottom: var(--space-md);
+ color: var(--text-muted);
+ border-bottom: 1px solid var(--border-color);
}
-.tasks-detail-desc {
- white-space: pre-wrap;
- font-family: var(--font-mono);
- font-size: 0.85rem;
+.recovery-sessions {
+ max-height: 300px;
+ overflow-y: auto;
padding: var(--space-md);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- background: var(--bg-secondary);
- color: var(--text-primary);
}
-.tasks-config-hint {
- padding: var(--space-lg);
- border: 1px dashed var(--border-color);
- border-radius: var(--radius-md);
+.recovery-session {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-md);
+ padding: var(--space-sm) var(--space-md);
+ margin-bottom: var(--space-xs);
background: var(--bg-tertiary);
+ border-radius: var(--radius-sm);
+ border-left: 3px solid var(--accent-primary);
}
-.tasks-config-title {
- font-weight: 700;
- margin-bottom: var(--space-sm);
-}
-
-.tasks-config-text {
- color: var(--text-secondary);
- line-height: 1.4;
-}
-
-/* Port panel - project detection styles */
-.port-name {
- cursor: pointer;
- transition: color 0.15s;
+.recovery-session.selected {
+ border-left-color: #48bb78;
+ background: rgba(72, 187, 120, 0.1);
}
-.port-name:hover {
- color: var(--accent-primary);
+.recovery-checkbox {
+ margin-top: 2px;
}
-.port-name.custom-label {
- color: #9f7aea;
+.recovery-session-info {
+ flex: 1;
+ min-width: 0;
}
-.port-path {
- display: block;
- font-size: 0.7rem;
- color: var(--accent-primary);
- opacity: 0.8;
- max-width: 200px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+.recovery-session-id {
+ font-weight: 600;
+ font-size: 0.9rem;
+ color: var(--text-primary);
}
-/* Port context - project/worktree info */
-.port-context {
- display: flex;
- align-items: center;
- gap: 0.3rem;
+.recovery-session-details {
font-size: 0.75rem;
+ color: var(--text-muted);
margin-top: 2px;
}
-.port-project {
- color: #9f7aea;
- font-weight: 500;
+.recovery-session-details span {
+ display: inline-block;
+ margin-right: var(--space-sm);
}
-.port-worktree {
- background: var(--accent-primary);
- color: white;
- padding: 1px 6px;
- border-radius: 3px;
- font-size: 0.7rem;
- font-weight: 500;
+.recovery-session-cwd {
+ color: var(--accent-primary);
}
-.port-subpath {
- color: var(--text-muted);
- font-size: 0.7rem;
+.recovery-session-agent {
+ background: var(--bg-primary);
+ padding: 1px 6px;
+ border-radius: 3px;
}
-/* ============================================
- Sidebar Ports Section
- ============================================ */
-
-.sidebar-section {
+.recovery-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-md) var(--space-lg);
border-top: 1px solid var(--border-color);
- margin-top: auto;
+ gap: var(--space-md);
}
-.sidebar-section-header {
+.recovery-actions {
display: flex;
- align-items: center;
gap: var(--space-sm);
+}
+
+.recovery-skip {
+ color: var(--text-muted);
+ font-size: 0.8rem;
+}
+
+.btn-recovery {
padding: var(--space-sm) var(--space-md);
+ border-radius: var(--radius-sm);
+ border: none;
cursor: pointer;
- user-select: none;
- font-size: 0.85rem;
font-weight: 500;
- color: var(--text-secondary);
- transition: background 0.15s;
+ transition: all 0.15s;
}
-.sidebar-section-header:hover {
- background: var(--bg-tertiary);
+.btn-recovery-all {
+ background: #48bb78;
+ color: white;
}
-.ports-count {
+.btn-recovery-all:hover {
+ background: #38a169;
+}
+
+.btn-recovery-selected {
background: var(--accent-primary);
- color: white;
- padding: 1px 6px;
- border-radius: 10px;
- font-size: 0.7rem;
- font-weight: 600;
- min-width: 18px;
- text-align: center;
+ color: var(--text-on-accent);
}
-.collapse-icon {
- margin-left: auto;
- font-size: 0.7rem;
- transition: transform 0.2s;
+.btn-recovery-selected:hover {
+ background: var(--accent-primary-hover);
}
-.sidebar-section.collapsed .collapse-icon {
- transform: rotate(-90deg);
+.btn-recovery-clear {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ border: 1px solid var(--border-color);
}
-.sidebar-section.collapsed .ports-sidebar-list {
- display: none;
+.btn-recovery-clear:hover {
+ border-color: var(--accent-danger);
+ background: rgba(248, 81, 73, 0.14);
}
-.ports-sidebar-list {
- max-height: 200px;
- overflow-y: auto;
- padding: var(--space-xs) 0;
+.btn-recovery-skip {
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
}
-.port-sidebar-item {
- display: flex;
- align-items: center;
- gap: var(--space-xs);
- padding: var(--space-xs) var(--space-md);
- font-size: 0.75rem;
- cursor: pointer;
- transition: background 0.15s;
- border-left: 2px solid transparent;
+.btn-recovery-skip:hover {
+ background: var(--bg-primary);
}
-.port-sidebar-item:hover {
- background: var(--bg-tertiary);
+.no-recovery {
+ text-align: center;
+ padding: var(--space-lg);
+ color: var(--text-muted);
}
-.port-sidebar-item.orchestrator { border-left-color: #9f7aea; }
-.port-sidebar-item.node { border-left-color: #68d391; }
-.port-sidebar-item.rails { border-left-color: #f56565; }
-.port-sidebar-item.python { border-left-color: #ffd93d; }
-.port-sidebar-item.vite { border-left-color: #48bb78; }
+/* ============================================
+ Activity Feed Styles
+ ============================================ */
-.port-sidebar-icon {
- font-size: 0.9rem;
- flex-shrink: 0;
+.activity-feed-modal .activity-feed-content {
+ max-width: 1200px;
+ width: 94vw;
+ max-height: 90vh;
+ display: flex;
+ flex-direction: column;
}
-.port-sidebar-info {
+.activity-list {
flex: 1;
- min-width: 0;
- overflow: hidden;
+ overflow-y: auto;
+ padding: var(--space-md) var(--space-lg);
}
-.port-sidebar-name {
- display: block;
- font-weight: 500;
- color: var(--text-primary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+.activity-empty {
+ padding: var(--space-lg);
+ color: var(--text-muted);
}
-.port-sidebar-context {
- display: block;
- font-size: 0.65rem;
- color: var(--text-muted);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+.activity-event {
+ border: 1px solid var(--border-color);
+ background: var(--bg-secondary);
+ border-radius: var(--radius-md);
+ padding: var(--space-md);
+ margin-bottom: var(--space-sm);
+}
+
+.activity-event.activity-failed {
+ border-color: rgba(248, 81, 73, 0.55);
+ box-shadow: 0 0 0 1px rgba(248, 81, 73, 0.18);
+}
+
+.activity-meta {
+ display: flex;
+ gap: var(--space-sm);
+ align-items: center;
+ justify-content: space-between;
}
-.port-sidebar-port {
- color: var(--accent-primary);
- font-weight: 500;
- font-size: 0.7rem;
- flex-shrink: 0;
+.activity-actions {
+ display: flex;
+ gap: var(--space-xs);
+ align-items: center;
+ margin-left: auto;
}
-.ports-sidebar-empty {
- padding: var(--space-sm) var(--space-md);
+.activity-action-btn {
+ padding: 4px 8px;
font-size: 0.75rem;
- color: var(--text-muted);
- font-style: italic;
+ line-height: 1;
}
-/* ============================================
- Dashboard Ports Section
- ============================================ */
-
-.ports-dashboard-section {
- margin-top: var(--space-lg);
+.activity-time {
+ color: var(--text-muted);
+ font-size: 0.8rem;
}
-.ports-dashboard-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
- gap: var(--space-md);
+.activity-kind {
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+ padding: 2px 8px;
+ border-radius: 999px;
+ border: 1px solid transparent;
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
}
-.port-dashboard-card {
- display: flex;
- align-items: center;
- gap: var(--space-sm);
- padding: var(--space-md);
- background: var(--bg-secondary);
- border-radius: var(--radius-md);
- border-left: 3px solid var(--border-color);
- cursor: pointer;
- transition: all 0.15s;
+.activity-kind-agent {
+ border-color: var(--accent-primary);
+ color: var(--accent-primary);
+ background: rgba(31, 111, 235, 0.12);
}
-.port-dashboard-card:hover {
- background: var(--bg-tertiary);
- transform: translateY(-2px);
- box-shadow: var(--shadow-soft);
+.activity-kind-session {
+ border-color: var(--accent-success);
+ color: var(--accent-success);
+ background: rgba(63, 185, 80, 0.12);
}
-.port-dashboard-card.orchestrator { border-left-color: #9f7aea; }
-.port-dashboard-card.node { border-left-color: #68d391; }
-.port-dashboard-card.rails { border-left-color: #f56565; }
-.port-dashboard-card.ruby { border-left-color: #f56565; }
-.port-dashboard-card.python { border-left-color: #ffd93d; }
-.port-dashboard-card.vite { border-left-color: #48bb78; }
+.activity-kind-server {
+ border-color: var(--accent-warning);
+ color: var(--accent-warning);
+ background: rgba(210, 153, 34, 0.12);
+}
-.port-card-icon {
- font-size: 1.5rem;
- flex-shrink: 0;
+.activity-kind-git {
+ border-color: var(--accent-success);
+ color: var(--accent-success);
+ background: rgba(63, 185, 80, 0.12);
}
-.port-card-info {
- flex: 1;
- min-width: 0;
- overflow: hidden;
+.activity-kind-pr {
+ border-color: var(--accent-primary);
+ color: var(--accent-primary);
+ background: rgba(31, 111, 235, 0.12);
}
-.port-card-name {
- display: block;
- font-weight: 600;
- color: var(--text-primary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+.activity-kind-tests {
+ border-color: var(--accent-warning);
+ color: var(--accent-warning);
+ background: rgba(210, 153, 34, 0.12);
}
-.port-card-context {
- display: block;
- font-size: 0.75rem;
- color: var(--text-muted);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+.activity-kind-build {
+ border-color: var(--accent-warning);
+ color: var(--accent-warning);
+ background: rgba(210, 153, 34, 0.12);
}
-.port-card-port {
- font-size: 1rem;
- font-weight: 700;
- color: var(--accent-primary);
- flex-shrink: 0;
+.activity-summary {
+ margin-top: var(--space-sm);
+ color: var(--text-primary);
+ font-size: 0.95rem;
}
-.ports-empty,
-.ports-loading {
- grid-column: 1 / -1;
- text-align: center;
- padding: var(--space-lg);
+.activity-data {
+ margin-top: var(--space-sm);
color: var(--text-muted);
- font-style: italic;
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+ white-space: pre-wrap;
+ word-break: break-word;
}
-/* ============================================
- Dashboard Split Row Layout
- ============================================ */
+/* ==========================================================
+ ONBOARDING OVERLAY (PERF-OPTIMIZED)
+ ========================================================== */
-.dashboard-split-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: var(--space-lg);
- margin-top: var(--space-lg);
+.onboarding-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 10000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 24px;
+ background:
+ radial-gradient(90% 60% at 8% 12%, rgba(31, 111, 235, 0.22), transparent 75%),
+ radial-gradient(78% 55% at 92% 88%, rgba(0, 195, 255, 0.14), transparent 80%),
+ rgba(5, 8, 15, 0.86);
+ overflow: auto;
+ transition: opacity 0.18s ease-out, visibility 0.18s linear;
}
.dashboard-half {
@@ -11669,13 +12895,18 @@ header h1 {
overflow-y: auto;
}
-.dashboard-half h2 {
- margin: 0 0 var(--space-sm) 0;
- font-size: 1rem;
- position: sticky;
- top: 0;
- background: var(--bg-secondary);
- padding-bottom: var(--space-xs);
+.onboarding-bg-glow {
+ position: absolute;
+ width: 52vw;
+ height: 52vw;
+ max-width: 640px;
+ max-height: 640px;
+ border-radius: 50%;
+ top: -16%;
+ left: -6%;
+ background: radial-gradient(circle, rgba(31, 111, 235, 0.2) 0%, rgba(31, 111, 235, 0) 72%);
+ opacity: 0.75;
+ pointer-events: none;
}
/* Grid layout for ports in dashboard half */
@@ -11698,8 +12929,9 @@ header h1 {
font-size: 1.1rem;
}
-.dashboard-half .port-card-info {
- width: 100%;
+.onboarding-header {
+ margin-bottom: 4px;
+ text-align: center;
}
.dashboard-half .port-card-name {
@@ -11725,9 +12957,14 @@ header h1 {
gap: var(--space-xs);
}
-.dashboard-half .quick-link-item {
- flex-direction: column;
- align-items: center;
+.onboarding-welcome-card {
+ padding: 28px;
+ border-radius: 18px;
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ background:
+ linear-gradient(155deg, rgba(56, 139, 253, 0.13), rgba(20, 29, 44, 0.42)),
+ rgba(255, 255, 255, 0.04);
+ box-shadow: 0 12px 26px rgba(0, 0, 0, 0.28);
text-align: center;
padding: 6px;
min-height: 52px;
@@ -11755,99 +12992,100 @@ header h1 {
gap: var(--space-xs);
}
-.dashboard-half .quick-links-section {
- display: contents;
+.onboarding-welcome-notes {
+ margin-top: 14px;
+ display: grid;
+ gap: 6px;
+ text-align: center;
}
-.dashboard-half .quick-links-section h3 {
- display: none;
+.onboarding-welcome-notes p {
+ margin: 0;
+ color: rgba(255, 255, 255, 0.74);
+ font-size: 0.9rem;
+ line-height: 1.45;
}
-@media (max-width: 900px) {
- .dashboard-split-row {
- grid-template-columns: 1fr;
- }
+.onboarding-welcome-actions {
+ margin-top: 46px;
+ justify-content: center;
}
-/* ============================================
- Quick Link Items (for dashboard half)
- ============================================ */
-
-.quick-link-item {
+.onboarding-stepper-row {
display: flex;
align-items: center;
- gap: var(--space-sm);
- padding: var(--space-sm) var(--space-md);
- background: var(--bg-tertiary);
- border-radius: var(--radius-sm);
- border-left: 3px solid var(--accent-primary);
- cursor: pointer;
- transition: all 0.15s;
- text-decoration: none;
- color: inherit;
- border: none;
+ justify-content: center;
width: 100%;
- text-align: left;
- font-size: inherit;
- font-family: inherit;
+ gap: 14px;
+ margin-bottom: 18px;
+ flex-wrap: wrap;
+ row-gap: 12px;
}
-.quick-link-item:hover {
- background: var(--bg-primary);
- transform: translateX(2px);
+.onboarding-stepper-item {
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
-.quick-link-icon {
- font-size: 1rem;
- flex-shrink: 0;
+.stepper-icon-box {
+ display: flex;
+ align-items: center;
+ position: relative;
}
-.quick-link-label {
- flex: 1;
- font-weight: 500;
- color: var(--text-primary);
+.stepper-diamond {
+ width: 14px;
+ height: 14px;
+ border-radius: 3px;
+ transform: rotate(45deg);
+ transition: transform 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
}
-.quick-links-empty {
- padding: var(--space-md);
- text-align: center;
- color: var(--text-muted);
- font-style: italic;
- grid-column: 1 / -1;
+.stepper-upcoming .stepper-diamond {
+ background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
+ box-shadow: inset 0 0 0 1px rgba(229, 231, 235, 0.26);
+ opacity: 0.95;
}
-/* ============================================
- Session Recovery Dialog
- ============================================ */
-
-.recovery-modal .modal-content {
- max-width: 600px;
- width: 90vw;
+.stepper-active .stepper-icon-box {
+ gap: 0;
}
-.recovery-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: var(--space-md) var(--space-lg);
- border-bottom: 1px solid var(--border-color);
- background: var(--bg-tertiary);
+.stepper-active .stepper-diamond {
+ background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
+ box-shadow: 0 0 10px rgba(59, 130, 246, 0.4), inset 0 0 0 1px rgba(255, 255, 255, 0.34);
+ transform: rotate(45deg) scale(1.15);
+ opacity: 1;
}
-.recovery-header h2 {
- margin: 0;
- font-size: 1.1rem;
- display: flex;
- align-items: center;
- gap: var(--space-sm);
+.stepper-done .stepper-diamond {
+ background: linear-gradient(135deg, #34d399 0%, #16a34a 100%);
+ box-shadow: 0 0 10px rgba(34, 197, 94, 0.38), inset 0 0 0 1px rgba(236, 253, 245, 0.34);
+ opacity: 1;
}
-.recovery-info {
- padding: var(--space-sm) var(--space-lg);
- background: var(--bg-secondary);
- font-size: 0.8rem;
- color: var(--text-muted);
- border-bottom: 1px solid var(--border-color);
+.stepper-active-label {
+ position: absolute;
+ top: 20px;
+ left: 50%;
+ transform: translateX(-50%);
+ font-size: 0.88rem;
+ color: rgba(255, 255, 255, 0.9);
+ font-weight: 520;
+ margin-left: 0;
+ white-space: nowrap;
+}
+
+.onboarding-step-card {
+ position: relative;
+ overflow: hidden;
+ padding: 26px;
+ border-radius: 18px;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ background: rgba(255, 255, 255, 0.045);
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28);
+ transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
}
.recovery-metrics {
@@ -11893,254 +13131,318 @@ header h1 {
padding: var(--space-md);
}
-.recovery-session {
+.onboarding-step-icon {
+ width: 42px;
+ height: 42px;
+ margin-bottom: 18px;
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.11);
+ color: rgba(255, 255, 255, 0.94);
display: flex;
- align-items: flex-start;
- gap: var(--space-md);
- padding: var(--space-sm) var(--space-md);
- margin-bottom: var(--space-xs);
- background: var(--bg-tertiary);
- border-radius: var(--radius-sm);
- border-left: 3px solid var(--accent-primary);
+ align-items: center;
+ justify-content: center;
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
-.recovery-session.selected {
- border-left-color: #48bb78;
- background: rgba(72, 187, 120, 0.1);
+.onboarding-step-icon-svg {
+ width: 22px;
+ height: 22px;
+ display: block;
}
-.recovery-checkbox {
- margin-top: 2px;
+.onboarding-step-icon-svg * {
+ vector-effect: non-scaling-stroke;
}
-.recovery-session-info {
- flex: 1;
- min-width: 0;
+.onboarding-step-title {
+ font-size: clamp(1.42rem, 2.9vw, 1.78rem);
+ font-weight: 620;
+ color: #ffffff;
+ margin-bottom: 10px;
}
-.recovery-session-id {
- font-weight: 600;
- font-size: 0.9rem;
- color: var(--text-primary);
+.onboarding-step-status-row {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ margin-bottom: 20px;
}
-.recovery-session-details {
- font-size: 0.75rem;
- color: var(--text-muted);
- margin-top: 2px;
+.onboarding-check {
+ margin-top: 3px;
+ flex-shrink: 0;
+ color: #3fb950;
}
-.recovery-session-details span {
- display: inline-block;
- margin-right: var(--space-sm);
+.onboarding-step-desc {
+ margin: 0;
+ font-size: 1rem;
+ line-height: 1.48;
+ color: rgba(255, 255, 255, 0.78);
}
-.recovery-session-cwd {
- color: var(--accent-primary);
+.onboarding-inline-status {
+ display: inline-block;
+ margin-left: 8px;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 0.9em;
+ background: rgba(0, 0, 0, 0.3);
}
-.recovery-session-agent {
- background: var(--bg-primary);
- padding: 1px 6px;
- border-radius: 3px;
-}
+.onboarding-inline-status.status-ok { color: #3fb950; }
+.onboarding-inline-status.status-missing { color: #f85149; }
+.onboarding-inline-status.status-pending { color: #58a6ff; }
-.recovery-footer {
+.onboarding-step-actions {
display: flex;
- justify-content: space-between;
- align-items: center;
- padding: var(--space-md) var(--space-lg);
- border-top: 1px solid var(--border-color);
- gap: var(--space-md);
+ gap: 10px;
+ flex-wrap: wrap;
}
-.recovery-actions {
- display: flex;
- gap: var(--space-sm);
+.onboarding-btn-secondary,
+.onboarding-btn-back,
+.onboarding-btn-primary {
+ border-radius: 10px;
+ cursor: pointer;
+ transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
-.recovery-skip {
- color: var(--text-muted);
- font-size: 0.8rem;
+.onboarding-btn-secondary {
+ padding: 10px 18px;
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ background: rgba(255, 255, 255, 0.09);
+ color: rgba(255, 255, 255, 0.92);
+ font-size: 0.94rem;
+ font-weight: 520;
}
-.btn-recovery {
- padding: var(--space-sm) var(--space-md);
- border-radius: var(--radius-sm);
- border: none;
- cursor: pointer;
- font-weight: 500;
- transition: all 0.15s;
+.onboarding-btn-secondary:hover:not(:disabled) {
+ background: rgba(255, 255, 255, 0.16);
+ border-color: rgba(255, 255, 255, 0.26);
+ transform: translateY(-1px);
}
-.btn-recovery-all {
- background: #48bb78;
- color: white;
+.onboarding-btn-secondary:disabled {
+ opacity: 0.52;
+ cursor: not-allowed;
}
-.btn-recovery-all:hover {
- background: #38a169;
+.onboarding-step-actions [data-setup-run] {
+ border-color: rgba(74, 222, 128, 0.55);
+ background: linear-gradient(135deg, #34d399 0%, #16a34a 100%);
+ color: #052e16;
+ box-shadow: 0 6px 16px rgba(22, 163, 74, 0.3);
}
-.btn-recovery-selected {
- background: var(--accent-primary);
- color: var(--text-on-accent);
+.onboarding-step-actions [data-setup-run]:hover:not(:disabled) {
+ border-color: rgba(134, 239, 172, 0.92);
+ background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
+ color: #052e16;
+ box-shadow: 0 8px 18px rgba(22, 163, 74, 0.38);
}
-.btn-recovery-selected:hover {
- background: var(--accent-primary-hover);
+.onboarding-step-actions [data-setup-run]:disabled {
+ border-color: rgba(255, 255, 255, 0.15);
+ background: rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.55);
+ box-shadow: none;
}
-.btn-recovery-clear {
- background: var(--bg-tertiary);
- color: var(--text-primary);
- border: 1px solid var(--border-color);
+.onboarding-nav-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 6px;
}
-.btn-recovery-clear:hover {
- border-color: var(--accent-danger);
- background: rgba(248, 81, 73, 0.14);
+.onboarding-nav-row.onboarding-welcome-actions {
+ justify-content: center;
}
-.btn-recovery-skip {
- background: var(--bg-tertiary);
- color: var(--text-secondary);
+.onboarding-btn-back {
+ padding: 13px 24px;
+ border: 1px solid rgba(255, 255, 255, 0.11);
+ background: rgba(255, 255, 255, 0.055);
+ color: rgba(255, 255, 255, 0.74);
+ font-size: 0.98rem;
+ font-weight: 600;
}
-.btn-recovery-skip:hover {
- background: var(--bg-primary);
+.onboarding-btn-back:hover:not(:disabled) {
+ background: rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.9);
}
-.no-recovery {
- text-align: center;
- padding: var(--space-lg);
- color: var(--text-muted);
+.onboarding-btn-primary {
+ padding: 13px 28px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ background: linear-gradient(135deg, #388bfd 0%, #1f6feb 100%);
+ color: #ffffff;
+ font-size: 0.98rem;
+ font-weight: 620;
+ box-shadow: 0 4px 13px rgba(31, 111, 235, 0.34);
}
-/* ============================================
- Activity Feed Styles
- ============================================ */
-
-.activity-feed-modal .activity-feed-content {
- max-width: 1200px;
- width: 94vw;
- max-height: 90vh;
- display: flex;
- flex-direction: column;
+.onboarding-btn-primary:hover:not(:disabled) {
+ background: linear-gradient(135deg, #58a6ff 0%, #388bfd 100%);
+ transform: translateY(-1px);
+ box-shadow: 0 6px 16px rgba(31, 111, 235, 0.44);
}
-.activity-list {
- flex: 1;
- overflow-y: auto;
- padding: var(--space-md) var(--space-lg);
+.onboarding-btn-primary:disabled {
+ border-color: transparent;
+ background: rgba(255, 255, 255, 0.11);
+ color: rgba(255, 255, 255, 0.45);
+ box-shadow: none;
+ cursor: not-allowed;
}
-.activity-empty {
- padding: var(--space-lg);
- color: var(--text-muted);
+.dependency-git-identity-fields {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ margin-bottom: 18px;
}
-.activity-event {
- border: 1px solid var(--border-color);
- background: var(--bg-secondary);
- border-radius: var(--radius-md);
- padding: var(--space-md);
- margin-bottom: var(--space-sm);
+.dependency-git-identity-field {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
}
-.activity-event.activity-failed {
- border-color: rgba(248, 81, 73, 0.55);
- box-shadow: 0 0 0 1px rgba(248, 81, 73, 0.18);
+.dependency-git-identity-field span {
+ font-size: 0.83rem;
+ color: rgba(255, 255, 255, 0.66);
}
-.activity-meta {
- display: flex;
- gap: var(--space-sm);
- align-items: center;
- justify-content: space-between;
+.dependency-git-identity-field input {
+ padding: 10px 13px;
+ border-radius: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.16);
+ background: rgba(0, 0, 0, 0.28);
+ color: #fff;
+ font-family: var(--font-sans);
+ font-size: 0.98rem;
+ transition: border-color 0.14s ease, background-color 0.14s ease, box-shadow 0.14s ease;
}
-.activity-actions {
- display: flex;
- gap: var(--space-xs);
- align-items: center;
- margin-left: auto;
+.dependency-git-identity-field input:focus {
+ outline: none;
+ border-color: #388bfd;
+ background: rgba(0, 0, 0, 0.38);
+ box-shadow: 0 0 0 2px rgba(56, 139, 253, 0.2);
}
-.activity-action-btn {
- padding: 4px 8px;
- font-size: 0.75rem;
- line-height: 1;
+.dependency-gh-login-helper-text {
+ margin-bottom: 10px;
+ font-size: 0.93rem;
+ color: rgba(255, 255, 255, 0.82);
}
-.activity-time {
- color: var(--text-muted);
- font-size: 0.8rem;
+.dependency-onboarding-command-wrap {
+ margin: 18px 0;
+ padding: 14px;
+ border-radius: 10px;
+ border: 1px solid rgba(255, 255, 255, 0.09);
+ background: rgba(0, 0, 0, 0.32);
}
-.activity-kind {
+.dependency-onboarding-command-wrap pre {
+ margin: 0;
+ color: #a5d6ff;
font-family: var(--font-mono);
- font-size: 0.8rem;
- padding: 2px 8px;
- border-radius: 999px;
- border: 1px solid transparent;
- background: var(--bg-tertiary);
- color: var(--text-secondary);
+ font-size: 0.83rem;
+ white-space: pre-wrap;
}
-.activity-kind-agent {
- border-color: var(--accent-primary);
- color: var(--accent-primary);
- background: rgba(31, 111, 235, 0.12);
+.dependency-gh-login-code-wrap {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
}
-.activity-kind-session {
- border-color: var(--accent-success);
- color: var(--accent-success);
- background: rgba(63, 185, 80, 0.12);
+.dependency-gh-login-helper-actions {
+ margin-top: 10px;
}
-.activity-kind-server {
- border-color: var(--accent-warning);
- color: var(--accent-warning);
- background: rgba(210, 153, 34, 0.12);
+.dependency-gh-login-code {
+ padding: 8px 14px;
+ border-radius: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ background: rgba(0, 0, 0, 0.5);
+ font-size: 1.1rem;
+ letter-spacing: 2px;
+ color: #fff;
}
-.activity-kind-git {
- border-color: var(--accent-success);
- color: var(--accent-success);
- background: rgba(63, 185, 80, 0.12);
+.onboarding-close-btn {
+ position: absolute;
+ top: 18px;
+ right: 18px;
+ z-index: 100;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ background: rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.78);
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
-.activity-kind-pr {
- border-color: var(--accent-primary);
- color: var(--accent-primary);
- background: rgba(31, 111, 235, 0.12);
+.onboarding-close-btn:hover {
+ background: rgba(255, 255, 255, 0.18);
+ border-color: rgba(255, 255, 255, 0.22);
+ color: #fff;
}
-.activity-kind-tests {
- border-color: var(--accent-warning);
- color: var(--accent-warning);
- background: rgba(210, 153, 34, 0.12);
+.onboarding-close-btn.hidden {
+ display: none;
}
-.activity-kind-build {
- border-color: var(--accent-warning);
- color: var(--accent-warning);
- background: rgba(210, 153, 34, 0.12);
-}
+@media (max-width: 800px) {
+ .onboarding-overlay {
+ padding: 12px;
+ }
-.activity-summary {
- margin-top: var(--space-sm);
- color: var(--text-primary);
- font-size: 0.95rem;
+ .onboarding-container {
+ padding: 20px;
+ max-height: calc(100vh - 24px);
+ }
+
+ .onboarding-step-card {
+ padding: 20px;
+ }
+
+ .onboarding-nav-row {
+ gap: 10px;
+ flex-wrap: wrap;
+ }
+
+ .onboarding-btn-back,
+ .onboarding-btn-primary {
+ flex: 1 1 100%;
+ justify-content: center;
+ }
}
-.activity-data {
- margin-top: var(--space-sm);
- color: var(--text-muted);
- font-family: var(--font-mono);
- font-size: 0.8rem;
- white-space: pre-wrap;
- word-break: break-word;
+@media (prefers-reduced-motion: reduce) {
+ .onboarding-overlay,
+ .onboarding-container,
+ .stepper-diamond,
+ .onboarding-step-card,
+ .onboarding-btn-secondary,
+ .onboarding-btn-back,
+ .onboarding-btn-primary,
+ .dependency-git-identity-field input,
+ .onboarding-close-btn {
+ animation: none !important;
+ transition: none !important;
+ }
}
diff --git a/client/styles/tabs.css b/client/styles/tabs.css
index 970f58cc..044628f4 100644
--- a/client/styles/tabs.css
+++ b/client/styles/tabs.css
@@ -11,6 +11,8 @@
z-index: 100;
overflow-x: auto;
overflow-y: hidden;
+ scrollbar-width: thin;
+ scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
.workspace-tabs {
@@ -233,20 +235,27 @@
/* Scrollbar for tab overflow */
.workspace-tabs-container::-webkit-scrollbar {
- height: 4px;
+ height: var(--scrollbar-size);
}
.workspace-tabs-container::-webkit-scrollbar-track {
- background: var(--bg-secondary);
+ background: var(--scrollbar-track);
+ border-radius: var(--scrollbar-radius);
}
.workspace-tabs-container::-webkit-scrollbar-thumb {
- background: var(--border-color);
- border-radius: 2px;
+ background: var(--scrollbar-thumb);
+ border-radius: var(--scrollbar-radius);
+ border: 2px solid var(--scrollbar-thumb-border);
+ background-clip: padding-box;
}
.workspace-tabs-container::-webkit-scrollbar-thumb:hover {
- background: var(--text-secondary);
+ background: var(--scrollbar-thumb-hover);
+}
+
+.workspace-tabs-container::-webkit-scrollbar-thumb:active {
+ background: var(--scrollbar-thumb-active);
}
/* Mobile/Small Screen Adjustments */
diff --git a/server/diagnosticsService.js b/server/diagnosticsService.js
index 7021c243..93e88416 100644
--- a/server/diagnosticsService.js
+++ b/server/diagnosticsService.js
@@ -9,14 +9,34 @@ const execFileAsync = util.promisify(execFile);
async function checkCommand(command, args, options = {}) {
const timeout = Number(options.timeoutMs) || 2500;
try {
- const { stdout, stderr } = await execFileAsync(command, args, {
+ const runOptions = {
timeout,
windowsHide: true,
maxBuffer: 1024 * 1024
- });
+ };
+
+ const commandStr = String(command || '').trim();
+ const argsArr = Array.isArray(args) ? args : [];
+ let result;
+ try {
+ result = await execFileAsync(commandStr, argsArr, runOptions);
+ } catch (error) {
+ const isWindowsScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(commandStr);
+ const shouldRetryWithCmd = isWindowsScript && (error?.code === 'EINVAL' || error?.code === 'ENOENT');
+ if (!shouldRetryWithCmd) throw error;
+ result = await execFileAsync('cmd.exe', ['/d', '/c', commandStr, ...argsArr], runOptions);
+ }
+
+ const { stdout, stderr } = result || {};
const output = String(stdout || stderr || '').trim();
const firstLine = output.split(/\r?\n/).find(Boolean) || '';
- return { ok: true, command, args, version: firstLine || null };
+ return {
+ ok: true,
+ command,
+ args,
+ version: firstLine || null,
+ output: output || null
+ };
} catch (error) {
const code = error?.code || null;
const message = String(error?.message || error || '').trim();
@@ -35,6 +55,86 @@ async function checkFirstAvailable(candidates) {
return await checkCommand(last.command, last.args, last.options);
}
+function uniqueCommandCandidates(candidates = []) {
+ const seen = new Set();
+ const out = [];
+ for (const candidate of candidates) {
+ const command = String(candidate?.command || '').trim();
+ if (!command) continue;
+ const args = Array.isArray(candidate?.args) ? candidate.args : [];
+ const key = `${command}::${JSON.stringify(args)}`;
+ if (seen.has(key)) continue;
+ seen.add(key);
+ out.push({ command, args, options: candidate?.options });
+ }
+ return out;
+}
+
+async function checkNpmGlobalPackage(npmCommand, packageName) {
+ const npm = String(npmCommand || '').trim();
+ const pkg = String(packageName || '').trim();
+ if (!npm || !pkg) {
+ return { ok: false, error: 'Missing npm command or package name' };
+ }
+
+ const res = await checkCommand(npm, ['list', '-g', pkg, '--depth=0'], { timeoutMs: 7000 });
+ const combined = String(res?.output || res?.version || '').trim();
+ const pkgPattern = new RegExp(`${pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}@([^\\s]+)`, 'i');
+ const versionMatch = combined.match(pkgPattern);
+ if (!res.ok || !versionMatch?.[1]) {
+ return {
+ ok: false,
+ command: npm,
+ args: ['list', '-g', pkg, '--depth=0'],
+ error: String(res?.error || `Package ${pkg} not found in npm global list`)
+ };
+ }
+
+ return {
+ ok: true,
+ command: `npm-global:${pkg}`,
+ args: ['list', '-g', pkg, '--depth=0'],
+ version: `${pkg}@${versionMatch[1]} (npm global)`
+ };
+}
+
+async function checkGitIdentity(gitCommand, gitInstalled) {
+ const command = String(gitCommand || 'git').trim() || 'git';
+ if (!gitInstalled) {
+ return {
+ ok: false,
+ command,
+ args: ['config', '--global', '--get', 'user.name'],
+ error: 'Git is not installed'
+ };
+ }
+
+ const nameCheck = await checkCommand(command, ['config', '--global', '--get', 'user.name']);
+ const emailCheck = await checkCommand(command, ['config', '--global', '--get', 'user.email']);
+ const name = String(nameCheck?.version || '').trim();
+ const email = String(emailCheck?.version || '').trim();
+
+ if (name && email) {
+ return {
+ ok: true,
+ command,
+ args: ['config', '--global', '--get', 'user.name,user.email'],
+ version: `${name} <${email}>`
+ };
+ }
+
+ const missing = [];
+ if (!name) missing.push('user.name');
+ if (!email) missing.push('user.email');
+
+ return {
+ ok: false,
+ command,
+ args: ['config', '--global', '--get', 'user.name,user.email'],
+ error: `Missing global Git setting(s): ${missing.join(', ')}`
+ };
+}
+
function findTool(tools, id) {
if (!Array.isArray(tools)) return null;
return tools.find((tool) => String(tool?.id || '') === String(id || '')) || null;
@@ -86,47 +186,128 @@ async function collectDiagnostics() {
const tools = [];
+ const nodeCandidates = uniqueCommandCandidates([
+ { command: 'node', args: ['--version'] },
+ { command: platform === 'win32' ? 'node.exe' : 'node', args: ['--version'] },
+ platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'nodejs', 'node.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'nodejs', 'node.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'nodejs', 'node.exe'), args: ['--version'] } : null,
+ { command: process.execPath || 'node', args: ['--version'] }
+ ]);
+ const nodeCheck = await checkFirstAvailable(nodeCandidates);
+ const nodeCommand = String(nodeCheck?.command || '').trim();
+ const nodeDir = nodeCommand ? path.dirname(nodeCommand) : '';
+
+ const npmCandidates = uniqueCommandCandidates([
+ { command: platform === 'win32' ? 'npm.cmd' : 'npm', args: ['--version'] },
+ platform === 'win32' ? { command: 'npm', args: ['--version'] } : null,
+ platform === 'win32' && nodeDir ? { command: path.join(nodeDir, 'npm.cmd'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'nodejs', 'npm.cmd'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'nodejs', 'npm.cmd'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'nodejs', 'npm.cmd'), args: ['--version'] } : null
+ ]);
+ const npmCheck = await checkFirstAvailable(npmCandidates);
+
tools.push({
id: 'node',
name: 'Node.js',
- ...(await checkCommand(process.execPath || 'node', ['--version']))
+ ...nodeCheck
});
tools.push({
id: 'npm',
name: 'npm',
- ...(await checkCommand(platform === 'win32' ? 'npm.cmd' : 'npm', ['--version']))
+ ...npmCheck
});
+ const gitCandidates = uniqueCommandCandidates([
+ { command: 'git', args: ['--version'] },
+ platform === 'win32' ? { command: 'git.exe', args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'Git', 'cmd', 'git.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'Git', 'bin', 'git.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'cmd', 'git.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'Git', 'bin', 'git.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Git', 'cmd', 'git.exe'), args: ['--version'] } : null
+ ]);
+
tools.push({
id: 'git',
name: 'Git',
- ...(await checkCommand('git', ['--version']))
+ ...(await checkFirstAvailable(gitCandidates))
+ });
+ const gitTool = tools[tools.length - 1];
+ tools.push({
+ id: 'gitIdentity',
+ name: 'Git identity',
+ ...(await checkGitIdentity(gitTool?.command, !!gitTool?.ok))
});
+ const ghCandidates = uniqueCommandCandidates([
+ { command: 'gh', args: ['--version'] },
+ platform === 'win32' ? { command: 'gh.exe', args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.ProgramFiles || '', 'GitHub CLI', 'gh.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env['ProgramFiles(x86)'] || '', 'GitHub CLI', 'gh.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'GitHub CLI', 'gh.exe'), args: ['--version'] } : null
+ ]);
+ const ghCheck = await checkFirstAvailable(ghCandidates);
tools.push({
id: 'gh',
name: 'GitHub CLI',
- ...(await checkCommand('gh', ['--version']))
+ ...ghCheck
});
// Auth status is the most common root cause of "0 files/commits" in PR tooling on Windows.
// We keep it lightweight: first line of `gh auth status` is enough to spot "not logged in".
+ const ghAuthCheck = ghCheck?.ok
+ ? await checkCommand(String(ghCheck.command || 'gh'), ['auth', 'status'])
+ : {
+ ok: false,
+ command: String(ghCheck?.command || 'gh'),
+ args: ['auth', 'status'],
+ error: 'GitHub CLI is not installed'
+ };
tools.push({
id: 'ghAuth',
name: 'GitHub CLI auth',
- ...(await checkCommand('gh', ['auth', 'status']))
+ ...ghAuthCheck
});
+ const claudeCandidates = uniqueCommandCandidates([
+ { command: 'claude', args: ['--version'] },
+ platform === 'win32' ? { command: 'claude.cmd', args: ['--version'] } : null,
+ platform === 'win32' ? { command: 'claude.exe', args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'claude.cmd'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'claude'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WinGet', 'Links', 'claude.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.USERPROFILE || '', '.local', 'bin', 'claude.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.USERPROFILE || '', '.claude', 'local', 'claude.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Claude', 'claude.exe'), args: ['--version'] } : null
+ ]);
tools.push({
id: 'claude',
name: 'Claude Code',
- ...(await checkCommand('claude', ['--version']))
+ ...(await checkFirstAvailable(claudeCandidates))
});
+ const codexCandidates = uniqueCommandCandidates([
+ { command: 'codex', args: ['--version'] },
+ platform === 'win32' ? { command: 'codex.cmd', args: ['--version'] } : null,
+ platform === 'win32' ? { command: 'codex.exe', args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'codex.cmd'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.APPDATA || '', 'npm', 'codex'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WinGet', 'Links', 'codex.exe'), args: ['--version'] } : null,
+ platform === 'win32' ? { command: path.join(process.env.USERPROFILE || '', '.local', 'bin', 'codex.exe'), args: ['--version'] } : null
+ ]);
+ let codexCheck = await checkFirstAvailable(codexCandidates);
+ if (!codexCheck?.ok && npmCheck?.ok) {
+ const npmPackageCheck = await checkNpmGlobalPackage(String(npmCheck.command || '').trim(), '@openai/codex');
+ if (npmPackageCheck?.ok) {
+ codexCheck = npmPackageCheck;
+ }
+ }
tools.push({
id: 'codex',
name: 'Codex CLI',
- ...(await checkCommand('codex', ['--version']))
+ ...codexCheck
});
tools.push({
diff --git a/server/index.js b/server/index.js
index e3a9a62b..0fe86de2 100644
--- a/server/index.js
+++ b/server/index.js
@@ -99,6 +99,13 @@ const voiceCommandService = require('./voiceCommandService');
const whisperService = require('./whisperService');
const sessionRecoveryService = require('./sessionRecoveryService');
const { collectDiagnostics, collectFirstRunDiagnostics, collectInstallWizard, runFirstRunRepair, runFirstRunSafeRepairs } = require('./diagnosticsService');
+const {
+ getSetupActions,
+ runSetupAction,
+ getSetupActionRun,
+ getLatestSetupActionRun,
+ configureGitIdentity
+} = require('./setupActionService');
const { PluginLoaderService } = require('./pluginLoaderService');
const { SchedulerService } = require('./schedulerService');
const { PagerService } = require('./pagerService');
@@ -4050,6 +4057,123 @@ app.get('/api/lifecycle/policy', (req, res) => {
}
});
+// Setup helper actions for first-run dependency wizard.
+app.get('/api/setup-actions', (req, res) => {
+ try {
+ const platform = process.platform;
+ const actions = getSetupActions(platform);
+ res.json({ ok: true, platform, actions });
+ } catch (error) {
+ logger.error('Failed to get setup actions', { error: error.message, stack: error.stack });
+ res.status(500).json({ ok: false, error: 'Failed to get setup actions' });
+ }
+});
+
+app.post('/api/setup-actions/run', (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', 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', (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 {
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/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 076c0231..97dbed8b 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -10,6 +10,14 @@ use uuid::Uuid;
use tauri_plugin_updater::UpdaterExt;
use url::Url;
+#[cfg(target_os = "windows")]
+use std::os::windows::process::CommandExt;
+
+#[cfg(target_os = "windows")]
+const CREATE_NO_WINDOW: u32 = 0x08000000;
+#[cfg(target_os = "windows")]
+const DETACHED_PROCESS: u32 = 0x00000008;
+
mod terminal;
mod file_watcher;
use terminal::{TerminalManager, TerminalOutput};
@@ -602,6 +610,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 +622,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!(
From d696ca7d65430e097a8f7921ab2f585ca144bc85 Mon Sep 17 00:00:00 2001
From: web3dev1337
Date: Mon, 2 Mar 2026 21:20:55 -0800
Subject: [PATCH 02/15] fix(windows): prevent updater plugin startup panic
---
src-tauri/tauri.conf.json | 5 +++++
1 file changed, 5 insertions(+)
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": ""
+ }
}
}
From 37c09c965bdb61243c88e47070ea74ae51b7b148 Mon Sep 17 00:00:00 2001
From: AnrokX <192667251+AnrokX@users.noreply.github.com>
Date: Wed, 4 Mar 2026 17:31:12 -0600
Subject: [PATCH 03/15] fix(onboarding): prevent welcome modal reopening after
completion
---
client/app.js | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/client/app.js b/client/app.js
index e4f8dec4..692d1ad2 100644
--- a/client/app.js
+++ b/client/app.js
@@ -10057,7 +10057,7 @@ class ClaudeOrchestrator {
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
- const loadAndRender = async ({ open = false, forceAutoShow = false, bootstrap = false } = {}) => {
+ const loadAndRender = async ({ open = false, forceAutoShow = false, bootstrap = false, explicitOpen = false } = {}) => {
if (state.loading) return false;
setLoading(true);
try {
@@ -10097,8 +10097,11 @@ class ClaudeOrchestrator {
applyOnboardingLockUI();
if (view.req?.coreReady) writeDismissed(false);
- const shouldAutoShow = forceAutoShow || (!readDismissed() && (!readCompleted() || !(view.req?.coreReady)));
- if (open || shouldAutoShow) {
+ const hasCompletedOnboarding = readCompleted();
+ const coreReady = !!view.req?.coreReady;
+ const shouldAutoShow = (!hasCompletedOnboarding || !coreReady) && (forceAutoShow || !readDismissed());
+ const shouldKeepVisible = open && !modal.classList.contains('hidden');
+ if (explicitOpen || shouldKeepVisible || shouldAutoShow) {
openModal();
} else {
setBootstrapPending(false);
@@ -10107,7 +10110,8 @@ class ClaudeOrchestrator {
} catch (err) {
summaryEl.textContent = `Dependency check failed: ${String(err?.message || err)}`;
listEl.innerHTML = 'Unable to load setup actions right now.
';
- if (open) openModal();
+ const shouldOpenOnError = explicitOpen || (open && !modal.classList.contains('hidden'));
+ if (shouldOpenOnError) openModal();
else if (!bootstrap) setBootstrapPending(false);
return false;
} finally {
@@ -10634,7 +10638,7 @@ class ClaudeOrchestrator {
openBtn.addEventListener('click', () => {
writeDismissed(false);
setCurrentStep(0);
- loadAndRender({ open: true, forceAutoShow: true });
+ loadAndRender({ open: true, forceAutoShow: true, explicitOpen: true });
});
}
if (closeBtn) {
From 67ab3f3a741c43c550e857b62b75d507c55e0473 Mon Sep 17 00:00:00 2001
From: AnrokX <192667251+AnrokX@users.noreply.github.com>
Date: Wed, 4 Mar 2026 18:01:26 -0600
Subject: [PATCH 04/15] fix(onboarding): harden setup wizard gating and route
policy
---
client/app.js | 8 +++++++-
client/index.html | 1 -
server/index.js | 6 +++---
3 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/client/app.js b/client/app.js
index 692d1ad2..23850196 100644
--- a/client/app.js
+++ b/client/app.js
@@ -9356,6 +9356,10 @@ class ClaudeOrchestrator {
body?.classList?.remove?.('dependency-onboarding-booting');
};
setBootstrapPending(true);
+ if (!isWindowsHost) {
+ setBootstrapPending(false);
+ return;
+ }
const dismissKey = 'orchestrator-dependency-setup-dismissed-v3';
const completedKey = 'orchestrator-dependency-onboarding-completed-v2';
@@ -9653,6 +9657,8 @@ class ClaudeOrchestrator {
};
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;
@@ -10099,7 +10105,7 @@ class ClaudeOrchestrator {
const hasCompletedOnboarding = readCompleted();
const coreReady = !!view.req?.coreReady;
- const shouldAutoShow = (!hasCompletedOnboarding || !coreReady) && (forceAutoShow || !readDismissed());
+ const shouldAutoShow = isWindowsHost && (!hasCompletedOnboarding || !coreReady) && (forceAutoShow || !readDismissed());
const shouldKeepVisible = open && !modal.classList.contains('hidden');
if (explicitOpen || shouldKeepVisible || shouldAutoShow) {
openModal();
diff --git a/client/index.html b/client/index.html
index c27ae39b..99b37010 100644
--- a/client/index.html
+++ b/client/index.html
@@ -679,7 +679,6 @@ Scheduler
Click โRefresh schedulerโ.
-
Pager / Pollcat
diff --git a/server/index.js b/server/index.js
index 0fe86de2..7f589ae0 100644
--- a/server/index.js
+++ b/server/index.js
@@ -4069,7 +4069,7 @@ app.get('/api/setup-actions', (req, res) => {
}
});
-app.post('/api/setup-actions/run', (req, res) => {
+app.post('/api/setup-actions/run', requirePolicyAction('write'), express.json(), (req, res) => {
try {
const actionId = String(req.body?.actionId || '').trim();
if (!actionId) {
@@ -4086,7 +4086,7 @@ app.post('/api/setup-actions/run', (req, res) => {
}
});
-app.post('/api/setup-actions/configure-git-identity', async (req, res) => {
+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();
@@ -4128,7 +4128,7 @@ app.get('/api/setup-actions/run-status', (req, res) => {
}
});
-app.post('/api/setup-actions/open-url', (req, res) => {
+app.post('/api/setup-actions/open-url', requirePolicyAction('write'), express.json(), (req, res) => {
try {
const rawUrl = String(req.body?.url || '').trim();
if (!rawUrl) {
From 9e15a296e218939805875757f0188f8748c89415 Mon Sep 17 00:00:00 2001
From: AnrokX <192667251+AnrokX@users.noreply.github.com>
Date: Wed, 4 Mar 2026 19:30:59 -0600
Subject: [PATCH 05/15] fix: restore startup workspace and defer session init
until workspace ready
---
server/index.js | 39 ++++++++++++++++++++++--------------
server/workspaceManager.js | 41 ++++++++++++++++++++++++--------------
2 files changed, 50 insertions(+), 30 deletions(-)
diff --git a/server/index.js b/server/index.js
index 7f589ae0..0a4e1831 100644
--- a/server/index.js
+++ b/server/index.js
@@ -407,6 +407,7 @@ sessionManager.setGitHelper(gitHelper);
// Initialize workspace system
let workspaceInitialized = false;
+let workspaceSystemReady = null;
async function initializeWorkspaceSystem() {
try {
logger.info('Initializing workspace system...');
@@ -432,21 +433,23 @@ async function initializeWorkspaceSystem() {
}
// Initialize workspace system before starting server
-initializeWorkspaceSystem().then(() => {
- logger.info('Workspace system initialized');
- loadPlugins()
- .then((status) => {
- logger.info('Plugin loader finished', {
- loaded: Array.isArray(status?.loaded) ? status.loaded.length : 0,
- failed: Array.isArray(status?.failed) ? status.failed.length : 0
- });
- })
- .catch((error) => {
- logger.error('Plugin loader failed', { error: error.message, stack: error.stack });
+workspaceSystemReady = initializeWorkspaceSystem()
+ .then(() => {
+ logger.info('Workspace system initialized');
+ return true;
+ })
+ .then(() => loadPlugins())
+ .then((status) => {
+ logger.info('Plugin loader finished', {
+ loaded: Array.isArray(status?.loaded) ? status.loaded.length : 0,
+ failed: Array.isArray(status?.failed) ? status.failed.length : 0
});
-}).catch(error => {
- logger.error('Workspace system initialization failed', { error: error.message, stack: error.stack });
-});
+ return true;
+ })
+ .catch(error => {
+ logger.error('Workspace system initialization failed', { error: error.message, stack: error.stack });
+ return false;
+ });
// WebSocket connection handling
io.on('connection', (socket) => {
@@ -7960,7 +7963,13 @@ httpServer.listen(PORT, HOST, () => {
}
})();
- sessionManager.initializeSessions()
+ workspaceSystemReady
+ .then((workspaceReady) => {
+ if (!workspaceReady) {
+ return;
+ }
+ return sessionManager.initializeSessions();
+ })
.then(() => {
if (!shouldAutoEnsureDiscordServices) return;
// Donโt block server startup; just best-effort keep Services running after restarts.
diff --git a/server/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
From 8a004fd8b459770fe9f857de3350473b8118818c Mon Sep 17 00:00:00 2001
From: AnrokX <192667251+AnrokX@users.noreply.github.com>
Date: Thu, 5 Mar 2026 20:54:47 -0600
Subject: [PATCH 06/15] fix: harden windows tauri node packaging
---
.nvmrc | 2 +-
package-lock.json | 22 +++---
package.json | 2 +-
scripts/ensure-pty.js | 52 ++++++++++++--
scripts/tauri/prepare-backend-resources.js | 79 +++++++++++++++++++---
server/sessionManager.js | 9 ++-
6 files changed, 138 insertions(+), 28 deletions(-)
diff --git a/.nvmrc b/.nvmrc
index 25649a2b..a45fd52c 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-24.9.0
+24
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/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/sessionManager.js b/server/sessionManager.js
index d439f289..d7fdbe28 100644
--- a/server/sessionManager.js
+++ b/server/sessionManager.js
@@ -2364,7 +2364,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;
From 740c6eb58df41103dbb28e5b1f08ee891d7ae5d9 Mon Sep 17 00:00:00 2001
From: AnrokX <192667251+AnrokX@users.noreply.github.com>
Date: Thu, 5 Mar 2026 22:36:53 -0600
Subject: [PATCH 07/15] fix: dedupe workspace switch retries
---
client/workspace-tab-manager.js | 5 -----
server/index.js | 23 +++++++++++++++++++----
2 files changed, 19 insertions(+), 9 deletions(-)
diff --git a/client/workspace-tab-manager.js b/client/workspace-tab-manager.js
index e9a33d90..037aa480 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;
}
diff --git a/server/index.js b/server/index.js
index 0a4e1831..4be824cc 100644
--- a/server/index.js
+++ b/server/index.js
@@ -454,6 +454,7 @@ workspaceSystemReady = initializeWorkspaceSystem()
// WebSocket connection handling
io.on('connection', (socket) => {
logger.info('Client connected', { socketId: socket.id });
+ let inFlightWorkspaceSwitchId = null;
// Send workspace info
const activeWorkspace = workspaceManager.getActiveWorkspace();
@@ -865,17 +866,27 @@ io.on('connection', (socket) => {
// Workspace management handlers
socket.on('switch-workspace', async ({ workspaceId }) => {
+ const requestedWorkspaceId = String(workspaceId || '').trim();
+ if (requestedWorkspaceId && inFlightWorkspaceSwitchId === requestedWorkspaceId) {
+ logger.info('Ignoring duplicate workspace switch request while switch is already in progress', {
+ workspaceId: requestedWorkspaceId,
+ socketId: socket.id
+ });
+ return;
+ }
+
+ inFlightWorkspaceSwitchId = requestedWorkspaceId || null;
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 });
- 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');
@@ -916,12 +927,16 @@ io.on('connection', (socket) => {
});
} catch (error) {
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 (inFlightWorkspaceSwitchId === requestedWorkspaceId) {
+ inFlightWorkspaceSwitchId = null;
+ }
}
});
From eb9e62c59ca206a06d141df8da53467fa7f58bd9 Mon Sep 17 00:00:00 2001
From: AnrokX <192667251+AnrokX@users.noreply.github.com>
Date: Fri, 6 Mar 2026 15:20:24 -0600
Subject: [PATCH 08/15] fix: streamline windows desktop startup
---
CODEBASE_DOCUMENTATION.md | 1 +
client/app.js | 57 ++++++++++++++++--
server/claudeVersionChecker.js | 5 +-
server/sessionManager.js | 13 -----
server/userSettingsService.js | 21 +++++++
.../sessionManager.initializeSessions.test.js | 58 +++++++++++++++++++
tests/unit/userSettingsDefaults.test.js | 31 ++++++++++
7 files changed, 167 insertions(+), 19 deletions(-)
create mode 100644 tests/unit/sessionManager.initializeSessions.test.js
diff --git a/CODEBASE_DOCUMENTATION.md b/CODEBASE_DOCUMENTATION.md
index 41397f7b..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
diff --git a/client/app.js b/client/app.js
index 23850196..0ba18a77 100644
--- a/client/app.js
+++ b/client/app.js
@@ -1001,6 +1001,8 @@ class ClaudeOrchestrator {
// Load user settings from server
await this.loadUserSettings();
+ this.syncDependencySetupWizardPreferences?.();
+ void this.bootstrapDependencySetupWizard?.();
this.applySidebarDesktopCollapsedFromPrefs();
this.refreshLicenseStatus?.().catch(() => {});
this.syncTerminalFiltersFromUserSettings();
@@ -1369,6 +1371,7 @@ class ClaudeOrchestrator {
this.applyThemeFromUserSettings();
this.applySimpleModeConfig();
this.maybeAutoOpenSimpleMode();
+ this.syncDependencySetupWizardPreferences?.();
});
// Workspace events
@@ -9345,6 +9348,8 @@ class ClaudeOrchestrator {
return false;
}
})();
+ const isDesktopWindowsApp = isWindowsHost && !!window.__TAURI__;
+ let desktopCompleted = false;
const setBootstrapPending = (pending) => {
if (!isWindowsHost) return;
@@ -9355,11 +9360,11 @@ class ClaudeOrchestrator {
}
body?.classList?.remove?.('dependency-onboarding-booting');
};
- setBootstrapPending(true);
if (!isWindowsHost) {
setBootstrapPending(false);
return;
}
+ setBootstrapPending(false);
const dismissKey = 'orchestrator-dependency-setup-dismissed-v3';
const completedKey = 'orchestrator-dependency-onboarding-completed-v2';
@@ -9381,6 +9386,13 @@ class ClaudeOrchestrator {
gitIdentityHelpVisible: false
};
+ const syncDesktopCompleted = () => {
+ if (!isDesktopWindowsApp) return false;
+ desktopCompleted = !!this.userSettings?.global?.ui?.onboarding?.desktopDependencySetup?.completed;
+ return desktopCompleted;
+ };
+ syncDesktopCompleted();
+
const readDismissed = () => {
try {
return localStorage.getItem(dismissKey) === 'true';
@@ -9399,6 +9411,9 @@ class ClaudeOrchestrator {
};
const readCompleted = () => {
+ if (isDesktopWindowsApp) {
+ return syncDesktopCompleted();
+ }
try {
return localStorage.getItem(completedKey) === 'true';
} catch {
@@ -9406,7 +9421,18 @@ class ClaudeOrchestrator {
}
};
- const writeCompleted = (value) => {
+ const writeCompleted = async (value) => {
+ if (isDesktopWindowsApp) {
+ const next = !!value;
+ desktopCompleted = next;
+ if (this.userSettings) {
+ await this.updateGlobalUserSetting('ui.onboarding.desktopDependencySetup', {
+ completed: next,
+ completedAt: next ? new Date().toISOString() : null
+ });
+ }
+ return;
+ }
try {
if (value) localStorage.setItem(completedKey, 'true');
else localStorage.removeItem(completedKey);
@@ -10340,6 +10366,11 @@ class ClaudeOrchestrator {
};
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) {
@@ -10550,7 +10581,7 @@ class ClaudeOrchestrator {
setStepSkipped(currentStep?.id, false);
}
if (state.currentStep >= (total - 1)) {
- writeCompleted(true);
+ await writeCompleted(true);
writeDismissed(false);
closeModal({ force: true });
this.showToast('Dependency onboarding complete.', 'success');
@@ -10661,7 +10692,24 @@ class ClaudeOrchestrator {
closeModal();
});
- runBootstrapLoad();
+ this.syncDependencySetupWizardPreferences = () => {
+ syncDesktopCompleted();
+ if (isDesktopWindowsApp && desktopCompleted) {
+ setBootstrapPending(false);
+ }
+ applyOnboardingLockUI();
+ };
+ this.bootstrapDependencySetupWizard = () => {
+ syncDesktopCompleted();
+ if (isDesktopWindowsApp && readCompleted()) {
+ setBootstrapPending(false);
+ return Promise.resolve(false);
+ }
+ return runBootstrapLoad();
+ };
+ if (this.userSettings) {
+ void this.bootstrapDependencySetupWizard();
+ }
}
notifyWorkflow({ type = 'info', message = '', sessionId = null, metadata = null } = {}) {
@@ -16251,6 +16299,7 @@ class ClaudeOrchestrator {
this.applySimpleModeConfig();
this.maybeAutoOpenSimpleMode();
this.applyUiVisibility();
+ this.syncDependencySetupWizardPreferences?.();
this.refreshBranchLabels();
this.updateTierFilterButtons();
} else {
diff --git a/server/claudeVersionChecker.js b/server/claudeVersionChecker.js
index 5b1e45f7..89978c78 100644
--- a/server/claudeVersionChecker.js
+++ b/server/claudeVersionChecker.js
@@ -19,7 +19,8 @@ class ClaudeVersionChecker {
return new Promise((resolve) => {
const process = spawn('claude', ['--version'], {
stdio: ['ignore', 'pipe', 'pipe'],
- timeout: 5000
+ timeout: 5000,
+ windowsHide: true
});
let stdout = '';
@@ -102,4 +103,4 @@ class ClaudeVersionChecker {
}
}
-module.exports = { ClaudeVersionChecker };
\ No newline at end of file
+module.exports = { ClaudeVersionChecker };
diff --git a/server/sessionManager.js b/server/sessionManager.js
index d7fdbe28..d92422ce 100644
--- a/server/sessionManager.js
+++ b/server/sessionManager.js
@@ -455,19 +455,6 @@ 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
- });
- })
- );
- }
}
}
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/tests/unit/sessionManager.initializeSessions.test.js b/tests/unit/sessionManager.initializeSessions.test.js
new file mode 100644
index 00000000..9f8d161e
--- /dev/null
+++ b/tests/unit/sessionManager.initializeSessions.test.js
@@ -0,0 +1,58 @@
+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');
+ expect(sm.updateGitBranch).toHaveBeenNthCalledWith(2, 'work2', '/tmp/test/work2');
+ });
+});
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');
+ });
});
From 5d0873557027a0e4dc6e687bf5219f4a2f71cb91 Mon Sep 17 00:00:00 2001
From: AnrokX <192667251+AnrokX@users.noreply.github.com>
Date: Fri, 6 Mar 2026 15:54:02 -0600
Subject: [PATCH 09/15] fix: calm windows desktop startup
---
client/app.js | 8 ++++++--
server/githubRepoService.js | 8 ++++++--
server/worktreeHelper.js | 14 +++++++++-----
tests/unit/githubRepoService.listRepos.test.js | 6 ++++++
4 files changed, 27 insertions(+), 9 deletions(-)
diff --git a/client/app.js b/client/app.js
index 0ba18a77..3282a188 100644
--- a/client/app.js
+++ b/client/app.js
@@ -10129,9 +10129,13 @@ class ClaudeOrchestrator {
applyOnboardingLockUI();
if (view.req?.coreReady) writeDismissed(false);
- const hasCompletedOnboarding = readCompleted();
+ let hasCompletedOnboarding = readCompleted();
const coreReady = !!view.req?.coreReady;
- const shouldAutoShow = isWindowsHost && (!hasCompletedOnboarding || !coreReady) && (forceAutoShow || !readDismissed());
+ if (isDesktopWindowsApp && coreReady && !hasCompletedOnboarding) {
+ await writeCompleted(true);
+ hasCompletedOnboarding = readCompleted();
+ }
+ const shouldAutoShow = isWindowsHost && !hasCompletedOnboarding && (forceAutoShow || !readDismissed());
const shouldKeepVisible = open && !modal.classList.contains('hidden');
if (explicitOpen || shouldKeepVisible || shouldAutoShow) {
openModal();
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/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/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 () => {
From 2f86a92e6fb5f8952ccc5cf7476fbc581e971cc6 Mon Sep 17 00:00:00 2001
From: AnrokX <192667251+AnrokX@users.noreply.github.com>
Date: Fri, 6 Mar 2026 16:24:13 -0600
Subject: [PATCH 10/15] debug: trace windows desktop launch
---
client/app.js | 275 +++++++++++++++++++++++++++-
client/dashboard.js | 2 +-
client/greenfield-wizard.js | 2 +-
client/quick-links.js | 2 +-
client/workspace-switcher.js | 4 +-
client/workspace-tab-manager.js | 2 +-
server/desktopLaunchTraceService.js | 74 ++++++++
server/index.js | 134 +++++++++++++-
server/sessionManager.js | 120 +++++++++++-
9 files changed, 594 insertions(+), 21 deletions(-)
create mode 100644 server/desktopLaunchTraceService.js
diff --git a/client/app.js b/client/app.js
index 3282a188..f4e5e952 100644
--- a/client/app.js
+++ b/client/app.js
@@ -116,9 +116,130 @@ 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() {
+ try {
+ const platform = String(navigator?.platform || '').toLowerCase();
+ const userAgent = String(navigator?.userAgent || '').toLowerCase();
+ return !!window.__TAURI__ && (platform.includes('win') || userAgent.includes('windows'));
+ } catch {
+ return false;
+ }
+ }
+
+ createDesktopLaunchTraceId() {
+ return `launch-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
+ }
+
+ 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;
+ 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;
@@ -848,6 +969,12 @@ class ClaudeOrchestrator {
async init() {
try {
+ void this.traceDesktopLaunch('client.init.start', {
+ tauri: !!window.__TAURI__,
+ userAgent: String(navigator?.userAgent || ''),
+ platform: String(navigator?.platform || '')
+ });
+
// Initialize managers
this.terminalManager = new TerminalManager(this);
this.terminalManager.autosuggestEnabled = this.settings.autoSuggestions !== false;
@@ -1030,6 +1157,9 @@ class ClaudeOrchestrator {
} 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');
}
}
@@ -1050,12 +1180,20 @@ class ClaudeOrchestrator {
// 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') {
@@ -1066,12 +1204,19 @@ class ClaudeOrchestrator {
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) {
@@ -1366,6 +1511,10 @@ class ClaudeOrchestrator {
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();
@@ -1377,6 +1526,12 @@ class ClaudeOrchestrator {
// 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;
@@ -1452,6 +1607,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) {
@@ -3578,6 +3738,11 @@ class ClaudeOrchestrator {
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
@@ -9350,6 +9515,9 @@ class ClaudeOrchestrator {
})();
const isDesktopWindowsApp = isWindowsHost && !!window.__TAURI__;
let desktopCompleted = false;
+ const traceOnboarding = (event, details = {}) => {
+ void this.traceDesktopLaunch(`client.onboarding.${event}`, details);
+ };
const setBootstrapPending = (pending) => {
if (!isWindowsHost) return;
@@ -9392,6 +9560,11 @@ class ClaudeOrchestrator {
return desktopCompleted;
};
syncDesktopCompleted();
+ traceOnboarding('wizard-ready', {
+ isWindowsHost,
+ isDesktopWindowsApp,
+ desktopCompleted
+ });
const readDismissed = () => {
try {
@@ -9425,11 +9598,24 @@ class ClaudeOrchestrator {
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;
}
@@ -10054,6 +10240,10 @@ class ClaudeOrchestrator {
const closeModal = ({ force = false } = {}) => {
const locked = applyOnboardingLockUI();
+ traceOnboarding('modal-close-requested', {
+ force,
+ locked
+ });
if (!force && locked) {
openModal();
return false;
@@ -10065,6 +10255,10 @@ class ClaudeOrchestrator {
};
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');
@@ -10091,11 +10285,17 @@ class ClaudeOrchestrator {
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'),
- fetch('/api/setup-actions')
+ fetch('/api/diagnostics', this.withLaunchTraceHeaders()),
+ fetch('/api/setup-actions', this.withLaunchTraceHeaders())
]);
const diagData = await diagRes.json().catch(() => ({}));
const actionsData = await actionsRes.json().catch(() => ({}));
@@ -10132,11 +10332,34 @@ class ClaudeOrchestrator {
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 {
@@ -10144,6 +10367,13 @@ class ClaudeOrchestrator {
}
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'));
@@ -10698,6 +10928,9 @@ class ClaudeOrchestrator {
this.syncDependencySetupWizardPreferences = () => {
syncDesktopCompleted();
+ traceOnboarding('preferences-synced', {
+ desktopCompleted
+ });
if (isDesktopWindowsApp && desktopCompleted) {
setBootstrapPending(false);
}
@@ -10705,8 +10938,14 @@ class ClaudeOrchestrator {
};
this.bootstrapDependencySetupWizard = () => {
syncDesktopCompleted();
+ traceOnboarding('bootstrap-requested', {
+ desktopCompleted
+ });
if (isDesktopWindowsApp && readCompleted()) {
setBootstrapPending(false);
+ traceOnboarding('bootstrap-skipped', {
+ reason: 'already-completed'
+ });
return Promise.resolve(false);
}
return runBootstrapLoad();
@@ -16294,10 +16533,15 @@ 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();
@@ -16308,9 +16552,15 @@ class ClaudeOrchestrator {
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)
+ });
}
}
@@ -16340,22 +16590,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)
+ });
}
}
@@ -17364,7 +17627,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 } = {}) {
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/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/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 037aa480..d67bfdd9 100644
--- a/client/workspace-tab-manager.js
+++ b/client/workspace-tab-manager.js
@@ -265,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/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/index.js b/server/index.js
index 4be824cc..968293cd 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');
@@ -215,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}`);
@@ -865,13 +936,28 @@ io.on('connection', (socket) => {
});
// Workspace management handlers
- socket.on('switch-workspace', async ({ workspaceId }) => {
- const requestedWorkspaceId = String(workspaceId || '').trim();
+ 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;
}
@@ -885,6 +971,13 @@ io.on('connection', (socket) => {
});
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(requestedWorkspaceId);
@@ -894,7 +987,12 @@ io.on('connection', (socket) => {
// Switch active workspace while preserving existing PTYs for other workspace tabs.
const { sessions: newSessions, backlog } =
- await sessionManager.switchWorkspacePreservingSessions(newWorkspace);
+ await sessionManager.switchWorkspacePreservingSessions(newWorkspace, {
+ reason: 'workspace-switch',
+ traceId,
+ source,
+ socketId: socket.id
+ });
// Emit success with ONLY the new workspace sessions (active workspace map)
logger.info('Sending workspace-changed event', {
@@ -919,6 +1017,15 @@ 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
+ });
activityFeed.track('workspace.switch.completed', {
fromWorkspaceId: previous?.id || null,
toWorkspaceId: newWorkspace?.id || null,
@@ -926,6 +1033,13 @@ io.on('connection', (socket) => {
socketId: socket.id
});
} catch (error) {
+ logDesktopLaunch('server.workspace-switch.failed', {
+ traceId,
+ source,
+ socketId: socket.id,
+ requestedWorkspaceId,
+ error: error.message
+ });
activityFeed.track('workspace.switch.failed', {
toWorkspaceId: requestedWorkspaceId || null,
socketId: socket.id,
@@ -3737,6 +3851,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 });
@@ -3752,6 +3869,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
@@ -3996,6 +4117,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 });
@@ -4080,6 +4202,10 @@ 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 });
@@ -7983,7 +8109,7 @@ httpServer.listen(PORT, HOST, () => {
if (!workspaceReady) {
return;
}
- return sessionManager.initializeSessions();
+ return sessionManager.initializeSessions({ reason: 'server-startup' });
})
.then(() => {
if (!shouldAutoEnsureDiscordServices) return;
diff --git a/server/sessionManager.js b/server/sessionManager.js
index d92422ce..23a2343f 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 {
@@ -161,7 +162,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 +179,13 @@ 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 });
+ await 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
+ });
// Return any buffered output that occurred while this workspace was inactive.
return {
@@ -243,9 +250,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 +327,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;
}
@@ -387,7 +417,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 +446,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', {
@@ -445,7 +483,11 @@ class SessionManager extends EventEmitter {
]),
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', {
@@ -478,6 +520,17 @@ class SessionManager extends EventEmitter {
// Wait for all sessions to be created in parallel
await Promise.all(sessionPromises);
logger.info('All sessions initialized', { count: sessionPromises.length });
+ 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) {
@@ -722,10 +775,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();
@@ -750,6 +826,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();
@@ -828,6 +915,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) {
@@ -932,6 +1031,17 @@ class SessionManager extends EventEmitter {
}, 5000);
} 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
From d0bde1bb517828aae1f7add3164872dbcc64e5f3 Mon Sep 17 00:00:00 2001
From: AnrokX <192667251+AnrokX@users.noreply.github.com>
Date: Fri, 6 Mar 2026 16:43:21 -0600
Subject: [PATCH 11/15] fix: stabilize windows desktop launch
---
client/app.js | 58 +++++++++++++++++++++++++++++++------------
src-tauri/Cargo.toml | 1 +
src-tauri/src/main.rs | 10 ++++++++
3 files changed, 53 insertions(+), 16 deletions(-)
diff --git a/client/app.js b/client/app.js
index f4e5e952..a0253115 100644
--- a/client/app.js
+++ b/client/app.js
@@ -128,17 +128,34 @@ class ClaudeOrchestrator {
}
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 !!window.__TAURI__ && (platform.includes('win') || userAgent.includes('windows'));
+ return platform.includes('win') || userAgent.includes('windows');
} catch {
return false;
}
}
- createDesktopLaunchTraceId() {
- return `launch-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
+ 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()) {
@@ -227,6 +244,15 @@ class ClaudeOrchestrator {
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,
@@ -971,6 +997,7 @@ class ClaudeOrchestrator {
try {
void this.traceDesktopLaunch('client.init.start', {
tauri: !!window.__TAURI__,
+ desktopRuntime: this.isDesktopWindowsRuntime(),
userAgent: String(navigator?.userAgent || ''),
platform: String(navigator?.platform || '')
});
@@ -9504,16 +9531,8 @@ class ClaudeOrchestrator {
const closeBtn = document.getElementById('dependency-setup-close');
if (!modal || !summaryEl || !listEl) return;
const body = document.body;
- const isWindowsHost = (() => {
- try {
- const platform = String(navigator?.platform || '').toLowerCase();
- const userAgent = String(navigator?.userAgent || '').toLowerCase();
- return platform.includes('win') || userAgent.includes('windows');
- } catch {
- return false;
- }
- })();
- const isDesktopWindowsApp = isWindowsHost && !!window.__TAURI__;
+ const isWindowsHost = this.isWindowsHostEnvironment();
+ const isDesktopWindowsApp = this.isDesktopWindowsRuntime();
let desktopCompleted = false;
const traceOnboarding = (event, details = {}) => {
void this.traceDesktopLaunch(`client.onboarding.${event}`, details);
@@ -10907,9 +10926,7 @@ class ClaudeOrchestrator {
if (openBtn) {
openBtn.addEventListener('click', () => {
- writeDismissed(false);
- setCurrentStep(0);
- loadAndRender({ open: true, forceAutoShow: true, explicitOpen: true });
+ this.openDependencySetupWizard?.({ resetStep: true, source: 'manual-open-button' });
});
}
if (closeBtn) {
@@ -10936,6 +10953,15 @@ class ClaudeOrchestrator {
}
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', {
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 97dbed8b..6841fc0b 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -529,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| {
From d56d47af389cee92332187d81f86a2953b29d0f8 Mon Sep 17 00:00:00 2001
From: AnrokX <192667251+AnrokX@users.noreply.github.com>
Date: Fri, 6 Mar 2026 17:26:58 -0600
Subject: [PATCH 12/15] fix: hide windows powershell terminal flashes
---
server/commanderService.js | 4 +-
server/sessionManager.js | 4 +-
.../sessionManager.initializeSessions.test.js | 60 +++++++++++++++++++
3 files changed, 65 insertions(+), 3 deletions(-)
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/sessionManager.js b/server/sessionManager.js
index 23a2343f..e8f699b5 100644
--- a/server/sessionManager.js
+++ b/server/sessionManager.js
@@ -50,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;
diff --git a/tests/unit/sessionManager.initializeSessions.test.js b/tests/unit/sessionManager.initializeSessions.test.js
index 9f8d161e..4dab96b0 100644
--- a/tests/unit/sessionManager.initializeSessions.test.js
+++ b/tests/unit/sessionManager.initializeSessions.test.js
@@ -55,4 +55,64 @@ describe('SessionManager.initializeSessions', () => {
expect(sm.updateGitBranch).toHaveBeenNthCalledWith(1, 'work1', '/tmp/test/work1');
expect(sm.updateGitBranch).toHaveBeenNthCalledWith(2, 'work2', '/tmp/test/work2');
});
+
+ 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');
+ });
});
From 248a0954fd2bee73b43b29112f41573da9eb483e Mon Sep 17 00:00:00 2001
From: AnrokX <192667251+AnrokX@users.noreply.github.com>
Date: Fri, 6 Mar 2026 18:45:39 -0600
Subject: [PATCH 13/15] fix: stop windows terminal flash probes
---
server/index.js | 60 +++++++++++++-
server/sessionManager.js | 38 +++++++--
.../sessionManager.initializeSessions.test.js | 82 +++++++++++++++++++
3 files changed, 168 insertions(+), 12 deletions(-)
diff --git a/server/index.js b/server/index.js
index 968293cd..0ac7eafe 100644
--- a/server/index.js
+++ b/server/index.js
@@ -962,6 +962,7 @@ io.on('connection', (socket) => {
}
inFlightWorkspaceSwitchId = requestedWorkspaceId || null;
+ let releaseInFlightWorkspaceSwitchInFinally = true;
try {
const previous = workspaceManager.getActiveWorkspace?.() || null;
activityFeed.track('workspace.switch.requested', {
@@ -986,12 +987,17 @@ io.on('connection', (socket) => {
await worktreeHelper.ensureWorktreesExist(newWorkspace);
// Switch active workspace while preserving existing PTYs for other workspace tabs.
- const { sessions: newSessions, backlog } =
+ const {
+ sessions: newSessions,
+ backlog,
+ initializePromise
+ } =
await sessionManager.switchWorkspacePreservingSessions(newWorkspace, {
reason: 'workspace-switch',
traceId,
source,
- socketId: socket.id
+ socketId: socket.id,
+ deferInitialize: true
});
// Emit success with ONLY the new workspace sessions (active workspace map)
@@ -1024,7 +1030,8 @@ io.on('connection', (socket) => {
previousWorkspaceId: previous?.id || null,
workspaceId: newWorkspace?.id || null,
sessionCount: Object.keys(newSessions || {}).length,
- backlogSessionCount: backlog && typeof backlog === 'object' ? Object.keys(backlog).length : 0
+ backlogSessionCount: backlog && typeof backlog === 'object' ? Object.keys(backlog).length : 0,
+ pendingSessionInitialization: !!initializePromise
});
activityFeed.track('workspace.switch.completed', {
fromWorkspaceId: previous?.id || null,
@@ -1032,6 +1039,51 @@ io.on('connection', (socket) => {
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,
@@ -1048,7 +1100,7 @@ io.on('connection', (socket) => {
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 (inFlightWorkspaceSwitchId === requestedWorkspaceId) {
+ if (releaseInFlightWorkspaceSwitchInFinally && inFlightWorkspaceSwitchId === requestedWorkspaceId) {
inFlightWorkspaceSwitchId = null;
}
}
diff --git a/server/sessionManager.js b/server/sessionManager.js
index e8f699b5..16465281 100644
--- a/server/sessionManager.js
+++ b/server/sessionManager.js
@@ -103,6 +103,13 @@ 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';
+ }
+
// Determine effective inactivity timeout per session (ms)
getSessionTimeout(session) {
if (!session) return this.sessionTimeout;
@@ -179,7 +186,7 @@ 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({
+ const initializePromise = this.initializeSessions({
preserveExisting: true,
reason: String(options.reason || '').trim() || 'workspace-switch',
traceId: String(options.traceId || '').trim() || null,
@@ -187,6 +194,16 @@ class SessionManager extends EventEmitter {
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 {
sessions: this.getSessionStates(),
@@ -1022,13 +1039,18 @@ 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', {
diff --git a/tests/unit/sessionManager.initializeSessions.test.js b/tests/unit/sessionManager.initializeSessions.test.js
index 4dab96b0..7a0df02a 100644
--- a/tests/unit/sessionManager.initializeSessions.test.js
+++ b/tests/unit/sessionManager.initializeSessions.test.js
@@ -115,4 +115,86 @@ describe('SessionManager.initializeSessions', () => {
expect(claudeConfig.args).toContain('-Command');
expect(serverConfig.args).toContain('-Command');
});
+
+ 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;
+ });
});
From 2f5d840dc507854513d814a286223c7be63579bd Mon Sep 17 00:00:00 2001
From: AnrokX <192667251+AnrokX@users.noreply.github.com>
Date: Fri, 6 Mar 2026 20:06:09 -0600
Subject: [PATCH 14/15] fix: reduce windows terminal churn
---
server/claudeVersionChecker.js | 64 ++++-
server/sessionManager.js | 222 +++++++++++-------
tests/unit/claudeVersionChecker.test.js | 56 +++++
.../unit/sessionManager.branchUpdate.test.js | 89 ++++++-
.../sessionManager.initializeSessions.test.js | 23 +-
5 files changed, 349 insertions(+), 105 deletions(-)
create mode 100644 tests/unit/claudeVersionChecker.test.js
diff --git a/server/claudeVersionChecker.js b/server/claudeVersionChecker.js
index 89978c78..81f08abd 100644
--- a/server/claudeVersionChecker.js
+++ b/server/claudeVersionChecker.js
@@ -15,8 +15,45 @@ 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,
@@ -52,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) {
diff --git a/server/sessionManager.js b/server/sessionManager.js
index 16465281..8f8cb8cf 100644
--- a/server/sessionManager.js
+++ b/server/sessionManager.js
@@ -61,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 {
@@ -110,6 +136,17 @@ class SessionManager extends EventEmitter {
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;
@@ -401,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}`
+ )
+ );
}
}
@@ -486,18 +516,7 @@ 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,
@@ -517,26 +536,15 @@ class SessionManager extends EventEmitter {
}
}
- // 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,
@@ -562,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() {
@@ -569,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) => {
@@ -576,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 => {
@@ -608,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);
}
@@ -678,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);
}
});
@@ -1264,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);
}
@@ -2004,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()
});
@@ -2019,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 => {
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/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
index 7a0df02a..35066caa 100644
--- a/tests/unit/sessionManager.initializeSessions.test.js
+++ b/tests/unit/sessionManager.initializeSessions.test.js
@@ -52,8 +52,21 @@ describe('SessionManager.initializeSessions', () => {
expect(sm.createSession).toHaveBeenCalledTimes(4);
expect(sm.updateGitBranch).toHaveBeenCalledTimes(2);
- expect(sm.updateGitBranch).toHaveBeenNthCalledWith(1, 'work1', '/tmp/test/work1');
- expect(sm.updateGitBranch).toHaveBeenNthCalledWith(2, 'work2', '/tmp/test/work2');
+ 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 () => {
@@ -114,6 +127,12 @@ describe('SessionManager.initializeSessions', () => {
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', () => {
From 581ee80094c105dd5e9d9096719929e4d6c6725b Mon Sep 17 00:00:00 2001
From: AnrokX <192667251+AnrokX@users.noreply.github.com>
Date: Sun, 8 Mar 2026 09:14:13 -0600
Subject: [PATCH 15/15] chore: add onboarding patch script and build snapshots
---
patch_onboarding.py | 97 ++++++++++++++++++++++
windows-onboarding-clean-main-d696ca7.zip | Bin 0 -> 1739972 bytes
windows-onboarding-clean-main.zip | Bin 0 -> 1739945 bytes
3 files changed, 97 insertions(+)
create mode 100644 patch_onboarding.py
create mode 100644 windows-onboarding-clean-main-d696ca7.zip
create mode 100644 windows-onboarding-clean-main.zip
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/windows-onboarding-clean-main-d696ca7.zip b/windows-onboarding-clean-main-d696ca7.zip
new file mode 100644
index 0000000000000000000000000000000000000000..8dca43137549fe09f854167c2f58dd85921c3b98
GIT binary patch
literal 1739972
zcmZ^KV|ZoTvUY4+9ox2T+qUhb!;X{Hv2EM7ZQDu5=p
~EhlpJ&Xqehk!j
zM^(+LS@Ttt0R@8r`mYN*!c_Oa|NP?y1q2O5Z(?iYW@^TurV0)OY|>F`rT8DaBm7Hu
zO9uxVm%k7CGvl8%vT#$&PyVc-4-5o^@wY+7ZdSIY^e&eFTv0AwNFhWJPWFM9PIyzlb+IqeLo{o
z=RJA_9;fOmm?eW`LcC>9E}sU>6EWN`ktc3yvEM#NazX3c%Pb8%4Hp}5K)KFUL)NfK
z=j)Y9nVmg_)=Z9k!={}y57Nu^*^V(%VyKpf{MEoa*g*E-1#z_}UbW9SLV`ai5!TKS
z9znX9ynn1E)Is443^0f;w5aYYPI3mh!sE?)XYphfiUg|K?F|D3hud=NHKw+Ppe@}=
zDT6Sz2P5U#oShcf;Ypwt*_ZX13PI&aWvp8MwDg4jP3(FEqvItX(|AHf$5W14F5^^K
zC_`2kO2mUL_5iN;h-Uh3Eszq5J(6I)Z0~4>J-)uEsd&}H<;Wv_~L%^6yuDW_*0G12@XW}
zwqR64TdcG^8zK|W08blChNZ+y#_)=z4sv`UTaifcc&YBh6e-wmYgEsMnE6Dkd*N
z`su!Dogi&*A{Ocg>oXGwiy*@RFVc~me{1UqU0?JZZzB2lU(x>VAW;4;nzPA&vU(^k
z-epN>AfS0%ARwB5JJ9jp1McebPZeL%+H%<7LjSUz02pQkIHY{CYQfAyR5yxS)GF%Fd6^yh$Ulu-v9CV!nlCgii{S9)kc#
zQ%Lymu4^$j-1;?CmtkG&2$q&}E-PS@HqUFpe$q`wlT54tWdPr*8m^wE?<|^6H51})Z
z
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