fix(web): settle orphan inflight bubbles via shared cancel helper
Wire cancelInflightAssistantMessages on stop so composer unlock and message status stay consistent across sessions.
This commit is contained in:
+2
-10
@@ -12,7 +12,6 @@ import {
|
||||
import { mapApiMembersToProjectMembers } from './lib/projectMemberAdapter';
|
||||
import { resolveCurrentDocRole } from './lib/currentDocRole';
|
||||
import {
|
||||
COMPOSER_INFLIGHT_STATUSES,
|
||||
resolveChatComposerStreaming,
|
||||
} from './lib/chatComposerStreaming';
|
||||
import {
|
||||
@@ -1215,7 +1214,7 @@ export default function App() {
|
||||
applyControlledInvalidationPlan,
|
||||
});
|
||||
|
||||
// 用户主动停止:仅中止当前 session 的活跃 turn,并结算仍挂在 inflight 的孤儿气泡。
|
||||
// 用户主动停止:中止当前 session 活跃 turn,并结算仍挂在 inflight 的孤儿气泡。
|
||||
const handleStopStreaming = () => {
|
||||
turnRuntime.abortActiveSessionTurns();
|
||||
taskRunStream.abortSession(activeSessionId);
|
||||
@@ -1224,14 +1223,7 @@ export default function App() {
|
||||
void cancelTaskRun(runId).catch(() => undefined);
|
||||
taskRunStream.setRunId(activeSessionId, null);
|
||||
}
|
||||
const inflight = COMPOSER_INFLIGHT_STATUSES as readonly string[];
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.role === 'assistant' && m.status && inflight.includes(m.status)
|
||||
? { ...m, status: 'cancelled' as const }
|
||||
: m,
|
||||
),
|
||||
);
|
||||
setMessages((prev) => cancelInflightAssistantMessages(prev, activeSessionId));
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { reduceSquadObservation } from './squadCompletionObservation';
|
||||
import {
|
||||
cancelInflightAssistantMessages,
|
||||
reduceSquadObservation,
|
||||
} from './squadCompletionObservation';
|
||||
|
||||
const turn = {
|
||||
userPromptMatched: true,
|
||||
@@ -59,4 +62,26 @@ describe('squadCompletionObservation', () => {
|
||||
assert.equal(verdict.action, 'wait');
|
||||
assert.equal(verdict.sawInflight, true);
|
||||
});
|
||||
|
||||
it('cancelInflightAssistantMessages settles only this-session assistant inflight', () => {
|
||||
const next = cancelInflightAssistantMessages(
|
||||
[
|
||||
{
|
||||
sessionId: 's1',
|
||||
role: 'assistant',
|
||||
status: 'streaming_status',
|
||||
livePhaseLabel: '任务执行中…',
|
||||
},
|
||||
{ sessionId: 's1', role: 'user', status: 'completed' },
|
||||
{ sessionId: 's2', role: 'assistant', status: 'streaming_status' },
|
||||
{ sessionId: 's1', role: 'assistant', status: 'completed' },
|
||||
],
|
||||
's1',
|
||||
);
|
||||
assert.equal(next[0].status, 'cancelled');
|
||||
assert.equal(next[0].livePhaseLabel, undefined);
|
||||
assert.equal(next[1].status, 'completed');
|
||||
assert.equal(next[2].status, 'streaming_status');
|
||||
assert.equal(next[3].status, 'completed');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,38 @@ export function isInflightChatStatus(status: string): boolean {
|
||||
return (INFLIGHT_CHAT_STATUSES as readonly string[]).includes(status);
|
||||
}
|
||||
|
||||
type InflightCancelableMessage = {
|
||||
sessionId?: string;
|
||||
role: string;
|
||||
status: string;
|
||||
livePhaseLabel?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop/abort alone can leave assistant bubbles in pending/streaming_status,
|
||||
* which keeps InputArea locked on "停止当前生成". Mark those as cancelled so
|
||||
* the composer returns to send-ready without page.evaluate.
|
||||
*/
|
||||
export function cancelInflightAssistantMessages<T extends InflightCancelableMessage>(
|
||||
messages: T[],
|
||||
sessionId: string,
|
||||
): T[] {
|
||||
return messages.map((message) => {
|
||||
if (
|
||||
message.sessionId === sessionId &&
|
||||
message.role === 'assistant' &&
|
||||
isInflightChatStatus(message.status)
|
||||
) {
|
||||
return {
|
||||
...message,
|
||||
status: 'cancelled',
|
||||
livePhaseLabel: undefined,
|
||||
};
|
||||
}
|
||||
return message;
|
||||
});
|
||||
}
|
||||
|
||||
export function isSuccessTerminalChatStatus(status: string): boolean {
|
||||
return (SUCCESS_TERMINAL_CHAT_STATUSES as readonly string[]).includes(status);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user