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

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import type React from 'react';
import type { TFunction } from 'i18next';
import { api } from '../../../utils/api';
import type { Project, ProjectSession } from '../../../types/app';
import type { Project, ProjectSession, SessionProvider } from '../../../types/app';
import type {
AdditionalSessionsByProject,
DeleteProjectConfirmation,
@@ -405,12 +405,30 @@ export function useSidebarController({
}, [onRefresh]);
const updateSessionSummary = useCallback(
async (_projectName: string, _sessionId: string, _summary: string) => {
// Session rename endpoint is not currently exposed on the API.
setEditingSession(null);
setEditingSessionName('');
async (_projectName: string, sessionId: string, summary: string, provider: SessionProvider) => {
const trimmed = summary.trim();
if (!trimmed) {
setEditingSession(null);
setEditingSessionName('');
return;
}
try {
const response = await api.renameSession(sessionId, trimmed, provider);
if (response.ok) {
await onRefresh();
} else {
console.error('[Sidebar] Failed to rename session:', response.status);
alert(t('messages.renameSessionFailed'));
}
} catch (error) {
console.error('[Sidebar] Error renaming session:', error);
alert(t('messages.renameSessionError'));
} finally {
setEditingSession(null);
setEditingSessionName('');
}
},
[],
[onRefresh, t],
);
const collapseSidebar = useCallback(() => {

View File

@@ -53,7 +53,7 @@ export const getSessionDate = (session: SessionWithProvider): Date => {
export const getSessionName = (session: SessionWithProvider, t: TFunction): string => {
if (session.__provider === 'cursor') {
return session.name || t('projects.untitledSession');
return session.summary || session.name || t('projects.untitledSession');
}
if (session.__provider === 'codex') {

View File

@@ -9,7 +9,7 @@ import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
import SidebarCollapsed from './subcomponents/SidebarCollapsed';
import SidebarContent from './subcomponents/SidebarContent';
import SidebarModals from './subcomponents/SidebarModals';
import type { Project } from '../../../types/app';
import type { Project, SessionProvider } from '../../../types/app';
import type { SidebarProjectListProps } from './subcomponents/SidebarProjectList';
import type { MCPServerStatus, SidebarProps } from '../types/types';
@@ -172,8 +172,8 @@ function Sidebar({
setEditingSession(null);
setEditingSessionName('');
},
onSaveEditingSession: (projectName, sessionId, summary) => {
void updateSessionSummary(projectName, sessionId, summary);
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: SessionProvider) => {
void updateSessionSummary(projectName, sessionId, summary, provider);
},
touchHandlerFactory: handleTouchClick,
t,

View File

@@ -45,7 +45,7 @@ type SidebarProjectItemProps = {
onEditingSessionNameChange: (value: string) => void;
onStartEditingSession: (sessionId: string, initialName: string) => void;
onCancelEditingSession: () => void;
onSaveEditingSession: (projectName: string, sessionId: string, summary: string) => void;
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: SessionProvider) => void;
touchHandlerFactory: TouchHandlerFactory;
t: TFunction;
};

View File

@@ -50,7 +50,7 @@ export type SidebarProjectListProps = {
onEditingSessionNameChange: (value: string) => void;
onStartEditingSession: (sessionId: string, initialName: string) => void;
onCancelEditingSession: () => void;
onSaveEditingSession: (projectName: string, sessionId: string, summary: string) => void;
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: SessionProvider) => void;
touchHandlerFactory: TouchHandlerFactory;
t: TFunction;
};

View File

@@ -18,7 +18,7 @@ type SidebarProjectSessionsProps = {
onEditingSessionNameChange: (value: string) => void;
onStartEditingSession: (sessionId: string, initialName: string) => void;
onCancelEditingSession: () => void;
onSaveEditingSession: (projectName: string, sessionId: string, summary: string) => void;
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: SessionProvider) => void;
onProjectSelect: (project: Project) => void;
onSessionSelect: (session: SessionWithProvider, projectName: string) => void;
onDeleteSession: (

View File

@@ -19,7 +19,7 @@ type SidebarSessionItemProps = {
onEditingSessionNameChange: (value: string) => void;
onStartEditingSession: (sessionId: string, initialName: string) => void;
onCancelEditingSession: () => void;
onSaveEditingSession: (projectName: string, sessionId: string, summary: string) => void;
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: SessionProvider) => void;
onProjectSelect: (project: Project) => void;
onSessionSelect: (session: SessionWithProvider, projectName: string) => void;
onDeleteSession: (
@@ -58,7 +58,7 @@ export default function SidebarSessionItem({
};
const saveEditedSession = () => {
onSaveEditingSession(project.name, session.id, editingSessionName);
onSaveEditingSession(project.name, session.id, editingSessionName, session.__provider);
};
const requestDeleteSession = () => {
@@ -161,9 +161,8 @@ export default function SidebarSessionItem({
</div>
</Button>
{!sessionView.isCursorSession && (
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-all duration-200">
{editingSession === session.id && !sessionView.isCodexSession ? (
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-all duration-200">
{editingSession === session.id ? (
<>
<input
type="text"
@@ -204,32 +203,31 @@ export default function SidebarSessionItem({
</>
) : (
<>
{!sessionView.isCodexSession && (
<button
className="w-6 h-6 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900/20 dark:hover:bg-gray-900/40 rounded flex items-center justify-center"
onClick={(event) => {
event.stopPropagation();
onStartEditingSession(session.id, session.summary || t('projects.newSession'));
}}
title={t('tooltips.editSessionName')}
>
<Edit2 className="w-3 h-3 text-gray-600 dark:text-gray-400" />
</button>
)}
<button
className="w-6 h-6 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/40 rounded flex items-center justify-center"
className="w-6 h-6 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900/20 dark:hover:bg-gray-900/40 rounded flex items-center justify-center"
onClick={(event) => {
event.stopPropagation();
requestDeleteSession();
onStartEditingSession(session.id, sessionView.sessionName);
}}
title={t('tooltips.deleteSession')}
title={t('tooltips.editSessionName')}
>
<Trash2 className="w-3 h-3 text-red-600 dark:text-red-400" />
<Edit2 className="w-3 h-3 text-gray-600 dark:text-gray-400" />
</button>
{!sessionView.isCursorSession && (
<button
className="w-6 h-6 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/40 rounded flex items-center justify-center"
onClick={(event) => {
event.stopPropagation();
requestDeleteSession();
}}
title={t('tooltips.deleteSession')}
>
<Trash2 className="w-3 h-3 text-red-600 dark:text-red-400" />
</button>
)}
</>
)}
</div>
)}
</div>
</div>
);

View File

@@ -93,6 +93,8 @@
"enterProjectPath": "Please enter a project path",
"deleteSessionFailed": "Failed to delete session. Please try again.",
"deleteSessionError": "Error deleting session. Please try again.",
"renameSessionFailed": "Failed to rename session. Please try again.",
"renameSessionError": "Error renaming session. Please try again.",
"deleteProjectFailed": "Failed to delete project. Please try again.",
"deleteProjectError": "Error deleting project. Please try again.",
"createProjectFailed": "Failed to create project. Please try again.",

View File

@@ -93,6 +93,8 @@
"enterProjectPath": "プロジェクトのパスを入力してください",
"deleteSessionFailed": "セッションの削除に失敗しました。もう一度お試しください。",
"deleteSessionError": "セッションの削除でエラーが発生しました。もう一度お試しください。",
"renameSessionFailed": "セッション名の変更に失敗しました。もう一度お試しください。",
"renameSessionError": "セッション名の変更でエラーが発生しました。もう一度お試しください。",
"deleteProjectFailed": "プロジェクトの削除に失敗しました。もう一度お試しください。",
"deleteProjectError": "プロジェクトの削除でエラーが発生しました。もう一度お試しください。",
"createProjectFailed": "プロジェクトの作成に失敗しました。もう一度お試しください。",

View File

@@ -93,6 +93,8 @@
"enterProjectPath": "프로젝트 경로를 입력해주세요",
"deleteSessionFailed": "세션 삭제 실패. 다시 시도해주세요.",
"deleteSessionError": "세션 삭제 오류. 다시 시도해주세요.",
"renameSessionFailed": "세션 이름 변경 실패. 다시 시도해주세요.",
"renameSessionError": "세션 이름 변경 오류. 다시 시도해주세요.",
"deleteProjectFailed": "프로젝트 삭제 실패. 다시 시도해주세요.",
"deleteProjectError": "프로젝트 삭제 오류. 다시 시도해주세요.",
"createProjectFailed": "프로젝트 생성 실패. 다시 시도해주세요.",

View File

@@ -93,6 +93,8 @@
"enterProjectPath": "请输入项目路径",
"deleteSessionFailed": "删除会话失败,请重试。",
"deleteSessionError": "删除会话时出错,请重试。",
"renameSessionFailed": "重命名会话失败,请重试。",
"renameSessionError": "重命名会话时出错,请重试。",
"deleteProjectFailed": "删除项目失败,请重试。",
"deleteProjectError": "删除项目时出错,请重试。",
"createProjectFailed": "创建项目失败,请重试。",

View File

@@ -77,6 +77,11 @@ export const api = {
authenticatedFetch(`/api/projects/${projectName}/sessions/${sessionId}`, {
method: 'DELETE',
}),
renameSession: (sessionId, summary, provider) =>
authenticatedFetch(`/api/sessions/${sessionId}/rename`, {
method: 'PUT',
body: JSON.stringify({ summary, provider }),
}),
deleteCodexSession: (sessionId) =>
authenticatedFetch(`/api/codex/sessions/${sessionId}`, {
method: 'DELETE',