mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-21 14:03:35 +08:00
fix(desktop): suppress task notification follow-up output (#886)
Keep task-notification-only follow-up turns out of the live desktop transcript so background command completion notices do not render as repeated thinking, assistant text, or completed-duration rows. Refs: #886 Tested: cd desktop && bun run test -- src/stores/chatStore.test.ts && bun run lint Confidence: high Scope-risk: narrow
This commit is contained in:
parent
8d12fdbcff
commit
dedd51f6f4
@ -3437,6 +3437,64 @@ describe('chatStore history mapping', () => {
|
|||||||
]))
|
]))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('suppresses assistant output for a task-notification-only follow-up turn', () => {
|
||||||
|
useChatStore.setState({
|
||||||
|
sessions: {
|
||||||
|
[TEST_SESSION_ID]: makeSession({
|
||||||
|
chatState: 'idle',
|
||||||
|
elapsedSeconds: 718,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
||||||
|
type: 'system_notification',
|
||||||
|
subtype: 'task_notification',
|
||||||
|
data: {
|
||||||
|
task_id: 'butp7dybq',
|
||||||
|
tool_use_id: 'toolu_bdrk_01SvH8CKoRoBcv1T1Gr9jWT3',
|
||||||
|
status: 'completed',
|
||||||
|
summary: 'Background command "1000 客户端压测并采样服务端内存" completed (exit code 0)',
|
||||||
|
output_file: '/tmp/butp7dybq.output',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
||||||
|
type: 'thinking',
|
||||||
|
text: "The earlier monitoring command has already been handled by subsequent work, so there's nothing more to add here.",
|
||||||
|
})
|
||||||
|
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
||||||
|
type: 'content_start',
|
||||||
|
blockType: 'text',
|
||||||
|
})
|
||||||
|
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
||||||
|
type: 'content_delta',
|
||||||
|
text: '那是早前的监控命令收尾通知,已被后续的多核压测取代,无需处理。交付已全部完成并验证通过。',
|
||||||
|
})
|
||||||
|
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
||||||
|
type: 'message_complete',
|
||||||
|
usage: { input_tokens: 12, output_tokens: 34 },
|
||||||
|
})
|
||||||
|
|
||||||
|
const session = useChatStore.getState().sessions[TEST_SESSION_ID]
|
||||||
|
expect(session?.messages).toMatchObject([
|
||||||
|
{
|
||||||
|
type: 'background_task',
|
||||||
|
task: {
|
||||||
|
taskId: 'butp7dybq',
|
||||||
|
toolUseId: 'toolu_bdrk_01SvH8CKoRoBcv1T1Gr9jWT3',
|
||||||
|
status: 'completed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(session?.messages).not.toEqual(expect.arrayContaining([
|
||||||
|
expect.objectContaining({ type: 'thinking' }),
|
||||||
|
expect.objectContaining({ type: 'assistant_text' }),
|
||||||
|
expect.objectContaining({ type: 'system', content: 'Completed in 11m 58s' }),
|
||||||
|
]))
|
||||||
|
expect(session?.chatState).toBe('idle')
|
||||||
|
expect(updateTabStatusMock).toHaveBeenLastCalledWith(TEST_SESSION_ID, 'idle')
|
||||||
|
})
|
||||||
|
|
||||||
it('flushes a delayed completion before a new user turn while background tasks keep running', () => {
|
it('flushes a delayed completion before a new user turn while background tasks keep running', () => {
|
||||||
useChatStore.setState({
|
useChatStore.setState({
|
||||||
sessions: {
|
sessions: {
|
||||||
|
|||||||
@ -113,6 +113,7 @@ export type PerSessionState = {
|
|||||||
agentTaskNotifications: Record<string, AgentTaskNotification>
|
agentTaskNotifications: Record<string, AgentTaskNotification>
|
||||||
backgroundAgentTasks?: Record<string, BackgroundAgentTask>
|
backgroundAgentTasks?: Record<string, BackgroundAgentTask>
|
||||||
pendingCompletedTurnElapsedSeconds?: number | null
|
pendingCompletedTurnElapsedSeconds?: number | null
|
||||||
|
suppressNextTaskNotificationResponse?: boolean
|
||||||
activeGoal?: ActiveGoalState | null
|
activeGoal?: ActiveGoalState | null
|
||||||
elapsedTimer: ReturnType<typeof setInterval> | null
|
elapsedTimer: ReturnType<typeof setInterval> | null
|
||||||
composerPrefill?: {
|
composerPrefill?: {
|
||||||
@ -150,6 +151,7 @@ const DEFAULT_SESSION_STATE: PerSessionState = {
|
|||||||
agentTaskNotifications: {},
|
agentTaskNotifications: {},
|
||||||
backgroundAgentTasks: {},
|
backgroundAgentTasks: {},
|
||||||
pendingCompletedTurnElapsedSeconds: null,
|
pendingCompletedTurnElapsedSeconds: null,
|
||||||
|
suppressNextTaskNotificationResponse: false,
|
||||||
activeGoal: null,
|
activeGoal: null,
|
||||||
elapsedTimer: null,
|
elapsedTimer: null,
|
||||||
composerPrefill: null,
|
composerPrefill: null,
|
||||||
@ -677,6 +679,17 @@ function isAgentBackgroundTask(task: Pick<BackgroundAgentTask, 'taskType' | 'sum
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldSuppressTaskNotificationResponse(session: PerSessionState): boolean {
|
||||||
|
const lastMessage = session.messages[session.messages.length - 1]
|
||||||
|
const hasVisibleActiveOutput =
|
||||||
|
session.streamingText.trim().length > 0 ||
|
||||||
|
Boolean(session.activeToolUseId) ||
|
||||||
|
session.chatState === 'streaming' ||
|
||||||
|
session.chatState === 'tool_executing' ||
|
||||||
|
session.chatState === 'permission_pending'
|
||||||
|
return !hasVisibleActiveOutput && lastMessage?.type !== 'user_text'
|
||||||
|
}
|
||||||
|
|
||||||
function mergeRestoredTerminalGoalEvents(
|
function mergeRestoredTerminalGoalEvents(
|
||||||
messages: UIMessage[],
|
messages: UIMessage[],
|
||||||
restoredMessages: UIMessage[],
|
restoredMessages: UIMessage[],
|
||||||
@ -1132,6 +1145,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|||||||
chatState: 'thinking',
|
chatState: 'thinking',
|
||||||
elapsedSeconds: 0,
|
elapsedSeconds: 0,
|
||||||
pendingCompletedTurnElapsedSeconds: null,
|
pendingCompletedTurnElapsedSeconds: null,
|
||||||
|
suppressNextTaskNotificationResponse: false,
|
||||||
streamingText: '',
|
streamingText: '',
|
||||||
streamingResponseChars: 0,
|
streamingResponseChars: 0,
|
||||||
statusVerb: isMemberSession ? '' : randomSpinnerVerb(),
|
statusVerb: isMemberSession ? '' : randomSpinnerVerb(),
|
||||||
@ -1245,6 +1259,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|||||||
pendingCompletedTurnElapsedSeconds: hasRunningBackgroundAgents
|
pendingCompletedTurnElapsedSeconds: hasRunningBackgroundAgents
|
||||||
? session.pendingCompletedTurnElapsedSeconds ?? null
|
? session.pendingCompletedTurnElapsedSeconds ?? null
|
||||||
: null,
|
: null,
|
||||||
|
suppressNextTaskNotificationResponse: false,
|
||||||
elapsedTimer: null,
|
elapsedTimer: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1544,6 +1559,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|||||||
queuedUserMessages: (currentSession.queuedUserMessages ?? [])
|
queuedUserMessages: (currentSession.queuedUserMessages ?? [])
|
||||||
.filter((message) => message.id !== messageId),
|
.filter((message) => message.id !== messageId),
|
||||||
...(pendingText.trim() ? { streamingText: '' } : {}),
|
...(pendingText.trim() ? { streamingText: '' } : {}),
|
||||||
|
suppressNextTaskNotificationResponse: false,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
@ -1567,6 +1583,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|||||||
apiRetry: null,
|
apiRetry: null,
|
||||||
streamingFallback: null,
|
streamingFallback: null,
|
||||||
pendingCompletedTurnElapsedSeconds: null,
|
pendingCompletedTurnElapsedSeconds: null,
|
||||||
|
suppressNextTaskNotificationResponse: false,
|
||||||
queuedUserMessages: [],
|
queuedUserMessages: [],
|
||||||
})) }))
|
})) }))
|
||||||
},
|
},
|
||||||
@ -1668,6 +1685,18 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|||||||
case 'content_start': {
|
case 'content_start': {
|
||||||
const session = get().sessions[sessionId]
|
const session = get().sessions[sessionId]
|
||||||
if (!session) break
|
if (!session) break
|
||||||
|
if (session.suppressNextTaskNotificationResponse && msg.blockType === 'text') {
|
||||||
|
consumePendingDelta(sessionId)
|
||||||
|
update(() => ({
|
||||||
|
streamingText: '',
|
||||||
|
activeThinkingId: null,
|
||||||
|
statusVerb: '',
|
||||||
|
}))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (session.suppressNextTaskNotificationResponse) {
|
||||||
|
update(() => ({ suppressNextTaskNotificationResponse: false }))
|
||||||
|
}
|
||||||
const pendingText = `${session.streamingText}${consumePendingDelta(sessionId)}`
|
const pendingText = `${session.streamingText}${consumePendingDelta(sessionId)}`
|
||||||
if (msg.blockType !== 'text' && pendingText.trim()) {
|
if (msg.blockType !== 'text' && pendingText.trim()) {
|
||||||
update((s) => ({
|
update((s) => ({
|
||||||
@ -1759,6 +1788,10 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'content_delta':
|
case 'content_delta':
|
||||||
|
if (get().sessions[sessionId]?.suppressNextTaskNotificationResponse) {
|
||||||
|
consumePendingDelta(sessionId)
|
||||||
|
break
|
||||||
|
}
|
||||||
let receivedLiveDelta = false
|
let receivedLiveDelta = false
|
||||||
if (msg.text !== undefined) {
|
if (msg.text !== undefined) {
|
||||||
if (!get().sessions[sessionId]) break
|
if (!get().sessions[sessionId]) break
|
||||||
@ -1818,6 +1851,15 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|||||||
break
|
break
|
||||||
|
|
||||||
case 'thinking':
|
case 'thinking':
|
||||||
|
if (get().sessions[sessionId]?.suppressNextTaskNotificationResponse) {
|
||||||
|
consumePendingDelta(sessionId)
|
||||||
|
update(() => ({
|
||||||
|
streamingText: '',
|
||||||
|
activeThinkingId: null,
|
||||||
|
statusVerb: '',
|
||||||
|
}))
|
||||||
|
break
|
||||||
|
}
|
||||||
update((s) => {
|
update((s) => {
|
||||||
const pendingText = `${s.streamingText}${consumePendingDelta(sessionId)}`
|
const pendingText = `${s.streamingText}${consumePendingDelta(sessionId)}`
|
||||||
const base = pendingText.trim()
|
const base = pendingText.trim()
|
||||||
@ -1981,6 +2023,31 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|||||||
case 'message_complete': {
|
case 'message_complete': {
|
||||||
const session = get().sessions[sessionId]
|
const session = get().sessions[sessionId]
|
||||||
if (!session) break
|
if (!session) break
|
||||||
|
if (session.suppressNextTaskNotificationResponse) {
|
||||||
|
consumePendingDelta(sessionId)
|
||||||
|
clearPendingToolInputDelta(sessionId)
|
||||||
|
if (session.elapsedTimer) clearInterval(session.elapsedTimer)
|
||||||
|
const hasRunningBackgroundAgents = hasRunningBackgroundTasks(session.backgroundAgentTasks)
|
||||||
|
update(() => ({
|
||||||
|
tokenUsage: msg.usage,
|
||||||
|
chatState: 'idle',
|
||||||
|
activeThinkingId: null,
|
||||||
|
pendingPermission: null,
|
||||||
|
pendingComputerUsePermission: null,
|
||||||
|
elapsedTimer: null,
|
||||||
|
apiRetry: null,
|
||||||
|
streamingFallback: null,
|
||||||
|
streamingText: '',
|
||||||
|
streamingToolInput: '',
|
||||||
|
suppressNextTaskNotificationResponse: false,
|
||||||
|
}))
|
||||||
|
useTabStore.getState().updateTabStatus(sessionId, hasRunningBackgroundAgents ? 'running' : 'idle')
|
||||||
|
refreshCompletedTranscriptHistory(get, sessionId)
|
||||||
|
for (const queuedMessage of get().sessions[sessionId]?.queuedUserMessages ?? []) {
|
||||||
|
get().sendQueuedUserMessage(sessionId, queuedMessage.id)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
const completedAt = Date.now()
|
const completedAt = Date.now()
|
||||||
const wasAgentRunning = session.chatState !== 'idle'
|
const wasAgentRunning = session.chatState !== 'idle'
|
||||||
const hasQueuedUserMessages = (session.queuedUserMessages?.length ?? 0) > 0
|
const hasQueuedUserMessages = (session.queuedUserMessages?.length ?? 0) > 0
|
||||||
@ -2046,6 +2113,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|||||||
messages: appendReplayedUserMessage(baseMessages, msg.content, Date.now()),
|
messages: appendReplayedUserMessage(baseMessages, msg.content, Date.now()),
|
||||||
...(pendingText.trim() ? { streamingText: '' } : {}),
|
...(pendingText.trim() ? { streamingText: '' } : {}),
|
||||||
activeThinkingId: null,
|
activeThinkingId: null,
|
||||||
|
suppressNextTaskNotificationResponse: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
@ -2080,6 +2148,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|||||||
pendingComputerUsePermission: null,
|
pendingComputerUsePermission: null,
|
||||||
apiRetry: null,
|
apiRetry: null,
|
||||||
streamingFallback: null,
|
streamingFallback: null,
|
||||||
|
suppressNextTaskNotificationResponse: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
useTabStore.getState().updateTabStatus(sessionId, 'error')
|
useTabStore.getState().updateTabStatus(sessionId, 'error')
|
||||||
@ -2279,8 +2348,14 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|||||||
shouldUpdateIdleTabStatus = session.chatState === 'idle'
|
shouldUpdateIdleTabStatus = session.chatState === 'idle'
|
||||||
hasRunningBackgroundAgentsAfterUpdate = hasRunningBackgroundTasks(backgroundAgentTasks)
|
hasRunningBackgroundAgentsAfterUpdate = hasRunningBackgroundTasks(backgroundAgentTasks)
|
||||||
const task = backgroundAgentTasks[taskEvent.taskId]
|
const task = backgroundAgentTasks[taskEvent.taskId]
|
||||||
|
const suppressNotificationResponse =
|
||||||
|
(taskEvent.status === 'completed' ||
|
||||||
|
taskEvent.status === 'failed' ||
|
||||||
|
taskEvent.status === 'stopped') &&
|
||||||
|
shouldSuppressTaskNotificationResponse(session)
|
||||||
return {
|
return {
|
||||||
...buildBackgroundTaskSessionUpdate(session, backgroundAgentTasks, task, now),
|
...buildBackgroundTaskSessionUpdate(session, backgroundAgentTasks, task, now),
|
||||||
|
...(suppressNotificationResponse ? { suppressNextTaskNotificationResponse: true } : {}),
|
||||||
agentTaskNotifications: {
|
agentTaskNotifications: {
|
||||||
...session.agentTaskNotifications,
|
...session.agentTaskNotifications,
|
||||||
...(toolUseId &&
|
...(toolUseId &&
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user