Skip to content
Merged
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
25 changes: 20 additions & 5 deletions db.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ db.exec(`
created TEXT,
modified TEXT,
messageCount INTEGER DEFAULT 0,
slug TEXT
slug TEXT,
aiTitle TEXT
)
`);

Expand Down Expand Up @@ -85,6 +86,19 @@ const migrations = [
try { db.exec('DROP TABLE IF EXISTS search_fts'); } catch {}
searchFtsRecreated = true;
},
// v3: Add aiTitle column for AI-generated session titles. Clear cache so a
// re-index repopulates the column. Also clear session_meta.name entries that
// were clobbered by AI titles in v0.0.29 (when ai-title was written into the
// user-name column). We cannot tell with certainty which names came from an
// AI title vs a manual rename, but the safe heuristic is: drop names whose
// value matches the JSONL aiTitle on next index. That post-index cleanup is
// not done here — instead we accept that any pre-fix AI-title pollution
// remains until the user renames manually, and only future indexes are clean.
(db) => {
try { db.exec('ALTER TABLE session_cache ADD COLUMN aiTitle TEXT'); } catch {}
try { db.exec('DELETE FROM session_cache'); } catch {}
try { db.exec('DELETE FROM cache_meta'); } catch {}
},
];

const currentDbVersion = (() => {
Expand Down Expand Up @@ -138,13 +152,14 @@ const stmts = {
cacheCount: db.prepare('SELECT COUNT(*) as cnt FROM session_cache'),
cacheGetAll: db.prepare('SELECT * FROM session_cache'),
cacheUpsert: db.prepare(`
INSERT INTO session_cache (sessionId, folder, projectPath, summary, firstPrompt, created, modified, messageCount, slug)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO session_cache (sessionId, folder, projectPath, summary, firstPrompt, created, modified, messageCount, slug, aiTitle)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(sessionId) DO UPDATE SET
folder = excluded.folder, projectPath = excluded.projectPath,
summary = excluded.summary, firstPrompt = excluded.firstPrompt,
created = excluded.created, modified = excluded.modified,
messageCount = excluded.messageCount, slug = excluded.slug
messageCount = excluded.messageCount, slug = excluded.slug,
aiTitle = excluded.aiTitle
`),
cacheGetByFolder: db.prepare('SELECT sessionId, modified FROM session_cache WHERE folder = ?'),
cacheGetFolder: db.prepare('SELECT folder FROM session_cache WHERE sessionId = ?'),
Expand Down Expand Up @@ -231,7 +246,7 @@ const upsertCachedSessionsBatch = db.transaction((sessions) => {
stmts.cacheUpsert.run(
s.sessionId, s.folder, s.projectPath, s.summary,
s.firstPrompt, s.created, s.modified, s.messageCount || 0,
s.slug || null
s.slug || null, s.aiTitle || null
);
}
});
Expand Down
2 changes: 1 addition & 1 deletion public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,7 @@ function openNewSession(project) {
}

async function showTerminalHeader(session) {
const displayName = cleanDisplayName(session.name || session.summary);
const displayName = cleanDisplayName(session.name || session.aiTitle || session.summary);
terminalHeaderName.textContent = displayName;
terminalHeaderId.textContent = session.sessionId;
terminalHeader.style.display = '';
Expand Down
2 changes: 1 addition & 1 deletion public/dialogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ async function showResumeSessionDialog(session) {
`<button class="permission-option dangerous${dangerousSkip ? ' selected' : ''}" data-mode="dangerous-skip"><span class="perm-name">Dangerous Skip</span><span class="perm-desc">Skip all safety prompts (use with caution)</span></button>`;
}

const sessionName = session.name || session.summary || session.sessionId.slice(0, 8);
const sessionName = session.name || session.aiTitle || session.summary || session.sessionId.slice(0, 8);

dialog.innerHTML = `
<h3>Resume Session — ${escapeHtml(sessionName)}</h3>
Expand Down
2 changes: 1 addition & 1 deletion public/grid-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function wrapInGridCard(sessionId) {
const session = sessionMap.get(sessionId) || (entry && entry.session);
if (!session || !entry) return;

const displayName = cleanDisplayName(session.name || session.summary) || sessionId;
const displayName = cleanDisplayName(session.name || session.aiTitle || session.summary) || sessionId;
const shortProject = session.projectPath ? session.projectPath.split('/').filter(Boolean).slice(-2).join('/') : '';

// Create card wrapper
Expand Down
2 changes: 1 addition & 1 deletion public/jsonl-viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ async function showJsonlViewer(session) {
terminalArea.style.display = 'none';
jsonlViewer.style.display = 'flex';

const displayName = session.name || session.summary || session.sessionId;
const displayName = session.name || session.aiTitle || session.summary || session.sessionId;
jsonlViewerTitle.textContent = displayName;
jsonlViewerSessionId.textContent = session.sessionId;
jsonlViewerBody.innerHTML = '';
Expand Down
13 changes: 7 additions & 6 deletions public/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function buildSlugGroup(slug, sessions) {
const bTime = lastActivityTime.get(b.sessionId) || new Date(b.modified);
return bTime > aTime ? b : a;
});
const displayName = cleanDisplayName(mostRecent.name || mostRecent.summary || slug);
const displayName = cleanDisplayName(mostRecent.name || mostRecent.aiTitle || mostRecent.summary || slug);
const mostRecentTime = lastActivityTime.get(mostRecent.sessionId) || new Date(mostRecent.modified);
const timeStr = formatDate(mostRecentTime);

Expand Down Expand Up @@ -654,7 +654,7 @@ function buildSessionItem(session) {

const modified = lastActivityTime.get(session.sessionId) || new Date(session.modified);
const timeStr = formatDate(modified);
const displayName = cleanDisplayName(session.name || session.summary);
const displayName = cleanDisplayName(session.name || session.aiTitle || session.summary);

const row = document.createElement('div');
row.className = 'session-row';
Expand Down Expand Up @@ -746,21 +746,22 @@ function startRename(summaryEl, session) {
const input = document.createElement('input');
input.type = 'text';
input.className = 'session-rename-input';
input.value = session.name || session.summary;
input.value = session.name || session.aiTitle || session.summary;

summaryEl.replaceWith(input);
input.focus();
input.select();

const save = async () => {
const newName = input.value.trim();
const nameToSave = (newName && newName !== session.summary) ? newName : null;
const fallback = session.aiTitle || session.summary;
const nameToSave = (newName && newName !== fallback) ? newName : null;
await window.api.renameSession(session.sessionId, nameToSave);
session.name = nameToSave;

const newSummary = document.createElement('div');
newSummary.className = 'session-summary';
newSummary.textContent = nameToSave || session.summary;
newSummary.textContent = nameToSave || fallback;
newSummary.addEventListener('dblclick', (e) => {
e.stopPropagation();
startRename(newSummary, session);
Expand All @@ -775,7 +776,7 @@ function startRename(summaryEl, session) {
input.removeEventListener('blur', save);
const restored = document.createElement('div');
restored.className = 'session-summary';
restored.textContent = session.name || session.summary;
restored.textContent = session.name || session.aiTitle || session.summary;
restored.addEventListener('dblclick', (ev) => {
ev.stopPropagation();
startRename(restored, session);
Expand Down
2 changes: 1 addition & 1 deletion read-session-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function readSessionFile(filePath, folder, projectPath) {
summary, firstPrompt: summary,
created: stat.birthtime.toISOString(),
modified: stat.mtime.toISOString(),
messageCount, textContent, slug, customTitle: customTitle || aiTitle,
messageCount, textContent, slug, customTitle, aiTitle,
};
} catch {
return null;
Expand Down
12 changes: 9 additions & 3 deletions session-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ function refreshFolder(folder) {
const s = readSessionFile(filePath, folder, projectPath);
if (s) {
sessionsToUpsert.push(s);
const name = s.customTitle || getMeta(s.sessionId)?.name || '';
// Title precedence: user rename (session_meta.name) > JSONL custom-title > JSONL ai-title.
// Only customTitle (Claude /title) promotes to session_meta.name — AI titles must NEVER
// be written there or they'd overwrite the user's UI rename on the next index pass.
const name = getMeta(s.sessionId)?.name || s.customTitle || s.aiTitle || '';
searchEntriesToUpsert.push({
id: s.sessionId, type: 'session', folder: s.folder,
title: (name ? name + ' ' : '') + s.summary, body: s.textContent,
Expand Down Expand Up @@ -193,6 +196,7 @@ function buildProjectsFromCache(showArchived) {
messageCount: row.messageCount,
projectPath: row.projectPath,
slug: row.slug || null,
aiTitle: row.aiTitle || null,
name: meta?.name || null,
starred: meta?.starred || 0,
archived: meta?.archived || 0,
Expand Down Expand Up @@ -330,11 +334,13 @@ function populateCacheViaWorker() {
sessionCount += sessions.length;
upsertCachedSessions(sessions);
for (const s of sessions) {
// Only JSONL custom-title (genuine user title) promotes to the DB name column.
// AI titles must not — see refreshFolder for the rationale.
if (s.customTitle) setName(s.sessionId, s.customTitle);
}
upsertSearchEntries(sessions.map(s => {
// customTitle comes from jsonl; fall back to session_meta.name (set via rename)
const name = s.customTitle || getMeta(s.sessionId)?.name || '';
// Search title precedence matches the sidebar: user rename > custom-title > ai-title.
const name = getMeta(s.sessionId)?.name || s.customTitle || s.aiTitle || '';
return {
id: s.sessionId, type: 'session', folder: s.folder,
title: (name ? name + ' ' : '') + s.summary,
Expand Down
Loading