@@ -43,6 +43,8 @@ import { PermissionPrompt, type PermissionPromptResult } from "./PermissionPromp
4343import { buildExitSummaryText } from "./exitSummary" ;
4444import { RawMode , useRawModeContext } from "./contexts" ;
4545import { renderMessageToStdout } from "./components/MessageView/utils" ;
46+ import { renderRawModeMessages } from "./utils" ;
47+ import { ANSI_CLEAR_SCREEN } from "./constants" ;
4648
4749const DEFAULT_MODEL = "deepseek-v4-pro" ;
4850const 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+
787808function isCollapsedThinking ( message : SessionMessage , expandedId : string | null ) : boolean {
788809 if ( message . role !== "assistant" ) {
789810 return false ;
0 commit comments