qwen-code/packages/web-shell/client/hooks/useBackgroundTasks.ts
ytahdn 8785216be5
feat(web-shell): add monitor task details (#7817)
* feat(web-shell): add monitor task details

* fix(web-shell): align monitor tab title with merged snapshot and reset expansion (#7817)

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-27 12:30:17 +00:00

114 lines
3.6 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import type { DaemonSessionTaskStatus } from '@qwen-code/sdk/daemon';
import { useActions } from '@qwen-code/webui/daemon-react-sdk';
import { TASKS_STATUS_ACTIVE_EVENT } from '../components/messages/TasksStatusMessage';
import { isComposerTask } from '../utils/composerTasks';
const TASKS_POLL_INTERVAL_MS = 3000;
const MAX_EMPTY_TASK_POLLS = 2;
function hasActiveTask(tasks: readonly DaemonSessionTaskStatus[]): boolean {
return tasks.some(
(task) => task.status === 'running' || task.status === 'paused',
);
}
export function useBackgroundTasks(
sessionId: string | undefined,
taskActivityKey: string,
connected: boolean,
refreshTrigger = 0,
): DaemonSessionTaskStatus[] {
const actions = useActions();
const [tasks, setTasks] = useState<DaemonSessionTaskStatus[]>([]);
const [pollingActive, setPollingActive] = useState(false);
const [tasksPanelActive, setTasksPanelActive] = useState(false);
const emptyPollsRef = useRef(0);
const tasksRefreshInFlightRef = useRef<{
sessionId: string;
request: object;
} | null>(null);
useEffect(() => {
setTasks([]);
setPollingActive(false);
emptyPollsRef.current = 0;
}, [connected, sessionId]);
useEffect(() => {
if (!connected || !sessionId || !taskActivityKey) return;
emptyPollsRef.current = 0;
setPollingActive(true);
}, [connected, sessionId, taskActivityKey]);
useEffect(() => {
if (!connected || !sessionId || refreshTrigger === 0) return;
emptyPollsRef.current = 0;
setPollingActive(true);
}, [connected, refreshTrigger, sessionId]);
useEffect(() => {
if (tasksPanelActive) return;
if (!connected || !sessionId || !pollingActive) return;
let disposed = false;
const refresh = () => {
if (tasksRefreshInFlightRef.current?.sessionId === sessionId) return;
const request = {};
tasksRefreshInFlightRef.current = { sessionId, request };
actions
.getTasks()
.then((snapshot) => {
if (disposed || snapshot.sessionId !== sessionId) return;
const composerTasks = snapshot.tasks.filter(isComposerTask);
setTasks(composerTasks);
if (composerTasks.length === 0) {
emptyPollsRef.current += 1;
if (emptyPollsRef.current >= MAX_EMPTY_TASK_POLLS) {
setPollingActive(false);
}
return;
}
emptyPollsRef.current = 0;
if (!hasActiveTask(composerTasks)) {
setPollingActive(false);
}
})
.catch((error: unknown) => {
if (disposed) return;
console.warn('[web-shell] failed to refresh tasks:', error);
})
.finally(() => {
if (tasksRefreshInFlightRef.current?.request === request) {
tasksRefreshInFlightRef.current = null;
}
});
};
refresh();
const id = setInterval(refresh, TASKS_POLL_INTERVAL_MS);
return () => {
disposed = true;
clearInterval(id);
};
}, [actions, connected, pollingActive, sessionId, tasksPanelActive]);
const tasksRef = useRef(tasks);
tasksRef.current = tasks;
useEffect(() => {
const onTasksPanelActive = (event: Event) => {
const detail = (event as CustomEvent<{ active?: boolean }>).detail;
const active = detail?.active === true;
setTasksPanelActive(active);
if (!active && hasActiveTask(tasksRef.current)) {
setPollingActive(true);
}
};
window.addEventListener(TASKS_STATUS_ACTIVE_EVENT, onTasksPanelActive);
return () =>
window.removeEventListener(TASKS_STATUS_ACTIVE_EVENT, onTasksPanelActive);
}, []);
return tasks;
}