merge(ui): LED projet — exploite les signaux d'interaction CLI custom (#198, QA verte)
This commit is contained in:
@ -5,6 +5,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import { DIProvider } from "@/app/di";
|
import { DIProvider } from "@/app/di";
|
||||||
import type { AgentProfile, ReplyChunk } from "@/domain";
|
import type { AgentProfile, ReplyChunk } from "@/domain";
|
||||||
import { PluginRuntimeProvider } from "@/features/plugins";
|
import { PluginRuntimeProvider } from "@/features/plugins";
|
||||||
|
import {
|
||||||
|
__resetProjectCliInteractionActivityForTests,
|
||||||
|
hasProjectCliInteraction,
|
||||||
|
} from "@/features/projects/projectCliInteractionActivity";
|
||||||
import {
|
import {
|
||||||
PluginCommandRegistry,
|
PluginCommandRegistry,
|
||||||
PluginLayoutRegistry,
|
PluginLayoutRegistry,
|
||||||
@ -71,6 +75,7 @@ describe("CustomAgentChatView", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
__clearCustomAgentChatViewCacheForTests();
|
__clearCustomAgentChatViewCacheForTests();
|
||||||
|
__resetProjectCliInteractionActivityForTests();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("repaints attachment labels from user prompt scrollback", async () => {
|
it("repaints attachment labels from user prompt scrollback", async () => {
|
||||||
@ -381,6 +386,7 @@ describe("CustomAgentChatView", () => {
|
|||||||
expect.any(Function),
|
expect.any(Function),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
expect(hasProjectCliInteraction("project-1")).toBe(true);
|
||||||
expect(screen.queryByRole("button", { name: "Cancel" })).toBeNull();
|
expect(screen.queryByRole("button", { name: "Cancel" })).toBeNull();
|
||||||
|
|
||||||
fireEvent.click(await screen.findByRole("button", { name: "Stop" }));
|
fireEvent.click(await screen.findByRole("button", { name: "Stop" }));
|
||||||
@ -396,6 +402,7 @@ describe("CustomAgentChatView", () => {
|
|||||||
kind: "structured",
|
kind: "structured",
|
||||||
});
|
});
|
||||||
await waitFor(() => expect(onSessionId).toHaveBeenCalledWith(null));
|
await waitFor(() => expect(onSessionId).toHaveBeenCalledWith(null));
|
||||||
|
expect(hasProjectCliInteraction("project-1")).toBe(false);
|
||||||
expect(agent.cancelAgentChat).not.toHaveBeenCalled();
|
expect(agent.cancelAgentChat).not.toHaveBeenCalled();
|
||||||
expect(agent.closeAgentChat).not.toHaveBeenCalled();
|
expect(agent.closeAgentChat).not.toHaveBeenCalled();
|
||||||
expect(
|
expect(
|
||||||
@ -514,6 +521,7 @@ describe("CustomAgentChatView", () => {
|
|||||||
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
|
||||||
|
|
||||||
await screen.findByText("done");
|
await screen.findByText("done");
|
||||||
|
expect(hasProjectCliInteraction("project-1")).toBe(false);
|
||||||
expect(screen.getAllByText("hello agent")).toHaveLength(1);
|
expect(screen.getAllByText("hello agent")).toHaveLength(1);
|
||||||
expect(screen.getByText("Terminé").parentElement?.parentElement?.className).toContain(
|
expect(screen.getByText("Terminé").parentElement?.parentElement?.className).toContain(
|
||||||
"justify-end",
|
"justify-end",
|
||||||
|
|||||||
@ -27,6 +27,11 @@ import type {
|
|||||||
} from "@/domain";
|
} from "@/domain";
|
||||||
import { useGateways } from "@/app/di";
|
import { useGateways } from "@/app/di";
|
||||||
import { usePluginRuntime } from "@/features/plugins/PluginRuntimeProvider";
|
import { usePluginRuntime } from "@/features/plugins/PluginRuntimeProvider";
|
||||||
|
import {
|
||||||
|
markProjectCliInteractionFinished,
|
||||||
|
markProjectCliInteractionStarted,
|
||||||
|
markProjectCliInteractionStopped,
|
||||||
|
} from "@/features/projects/projectCliInteractionActivity";
|
||||||
import { Button, Spinner, cn } from "@/shared";
|
import { Button, Spinner, cn } from "@/shared";
|
||||||
import type { ChatAttachmentInput, ChatDraftScope } from "@/ports";
|
import type { ChatAttachmentInput, ChatDraftScope } from "@/ports";
|
||||||
|
|
||||||
@ -638,6 +643,8 @@ export function CustomAgentChatView({
|
|||||||
detachAgentChatRef.current = agent.detachAgentChat;
|
detachAgentChatRef.current = agent.detachAgentChat;
|
||||||
const slashQuerySeqRef = useRef(0);
|
const slashQuerySeqRef = useRef(0);
|
||||||
const draftSaveSeqRef = useRef(0);
|
const draftSaveSeqRef = useRef(0);
|
||||||
|
const interactionSeqRef = useRef(0);
|
||||||
|
const activeInteractionIdRef = useRef<string | null>(null);
|
||||||
const draftHydratedRef = useRef(false);
|
const draftHydratedRef = useRef(false);
|
||||||
const draftEditedBeforeHydrationRef = useRef(false);
|
const draftEditedBeforeHydrationRef = useRef(false);
|
||||||
const lastPersistedDraftRef = useRef("");
|
const lastPersistedDraftRef = useRef("");
|
||||||
@ -731,10 +738,33 @@ export function CustomAgentChatView({
|
|||||||
const receive = useCallback(
|
const receive = useCallback(
|
||||||
(chunk: ReplyChunk) => {
|
(chunk: ReplyChunk) => {
|
||||||
scrollbackChunkCountRef.current += 1;
|
scrollbackChunkCountRef.current += 1;
|
||||||
|
const activeInteractionId = activeInteractionIdRef.current;
|
||||||
|
if (
|
||||||
|
activeInteractionId &&
|
||||||
|
chunk.kind === "progress" &&
|
||||||
|
chunk.progress.kind === "turn"
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
chunk.progress.stage === "started" ||
|
||||||
|
chunk.progress.stage === "delta"
|
||||||
|
) {
|
||||||
|
markProjectCliInteractionStarted(projectId, activeInteractionId);
|
||||||
|
}
|
||||||
|
if (chunk.progress.stage === "completed") {
|
||||||
|
markProjectCliInteractionFinished(projectId, activeInteractionId);
|
||||||
|
activeInteractionIdRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
updateChatViewState((prev) => foldChunk(prev, chunk));
|
updateChatViewState((prev) => foldChunk(prev, chunk));
|
||||||
if (chunk.kind === "final" || chunk.kind === "error") setBusy(false);
|
if (chunk.kind === "final" || chunk.kind === "error") {
|
||||||
|
if (activeInteractionIdRef.current) {
|
||||||
|
markProjectCliInteractionFinished(projectId, activeInteractionIdRef.current);
|
||||||
|
activeInteractionIdRef.current = null;
|
||||||
|
}
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[updateChatViewState],
|
[projectId, updateChatViewState],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -1328,6 +1358,14 @@ export function CustomAgentChatView({
|
|||||||
const attachmentInputs = outgoingAttachments.map((item) => item.input);
|
const attachmentInputs = outgoingAttachments.map((item) => item.input);
|
||||||
const attachmentLabels = outgoingAttachments.map((item) => item.label);
|
const attachmentLabels = outgoingAttachments.map((item) => item.label);
|
||||||
const displayText = text || "Pièce jointe";
|
const displayText = text || "Pièce jointe";
|
||||||
|
const interactionId = `${agentId}:${++interactionSeqRef.current}`;
|
||||||
|
activeInteractionIdRef.current = interactionId;
|
||||||
|
markProjectCliInteractionStarted(projectId, interactionId);
|
||||||
|
const finishInteraction = () => {
|
||||||
|
if (activeInteractionIdRef.current !== interactionId) return;
|
||||||
|
markProjectCliInteractionFinished(projectId, interactionId);
|
||||||
|
activeInteractionIdRef.current = null;
|
||||||
|
};
|
||||||
setDraft("");
|
setDraft("");
|
||||||
setAttachments([]);
|
setAttachments([]);
|
||||||
void clearPersistedDraft();
|
void clearPersistedDraft();
|
||||||
@ -1356,6 +1394,7 @@ export function CustomAgentChatView({
|
|||||||
retryAttachNotFound: true,
|
retryAttachNotFound: true,
|
||||||
}));
|
}));
|
||||||
await sendTurn(sid);
|
await sendTurn(sid);
|
||||||
|
finishInteraction();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isNotFound(e)) {
|
if (isNotFound(e)) {
|
||||||
try {
|
try {
|
||||||
@ -1366,8 +1405,10 @@ export function CustomAgentChatView({
|
|||||||
retryAttachNotFound: true,
|
retryAttachNotFound: true,
|
||||||
});
|
});
|
||||||
await sendTurn(recoveredSession);
|
await sendTurn(recoveredSession);
|
||||||
|
finishInteraction();
|
||||||
return;
|
return;
|
||||||
} catch (recoveryError) {
|
} catch (recoveryError) {
|
||||||
|
finishInteraction();
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
setError(describe(recoveryError));
|
setError(describe(recoveryError));
|
||||||
updateChatViewState((prev) => ({
|
updateChatViewState((prev) => ({
|
||||||
@ -1377,6 +1418,7 @@ export function CustomAgentChatView({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finishInteraction();
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
setError(describe(e));
|
setError(describe(e));
|
||||||
updateChatViewState((prev) => ({
|
updateChatViewState((prev) => ({
|
||||||
@ -1393,6 +1435,10 @@ export function CustomAgentChatView({
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const stopped = await agent.stopLiveAgent(projectId, agentId);
|
const stopped = await agent.stopLiveAgent(projectId, agentId);
|
||||||
|
if (activeInteractionIdRef.current) {
|
||||||
|
markProjectCliInteractionStopped(projectId, activeInteractionIdRef.current);
|
||||||
|
activeInteractionIdRef.current = null;
|
||||||
|
}
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
setOpening(false);
|
setOpening(false);
|
||||||
setCurrentSession(null);
|
setCurrentSession(null);
|
||||||
@ -1415,6 +1461,10 @@ export function CustomAgentChatView({
|
|||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isNotFound(e)) {
|
if (isNotFound(e)) {
|
||||||
|
if (activeInteractionIdRef.current) {
|
||||||
|
markProjectCliInteractionStopped(projectId, activeInteractionIdRef.current);
|
||||||
|
activeInteractionIdRef.current = null;
|
||||||
|
}
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
setOpening(false);
|
setOpening(false);
|
||||||
setCurrentSession(null);
|
setCurrentSession(null);
|
||||||
|
|||||||
@ -81,9 +81,7 @@ describe("ProjectTabs project activity dots (#159)", () => {
|
|||||||
renderTabs({ items: TABS });
|
renderTabs({ items: TABS });
|
||||||
|
|
||||||
const tab = screen.getByRole("tab", { name: /alpha/ });
|
const tab = screen.getByRole("tab", { name: /alpha/ });
|
||||||
expect(
|
expect(within(tab).getByTitle("alpha : Prêt")).toBeTruthy();
|
||||||
within(tab).getByTitle("alpha : aucune conversation active"),
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders idle and busy project activity states", () => {
|
it("renders idle and busy project activity states", () => {
|
||||||
@ -92,7 +90,7 @@ describe("ProjectTabs project activity dots (#159)", () => {
|
|||||||
activityByProjectId: { a: "idle", b: "busy" },
|
activityByProjectId: { a: "idle", b: "busy" },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByTitle("alpha : conversation ouverte")).toBeTruthy();
|
expect(screen.getByTitle("alpha : Prêt")).toBeTruthy();
|
||||||
expect(screen.getByTitle("beta : activité en cours")).toBeTruthy();
|
expect(screen.getByTitle("beta : Échange en cours")).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -96,10 +96,8 @@ function ProjectActivityDot({
|
|||||||
}) {
|
}) {
|
||||||
const title =
|
const title =
|
||||||
status === "busy"
|
status === "busy"
|
||||||
? `${label} : activité en cours`
|
? `${label} : Échange en cours`
|
||||||
: status === "idle"
|
: `${label} : Prêt`;
|
||||||
? `${label} : conversation ouverte`
|
|
||||||
: `${label} : aucune conversation active`;
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-testid={`project-activity-${label}`}
|
data-testid={`project-activity-${label}`}
|
||||||
@ -110,7 +108,7 @@ function ProjectActivityDot({
|
|||||||
status === "busy" &&
|
status === "busy" &&
|
||||||
"animate-pulse border-warning bg-warning shadow-[0_0_0_2px_rgba(212,155,58,0.16)]",
|
"animate-pulse border-warning bg-warning shadow-[0_0_0_2px_rgba(212,155,58,0.16)]",
|
||||||
status === "idle" && "border-success bg-success",
|
status === "idle" && "border-success bg-success",
|
||||||
status === "none" && "border-border-strong bg-muted",
|
status === "none" && "border-success bg-success",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { projectActivityStatus } from "./projectActivity";
|
import { projectActivityStatus } from "./projectActivity";
|
||||||
|
import {
|
||||||
|
__resetProjectCliInteractionActivityForTests,
|
||||||
|
hasProjectCliInteraction,
|
||||||
|
markProjectCliInteractionFinished,
|
||||||
|
markProjectCliInteractionStarted,
|
||||||
|
markProjectCliInteractionStopped,
|
||||||
|
} from "./projectCliInteractionActivity";
|
||||||
|
|
||||||
describe("projectActivityStatus (#159)", () => {
|
describe("projectActivityStatus (#159)", () => {
|
||||||
it("is none when no custom CLI conversation is visible in the work-state", () => {
|
it("is none when no custom CLI conversation is visible in the work-state", () => {
|
||||||
@ -74,3 +81,28 @@ describe("projectActivityStatus (#159)", () => {
|
|||||||
).toBe("busy");
|
).toBe("busy");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("project CLI interaction activity (#198)", () => {
|
||||||
|
it("keeps a project active until every overlapping interaction is finished", () => {
|
||||||
|
__resetProjectCliInteractionActivityForTests();
|
||||||
|
|
||||||
|
markProjectCliInteractionStarted("p1", "turn-1");
|
||||||
|
markProjectCliInteractionStarted("p1", "turn-2");
|
||||||
|
expect(hasProjectCliInteraction("p1")).toBe(true);
|
||||||
|
|
||||||
|
markProjectCliInteractionFinished("p1", "turn-1");
|
||||||
|
expect(hasProjectCliInteraction("p1")).toBe(true);
|
||||||
|
|
||||||
|
markProjectCliInteractionFinished("p1", "turn-2");
|
||||||
|
expect(hasProjectCliInteraction("p1")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stopped clears the explicit interaction immediately", () => {
|
||||||
|
__resetProjectCliInteractionActivityForTests();
|
||||||
|
|
||||||
|
markProjectCliInteractionStarted("p1", "turn-1");
|
||||||
|
markProjectCliInteractionStopped("p1", "turn-1");
|
||||||
|
|
||||||
|
expect(hasProjectCliInteraction("p1")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -0,0 +1,71 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type Listener = () => void;
|
||||||
|
|
||||||
|
const activeByProject = new Map<string, Set<string>>();
|
||||||
|
const listeners = new Set<Listener>();
|
||||||
|
|
||||||
|
function emit(): void {
|
||||||
|
for (const listener of listeners) listener();
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeSet(projectId: string): Set<string> {
|
||||||
|
let set = activeByProject.get(projectId);
|
||||||
|
if (!set) {
|
||||||
|
set = new Set();
|
||||||
|
activeByProject.set(projectId, set);
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markProjectCliInteractionStarted(
|
||||||
|
projectId: string,
|
||||||
|
interactionId: string,
|
||||||
|
): void {
|
||||||
|
const set = activeSet(projectId);
|
||||||
|
const before = set.size;
|
||||||
|
set.add(interactionId);
|
||||||
|
if (set.size !== before) emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markProjectCliInteractionFinished(
|
||||||
|
projectId: string,
|
||||||
|
interactionId: string,
|
||||||
|
): void {
|
||||||
|
const set = activeByProject.get(projectId);
|
||||||
|
if (!set?.delete(interactionId)) return;
|
||||||
|
if (set.size === 0) activeByProject.delete(projectId);
|
||||||
|
emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markProjectCliInteractionStopped(
|
||||||
|
projectId: string,
|
||||||
|
interactionId: string,
|
||||||
|
): void {
|
||||||
|
markProjectCliInteractionFinished(projectId, interactionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasProjectCliInteraction(projectId: string): boolean {
|
||||||
|
return (activeByProject.get(projectId)?.size ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribeProjectCliInteractionActivity(
|
||||||
|
listener: Listener,
|
||||||
|
): () => void {
|
||||||
|
listeners.add(listener);
|
||||||
|
return () => listeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProjectCliInteractionActivityVersion(): number {
|
||||||
|
const [version, setVersion] = useState(0);
|
||||||
|
useEffect(
|
||||||
|
() => subscribeProjectCliInteractionActivity(() => setVersion((v) => v + 1)),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function __resetProjectCliInteractionActivityForTests(): void {
|
||||||
|
activeByProject.clear();
|
||||||
|
emit();
|
||||||
|
}
|
||||||
48
frontend/src/features/projects/useProjectActivity.test.tsx
Normal file
48
frontend/src/features/projects/useProjectActivity.test.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { DIProvider } from "@/app/di";
|
||||||
|
import { MockWorkStateGateway } from "@/adapters/mock";
|
||||||
|
import type { Gateways } from "@/ports";
|
||||||
|
import {
|
||||||
|
__resetProjectCliInteractionActivityForTests,
|
||||||
|
markProjectCliInteractionStarted,
|
||||||
|
markProjectCliInteractionStopped,
|
||||||
|
} from "./projectCliInteractionActivity";
|
||||||
|
import { useProjectActivity } from "./useProjectActivity";
|
||||||
|
|
||||||
|
function ActivityProbe({ projectId }: { projectId: string }) {
|
||||||
|
const activity = useProjectActivity([projectId]);
|
||||||
|
return <span data-testid="activity">{activity[projectId]}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("useProjectActivity CLI interaction projection (#198)", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
__resetProjectCliInteractionActivityForTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects explicit CLI interaction signals over the work-state snapshot", async () => {
|
||||||
|
const projectId = "project-1";
|
||||||
|
const workState = new MockWorkStateGateway();
|
||||||
|
workState._setProjectWorkState(projectId, { agents: [], conversations: [] });
|
||||||
|
act(() => {
|
||||||
|
markProjectCliInteractionStarted(projectId, "turn-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DIProvider gateways={{ workState } as unknown as Gateways}>
|
||||||
|
<ActivityProbe projectId={projectId} />
|
||||||
|
</DIProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect((await screen.findByTestId("activity")).textContent).toBe("busy");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
markProjectCliInteractionStopped(projectId, "turn-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("activity").textContent).toBe("none"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -6,6 +6,10 @@ import {
|
|||||||
projectActivityStatus,
|
projectActivityStatus,
|
||||||
type ProjectActivityStatus,
|
type ProjectActivityStatus,
|
||||||
} from "./projectActivity";
|
} from "./projectActivity";
|
||||||
|
import {
|
||||||
|
hasProjectCliInteraction,
|
||||||
|
useProjectCliInteractionActivityVersion,
|
||||||
|
} from "./projectCliInteractionActivity";
|
||||||
|
|
||||||
export type ProjectActivityMap = Record<string, ProjectActivityStatus>;
|
export type ProjectActivityMap = Record<string, ProjectActivityStatus>;
|
||||||
|
|
||||||
@ -31,6 +35,7 @@ export function useProjectActivity(projectIds: string[]): ProjectActivityMap {
|
|||||||
const [states, setStates] = useState<Record<string, ProjectWorkState | null>>(
|
const [states, setStates] = useState<Record<string, ProjectWorkState | null>>(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
const cliInteractionVersion = useProjectCliInteractionActivityVersion();
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
if (stableProjectIds.length === 0) {
|
if (stableProjectIds.length === 0) {
|
||||||
@ -77,8 +82,10 @@ export function useProjectActivity(projectIds: string[]): ProjectActivityMap {
|
|||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const next: ProjectActivityMap = {};
|
const next: ProjectActivityMap = {};
|
||||||
for (const projectId of stableProjectIds) {
|
for (const projectId of stableProjectIds) {
|
||||||
next[projectId] = projectActivityStatus(states[projectId]);
|
next[projectId] = hasProjectCliInteraction(projectId)
|
||||||
|
? "busy"
|
||||||
|
: projectActivityStatus(states[projectId]);
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
}, [stableProjectIds, states]);
|
}, [cliInteractionVersion, stableProjectIds, states]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user