mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-27 15:13:37 +08:00
* feat(sidebar): delete all sessions for a project
Add a "Delete all sessions" option to the project context menu so a
project can be removed from the sidebar entirely (not just hidden).
Hide-from-sidebar is a UI-only soft toggle; this is a destructive
operation that wipes the on-disk artefacts.
Backend
- POST /api/projects/sessions/clear accepts { projectId } or
{ workDir } (the desktop client passes workDir; backend sanitizes).
- Deletes every .jsonl transcript and matching .summary.json sidecar
under ~/.claude/projects/<sanitized-id>/. Sub-directories and other
non-session files (memory/, user notes) are preserved.
- If the directory becomes empty, removes it so the project drops off
the sidebar listing entirely.
- Defence in depth: rejects path-separator/null-byte/`.`/`..` ids and
re-checks the resolved path stays under projectsDir.
- 11 backend tests covering traversal/null-byte/dot rejection, missing
dir 200, full-clear+rmdir, non-jsonl preservation, summary sidecar
deletion, workDir absolute-path acceptance, relative-path rejection,
and 405 on GET.
Frontend
- Sidebar project context menu: red "Delete all sessions" entry
(Trash2 icon, danger) below the hide entry.
- ConfirmDialog with project title + session count + irreversibility
warning. Disconnects open sessions and closes their tabs before the
call so the websocket layer doesn't keep stale handles.
- After success, fetchSessions() refreshes the list — the project
disappears if its dir was removed, or shows up empty if memory/
remained.
- 5 new i18n keys across en/zh/jp/kr/zh-TW.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(sidebar): cover projectsApi.clearSessions client behaviour
Wire-level guard for the desktop API helper introduced in this PR — the
sidebar's "Delete all sessions" flow depends on it sending the right
endpoint and body, and surfacing server errors back to the caller for
toast handling.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: 你的姓名 <you@example.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
36 lines
1.1 KiB
TypeScript
36 lines
1.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import '@testing-library/jest-dom'
|
|
|
|
const apiPostMock = vi.fn()
|
|
|
|
vi.mock('./client', () => ({
|
|
api: {
|
|
post: (url: string, body?: unknown) => apiPostMock(url, body),
|
|
get: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
import { projectsApi } from './projects'
|
|
|
|
beforeEach(() => {
|
|
apiPostMock.mockReset()
|
|
})
|
|
|
|
describe('projectsApi.clearSessions', () => {
|
|
it('posts to /api/projects/sessions/clear with workDir in body', async () => {
|
|
apiPostMock.mockResolvedValue({ ok: true, deletedSessions: 3, projectDirRemoved: true })
|
|
|
|
const result = await projectsApi.clearSessions('/work/alpha')
|
|
|
|
expect(apiPostMock).toHaveBeenCalledTimes(1)
|
|
expect(apiPostMock).toHaveBeenCalledWith('/api/projects/sessions/clear', { workDir: '/work/alpha' })
|
|
expect(result.deletedSessions).toBe(3)
|
|
expect(result.projectDirRemoved).toBe(true)
|
|
})
|
|
|
|
it('propagates server errors so the caller can show a toast', async () => {
|
|
apiPostMock.mockRejectedValue(new Error('Forbidden'))
|
|
await expect(projectsApi.clearSessions('/work/beta')).rejects.toThrow('Forbidden')
|
|
})
|
|
})
|