22// SPDX-License-Identifier: Apache-2.0
33
44import { WaveAIModel } from "@/app/aipanel/waveai-model" ;
5+ import { BlockModel } from "@/app/block/block-model" ;
56import { BlockNodeModel } from "@/app/block/blocktypes" ;
67import { appHandleKeyDown } from "@/app/store/keymodel" ;
8+ import { FocusManager } from "@/app/store/focusManager" ;
79import { modalsModel } from "@/app/store/modalmodel" ;
10+ import { setBadge } from "@/app/store/badge" ;
811import type { TabModel } from "@/app/store/tab-model" ;
912import { waveEventSubscribeSingle } from "@/app/store/wps" ;
1013import { RpcApi } from "@/app/store/wshclientapi" ;
@@ -14,6 +17,7 @@ import { TermClaudeIcon, TerminalView } from "@/app/view/term/term";
1417import { TermWshClient } from "@/app/view/term/term-wsh" ;
1518import { VDomModel } from "@/app/view/vdom/vdom-model" ;
1619import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model" ;
20+ import { getLayoutModelForStaticTab } from "@/layout/lib/layoutModelHooks" ;
1721import {
1822 atoms ,
1923 createBlock ,
@@ -73,6 +77,7 @@ export class TermViewModel implements ViewModel {
7377 shellProcFullStatus : jotai . PrimitiveAtom < BlockControllerRuntimeStatus > ;
7478 shellProcStatus : jotai . Atom < string > ;
7579 shellProcStatusUnsubFn : ( ) => void ;
80+ blockDoneUnsubFn : ( ) => void ;
7681 blockJobStatusAtom : jotai . PrimitiveAtom < BlockJobStatusData > ;
7782 blockJobStatusVersionTs : number ;
7883 blockJobStatusUnsubFn : ( ) => void ;
@@ -346,6 +351,13 @@ export class TermViewModel implements ViewModel {
346351 this . updateShellProcStatus ( event . data ) ;
347352 } ,
348353 } ) ;
354+ this . blockDoneUnsubFn = waveEventSubscribeSingle ( {
355+ eventType : "block:done" ,
356+ scope : WOS . makeORef ( "block" , blockId ) ,
357+ handler : ( event ) => {
358+ this . handleBlockDoneEvent ( event . data ) ;
359+ } ,
360+ } ) ;
349361 this . shellProcStatus = jotai . atom ( ( get ) => {
350362 const fullStatus = get ( this . shellProcFullStatus ) ;
351363 return fullStatus ?. shellprocstatus ?? "init" ;
@@ -565,6 +577,74 @@ export class TermViewModel implements ViewModel {
565577 }
566578 }
567579
580+ getLastTerminalLine ( ) : string {
581+ const term = this . termRef . current ?. terminal ;
582+ if ( term == null ) return "" ;
583+ const buf = term . buffer . active ;
584+ for ( let i = buf . length - 1 ; i >= 0 ; i -- ) {
585+ const line = buf . getLine ( i ) ;
586+ if ( line == null ) continue ;
587+ const text = line . translateToString ( true ) . trim ( ) ;
588+ if ( text . length > 0 ) return text ;
589+ }
590+ return "" ;
591+ }
592+
593+ handleBlockDoneEvent ( data : BlockDoneEventData ) {
594+ if ( data == null || data . blockid !== this . blockId ) {
595+ return ;
596+ }
597+ const exitCode = data . exitcode ?? 0 ;
598+ const title = data . title || ( exitCode === 0 ? "Command Finished" : "Command Failed" ) ;
599+ let body = data . message ;
600+ if ( ! body ) {
601+ body = this . getLastTerminalLine ( ) || `exit code ${ exitCode } ` ;
602+ }
603+ this . triggerCompletionNotifications ( exitCode , title , body ) ;
604+ }
605+
606+ triggerCompletionNotifications ( exitCode : number , title : string , notifyBody ?: string ) {
607+ const focusManager = FocusManager . getInstance ( ) ;
608+ const focusedBlockId = globalStore . get ( focusManager . blockFocusAtom ) ;
609+ if ( focusedBlockId === this . blockId ) {
610+ return ;
611+ }
612+
613+ const doneSoundEnabled = globalStore . get ( getOverrideConfigAtom ( this . blockId , "term:donesound" ) ) ?? true ;
614+ if ( doneSoundEnabled ) {
615+ fireAndForget ( ( ) =>
616+ RpcApi . ElectronSystemBellCommand ( TabRpcClient , { route : "electron" } )
617+ ) ;
618+ }
619+
620+ const doneNotifyEnabled = globalStore . get ( getOverrideConfigAtom ( this . blockId , "term:donenotify" ) ) ?? true ;
621+ if ( doneNotifyEnabled ) {
622+ const body = notifyBody || `exit code ${ exitCode } ` ;
623+ getApi ( ) . showCompletionNotification ( this . tabModel . tabId , this . blockId , title , body ) ;
624+ }
625+
626+ const doneAutoFocusEnabled = globalStore . get ( getOverrideConfigAtom ( this . blockId , "term:doneautofocus" ) ) ?? false ;
627+ if ( doneAutoFocusEnabled ) {
628+ getApi ( ) . setActiveTab ( this . tabModel . tabId ) ;
629+ setTimeout ( ( ) => {
630+ const layoutModel = getLayoutModelForStaticTab ( ) ;
631+ const node = layoutModel ?. getNodeByBlockId ( this . blockId ) ;
632+ if ( node ?. id ) {
633+ layoutModel . focusNode ( node . id ) ;
634+ }
635+ } , 150 ) ;
636+ }
637+
638+ BlockModel . getInstance ( ) . setCompletionHighlight ( this . blockId , exitCode ) ;
639+
640+ setBadge ( this . blockId , {
641+ badgeid : `done-${ this . blockId } ` ,
642+ icon : "bell" ,
643+ color : exitCode === 0 ? "#3b82f6" : "#ef4444" ,
644+ priority : 5 ,
645+ } ) ;
646+ }
647+
568648 getVDomModel ( ) : VDomModel {
569649 const vdomBlockId = globalStore . get ( this . vdomBlockId ) ;
570650 if ( ! vdomBlockId ) {
0 commit comments