Skip to content

Commit 50e18f1

Browse files
committed
Merge remote-tracking branch 'origin/main' into permission-mechanism
2 parents add625d + a5a53ea commit 50e18f1

4 files changed

Lines changed: 218 additions & 12 deletions

File tree

src/session.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1573,6 +1573,28 @@ ${skillMd}
15731573
return index.entries.find((entry) => entry.id === sessionId) ?? null;
15741574
}
15751575

1576+
/**
1577+
* Delete a session by its ID.
1578+
* Removes the session entry from the index and deletes the associated messages file.
1579+
* Returns true if the session was found and deleted, false otherwise.
1580+
*/
1581+
deleteSession(sessionId: string): boolean {
1582+
const index = this.loadSessionsIndex();
1583+
const entryIndex = index.entries.findIndex((entry) => entry.id === sessionId);
1584+
if (entryIndex === -1) {
1585+
return false;
1586+
}
1587+
1588+
// Remove from index
1589+
index.entries.splice(entryIndex, 1);
1590+
this.saveSessionsIndex(index);
1591+
1592+
// Remove messages file
1593+
this.removeSessionMessages([sessionId]);
1594+
1595+
return true;
1596+
}
1597+
15761598
listSessionMessages(sessionId: string): SessionMessage[] {
15771599
const messagePath = this.getSessionMessagesPath(sessionId);
15781600
if (!fs.existsSync(messagePath)) {

src/tests/session.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2334,6 +2334,135 @@ test("SessionManager adjusts the active Bash timeout control and session metadat
23342334
assert.equal(processInfo?.deadlineAt, new Date(timeoutInfo.deadlineAtMs).toISOString());
23352335
});
23362336

2337+
test("SessionManager.deleteSession removes session entry from the index", () => {
2338+
const workspace = createTempDir("deepcode-delete-workspace-");
2339+
const home = createTempDir("deepcode-delete-home-");
2340+
setHomeDir(home);
2341+
2342+
const manager = createSessionManager(workspace, "machine-id-delete");
2343+
(manager as any).activateSession = async () => {};
2344+
2345+
// Create two sessions
2346+
const session1 = createSessionAndMessages(manager, "session-delete-1", "First session");
2347+
const session2 = createSessionAndMessages(manager, "session-delete-2", "Second session");
2348+
2349+
assert.equal(manager.listSessions().length, 2);
2350+
2351+
// Delete the first session
2352+
const result = manager.deleteSession(session1);
2353+
assert.equal(result, true);
2354+
2355+
const remaining = manager.listSessions();
2356+
assert.equal(remaining.length, 1);
2357+
assert.equal(remaining[0]?.id, session2);
2358+
});
2359+
2360+
test("SessionManager.deleteSession removes the messages file", () => {
2361+
const workspace = createTempDir("deepcode-delete-msg-workspace-");
2362+
const home = createTempDir("deepcode-delete-msg-home-");
2363+
setHomeDir(home);
2364+
2365+
const manager = createSessionManager(workspace, "machine-id-delete-msg");
2366+
(manager as any).activateSession = async () => {};
2367+
2368+
const sessionId = createSessionAndMessages(manager, "session-delete-msg", "Test session");
2369+
const messagePath = path.join(
2370+
home,
2371+
".deepcode",
2372+
"projects",
2373+
workspace.replace(/[\\\\/]/g, "-").replace(/:/g, ""),
2374+
`${sessionId}.jsonl`
2375+
);
2376+
2377+
// Verify messages file exists
2378+
assert.ok(fs.existsSync(messagePath));
2379+
2380+
manager.deleteSession(sessionId);
2381+
2382+
// Verify messages file is removed
2383+
assert.equal(fs.existsSync(messagePath), false);
2384+
});
2385+
2386+
test("SessionManager.deleteSession returns false when session does not exist", () => {
2387+
const workspace = createTempDir("deepcode-delete-nonexist-workspace-");
2388+
const home = createTempDir("deepcode-delete-nonexist-home-");
2389+
setHomeDir(home);
2390+
2391+
const manager = createSessionManager(workspace, "machine-id-delete-nonexist");
2392+
2393+
const result = manager.deleteSession("nonexistent-session-id");
2394+
assert.equal(result, false);
2395+
assert.equal(manager.listSessions().length, 0);
2396+
});
2397+
2398+
test("SessionManager.deleteSession does not affect other sessions", () => {
2399+
const workspace = createTempDir("deepcode-delete-others-workspace-");
2400+
const home = createTempDir("deepcode-delete-others-home-");
2401+
setHomeDir(home);
2402+
2403+
const manager = createSessionManager(workspace, "machine-id-delete-others");
2404+
(manager as any).activateSession = async () => {};
2405+
2406+
const session1 = createSessionAndMessages(manager, "session-keep-1", "Keep session 1");
2407+
const session2 = createSessionAndMessages(manager, "session-keep-2", "Keep session 2");
2408+
2409+
// Delete non-existent session
2410+
const result = manager.deleteSession("non-existent");
2411+
assert.equal(result, false);
2412+
assert.equal(manager.listSessions().length, 2);
2413+
2414+
// Delete one session
2415+
assert.equal(manager.deleteSession(session1), true);
2416+
assert.equal(manager.listSessions().length, 1);
2417+
assert.equal(manager.listSessions()[0]?.id, session2);
2418+
2419+
// The remaining session should still have its messages accessible
2420+
const messages = manager.listSessionMessages(session2);
2421+
assert.ok(messages.length > 0);
2422+
});
2423+
2424+
/**
2425+
* Helper: creates a session and writes a few messages to it so we can test
2426+
* that deleteSession removes both the index entry and the messages file.
2427+
*/
2428+
function createSessionAndMessages(manager: SessionManager, sessionId: string, summary: string): string {
2429+
const now = new Date().toISOString();
2430+
const index = (manager as any).loadSessionsIndex();
2431+
index.entries.push({
2432+
id: sessionId,
2433+
summary,
2434+
assistantReply: null,
2435+
assistantThinking: null,
2436+
assistantRefusal: null,
2437+
toolCalls: null,
2438+
status: "completed",
2439+
failReason: null,
2440+
usage: null,
2441+
usagePerModel: null,
2442+
activeTokens: 0,
2443+
createTime: now,
2444+
updateTime: now,
2445+
processes: null,
2446+
});
2447+
(manager as any).saveSessionsIndex(index);
2448+
2449+
// Write a couple of message lines to the messages file
2450+
const projectDir = (manager as any).getProjectStorage().projectDir;
2451+
const messagePath = path.join(projectDir, `${sessionId}.jsonl`);
2452+
const msg = JSON.stringify({
2453+
id: "msg-1",
2454+
sessionId,
2455+
role: "user",
2456+
content: summary,
2457+
visible: true,
2458+
createTime: now,
2459+
updateTime: now,
2460+
});
2461+
fs.writeFileSync(messagePath, `${msg}\n`, "utf8");
2462+
2463+
return sessionId;
2464+
}
2465+
23372466
function hasGit(): boolean {
23382467
try {
23392468
execFileSync("git", ["--version"], { stdio: "ignore" });

src/ui/App.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,14 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
718718
sessions={sessions}
719719
onSelect={(id) => void handleSelectSession(id)}
720720
onCancel={() => setView("chat")}
721+
onDelete={(id) => {
722+
// If the deleted session is the active one, clear it
723+
if (sessionManager.getActiveSessionId() === id) {
724+
sessionManager.setActiveSessionId(null);
725+
}
726+
sessionManager.deleteSession(id);
727+
refreshSessionsList();
728+
}}
721729
/>
722730
) : view === "undo" ? (
723731
<UndoSelector

src/ui/SessionList.tsx

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ type Props = {
66
sessions: SessionEntry[];
77
onSelect: (sessionId: string) => void;
88
onCancel: () => void;
9+
onDelete?: (sessionId: string) => void;
910
};
1011

1112
/**
@@ -36,9 +37,10 @@ export function filterSessions(sessions: SessionEntry[], query: string): Session
3637
});
3738
}
3839

39-
export function SessionList({ sessions, onSelect, onCancel }: Props): React.ReactElement {
40+
export function SessionList({ sessions, onSelect, onCancel, onDelete }: Props): React.ReactElement {
4041
const [index, setIndex] = useState(0);
4142
const [searchQuery, setSearchQuery] = useState("");
43+
const [confirmDeleteSessionId, setConfirmDeleteSessionId] = useState<string | null>(null);
4244
const { columns, rows } = useWindowSize();
4345

4446
// Filter sessions by search query
@@ -77,7 +79,23 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac
7779
setIndex(0);
7880
}, []);
7981

82+
const selectedSession = filteredSessions[safeIndex];
83+
8084
useInput((input, key) => {
85+
// If in delete confirmation mode, handle confirm/cancel
86+
if (confirmDeleteSessionId) {
87+
if (key.return) {
88+
onDelete?.(confirmDeleteSessionId);
89+
setConfirmDeleteSessionId(null);
90+
return;
91+
}
92+
if (key.escape) {
93+
setConfirmDeleteSessionId(null);
94+
return;
95+
}
96+
return;
97+
}
98+
8199
// ESC: clear search first, then cancel
82100
if (key.escape) {
83101
if (searchQuery) {
@@ -95,13 +113,25 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac
95113
return;
96114
}
97115

98-
// Backspace / Delete: remove last search character
99-
if (key.backspace || key.delete) {
116+
// Backspace: remove last search character
117+
if (key.backspace) {
118+
if (searchQuery) {
119+
handleBackspace();
120+
return;
121+
}
122+
}
123+
124+
// Delete key: remove search character, or start delete confirmation
125+
if (key.delete) {
100126
if (searchQuery) {
101127
handleBackspace();
102128
return;
103129
}
104-
// If no search query, navigation keys below handle the rest
130+
// No search query: start delete confirmation if session is selected
131+
if (selectedSession && onDelete) {
132+
setConfirmDeleteSessionId(selectedSession.id);
133+
return;
134+
}
105135
}
106136

107137
// Printable character: append to search query
@@ -211,20 +241,23 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac
211241
) : (
212242
visibleSessions.map((session, i) => {
213243
const actualIndex = scrollOffset + i;
244+
const isSelected = actualIndex === safeIndex;
245+
const isConfirming = confirmDeleteSessionId === session.id;
214246
return (
215247
<Box key={session.id} height={2} marginBottom={1}>
216248
<Box>
217-
<Text color="#229ac3">{actualIndex === safeIndex ? "> " : " "}</Text>
249+
<Text color="#229ac3">{isSelected ? "> " : " "}</Text>
218250
</Box>
219251
<Box flexDirection="column" flexGrow={1}>
220252
<Box width={"100%"}>
221-
<Text
222-
{...(actualIndex === safeIndex ? { bold: true } : {})}
223-
color={actualIndex === safeIndex ? "#229ac3" : undefined}
224-
>
253+
<Text {...(isSelected ? { bold: true } : {})} color={isSelected ? "#229ac3" : undefined}>
225254
{formatSessionTitle(session.summary || "Untitled")}
226255
</Text>
227-
<Text dimColor> ({formatSessionStatus(session.status)})</Text>
256+
{isConfirming ? (
257+
<Text color="yellow"> [Delete? Enter=yes, Esc=no]</Text>
258+
) : (
259+
<Text dimColor> ({formatSessionStatus(session.status)})</Text>
260+
)}
228261
</Box>
229262
<Box width="100%">
230263
<Text dimColor>{formatTimestamp(session.updateTime)} </Text>
@@ -245,14 +278,28 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac
245278
</Box>
246279
{/* Footer */}
247280
<Box flexDirection="column">
248-
{hasActiveSearch ? (
281+
{confirmDeleteSessionId ? (
282+
<Box>
283+
<Text color="yellow">Delete this session? </Text>
284+
<Text bold color="green">
285+
Enter
286+
</Text>
287+
<Text dimColor> to confirm · </Text>
288+
<Text bold color="red">
289+
Esc
290+
</Text>
291+
<Text dimColor> to cancel</Text>
292+
</Box>
293+
) : hasActiveSearch ? (
249294
<Box>
250295
<Text dimColor>Esc clear search · </Text>
251296
<Text dimColor>↑/↓ navigate · Enter select · Esc again to cancel</Text>
252297
</Box>
253298
) : (
254299
<Box>
255-
<Text dimColor>Type to search · ↑/↓ navigate · PgUp/PgDn page · Enter select · Esc cancel</Text>
300+
<Text dimColor>
301+
Type to search · ↑/↓ navigate · PgUp/PgDn page · Enter select · Esc cancel · Del delete
302+
</Text>
256303
</Box>
257304
)}
258305
</Box>

0 commit comments

Comments
 (0)