fix(web-shell): clear stale floating todos (#6425)

* fix(web-shell): clear stale floating todos

* test(web-shell): cover floating todo reset

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
This commit is contained in:
ytahdn 2026-07-07 16:59:53 +08:00 committed by GitHub
parent c7d22dc1d4
commit ce2fee926f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 28 additions and 6 deletions

View file

@ -75,6 +75,10 @@ function userMessage(id: string): Message {
return { id, role: 'user', content: 'hello' };
}
function userShellMessage(id: string): Message {
return { id, role: 'user_shell', command: 'npm test', output: '' };
}
function assistantMessage(id: string): Message {
return { id, role: 'assistant', content: 'working on it' };
}
@ -110,12 +114,21 @@ describe('getFloatingTodos', () => {
expect(state.sourceCallId).toBeNull();
});
it('keeps an active list visible across later user messages', () => {
it('clears an active list after the next user message', () => {
const state = getFloatingTodos([
todoWriteMessage('m1', [todo('1', 'in_progress')]),
userMessage('u1'),
]);
expect(state.todos).toHaveLength(1);
expect(state.todos).toHaveLength(0);
expect(state.allCompleted).toBe(false);
});
it('clears an active list after the next user shell message', () => {
const state = getFloatingTodos([
todoWriteMessage('m1', [todo('1', 'in_progress')]),
userShellMessage('shell-1'),
]);
expect(state.todos).toHaveLength(0);
expect(state.allCompleted).toBe(false);
});
@ -145,6 +158,17 @@ describe('getFloatingTodos', () => {
expect(state.sourceMessageId).toBe('m2');
});
it('shows a new active list started after clearing a stale active one', () => {
const state = getFloatingTodos([
todoWriteMessage('m1', [todo('1', 'in_progress')]),
userMessage('u1'),
todoWriteMessage('m2', [todo('2', 'pending')]),
]);
expect(state.todos.map((t) => t.id)).toEqual(['2']);
expect(state.allCompleted).toBe(false);
expect(state.sourceMessageId).toBe('m2');
});
it('ignores user messages sent before the todo update', () => {
const state = getFloatingTodos([
userMessage('u1'),

View file

@ -96,7 +96,7 @@ export function getFloatingTodos(
let userMessageAfter = false;
for (const message of messages) {
if (message.role === 'user') {
if (message.role === 'user' || message.role === 'user_shell') {
userMessageAfter = true;
continue;
}
@ -122,9 +122,7 @@ export function getFloatingTodos(
if (todos.length === 0) return EMPTY_FLOATING_TODOS;
const allCompleted = !hasActiveTodos(todos);
// A finished list stays visible (the "all done" moment) only until the
// user sends the next prompt.
if (allCompleted && userMessageAfter) return EMPTY_FLOATING_TODOS;
if (userMessageAfter) return EMPTY_FLOATING_TODOS;
return { todos, allCompleted, sourceMessageId, sourceCallId };
}