@@ -189,7 +189,7 @@ function isNotificationClicked(notificationId: string): boolean {
189189interface NotificationItem {
190190 id: string ;
191191 recordId: string ;
192- type: ' completion' | ' approval' ;
192+ type: ' completion' | ' approval' | ' idle ' ;
193193 sessionId: string ;
194194 projectId: string ;
195195 projectName? : string ;
@@ -206,9 +206,10 @@ interface NotificationItem {
206206 lastUserInput? : string ;
207207 assistantState? : string ;
208208 processStatus? : ' idle' | ' busy' | ' unknown' ;
209+ interrupted? : boolean ;
209210}
210211
211- type NotificationType = ' completion' | ' approval' ;
212+ type NotificationType = ' completion' | ' approval' | ' idle ' ;
212213
213214interface AssistantInfo {
214215 type? : string ;
@@ -376,6 +377,11 @@ function matchesDisplayMode(notification: NotificationItem) {
376377}
377378
378379function isIdleNotification(notification : NotificationItem ) {
380+ // idle 类型通知始终是空闲状态
381+ if (notification .type === ' idle' ) {
382+ return true ;
383+ }
384+
379385 if (notification .type === ' approval' ) {
380386 return true ;
381387 }
@@ -462,7 +468,15 @@ function getProjectBranchLabel(notification: NotificationItem) {
462468
463469function getCompletionHeader(notification : NotificationItem ) {
464470 const projectLabel = getProjectBranchLabel (notification );
465- const titleKey = notification .state === ' working' ? ' terminal.aiWorking' : ' terminal.aiCompleted' ;
471+ let titleKey: string ;
472+ if (notification .state === ' working' ) {
473+ titleKey = ' terminal.aiWorking' ;
474+ } else if (notification .interrupted || isNotificationClicked (notification .id )) {
475+ // 被中断或用户已看过的完成通知显示"空闲"
476+ titleKey = ' terminal.aiIdle' ;
477+ } else {
478+ titleKey = ' terminal.aiCompleted' ;
479+ }
466480 const baseTitle = t (titleKey );
467481 return projectLabel ? ` ${baseTitle } - ${projectLabel } ` : baseTitle ;
468482}
@@ -474,25 +488,32 @@ function getApprovalHeader(notification: NotificationItem) {
474488 : t (' terminal.aiNeedsApproval' );
475489}
476490
491+ function getIdleHeader(notification : NotificationItem ) {
492+ const projectLabel = getProjectBranchLabel (notification );
493+ const baseTitle = t (' terminal.aiIdle' );
494+ return projectLabel ? ` ${baseTitle } - ${projectLabel } ` : baseTitle ;
495+ }
496+
477497function getNotificationHeader(notification : NotificationItem ) {
478- return notification .type === ' completion'
479- ? getCompletionHeader (notification )
480- : getApprovalHeader (notification );
498+ if (notification .type === ' completion' ) {
499+ return getCompletionHeader (notification );
500+ }
501+ if (notification .type === ' idle' ) {
502+ return getIdleHeader (notification );
503+ }
504+ return getApprovalHeader (notification );
481505}
482506
483507function formatCompletionBody(notification : NotificationItem ) {
484508 return notification .title ;
485509}
486510
487511function getNotificationDescription(notification : NotificationItem ) {
488- const body =
489- notification .type === ' completion'
490- ? formatCompletionBody (notification )
491- : ` ${t (' terminal.isWaitingForApproval' )} - ${notification .title } ` ;
492- // 工作中和任务完成的卡片第二行不显示分支名
493- if (notification .type === ' completion' ) {
494- return body ;
512+ // 工作中、任务完成和空闲的卡片第二行不显示分支名
513+ if (notification .type === ' completion' || notification .type === ' idle' ) {
514+ return notification .title ;
495515 }
516+ const body = ` ${t (' terminal.isWaitingForApproval' )} - ${notification .title } ` ;
496517 const location = getLocationLabel (notification );
497518 return location ? ` [${location }] ${body } ` : body ;
498519}
@@ -573,6 +594,7 @@ function mapCompletionRecord(record: CompletionRecordResponse): NotificationItem
573594 const assistantType = record .assistant ?.type ;
574595 const processStatus = session ?.processStatus as ' idle' | ' busy' | ' unknown' | undefined ;
575596 const assistantState = session ?.aiAssistant ?.state ;
597+ const interrupted = session ?.aiAssistant ?.interrupted === true ;
576598 // 直接使用后端返回的 lastUserInput,不回退到前端数据
577599 const lastUserInput = record .lastUserInput ?.trim () || ' ' ;
578600
@@ -595,6 +617,7 @@ function mapCompletionRecord(record: CompletionRecordResponse): NotificationItem
595617 assistantState ,
596618 processStatus ,
597619 lastUserInput: lastUserInput || undefined ,
620+ interrupted ,
598621 };
599622}
600623
@@ -633,7 +656,18 @@ function sortNotifications(list: NotificationItem[]) {
633656}
634657
635658function setNotificationsForType(type : NotificationType , items : NotificationItem []) {
636- const others = notifications .value .filter (item => item .type !== type );
659+ // 移除与新通知相同 session 的 idle 通知
660+ const sessionIdsWithNewNotifications = new Set (items .map (item => item .sessionId ));
661+ const others = notifications .value .filter (item => {
662+ if (item .type === type ) {
663+ return false ; // 移除旧的同类型通知
664+ }
665+ // 移除与新通知相同 session 的 idle 通知
666+ if (item .type === ' idle' && sessionIdsWithNewNotifications .has (item .sessionId )) {
667+ return false ;
668+ }
669+ return true ;
670+ });
637671 notifications .value = sortNotifications ([... others , ... items ]);
638672 if (type === ' completion' ) {
639673 autoMarkActiveCompletionNotifications ();
@@ -683,6 +717,9 @@ function getNotificationClass(notification: NotificationItem) {
683717 if (notification .type === ' completion' ) {
684718 return notification .state === ' working' ? ' notification-working' : ' notification-completion' ;
685719 }
720+ if (notification .type === ' idle' ) {
721+ return ' notification-idle' ;
722+ }
686723 return ' notification-approval' ;
687724}
688725
@@ -692,6 +729,11 @@ function handleAIWorking(event: any) {
692729 return ;
693730 }
694731
732+ // AI 开始工作时,移除该 session 的 idle 通知
733+ notifications .value = notifications .value .filter (
734+ n => ! (n .sessionId === sessionId && n .type === ' idle' )
735+ );
736+
695737 const eventCommand =
696738 typeof event ?.latestCommand === ' string' && event .latestCommand .trim ()
697739 ? event .latestCommand .trim ()
@@ -785,14 +827,68 @@ function handleAIApproval() {
785827
786828// 处理 AI 关闭事件
787829function handleAIClosed(data : { sessionId: string }) {
788- console .log (' [AI Notification] AI closed, refreshing records' , data );
789- // 刷新通知列表以移除该 session 的通知
830+ const { sessionId } = data ;
831+ console .log (' [AI Notification] AI closed, removing all notifications for session' , { sessionId });
832+
833+ // 移除该 session 的所有通知(idle、completion、approval)
834+ notifications .value = notifications .value .filter (n => n .sessionId !== sessionId );
835+
836+ // 刷新通知列表以确保后端状态同步
790837 window .setTimeout (() => {
791838 void fetchCompletionRecords ();
792839 void fetchApprovalRecords ();
793840 }, 150 );
794841}
795842
843+ // 处理 AI Agent 检测事件
844+ interface AIDetectedEvent {
845+ sessionId: string ;
846+ sessionTitle: string ;
847+ projectId: string ;
848+ projectName? : string ;
849+ worktreeId? : string ;
850+ detectedAt: Date ;
851+ assistantName? : string ;
852+ assistantType? : string ;
853+ }
854+
855+ function handleAIDetected(event : AIDetectedEvent ) {
856+ const { sessionId, sessionTitle, projectId, projectName, worktreeId, detectedAt, assistantName, assistantType } = event ;
857+
858+ // 检查是否已存在该 session 的通知
859+ const existingNotification = notifications .value .find (
860+ n => n .sessionId === sessionId && n .type === ' idle'
861+ );
862+ if (existingNotification ) {
863+ return ;
864+ }
865+
866+ // 解析分支名
867+ const branchName = resolveBranchName (projectId , worktreeId );
868+
869+ // 创建空闲通知
870+ const idleNotification: NotificationItem = {
871+ id: ` idle-${sessionId } ` ,
872+ recordId: ` idle-${sessionId } ` ,
873+ type: ' idle' ,
874+ sessionId ,
875+ projectId ,
876+ projectName: projectName || getProjectNameById (projectId ),
877+ worktreeId ,
878+ branchName ,
879+ title: sessionTitle || ' Terminal' ,
880+ assistantName: assistantName || ' ' ,
881+ assistantType ,
882+ assistantIcon: getAssistantIconByType (assistantType ),
883+ assistantColor: getAssistantColorByType (assistantType ),
884+ timestamp: detectedAt ,
885+ processStatus: ' idle' ,
886+ };
887+
888+ notifications .value = sortNotifications ([... notifications .value , idleNotification ]);
889+ console .log (' [AI Notification] AI Agent detected, adding idle notification' , { sessionId , projectId , assistantName });
890+ }
891+
796892// 点击通知,切换到对应的终端
797893async function handleNotificationClick(notification : NotificationItem ) {
798894 // 记录该通知已被点击
@@ -826,6 +922,11 @@ async function handleNotificationClick(notification: NotificationItem) {
826922// 关闭通知
827923async function dismissNotification(notification : NotificationItem ) {
828924 try {
925+ if (notification .type === ' idle' ) {
926+ // idle 类型通知只是本地的,直接移除
927+ removeNotificationLocally (notification .recordId );
928+ return ;
929+ }
829930 if (notification .type === ' completion' ) {
830931 await Apis .terminalSession
831932 .terminalCompletionRecordDismiss ({
@@ -884,6 +985,11 @@ function handleSessionClose(sessionId: string) {
884985 // 清理状态跟踪
885986 sessionHasAI .delete (sessionId );
886987
988+ // 移除该 session 的 idle 通知
989+ notifications .value = notifications .value .filter (
990+ n => ! (n .sessionId === sessionId && n .type === ' idle' )
991+ );
992+
887993 void fetchCompletionRecords ();
888994 void fetchApprovalRecords ();
889995}
@@ -950,6 +1056,7 @@ onMounted(() => {
9501056 terminalStore .emitter .on (' ai:approval-needed' , handleAIApproval );
9511057 terminalStore .emitter .on (' ai:working' , handleAIWorking );
9521058 terminalStore .emitter .on (' ai:closed' , handleAIClosed );
1059+ terminalStore .emitter .on (' ai:detected' , handleAIDetected );
9531060 terminalStore .emitter .on (' terminal:viewed' , handleTerminalViewedEvent );
9541061
9551062 // useAutoReq 已设置 immediate: true,会自动发起首次请求
@@ -964,6 +1071,7 @@ onUnmounted(() => {
9641071 terminalStore .emitter .off (' ai:approval-needed' , handleAIApproval );
9651072 terminalStore .emitter .off (' ai:working' , handleAIWorking );
9661073 terminalStore .emitter .off (' ai:closed' , handleAIClosed );
1074+ terminalStore .emitter .off (' ai:detected' , handleAIDetected );
9671075 terminalStore .emitter .off (' terminal:viewed' , handleTerminalViewedEvent );
9681076
9691077 // 取消订阅所有终端
@@ -1173,9 +1281,6 @@ watch(
11731281 </template >
11741282 <!-- 普通模式:保持原有显示逻辑 -->
11751283 <template v-else >
1176- <span v-if =" notification.type !== 'completion' && getLocationLabel(notification)" class =" project-badge" >
1177- [{{ getLocationLabel(notification) }}]
1178- </span >
11791284 <span class =" notification-text" >
11801285 <span class =" notification-tab-label" >
11811286 {{ getTabLabel(notification) }}
@@ -1416,6 +1521,20 @@ watch(
14161521 box-shadow : 0 12px 28px rgba (15 , 23 , 42 , 0.12 ) !important ;
14171522}
14181523
1524+ /* 空闲通知样式 - 灰色边框,白色背景 */
1525+ .notification-idle {
1526+ --notification-idle-fill : rgba (156 , 163 , 175 , 0.15 );
1527+ --notification-idle-accent : rgba (156 , 163 , 175 , 0.8 );
1528+ background : #ffffff ;
1529+ border-color : rgba (156 , 163 , 175 , 0.3 );
1530+ border-left-color : #9ca3af ;
1531+ box-shadow : 0 12px 28px rgba (15 , 23 , 42 , 0.12 );
1532+ }
1533+
1534+ .notification-idle .notification-icon {
1535+ color : var (--notification-idle-accent , #9ca3af );
1536+ }
1537+
14191538/* 工作中 / 审批通知在已读后保持原样 */
14201539.notification-approval {
14211540 --notification-approval-fill : var (--kanban-terminal-tab-approval-bg , rgba (247 , 144 , 9 , 0.25 ));
0 commit comments