返回 DeepSeek-Reasonix
RuntimeActivityIndicator.tsx
根目录 / desktop / frontend / src / components / RuntimeActivityIndicator.tsx
1 import { useSyncExternalStore } from "react";
2 import { runtimeStateStore, selectRuntime } from "../lib/runtimeStateStore";
3 import { useT } from "../lib/i18n";
4 import { asArray } from "../lib/array";
5 import type { ProjectNode } from "../lib/types";
6
7 /**
8 * Workspace identity the runtime store is matched against. Sidebars differ in
9 * how they list sessions, so the activity rule lives here instead of being
10 * reimplemented per surface; `topics` only supplies human labels and
11 * `fallbackActive` only covers surfaces with no runtime snapshot yet.
12 */
13 export type RuntimeActivityTarget = {
14 scope: "global" | "project";
15 root: string;
16 remote?: { hostId: string; workspace: string };
17 topics?: ProjectNode[];
18 fallbackActive?: boolean;
19 };
20
21 export default function RuntimeActivityIndicator({ target }: { target: RuntimeActivityTarget }) {
22 const snapshot = useSyncExternalStore(runtimeStateStore.subscribe, runtimeStateStore.getSnapshot);
23 const failed = useSyncExternalStore(runtimeStateStore.subscribe, runtimeStateStore.getFailed);
24 const t = useT();
25 const matched = snapshot?.sessions.filter(session => target.remote
26 ? session.remote && session.hostId === target.remote.hostId && session.workspaceRoot === target.remote.workspace
27 : session.scope === target.scope && (target.scope === "global" || session.workspaceRoot === target.root));
28 const sessions = matched && [...new Map(matched.map(session => [`${session.hostId ?? "local"}\0${session.workspaceRoot}\0${session.sessionPath}`, session])).values()];
29 if (sessions) {
30 const active = sessions.filter(session => {
31 const view = selectRuntime(session, failed);
32 return view.unknown || (view.known && (view.kind !== "idle" || session.state.backgroundJobs > 0));
33 });
34 if (!active.length) return null;
35 const details = active.map(session => {
36 const state = session.state;
37 const topic = asArray(target.topics).find(node => node.topicId === session.topicId || (session.sessionPath && node.sessionPath === session.sessionPath));
38 const labels: string[] = [];
39 if (failed || session.freshness !== "synced") labels.push(t("runtime.unknown"));
40 if (state.phase === "finishing") labels.push(t("runtime.finishing"));
41 if (state.cancelRequested) labels.push(t("status.jobStopping"));
42 if (state.pendingPrompt) labels.push(t("projectTree.status.waitingConfirmation"));
43 if (state.phase === "executing") labels.push(t(state.activity === "streaming" ? "projectTree.status.streaming" : "projectTree.status.thinking"));
44 if (state.backgroundJobs) labels.push(t("runtime.background", { count: state.backgroundJobs }));
45 return `${topic?.label || session.tabId}: ${labels.join(" · ")}`;
46 }).join("; ");
47 const spinning = active.some(session => selectRuntime(session, failed).spinning);
48 return <span className={`runtime-activity-indicator${spinning ? "" : " runtime-activity-indicator--static"}`} style={spinning ? undefined : { animation: "none" }} role="status" aria-label={details} title={details} />;
49 }
50 if (!target.fallbackActive) return null;
51 return <span className="runtime-activity-indicator" role="status" aria-label={t("projectTree.status.thinking")} />;
52 }
53
53 lines Plain Text