feat: implement session rename with SQLite storage (#413)

* feat: implement session rename with SQLite storage (closes #72, fixes #358)

- Add session_names table to store custom display names per provider
- Add PUT /api/sessions/:sessionId/rename endpoint
- Replace stub updateSessionSummary with real API call
- Apply custom names across all providers (Claude, Codex, Cursor)
- Fix project rename destroying config (spread merge instead of overwrite)
- Thread provider parameter through sidebar component chain
- Add i18n error messages for rename failures (en, ja, ko, zh-CN)

* fix: address CodeRabbit review feedback for session rename

- Log migration errors instead of swallowing them silently (db.js)
- Add try/catch to applyCustomSessionNames to prevent getProjects abort
- Move applyCustomSessionNames to db.js as shared helper (DRY)
- Fix Cursor getSessionName to check session.summary for custom names
- Move edit state clearing to finally block in updateSessionSummary
- Sanitize sessionId, add 500-char summary limit, validate provider whitelist
- Remove dead applyCustomSessionNames call on empty manual project sessions

* fix: reject sessionId on mismatch instead of silent normalization

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: enable rename for all providers, add Gemini support, clean up orphans

- Enable rename UI (pencil icon) for Codex, Cursor, and Gemini sessions
- Keep delete button hidden for Cursor (no backend delete endpoint)
- Add 'gemini' to VALID_PROVIDERS and hoist to module scope
- Add sessionNamesDb.deleteName on session delete (claude, codex, gemini)
- Fix token-usage endpoint sessionId mismatch validation
- Remove redundant try/catch in sessionNamesDb methods
- Let session_names migration errors propagate to outer handler

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Haileyesus <118998054+blackmammoth@users.noreply.github.com>
This commit is contained in:
PaloSP
2026-03-03 16:11:26 +01:00
committed by GitHub
parent 4da27ae5f1
commit 198e3da89b
19 changed files with 201 additions and 42 deletions

View File

@@ -91,6 +91,18 @@ const runMigrations = () => {
db.exec('ALTER TABLE users ADD COLUMN has_completed_onboarding BOOLEAN DEFAULT 0');
}
// Create session_names table if it doesn't exist (for existing installations)
db.exec(`CREATE TABLE IF NOT EXISTS session_names (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
provider TEXT NOT NULL DEFAULT 'claude',
custom_name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(session_id, provider)
)`);
db.exec('CREATE INDEX IF NOT EXISTS idx_session_names_lookup ON session_names(session_id, provider)');
console.log('Database migrations completed successfully');
} catch (error) {
console.error('Error running migrations:', error.message);
@@ -348,6 +360,60 @@ const credentialsDb = {
}
};
// Session custom names database operations
const sessionNamesDb = {
// Set (insert or update) a custom session name
setName: (sessionId, provider, customName) => {
db.prepare(`
INSERT INTO session_names (session_id, provider, custom_name)
VALUES (?, ?, ?)
ON CONFLICT(session_id, provider)
DO UPDATE SET custom_name = excluded.custom_name, updated_at = CURRENT_TIMESTAMP
`).run(sessionId, provider, customName);
},
// Get a single custom session name
getName: (sessionId, provider) => {
const row = db.prepare(
'SELECT custom_name FROM session_names WHERE session_id = ? AND provider = ?'
).get(sessionId, provider);
return row?.custom_name || null;
},
// Batch lookup — returns Map<sessionId, customName>
getNames: (sessionIds, provider) => {
if (!sessionIds.length) return new Map();
const placeholders = sessionIds.map(() => '?').join(',');
const rows = db.prepare(
`SELECT session_id, custom_name FROM session_names
WHERE session_id IN (${placeholders}) AND provider = ?`
).all(...sessionIds, provider);
return new Map(rows.map(r => [r.session_id, r.custom_name]));
},
// Delete a custom session name
deleteName: (sessionId, provider) => {
return db.prepare(
'DELETE FROM session_names WHERE session_id = ? AND provider = ?'
).run(sessionId, provider).changes > 0;
},
};
// Apply custom session names from the database (overrides CLI-generated summaries)
function applyCustomSessionNames(sessions, provider) {
if (!sessions?.length) return;
try {
const ids = sessions.map(s => s.id);
const customNames = sessionNamesDb.getNames(ids, provider);
for (const session of sessions) {
const custom = customNames.get(session.id);
if (custom) session.summary = custom;
}
} catch (error) {
console.warn(`[DB] Failed to apply custom session names for ${provider}:`, error.message);
}
}
// Backward compatibility - keep old names pointing to new system
const githubTokensDb = {
createGithubToken: (userId, tokenName, githubToken, description = null) => {
@@ -373,5 +439,7 @@ export {
userDb,
apiKeysDb,
credentialsDb,
sessionNamesDb,
applyCustomSessionNames,
githubTokensDb // Backward compatibility
};

View File

@@ -49,4 +49,17 @@ CREATE TABLE IF NOT EXISTS user_credentials (
CREATE INDEX IF NOT EXISTS idx_user_credentials_user_id ON user_credentials(user_id);
CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(credential_type);
CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);
CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);
-- Session custom names (provider-agnostic display name overrides)
CREATE TABLE IF NOT EXISTS session_names (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
provider TEXT NOT NULL DEFAULT 'claude',
custom_name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(session_id, provider)
);
CREATE INDEX IF NOT EXISTS idx_session_names_lookup ON session_names(session_id, provider);