@@ -63,12 +63,41 @@ interface ChatState {
6363 newSession : ( ) => void ;
6464 loadHistory : ( ) => Promise < void > ;
6565 sendMessage : ( text : string , attachments ?: { type : string ; mimeType : string ; fileName : string ; content : string } [ ] ) => Promise < void > ;
66+ abortRun : ( ) => Promise < void > ;
6667 handleChatEvent : ( event : Record < string , unknown > ) => void ;
6768 toggleThinking : ( ) => void ;
6869 refresh : ( ) => Promise < void > ;
6970 clearError : ( ) => void ;
7071}
7172
73+ function isToolOnlyMessage ( message : RawMessage | undefined ) : boolean {
74+ if ( ! message ) return false ;
75+ if ( message . role === 'toolresult' ) return true ;
76+
77+ const content = message . content ;
78+ if ( ! Array . isArray ( content ) ) return false ;
79+
80+ let hasTool = false ;
81+ let hasText = false ;
82+ let hasNonToolContent = false ;
83+
84+ for ( const block of content as ContentBlock [ ] ) {
85+ if ( block . type === 'tool_use' || block . type === 'tool_result' ) {
86+ hasTool = true ;
87+ continue ;
88+ }
89+ if ( block . type === 'text' && block . text && block . text . trim ( ) ) {
90+ hasText = true ;
91+ continue ;
92+ }
93+ if ( block . type === 'image' || block . type === 'thinking' ) {
94+ hasNonToolContent = true ;
95+ }
96+ }
97+
98+ return hasTool && ! hasText && ! hasNonToolContent ;
99+ }
100+
72101// ── Store ────────────────────────────────────────────────────────
73102
74103export const useChatStore = create < ChatState > ( ( set , get ) => ( {
@@ -260,6 +289,23 @@ export const useChatStore = create<ChatState>((set, get) => ({
260289 }
261290 } ,
262291
292+ // ── Abort active run ──
293+
294+ abortRun : async ( ) => {
295+ const { currentSessionKey } = get ( ) ;
296+ set ( { sending : false , streamingText : '' , streamingMessage : null } ) ;
297+
298+ try {
299+ await window . electron . ipcRenderer . invoke (
300+ 'gateway:rpc' ,
301+ 'chat.abort' ,
302+ { sessionKey : currentSessionKey } ,
303+ ) ;
304+ } catch ( err ) {
305+ set ( { error : String ( err ) } ) ;
306+ }
307+ } ,
308+
263309 // ── Handle incoming chat events from Gateway ──
264310
265311 handleChatEvent : ( event : Record < string , unknown > ) => {
@@ -282,20 +328,32 @@ export const useChatStore = create<ChatState>((set, get) => ({
282328 // Message complete - add to history and clear streaming
283329 const finalMsg = event . message as RawMessage | undefined ;
284330 if ( finalMsg ) {
285- const msgId = finalMsg . id || `run-${ runId } ` ;
331+ const toolOnly = isToolOnlyMessage ( finalMsg ) ;
332+ const msgId = finalMsg . id || ( toolOnly ? `run-${ runId } -tool-${ Date . now ( ) } ` : `run-${ runId } ` ) ;
286333 set ( ( s ) => {
287334 // Check if message already exists (prevent duplicates)
288335 const alreadyExists = s . messages . some ( m => m . id === msgId ) ;
289336 if ( alreadyExists ) {
290337 // Just clear streaming state, don't add duplicate
291- return {
338+ return toolOnly ? {
339+ streamingText : '' ,
340+ streamingMessage : null ,
341+ } : {
292342 streamingText : '' ,
293343 streamingMessage : null ,
294344 sending : false ,
295345 activeRunId : null ,
296346 } ;
297347 }
298- return {
348+ return toolOnly ? {
349+ messages : [ ...s . messages , {
350+ ...finalMsg ,
351+ role : finalMsg . role || 'assistant' ,
352+ id : msgId ,
353+ } ] ,
354+ streamingText : '' ,
355+ streamingMessage : null ,
356+ } : {
299357 messages : [ ...s . messages , {
300358 ...finalMsg ,
301359 role : finalMsg . role || 'assistant' ,
0 commit comments