Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions scripts/start-mobile-codex-stack.ps1
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +16 to +19
}
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
51 changes: 51 additions & 0 deletions upstream-overrides/claudecodeui-1.25.2/server/database/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -729,5 +779,6 @@ export {
sessionNamesDb,
applyCustomSessionNames,
appConfigDb,
userPreferencesDb,
githubTokensDb // Backward compatibility
};
17 changes: 14 additions & 3 deletions upstream-overrides/claudecodeui-1.25.2/server/database/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
166 changes: 166 additions & 0 deletions upstream-overrides/claudecodeui-1.25.2/server/routes/user.js
Original file line number Diff line number Diff line change
@@ -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);
Comment on lines +28 to +31

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;
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
loadStarredProjects,
persistStarredProjects,
readProjectSortOrder,
STARRED_PROJECTS_PREFERENCE_KEY,
sortProjects,
} from '../utils/utils';

Expand Down Expand Up @@ -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);
Comment on lines +173 to +175
} catch (error) {
console.error('Error loading starred projects preference:', error);
}
};

void loadStarredPreference();

return () => {
cancelled = true;
};
}, []);

useEffect(() => {
const loadSortOrder = () => {
setProjectSortOrder(readProjectSortOrder());
Expand Down Expand Up @@ -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);
});
Comment on lines +352 to +354
return next;
});
}, []);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export const persistStarredProjects = (starredProjects: Set<string>) => {
}
};

export const STARRED_PROJECTS_PREFERENCE_KEY = 'starred_projects';

export const getSessionDate = (session: SessionWithProvider): Date => {
if (session.__provider === 'cursor') {
return new Date(session.createdAt || 0);
Expand Down
6 changes: 6 additions & 0 deletions upstream-overrides/claudecodeui-1.25.2/src/utils/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down
Loading