Skip to content

Commit abed149

Browse files
committed
feat(ui): 增加会话删除及相关UI重置功能
- 新增 handleDeleteSession 方法,支持删除会话并更新会话列表 - 删除当前激活会话时,清除屏幕、重置UI状态并显示欢迎界面 - 删除按钮调用封装的删除处理函数,统一逻辑 - 优化 SessionList 中搜索逻辑,调整删除和退格键处理 - 移除 SessionList 文件内重复的 truncate 函数实现
1 parent 4c0587e commit abed149

5 files changed

Lines changed: 132 additions & 97 deletions

File tree

src/ui/App.tsx

Lines changed: 101 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import { PermissionPrompt, type PermissionPromptResult } from "./PermissionPromp
4343
import { buildExitSummaryText } from "./exitSummary";
4444
import { RawMode, useRawModeContext } from "./contexts";
4545
import { renderMessageToStdout } from "./components/MessageView/utils";
46+
import { renderRawModeMessages } from "./utils";
47+
import { ANSI_CLEAR_SCREEN } from "./constants";
4648

4749
const DEFAULT_MODEL = "deepseek-v4-pro";
4850
const DEFAULT_BASE_URL = "https://api.deepseek.com";
@@ -55,7 +57,7 @@ type AppProps = {
5557
onRestart?: () => void;
5658
};
5759

58-
export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactElement {
60+
function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactElement {
5961
const { exit } = useApp();
6062
const { stdout, write } = useStdout();
6163
const { columns, rows } = useWindowSize();
@@ -142,6 +144,33 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
142144
});
143145
}, [projectRoot]);
144146

147+
/**
148+
* Navigate to a sub-view.
149+
*/
150+
const navigateToSubView = useCallback((targetView: View) => {
151+
setShowWelcome(false);
152+
setView(targetView);
153+
}, []);
154+
155+
/**
156+
* Reset the static view to the welcome screen.
157+
*/
158+
const resetStaticView = useCallback(
159+
(loadedMessages: SessionMessage[], options?: { clearScreen?: boolean }) => {
160+
if (options?.clearScreen) {
161+
process.stdout.write(ANSI_CLEAR_SCREEN);
162+
}
163+
setMessages([]);
164+
setWelcomeNonce((n) => n + 1);
165+
navigateToSubView("chat");
166+
setTimeout(() => {
167+
setMessages(loadedMessages);
168+
setShowWelcome(true);
169+
}, 0);
170+
},
171+
[navigateToSubView]
172+
);
173+
145174
useEffect(() => {
146175
if (!busy) {
147176
return;
@@ -170,6 +199,26 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
170199
[sessionManager]
171200
);
172201

202+
/**
203+
* Reset the app to the welcome screen.
204+
*/
205+
const resetToWelcome = useCallback(async () => {
206+
writeRef.current(ANSI_CLEAR_SCREEN);
207+
sessionManager.setActiveSessionId(null);
208+
setStatusLine("");
209+
setErrorLine(null);
210+
setRunningProcesses(null);
211+
setActiveStatus(null);
212+
setActiveAskPermissions(undefined);
213+
setPendingPermissionReply(null);
214+
setDismissedQuestionIds(new Set());
215+
resetStaticView([]);
216+
await refreshSkills();
217+
}, [sessionManager, resetStaticView, refreshSkills]);
218+
219+
/**
220+
* Refresh the list of sessions.
221+
*/
173222
useEffect(() => {
174223
refreshSessionsList();
175224
void refreshSkills();
@@ -182,11 +231,17 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
182231
createOpenAIClient(projectRoot);
183232
}, [projectRoot]);
184233

234+
/**
235+
* Initialize MCP servers.
236+
*/
185237
useLayoutEffect(() => {
186238
const settings = resolveCurrentSettings(projectRoot);
187239
void sessionManager.initMcpServers(settings.mcpServers);
188240
}, [projectRoot, sessionManager]);
189241

242+
/**
243+
* Dispose the session manager on unmount.
244+
*/
190245
useEffect(() => {
191246
return () => {
192247
sessionManager.dispose();
@@ -216,33 +271,19 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
216271
if (onRestart) {
217272
onRestart();
218273
} else {
219-
writeRef.current("\u001B[2J\u001B[3J\u001B[H");
220-
sessionManager.setActiveSessionId(null);
221-
setMessages([]);
222-
setStatusLine("");
223-
setErrorLine(null);
224-
setRunningProcesses(null);
225-
setActiveStatus(null);
226-
setActiveAskPermissions(undefined);
227-
setPendingPermissionReply(null);
228-
setDismissedQuestionIds(new Set());
229-
setShowWelcome(true);
230-
setWelcomeNonce((n) => n + 1);
231-
await refreshSkills();
274+
await resetToWelcome();
232275
refreshSessionsList();
233276
}
234277
return;
235278
}
236279
if (submission.command === "resume") {
237-
setShowWelcome(false);
238280
refreshSessionsList();
239-
setView("session-list");
281+
navigateToSubView("session-list");
240282
return;
241283
}
242284
if (submission.command === "continue" && isCurrentSessionEmpty(sessionManager)) {
243-
setShowWelcome(false);
244285
refreshSessionsList();
245-
setView("session-list");
286+
navigateToSubView("session-list");
246287
return;
247288
}
248289
if (submission.command === "undo") {
@@ -251,15 +292,13 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
251292
setErrorLine("No active session to undo.");
252293
return;
253294
}
254-
setShowWelcome(false);
255295
setUndoTargets(sessionManager.listUndoTargets(activeSessionId));
256-
setView("undo");
296+
navigateToSubView("undo");
257297
return;
258298
}
259299
if (submission.command === "mcp") {
260-
setShowWelcome(false);
261300
setMcpStatuses(sessionManager.getMcpStatus());
262-
setView("mcp-status");
301+
navigateToSubView("mcp-status");
263302
return;
264303
}
265304

@@ -311,7 +350,16 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
311350
setRunningProcesses(null);
312351
}
313352
},
314-
[exit, onRestart, pendingPermissionReply, sessionManager, refreshSkills, refreshSessionsList]
353+
[
354+
sessionManager,
355+
pendingPermissionReply,
356+
exit,
357+
onRestart,
358+
refreshSkills,
359+
refreshSessionsList,
360+
navigateToSubView,
361+
resetToWelcome,
362+
]
315363
);
316364

317365
const handleInterrupt = useCallback(() => {
@@ -384,16 +432,9 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
384432

385433
const reloadActiveSessionView = useCallback(
386434
(sessionId: string): void => {
387-
process.stdout.write("\u001B[2J\u001B[3J\u001B[H");
388-
setMessages([]);
389-
setShowWelcome(false);
390-
setWelcomeNonce((n) => n + 1);
391-
setTimeout(() => {
392-
setMessages(loadVisibleMessages(sessionManager, sessionId));
393-
setShowWelcome(true);
394-
}, 0);
435+
resetStaticView(loadVisibleMessages(sessionManager, sessionId), { clearScreen: true });
395436
},
396-
[sessionManager]
437+
[resetStaticView, sessionManager]
397438
);
398439

399440
useEffect(() => {
@@ -411,21 +452,9 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
411452

412453
const handleSelectSession = useCallback(
413454
async (sessionId: string) => {
414-
const currentSessionId = sessionManager.getActiveSessionId();
415-
if (currentSessionId !== sessionId) {
416-
process.stdout.write("\u001B[2J\u001B[3J\u001B[H");
417-
}
418455
sessionManager.setActiveSessionId(sessionId);
419456
// Clear first so <Static> resets its index to 0.
420-
setMessages([]);
421-
setShowWelcome(false);
422-
setWelcomeNonce((n) => n + 1);
423-
setView("chat");
424-
// Load messages after the reset so all static items are rendered.
425-
setTimeout(() => {
426-
setMessages(loadVisibleMessages(sessionManager, sessionId));
427-
setShowWelcome(true);
428-
}, 0);
457+
resetStaticView(loadVisibleMessages(sessionManager, sessionId), { clearScreen: true });
429458
const session = sessionManager.getSession(sessionId);
430459
setStatusLine(session ? buildStatusLine(session) : "");
431460
setRunningProcesses(session?.processes ?? null);
@@ -436,7 +465,26 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
436465
}
437466
await refreshSkills(sessionId);
438467
},
439-
[pendingPermissionReply, sessionManager, refreshSkills]
468+
[sessionManager, resetStaticView, pendingPermissionReply, refreshSkills]
469+
);
470+
471+
const handleDeleteSession = useCallback(
472+
async (id: string): Promise<void> => {
473+
const isActiveSession = sessionManager.getActiveSessionId() === id;
474+
475+
// If the deleted session is the active one, clear the active session first
476+
if (isActiveSession) {
477+
sessionManager.setActiveSessionId(null);
478+
}
479+
480+
sessionManager.deleteSession(id);
481+
refreshSessionsList();
482+
483+
if (isActiveSession) {
484+
await resetToWelcome();
485+
}
486+
},
487+
[sessionManager, refreshSessionsList, resetToWelcome]
440488
);
441489

442490
const handleUndoRestore = useCallback(
@@ -487,25 +535,13 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
487535
setShowWelcome(false);
488536
setMessages([]);
489537
// Clear screen to remove stale formatted text.
490-
process.stdout.write("\u001B[2J\u001B[3J\u001B[H");
538+
process.stdout.write(ANSI_CLEAR_SCREEN);
491539

492540
setTimeout(() => {
493541
if (nextMode === RawMode.Raw) {
494542
// Write all messages directly to stdout for raw scrollback mode.
495543
const allMessages = activeSessionId ? loadVisibleMessages(sessionManager, activeSessionId) : [];
496-
for (const msg of allMessages) {
497-
process.stdout.write("\n");
498-
process.stdout.write(renderMessageToStdout(msg, nextMode) + "\n\n");
499-
}
500-
if (allMessages.length > 0) {
501-
process.stdout.write("\n\n");
502-
process.stdout.write(chalk.dim("Press ESC to exit raw mode"));
503-
} else {
504-
process.stdout.write("\n");
505-
process.stdout.write(chalk.dim("(No messages in this session yet. Start chatting to see them here.)"));
506-
process.stdout.write("\n\n");
507-
process.stdout.write(chalk.dim("Press ESC to exit raw mode"));
508-
}
544+
renderRawModeMessages(allMessages, nextMode);
509545
} else if (activeSessionId) {
510546
// Switch to chat view to render messages.
511547
handleSelectSession(activeSessionId);
@@ -538,22 +574,10 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
538574
if (mode === RawMode.Raw) {
539575
// In raw mode, re-render all messages directly to stdout at the new width.
540576
// Use process.stdout.write instead of writeRef to avoid Ink interference.
541-
process.stdout.write("\u001B[2J\u001B[3J\u001B[H");
577+
process.stdout.write(ANSI_CLEAR_SCREEN);
542578
const activeSessionId = sessionManager.getActiveSessionId();
543579
const allMessages = activeSessionId ? loadVisibleMessages(sessionManager, activeSessionId) : [];
544-
for (const msg of allMessages) {
545-
process.stdout.write("\n");
546-
process.stdout.write(renderMessageToStdout(msg, mode) + "\n\n");
547-
}
548-
if (allMessages.length > 0) {
549-
process.stdout.write("\n\n");
550-
process.stdout.write(chalk.dim("Press ESC to exit raw mode"));
551-
} else {
552-
process.stdout.write("\n");
553-
process.stdout.write(chalk.dim("(No messages in this session yet. Start chatting to see them here.)"));
554-
process.stdout.write("\n\n");
555-
process.stdout.write(chalk.dim("Press ESC to exit raw mode"));
556-
}
580+
renderRawModeMessages(allMessages, mode);
557581
return;
558582
}
559583

@@ -719,12 +743,7 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
719743
onSelect={(id) => void handleSelectSession(id)}
720744
onCancel={() => setView("chat")}
721745
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();
746+
void handleDeleteSession(id);
728747
}}
729748
/>
730749
) : view === "undo" ? (
@@ -784,6 +803,8 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
784803
);
785804
}
786805

806+
export default App;
807+
787808
function isCollapsedThinking(message: SessionMessage, expandedId: string | null): boolean {
788809
if (message.role !== "assistant") {
789810
return false;

src/ui/AppContainer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from "react";
22
import { AppContext } from "./contexts";
3-
import { App } from "./App";
3+
import App from "./App";
44
import { RawModeProvider } from "./contexts/RawModeContext";
55

66
const AppContainer: React.FC<{

src/ui/SessionList.tsx

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useState, useMemo, useCallback } from "react";
22
import { Box, Text, useInput, useWindowSize } from "ink";
33
import type { SessionEntry, SessionStatus } from "../session";
4+
import { truncate } from "./components/MessageView/utils";
45

56
type Props = {
67
sessions: SessionEntry[];
@@ -113,17 +114,10 @@ export function SessionList({ sessions, onSelect, onCancel, onDelete }: Props):
113114
return;
114115
}
115116

116-
// Backspace: remove last search character
117-
if (key.backspace) {
118-
if (searchQuery) {
119-
handleBackspace();
120-
return;
121-
}
122-
}
123-
124117
// Delete key: remove search character, or start delete confirmation
125-
if (key.delete) {
118+
if (key.delete || key.backspace) {
126119
if (searchQuery) {
120+
// remove last search character
127121
handleBackspace();
128122
return;
129123
}
@@ -342,10 +336,3 @@ export function formatSessionStatus(status: SessionStatus): string {
342336
return status;
343337
}
344338
}
345-
346-
function truncate(value: string, max: number): string {
347-
if (value.length <= max) {
348-
return value;
349-
}
350-
return `${value.slice(0, max)}…`;
351-
}

src/ui/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22

33
/** Separator used when rendering command arguments inline (e.g., `arg1 | arg2 | arg3`). */
44
export const ARGS_SEPARATOR = " | ";
5+
6+
/** ANSI escape code to clear the screen. */
7+
export const ANSI_CLEAR_SCREEN = "\u001B[2J\u001B[3J\u001B[H";

src/ui/utils/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import chalk from "chalk";
2+
import type { SessionMessage } from "../../session";
3+
import { renderMessageToStdout } from "../components/MessageView/utils";
4+
import type { RawMode } from "../contexts";
5+
6+
/**
7+
* Render all messages directly to stdout for Raw mode display.
8+
* Writes each message followed by the "Press ESC to exit raw mode" footer.
9+
*/
10+
export function renderRawModeMessages(allMessages: SessionMessage[], mode: string | RawMode): void {
11+
for (const msg of allMessages) {
12+
process.stdout.write("\n");
13+
process.stdout.write(renderMessageToStdout(msg, mode as RawMode) + "\n\n");
14+
}
15+
if (allMessages.length > 0) {
16+
process.stdout.write("\n\n");
17+
process.stdout.write(chalk.dim("Press ESC to exit raw mode"));
18+
} else {
19+
process.stdout.write("\n");
20+
process.stdout.write(chalk.dim("(No messages in this session yet. Start chatting to see them here.)"));
21+
process.stdout.write("\n\n");
22+
process.stdout.write(chalk.dim("Press ESC to exit raw mode"));
23+
}
24+
}

0 commit comments

Comments
 (0)