mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
Keep CLI task state aligned with the active session
The desktop task bar used a single global task store, but session switches could leave it pointed at the previous conversation. Re-entering an already connected session skipped task refresh entirely, and changing task list IDs did not clear stale tasks before the next fetch resolved. This change refreshes CLI tasks on every session activation, clears stale task state immediately when the tracked session changes, and locks that behavior with regression tests around reconnect and cross-session task isolation. Constraint: The current desktop task bar architecture uses one shared CLI task store across sessions Rejected: Per-session task store refactor | too broad for a targeted bug fix Confidence: high Scope-risk: narrow Reversibility: clean Directive: If tasks become session-scoped later, re-evaluate the eager refresh in connectToSession before removing it Tested: cd desktop && bun run lint Tested: cd desktop && bun run test src/stores/chatStore.test.ts src/stores/cliTaskStore.test.ts src/pages/ActiveSession.test.tsx Not-tested: Manual desktop UI verification against a packaged app build
This commit is contained in:
parent
802b94c5cd
commit
eaa4a8435a
@ -100,7 +100,7 @@ describe('ActiveSession task polling', () => {
|
||||
|
||||
expect(
|
||||
fetchSessionTasks.mock.calls.filter(([currentSessionId]) => currentSessionId === sessionId),
|
||||
).toHaveLength(3)
|
||||
).toHaveLength(4)
|
||||
|
||||
unmount()
|
||||
useCLITaskStore.setState(originalCliTaskState)
|
||||
|
||||
@ -8,6 +8,11 @@ const {
|
||||
handleTeamCreatedMock,
|
||||
handleTeamUpdateMock,
|
||||
handleTeamDeletedMock,
|
||||
fetchSessionTasksMock,
|
||||
clearTasksMock,
|
||||
setTasksFromTodosMock,
|
||||
markCompletedAndDismissedMock,
|
||||
refreshTasksMock,
|
||||
} = vi.hoisted(() => ({
|
||||
sendMock: vi.fn(),
|
||||
getMemberBySessionIdMock: vi.fn<(sessionId: string) => any>(() => null),
|
||||
@ -15,6 +20,11 @@ const {
|
||||
handleTeamCreatedMock: vi.fn(),
|
||||
handleTeamUpdateMock: vi.fn(),
|
||||
handleTeamDeletedMock: vi.fn(),
|
||||
fetchSessionTasksMock: vi.fn(),
|
||||
clearTasksMock: vi.fn(),
|
||||
setTasksFromTodosMock: vi.fn(),
|
||||
markCompletedAndDismissedMock: vi.fn(),
|
||||
refreshTasksMock: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../api/websocket', () => ({
|
||||
@ -66,12 +76,13 @@ vi.mock('./sessionStore', () => ({
|
||||
vi.mock('./cliTaskStore', () => ({
|
||||
useCLITaskStore: {
|
||||
getState: () => ({
|
||||
fetchSessionTasks: vi.fn(),
|
||||
fetchSessionTasks: fetchSessionTasksMock,
|
||||
tasks: [],
|
||||
clearTasks: vi.fn(),
|
||||
setTasksFromTodos: vi.fn(),
|
||||
markCompletedAndDismissed: vi.fn(),
|
||||
refreshTasks: vi.fn(),
|
||||
sessionId: null,
|
||||
clearTasks: clearTasksMock,
|
||||
setTasksFromTodos: setTasksFromTodosMock,
|
||||
markCompletedAndDismissed: markCompletedAndDismissedMock,
|
||||
refreshTasks: refreshTasksMock,
|
||||
}),
|
||||
},
|
||||
}))
|
||||
@ -87,6 +98,11 @@ describe('chatStore history mapping', () => {
|
||||
getMemberBySessionIdMock.mockReset()
|
||||
getMemberBySessionIdMock.mockReturnValue(null)
|
||||
sendMessageToMemberMock.mockReset()
|
||||
fetchSessionTasksMock.mockReset()
|
||||
clearTasksMock.mockReset()
|
||||
setTasksFromTodosMock.mockReset()
|
||||
markCompletedAndDismissedMock.mockReset()
|
||||
refreshTasksMock.mockReset()
|
||||
useChatStore.setState({
|
||||
...initialState,
|
||||
sessions: {},
|
||||
@ -502,4 +518,33 @@ describe('chatStore history mapping', () => {
|
||||
pending: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('refreshes CLI tasks when switching to an already-connected session', () => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
[TEST_SESSION_ID]: {
|
||||
messages: [],
|
||||
chatState: 'idle',
|
||||
connectionState: 'connected',
|
||||
streamingText: '',
|
||||
streamingToolInput: '',
|
||||
activeToolUseId: null,
|
||||
activeToolName: null,
|
||||
activeThinkingId: null,
|
||||
pendingPermission: null,
|
||||
pendingComputerUsePermission: null,
|
||||
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
||||
elapsedSeconds: 0,
|
||||
statusVerb: '',
|
||||
slashCommands: [],
|
||||
agentTaskNotifications: {},
|
||||
elapsedTimer: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
useChatStore.getState().connectToSession(TEST_SESSION_ID)
|
||||
|
||||
expect(fetchSessionTasksMock).toHaveBeenCalledWith(TEST_SESSION_ID)
|
||||
})
|
||||
})
|
||||
|
||||
@ -120,6 +120,8 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
getSession: (sessionId) => get().sessions[sessionId] ?? createDefaultSessionState(),
|
||||
|
||||
connectToSession: (sessionId) => {
|
||||
void useCLITaskStore.getState().fetchSessionTasks(sessionId)
|
||||
|
||||
const existing = get().sessions[sessionId]
|
||||
if (existing && existing.connectionState !== 'disconnected') return
|
||||
|
||||
@ -144,7 +146,6 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
})
|
||||
|
||||
get().loadHistory(sessionId)
|
||||
useCLITaskStore.getState().fetchSessionTasks(sessionId)
|
||||
sessionsApi.getSlashCommands(sessionId)
|
||||
.then(({ commands }) => {
|
||||
if (get().sessions[sessionId]) {
|
||||
|
||||
70
desktop/src/stores/cliTaskStore.test.ts
Normal file
70
desktop/src/stores/cliTaskStore.test.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cliTasksApi } from '../api/cliTasks'
|
||||
import type { CLITask, TaskStatus } from '../types/cliTask'
|
||||
import { useCLITaskStore } from './cliTaskStore'
|
||||
|
||||
vi.mock('../api/cliTasks', () => ({
|
||||
cliTasksApi: {
|
||||
getTasksForList: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
function makeTask(taskListId: string, status: TaskStatus = 'in_progress'): CLITask {
|
||||
return {
|
||||
id: '1',
|
||||
subject: 'Keep current session isolated',
|
||||
description: '',
|
||||
status,
|
||||
blocks: [],
|
||||
blockedBy: [],
|
||||
taskListId,
|
||||
}
|
||||
}
|
||||
|
||||
describe('cliTaskStore', () => {
|
||||
beforeEach(() => {
|
||||
useCLITaskStore.getState().clearTasks()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
useCLITaskStore.getState().clearTasks()
|
||||
})
|
||||
|
||||
it('clears stale tasks immediately when switching tracked sessions', async () => {
|
||||
let resolveRequest: ((value: { tasks: ReturnType<typeof makeTask>[] }) => void) | null = null
|
||||
|
||||
vi.mocked(cliTasksApi.getTasksForList).mockImplementation(
|
||||
(sessionId: string) =>
|
||||
new Promise<{ tasks: ReturnType<typeof makeTask>[] }>((resolve) => {
|
||||
if (sessionId === 'session-2') resolveRequest = resolve
|
||||
}),
|
||||
)
|
||||
|
||||
useCLITaskStore.setState({
|
||||
sessionId: 'session-1',
|
||||
tasks: [makeTask('session-1')],
|
||||
expanded: true,
|
||||
completedAndDismissed: true,
|
||||
dismissedCompletionKey: 'session-1::done',
|
||||
})
|
||||
|
||||
const fetchPromise = useCLITaskStore.getState().fetchSessionTasks('session-2')
|
||||
|
||||
expect(useCLITaskStore.getState()).toMatchObject({
|
||||
sessionId: 'session-2',
|
||||
tasks: [],
|
||||
expanded: false,
|
||||
completedAndDismissed: false,
|
||||
dismissedCompletionKey: null,
|
||||
})
|
||||
|
||||
expect(resolveRequest).not.toBeNull()
|
||||
resolveRequest!({ tasks: [makeTask('session-2', 'completed')] })
|
||||
await fetchPromise
|
||||
|
||||
expect(useCLITaskStore.getState().tasks).toMatchObject([
|
||||
{ taskListId: 'session-2', status: 'completed' },
|
||||
])
|
||||
})
|
||||
})
|
||||
@ -83,7 +83,16 @@ export const useCLITaskStore = create<CLITaskStore>((set, get) => ({
|
||||
dismissedCompletionKey: null,
|
||||
|
||||
fetchSessionTasks: async (sessionId) => {
|
||||
set({ sessionId })
|
||||
if (get().sessionId !== sessionId) {
|
||||
set({
|
||||
sessionId,
|
||||
tasks: [],
|
||||
completedAndDismissed: false,
|
||||
dismissedCompletionKey: null,
|
||||
expanded: false,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const { tasks } = await cliTasksApi.getTasksForList(sessionId)
|
||||
// Only update if still tracking the same session
|
||||
@ -96,7 +105,7 @@ export const useCLITaskStore = create<CLITaskStore>((set, get) => ({
|
||||
} catch {
|
||||
// No tasks for this session — that's fine
|
||||
if (get().sessionId === sessionId) {
|
||||
set({ tasks: [], completedAndDismissed: false, dismissedCompletionKey: null })
|
||||
set({ tasks: [], completedAndDismissed: false, dismissedCompletionKey: null, expanded: false })
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user