diff --git a/scripts/start-mobile-codex-stack.ps1 b/scripts/start-mobile-codex-stack.ps1 index 2cfe214..e9bb5a4 100644 --- a/scripts/start-mobile-codex-stack.ps1 +++ b/scripts/start-mobile-codex-stack.ps1 @@ -1,5 +1,34 @@ $workspace = Split-Path -Parent $PSScriptRoot +$powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' +$startScript = Join-Path $workspace 'scripts\start-mobile-codex.ps1' + +Start-Process -FilePath $powershell -WorkingDirectory $workspace -ArgumentList @( + '-NoProfile', + '-ExecutionPolicy', + 'Bypass', + '-Command', + "& '$startScript'" +) -WindowStyle Hidden | Out-Null +Start-Sleep -Seconds 2 + +$appReady = $false +for ($i = 0; $i -lt 15; $i++) { + $listener = Get-NetTCPConnection -State Listen -LocalPort 3001 -ErrorAction SilentlyContinue + if ($listener) { + $appReady = $true + break + } + Start-Sleep -Seconds 1 +} + +if (-not $appReady) { + $stderrLog = Join-Path $workspace 'tmp\logs\mobile-codex-app.stderr.log' + $tail = if (Test-Path $stderrLog) { + (Get-Content $stderrLog -Tail 40) -join [Environment]::NewLine + } else { + 'stderr log not found.' + } + throw "Mobile Codex app failed to start on port 3001.`nRecent stderr:`n$tail" +} -Start-Process -FilePath 'powershell' -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File',(Join-Path $workspace 'scripts\start-mobile-codex.ps1')) -WindowStyle Hidden | Out-Null -Start-Sleep -Seconds 5 powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'scripts\start-mobile-codex-nginx.ps1') | Out-Null diff --git a/upstream-overrides/claudecodeui-1.25.2/server/database/db.js b/upstream-overrides/claudecodeui-1.25.2/server/database/db.js index 9b492e8..72411f5 100644 --- a/upstream-overrides/claudecodeui-1.25.2/server/database/db.js +++ b/upstream-overrides/claudecodeui-1.25.2/server/database/db.js @@ -119,6 +119,18 @@ const runMigrations = () => { )`); db.exec('CREATE INDEX IF NOT EXISTS idx_session_names_lookup ON session_names(session_id, provider)'); + db.exec(`CREATE TABLE IF NOT EXISTS user_preferences ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + preference_key TEXT NOT NULL, + preference_value TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE(user_id, preference_key) + )`); + db.exec('CREATE INDEX IF NOT EXISTS idx_user_preferences_lookup ON user_preferences(user_id, preference_key)'); + db.exec(`CREATE TABLE IF NOT EXISTS trusted_devices ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, @@ -700,6 +712,44 @@ const appConfigDb = { } }; +const userPreferencesDb = { + getPreference: (userId, key) => { + try { + const row = db.prepare( + 'SELECT preference_value FROM user_preferences WHERE user_id = ? AND preference_key = ?' + ).get(userId, key); + return row?.preference_value ?? null; + } catch (err) { + throw err; + } + }, + + setPreference: (userId, key, value) => { + try { + db.prepare(` + INSERT INTO user_preferences (user_id, preference_key, preference_value) + VALUES (?, ?, ?) + ON CONFLICT(user_id, preference_key) DO UPDATE SET + preference_value = excluded.preference_value, + updated_at = CURRENT_TIMESTAMP + `).run(userId, key, value); + return true; + } catch (err) { + throw err; + } + }, + + deletePreference: (userId, key) => { + try { + return db.prepare( + 'DELETE FROM user_preferences WHERE user_id = ? AND preference_key = ?' + ).run(userId, key).changes > 0; + } catch (err) { + throw err; + } + } +}; + // Backward compatibility - keep old names pointing to new system const githubTokensDb = { createGithubToken: (userId, tokenName, githubToken, description = null) => { @@ -729,5 +779,6 @@ export { sessionNamesDb, applyCustomSessionNames, appConfigDb, + userPreferencesDb, githubTokensDb // Backward compatibility }; diff --git a/upstream-overrides/claudecodeui-1.25.2/server/database/init.sql b/upstream-overrides/claudecodeui-1.25.2/server/database/init.sql index 0010276..9b5e38b 100644 --- a/upstream-overrides/claudecodeui-1.25.2/server/database/init.sql +++ b/upstream-overrides/claudecodeui-1.25.2/server/database/init.sql @@ -110,7 +110,18 @@ CREATE INDEX IF NOT EXISTS idx_device_approval_token ON device_approval_requests -- App configuration table (auto-generated secrets, settings, etc.) CREATE TABLE IF NOT EXISTS app_config ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS user_preferences ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + preference_key TEXT NOT NULL, + preference_value TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE(user_id, preference_key) ); diff --git a/upstream-overrides/claudecodeui-1.25.2/server/routes/user.js b/upstream-overrides/claudecodeui-1.25.2/server/routes/user.js new file mode 100644 index 0000000..0e3639e --- /dev/null +++ b/upstream-overrides/claudecodeui-1.25.2/server/routes/user.js @@ -0,0 +1,166 @@ +import express from 'express'; +import { userDb, userPreferencesDb } from '../database/db.js'; +import { authenticateToken } from '../middleware/auth.js'; +import { getSystemGitConfig } from '../utils/gitConfig.js'; +import { spawn } from 'child_process'; + +const router = express.Router(); + +function spawnAsync(command, args, options = {}) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { ...options, shell: false }); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', (data) => { stdout += data.toString(); }); + child.stderr.on('data', (data) => { stderr += data.toString(); }); + child.on('error', (error) => { reject(error); }); + child.on('close', (code) => { + if (code === 0) { resolve({ stdout, stderr }); return; } + const error = new Error(`Command failed: ${command} ${args.join(' ')}`); + error.code = code; + error.stdout = stdout; + error.stderr = stderr; + reject(error); + }); + }); +} + +router.get('/git-config', authenticateToken, async (req, res) => { + try { + const userId = req.user.id; + let gitConfig = userDb.getGitConfig(userId); + + if (!gitConfig || (!gitConfig.git_name && !gitConfig.git_email)) { + const systemConfig = await getSystemGitConfig(); + + if (systemConfig.git_name || systemConfig.git_email) { + userDb.updateGitConfig(userId, systemConfig.git_name, systemConfig.git_email); + gitConfig = systemConfig; + console.log(`Auto-populated git config from system for user ${userId}: ${systemConfig.git_name} <${systemConfig.git_email}>`); + } + } + + res.json({ + success: true, + gitName: gitConfig?.git_name || null, + gitEmail: gitConfig?.git_email || null + }); + } catch (error) { + console.error('Error getting git config:', error); + res.status(500).json({ error: 'Failed to get git configuration' }); + } +}); + +router.post('/git-config', authenticateToken, async (req, res) => { + try { + const userId = req.user.id; + const { gitName, gitEmail } = req.body; + + if (!gitName || !gitEmail) { + return res.status(400).json({ error: 'Git name and email are required' }); + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(gitEmail)) { + return res.status(400).json({ error: 'Invalid email format' }); + } + + userDb.updateGitConfig(userId, gitName, gitEmail); + + try { + await spawnAsync('git', ['config', '--global', 'user.name', gitName]); + await spawnAsync('git', ['config', '--global', 'user.email', gitEmail]); + console.log(`Applied git config globally: ${gitName} <${gitEmail}>`); + } catch (gitError) { + console.error('Error applying git config:', gitError); + } + + res.json({ + success: true, + gitName, + gitEmail + }); + } catch (error) { + console.error('Error updating git config:', error); + res.status(500).json({ error: 'Failed to update git configuration' }); + } +}); + +router.post('/complete-onboarding', authenticateToken, async (req, res) => { + try { + const userId = req.user.id; + userDb.completeOnboarding(userId); + + res.json({ + success: true, + message: 'Onboarding completed successfully' + }); + } catch (error) { + console.error('Error completing onboarding:', error); + res.status(500).json({ error: 'Failed to complete onboarding' }); + } +}); + +router.get('/onboarding-status', authenticateToken, async (req, res) => { + try { + const userId = req.user.id; + const hasCompleted = userDb.hasCompletedOnboarding(userId); + + res.json({ + success: true, + hasCompletedOnboarding: hasCompleted + }); + } catch (error) { + console.error('Error checking onboarding status:', error); + res.status(500).json({ error: 'Failed to check onboarding status' }); + } +}); + +router.get('/preferences/:key', authenticateToken, async (req, res) => { + try { + const key = String(req.params.key || '').trim(); + if (!key) { + return res.status(400).json({ error: 'Preference key is required' }); + } + + const rawValue = userPreferencesDb.getPreference(req.user.id, key); + let value = rawValue; + if (rawValue !== null) { + try { + value = JSON.parse(rawValue); + } catch { + value = rawValue; + } + } + + res.json({ + success: true, + value, + }); + } catch (error) { + console.error('Error getting user preference:', error); + res.status(500).json({ error: 'Failed to get user preference' }); + } +}); + +router.put('/preferences/:key', authenticateToken, async (req, res) => { + try { + const key = String(req.params.key || '').trim(); + if (!key) { + return res.status(400).json({ error: 'Preference key is required' }); + } + + const { value } = req.body ?? {}; + if (value === undefined) { + return res.status(400).json({ error: 'Preference value is required' }); + } + + userPreferencesDb.setPreference(req.user.id, key, JSON.stringify(value)); + res.json({ success: true }); + } catch (error) { + console.error('Error setting user preference:', error); + res.status(500).json({ error: 'Failed to set user preference' }); + } +}); + +export default router; diff --git a/upstream-overrides/claudecodeui-1.25.2/src/components/sidebar/hooks/useSidebarController.ts b/upstream-overrides/claudecodeui-1.25.2/src/components/sidebar/hooks/useSidebarController.ts index 1d73ae0..091c3a3 100644 --- a/upstream-overrides/claudecodeui-1.25.2/src/components/sidebar/hooks/useSidebarController.ts +++ b/upstream-overrides/claudecodeui-1.25.2/src/components/sidebar/hooks/useSidebarController.ts @@ -18,6 +18,7 @@ import { loadStarredProjects, persistStarredProjects, readProjectSortOrder, + STARRED_PROJECTS_PREFERENCE_KEY, sortProjects, } from '../utils/utils'; @@ -154,6 +155,36 @@ export function useSidebarController({ } }, [projects, isLoading]); + useEffect(() => { + let cancelled = false; + + const loadStarredPreference = async () => { + try { + const response = await api.user.getPreference(STARRED_PROJECTS_PREFERENCE_KEY); + if (!response.ok) { + return; + } + + const payload = await response.json(); + if (cancelled || !Array.isArray(payload?.value)) { + return; + } + + const next = new Set(payload.value.filter((item: unknown) => typeof item === 'string')); + setStarredProjects(next); + persistStarredProjects(next); + } catch (error) { + console.error('Error loading starred projects preference:', error); + } + }; + + void loadStarredPreference(); + + return () => { + cancelled = true; + }; + }, []); + useEffect(() => { const loadSortOrder = () => { setProjectSortOrder(readProjectSortOrder()); @@ -318,6 +349,9 @@ export function useSidebarController({ } persistStarredProjects(next); + void api.user.setPreference(STARRED_PROJECTS_PREFERENCE_KEY, [...next]).catch((error) => { + console.error('Error saving starred projects preference:', error); + }); return next; }); }, []); diff --git a/upstream-overrides/claudecodeui-1.25.2/src/components/sidebar/utils/utils.ts b/upstream-overrides/claudecodeui-1.25.2/src/components/sidebar/utils/utils.ts index 347f60a..18d3256 100644 --- a/upstream-overrides/claudecodeui-1.25.2/src/components/sidebar/utils/utils.ts +++ b/upstream-overrides/claudecodeui-1.25.2/src/components/sidebar/utils/utils.ts @@ -40,6 +40,8 @@ export const persistStarredProjects = (starredProjects: Set) => { } }; +export const STARRED_PROJECTS_PREFERENCE_KEY = 'starred_projects'; + export const getSessionDate = (session: SessionWithProvider): Date => { if (session.__provider === 'cursor') { return new Date(session.createdAt || 0); diff --git a/upstream-overrides/claudecodeui-1.25.2/src/utils/api.js b/upstream-overrides/claudecodeui-1.25.2/src/utils/api.js index b0d41ac..06bb29b 100644 --- a/upstream-overrides/claudecodeui-1.25.2/src/utils/api.js +++ b/upstream-overrides/claudecodeui-1.25.2/src/utils/api.js @@ -222,6 +222,12 @@ export const api = { method: 'POST', body: JSON.stringify({ gitName, gitEmail }), }), + getPreference: (key) => authenticatedFetch(`/api/user/preferences/${encodeURIComponent(key)}`), + setPreference: (key, value) => + authenticatedFetch(`/api/user/preferences/${encodeURIComponent(key)}`, { + method: 'PUT', + body: JSON.stringify({ value }), + }), onboardingStatus: () => authenticatedFetch('/api/user/onboarding-status'), completeOnboarding: () => authenticatedFetch('/api/user/complete-onboarding', {