Skip to content

Commit 631bb7b

Browse files
committed
feat: 现在刚启动的agent就会加入右上角提醒列表
1 parent f5acd02 commit 631bb7b

File tree

8 files changed

+181
-42
lines changed

8 files changed

+181
-42
lines changed

service/terminal/manager.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,8 +486,8 @@ func (m *Manager) monitorAssistantRecordsWithStream(session *Session, stream *Se
486486
switch event.Type {
487487
case StreamEventMetadata:
488488
metadata := event.Metadata
489-
if metadata == nil || metadata.AIAssistant == nil {
490-
// AI 助手 detach ,清除该 session 的所有记录
489+
if metadata == nil || metadata.AIAssistant == nil || !metadata.AIAssistant.Detected {
490+
// AI 助手 detach 或关闭时,清除该 session 的所有记录
491491
if lastState != string(types.StateUnknown) {
492492
m.recordManager.ClearSessionRecords(session.ID())
493493
lastState = string(types.StateUnknown)

service/terminal/session.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -519,11 +519,12 @@ func (s *Session) checkAndBroadcastMetadata() {
519519
}
520520

521521
metadata := &SessionMetadata{
522-
ProcessPID: pid,
523-
ProcessStatus: process.GetProcessStatus(pid),
524-
ProcessHasChildren: process.IsProcessBusy(pid),
525-
TaskID: s.TaskID(),
526-
Title: s.Title(),
522+
ProcessPID: pid,
523+
ProcessStatus: process.GetProcessStatus(pid),
524+
ProcessHasChildren: process.IsProcessBusy(pid),
525+
TaskID: s.TaskID(),
526+
Title: s.Title(),
527+
AIAssistantRecentInput: s.LastRecentInput(),
527528
}
528529

529530
tracker := s.assistantTracker
@@ -599,7 +600,8 @@ func (s *Session) metadataChanged(old, new *SessionMetadata) bool {
599600
old.ProcessHasChildren != new.ProcessHasChildren ||
600601
old.RunningCommand != new.RunningCommand ||
601602
old.TaskID != new.TaskID ||
602-
old.AISessionID != new.AISessionID {
603+
old.AISessionID != new.AISessionID ||
604+
old.AIAssistantRecentInput != new.AIAssistantRecentInput {
603605
return true
604606
}
605607

ui/components.d.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,13 @@ declare module 'vue' {
2929
KanbanColumn: typeof import('./src/components/kanban/KanbanColumn.vue')['default']
3030
LanguageSwitcher: typeof import('./src/components/common/LanguageSwitcher.vue')['default']
3131
NAlert: typeof import('naive-ui')['NAlert']
32-
NAvatar: typeof import('naive-ui')['NAvatar']
3332
NBadge: typeof import('naive-ui')['NBadge']
3433
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
3534
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
3635
NButton: typeof import('naive-ui')['NButton']
3736
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
3837
NCard: typeof import('naive-ui')['NCard']
3938
NCheckbox: typeof import('naive-ui')['NCheckbox']
40-
NCollapse: typeof import('naive-ui')['NCollapse']
41-
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
4239
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
4340
NColorPicker: typeof import('naive-ui')['NColorPicker']
4441
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
@@ -53,25 +50,20 @@ declare module 'vue' {
5350
NEmpty: typeof import('naive-ui')['NEmpty']
5451
NForm: typeof import('naive-ui')['NForm']
5552
NFormItem: typeof import('naive-ui')['NFormItem']
56-
NGi: typeof import('naive-ui')['NGi']
5753
NGlobalStyle: typeof import('naive-ui')['NGlobalStyle']
58-
NGrid: typeof import('naive-ui')['NGrid']
59-
NH3: typeof import('naive-ui')['NH3']
6054
NIcon: typeof import('naive-ui')['NIcon']
6155
NInput: typeof import('naive-ui')['NInput']
6256
NInputNumber: typeof import('naive-ui')['NInputNumber']
6357
NLayout: typeof import('naive-ui')['NLayout']
6458
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
6559
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
66-
NLi: typeof import('naive-ui')['NLi']
6760
NList: typeof import('naive-ui')['NList']
6861
NListItem: typeof import('naive-ui')['NListItem']
6962
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
7063
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
7164
NModal: typeof import('naive-ui')['NModal']
7265
NModalProvider: typeof import('naive-ui')['NModalProvider']
7366
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
74-
NOl: typeof import('naive-ui')['NOl']
7567
NotePad: typeof import('./src/components/notepad/NotePad.vue')['default']
7668
NPageHeader: typeof import('naive-ui')['NPageHeader']
7769
NPopover: typeof import('naive-ui')['NPopover']
@@ -82,17 +74,12 @@ declare module 'vue' {
8274
NSlider: typeof import('naive-ui')['NSlider']
8375
NSpace: typeof import('naive-ui')['NSpace']
8476
NSpin: typeof import('naive-ui')['NSpin']
85-
NStatistic: typeof import('naive-ui')['NStatistic']
86-
NStep: typeof import('naive-ui')['NStep']
87-
NSteps: typeof import('naive-ui')['NSteps']
8877
NSwitch: typeof import('naive-ui')['NSwitch']
8978
NTabPane: typeof import('naive-ui')['NTabPane']
9079
NTabs: typeof import('naive-ui')['NTabs']
9180
NTag: typeof import('naive-ui')['NTag']
9281
NText: typeof import('naive-ui')['NText']
9382
NTooltip: typeof import('naive-ui')['NTooltip']
94-
NUl: typeof import('naive-ui')['NUl']
95-
NVirtualList: typeof import('naive-ui')['NVirtualList']
9683
ProjectCreateDialog: typeof import('./src/components/project/ProjectCreateDialog.vue')['default']
9784
ProjectEditDialog: typeof import('./src/components/project/ProjectEditDialog.vue')['default']
9885
RecentProjects: typeof import('./src/components/project/RecentProjects.vue')['default']

ui/src/components/terminal/AINotificationBar.vue

Lines changed: 138 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ function isNotificationClicked(notificationId: string): boolean {
189189
interface 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
213214
interface AssistantInfo {
214215
type?: string;
@@ -376,6 +377,11 @@ function matchesDisplayMode(notification: NotificationItem) {
376377
}
377378
378379
function 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
463469
function 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+
477497
function 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
483507
function formatCompletionBody(notification: NotificationItem) {
484508
return notification.title;
485509
}
486510
487511
function 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
635658
function 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 关闭事件
787829
function 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
// 点击通知,切换到对应的终端
797893
async function handleNotificationClick(notification: NotificationItem) {
798894
// 记录该通知已被点击
@@ -826,6 +922,11 @@ async function handleNotificationClick(notification: NotificationItem) {
826922
// 关闭通知
827923
async 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

Comments
 (0)