fix: simplify kanban pr footer (#2374)
This commit is contained in:
parent
6cc7cc3e4e
commit
cbaffd2cb4
|
|
@ -11,7 +11,7 @@ on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
important:
|
important:
|
||||||
description: 'Mark this nightly as an important update (escalates the in-app restart prompt). Retro-flag an already-published nightly by re-running only the publish-feed job with this input set.'
|
description: "Mark this nightly as an important update (escalates the in-app restart prompt). Retro-flag an already-published nightly by re-running only the publish-feed job with this input set."
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,20 +38,10 @@ function respondWithProjectAndPRs() {
|
||||||
projectId: "proj-1",
|
projectId: "proj-1",
|
||||||
displayName: "fix the bug",
|
displayName: "fix the bug",
|
||||||
harness: "claude-code",
|
harness: "claude-code",
|
||||||
status: "pr_open",
|
status: "draft",
|
||||||
isTerminated: false,
|
isTerminated: false,
|
||||||
updatedAt: "2026-06-10T16:15:04Z",
|
updatedAt: "2026-06-10T16:15:04Z",
|
||||||
prs: [
|
prs: [
|
||||||
{
|
|
||||||
number: 278,
|
|
||||||
state: "open",
|
|
||||||
url: "https://github.com/aoagents/ReverbCode/pull/278",
|
|
||||||
ci: "passing",
|
|
||||||
review: "approved",
|
|
||||||
mergeability: "clean",
|
|
||||||
reviewComments: false,
|
|
||||||
updatedAt: "2026-06-10T16:15:04Z",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
number: 279,
|
number: 279,
|
||||||
state: "draft",
|
state: "draft",
|
||||||
|
|
@ -62,6 +52,46 @@ function respondWithProjectAndPRs() {
|
||||||
reviewComments: false,
|
reviewComments: false,
|
||||||
updatedAt: "2026-06-10T16:20:04Z",
|
updatedAt: "2026-06-10T16:20:04Z",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
number: 278,
|
||||||
|
state: "open",
|
||||||
|
url: "https://github.com/aoagents/ReverbCode/pull/278",
|
||||||
|
ci: "passing",
|
||||||
|
review: "review_required",
|
||||||
|
mergeability: "clean",
|
||||||
|
reviewComments: false,
|
||||||
|
updatedAt: "2026-06-10T16:15:04Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 280,
|
||||||
|
state: "open",
|
||||||
|
url: "https://github.com/aoagents/ReverbCode/issues/280",
|
||||||
|
ci: "passing",
|
||||||
|
review: "approved",
|
||||||
|
mergeability: "clean",
|
||||||
|
reviewComments: false,
|
||||||
|
updatedAt: "2026-06-10T16:25:04Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 281,
|
||||||
|
state: "merged",
|
||||||
|
url: "https://github.com/aoagents/ReverbCode/pull/281",
|
||||||
|
ci: "passing",
|
||||||
|
review: "approved",
|
||||||
|
mergeability: "mergeable",
|
||||||
|
reviewComments: false,
|
||||||
|
updatedAt: "2026-06-10T16:30:04Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 282,
|
||||||
|
state: "closed",
|
||||||
|
url: "https://github.com/aoagents/ReverbCode/pull/282",
|
||||||
|
ci: "passing",
|
||||||
|
review: "approved",
|
||||||
|
mergeability: "unknown",
|
||||||
|
reviewComments: false,
|
||||||
|
updatedAt: "2026-06-10T16:35:04Z",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -179,11 +209,40 @@ beforeEach(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("PR hydration for a normal project (#251)", () => {
|
describe("PR hydration for a normal project (#251)", () => {
|
||||||
it("renders every session PR on the Board card instead of 'no PR yet'", async () => {
|
it("renders Board card PR numbers with lifecycle statuses only instead of 'no PR yet'", async () => {
|
||||||
renderWithProviders(<SessionsBoard />);
|
renderWithProviders(<SessionsBoard />);
|
||||||
|
|
||||||
expect(await screen.findByText("PR #278 · open")).toBeInTheDocument();
|
expect(await screen.findByRole("link", { name: "#278" })).toHaveAttribute(
|
||||||
expect(screen.getByText("PR #279 · draft")).toBeInTheDocument();
|
"href",
|
||||||
|
"https://github.com/aoagents/ReverbCode/pull/278",
|
||||||
|
);
|
||||||
|
expect(screen.getByText("open")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("link", { name: "#279" })).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"https://github.com/aoagents/ReverbCode/pull/279",
|
||||||
|
);
|
||||||
|
expect(screen.getByRole("link", { name: "#280" })).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"https://github.com/aoagents/ReverbCode/pull/280",
|
||||||
|
);
|
||||||
|
expect(screen.getByRole("link", { name: "#281" })).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"https://github.com/aoagents/ReverbCode/pull/281",
|
||||||
|
);
|
||||||
|
expect(screen.getByRole("link", { name: "#282" })).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"https://github.com/aoagents/ReverbCode/pull/282",
|
||||||
|
);
|
||||||
|
expect(screen.getByLabelText("#278, #280 open")).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText("#279 draft")).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText("#281 merged")).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText("#282 closed")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("draft")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("merged")).toHaveClass("text-accent");
|
||||||
|
expect(screen.getByText("closed")).toHaveClass("text-error");
|
||||||
|
expect(screen.queryByText("review pending")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("CI")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Needs attention")).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText("no PR yet")).not.toBeInTheDocument();
|
expect(screen.queryByText("no PR yet")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -191,14 +250,13 @@ describe("PR hydration for a normal project (#251)", () => {
|
||||||
respondWithAttentionPR();
|
respondWithAttentionPR();
|
||||||
renderWithProviders(<SessionsBoard />);
|
renderWithProviders(<SessionsBoard />);
|
||||||
|
|
||||||
expect(await screen.findByRole("link", { name: "PR" })).toHaveAttribute(
|
expect(await screen.findByRole("link", { name: "#278" })).toHaveAttribute(
|
||||||
"href",
|
"href",
|
||||||
"https://github.com/aoagents/ReverbCode/pull/278",
|
"https://github.com/aoagents/ReverbCode/pull/278",
|
||||||
);
|
);
|
||||||
expect(screen.getByRole("link", { name: "conflicts" })).toHaveAttribute(
|
expect(screen.getByText("open")).toBeInTheDocument();
|
||||||
"href",
|
expect(screen.queryByText("changes requested")).not.toBeInTheDocument();
|
||||||
"https://github.com/aoagents/ReverbCode/pull/278/conflicts",
|
expect(screen.queryByRole("link", { name: "conflicts" })).not.toBeInTheDocument();
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("lists every session PR on the PR page instead of being empty", async () => {
|
it("lists every session PR on the PR page instead of being empty", async () => {
|
||||||
|
|
@ -206,7 +264,10 @@ describe("PR hydration for a normal project (#251)", () => {
|
||||||
|
|
||||||
expect(await screen.findByText("#278")).toBeInTheDocument();
|
expect(await screen.findByText("#278")).toBeInTheDocument();
|
||||||
expect(screen.getByText("#279")).toBeInTheDocument();
|
expect(screen.getByText("#279")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("#280")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("#281")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("#282")).toBeInTheDocument();
|
||||||
expect(screen.queryByText("No open pull requests.")).not.toBeInTheDocument();
|
expect(screen.queryByText("No open pull requests.")).not.toBeInTheDocument();
|
||||||
expect(screen.getAllByText("fix the bug")).toHaveLength(2);
|
expect(screen.getAllByText("fix the bug")).toHaveLength(5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -41,13 +41,18 @@ export function OrchestratorReplacementDialog({
|
||||||
<AlertTriangle className="size-4" aria-hidden="true" />
|
<AlertTriangle className="size-4" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<Dialog.Title className="text-sm font-medium text-foreground">Orchestrator replacement failed</Dialog.Title>
|
<Dialog.Title className="text-sm font-medium text-foreground">
|
||||||
|
Orchestrator replacement failed
|
||||||
|
</Dialog.Title>
|
||||||
<Dialog.Description className="mt-2 text-[13px] leading-5 text-muted-foreground">
|
<Dialog.Description className="mt-2 text-[13px] leading-5 text-muted-foreground">
|
||||||
{error ?? "The project orchestrator could not be replaced."}
|
{error ?? "The project orchestrator could not be replaced."}
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Close asChild>
|
<Dialog.Close asChild>
|
||||||
<button className="rounded-md p-1 text-passive hover:bg-interactive-hover hover:text-foreground" type="button">
|
<button
|
||||||
|
className="rounded-md p-1 text-passive hover:bg-interactive-hover hover:text-foreground"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
<X className="size-4" aria-hidden="true" />
|
<X className="size-4" aria-hidden="true" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,11 @@ beforeEach(() => {
|
||||||
putMock.mockReset();
|
putMock.mockReset();
|
||||||
postMock.mockReset();
|
postMock.mockReset();
|
||||||
putMock.mockResolvedValue({ data: { project: {} }, error: undefined });
|
putMock.mockResolvedValue({ data: { project: {} }, error: undefined });
|
||||||
postMock.mockResolvedValue({ data: { orchestrator: { id: "proj-1-orch-2" } }, error: undefined, response: { status: 200 } });
|
postMock.mockResolvedValue({
|
||||||
|
data: { orchestrator: { id: "proj-1-orch-2" } },
|
||||||
|
error: undefined,
|
||||||
|
response: { status: 200 },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ProjectSettingsForm", () => {
|
describe("ProjectSettingsForm", () => {
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,8 @@ import { OrchestratorIcon } from "./icons";
|
||||||
import { NewTaskDialog } from "./NewTaskDialog";
|
import { NewTaskDialog } from "./NewTaskDialog";
|
||||||
import { spawnOrchestrator } from "../lib/spawn-orchestrator";
|
import { spawnOrchestrator } from "../lib/spawn-orchestrator";
|
||||||
import { restartProjectOrchestrator } from "../lib/restart-orchestrator";
|
import { restartProjectOrchestrator } from "../lib/restart-orchestrator";
|
||||||
import { prDiffSummary, sessionPRDisplaySummaries } from "../lib/pr-display";
|
import { prBrowserUrl, sessionPRDisplaySummaries } from "../lib/pr-display";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
import { PRAttentionPanel, PRStatusStrip } from "./PRSummaryDisplay";
|
|
||||||
import { useUiStore } from "../stores/ui-store";
|
import { useUiStore } from "../stores/ui-store";
|
||||||
|
|
||||||
type SessionsBoardProps = {
|
type SessionsBoardProps = {
|
||||||
|
|
@ -169,7 +168,13 @@ export function SessionsBoard({ projectId }: SessionsBoardProps) {
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<OrchestratorIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
<OrchestratorIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||||
{isProjectRestarting ? "Restarting..." : isSpawning ? "Spawning..." : orchestrator ? "Orchestrator" : "Spawn Orchestrator"}
|
{isProjectRestarting
|
||||||
|
? "Restarting..."
|
||||||
|
: isSpawning
|
||||||
|
? "Spawning..."
|
||||||
|
: orchestrator
|
||||||
|
? "Orchestrator"
|
||||||
|
: "Spawn Orchestrator"}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
|
|
@ -314,13 +319,8 @@ function SessionCard({ session, onOpen }: { session: WorkspaceSession; onOpen: (
|
||||||
onOpen();
|
onOpen();
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="w-full rounded-[7px] border border-border bg-surface text-left transition-colors hover:border-border-strong">
|
||||||
className="w-full rounded-[7px] border border-border bg-surface text-left transition-colors hover:border-border-strong"
|
<div onClick={onOpen} onKeyDown={handleKeyDown} role="button" tabIndex={0}>
|
||||||
onClick={onOpen}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 px-[13px] pb-[9px] pt-3">
|
<div className="flex items-center gap-2 px-[13px] pb-[9px] pt-3">
|
||||||
<span className={cn("inline-flex items-center gap-1.5 text-[11px] font-medium", badge.className)}>
|
<span className={cn("inline-flex items-center gap-1.5 text-[11px] font-medium", badge.className)}>
|
||||||
<span className={cn("h-[7px] w-[7px] rounded-full bg-current")} />
|
<span className={cn("h-[7px] w-[7px] rounded-full bg-current")} />
|
||||||
|
|
@ -348,39 +348,74 @@ function SessionCard({ session, onOpen }: { session: WorkspaceSession; onOpen: (
|
||||||
{session.title}
|
{session.title}
|
||||||
</div>
|
</div>
|
||||||
{showBranch && <div className="px-[13px] pb-2.5 font-mono text-[10.5px] text-passive">{branch}</div>}
|
{showBranch && <div className="px-[13px] pb-2.5 font-mono text-[10.5px] text-passive">{branch}</div>}
|
||||||
<div className="border-t border-border px-[13px] py-2 font-mono text-[10.5px] text-passive">
|
</div>
|
||||||
{prSummaries.length > 0 ? (
|
<div
|
||||||
<div className="flex flex-col gap-2">
|
className="border-t border-border px-[13px] py-2 font-mono text-[10.5px] text-passive"
|
||||||
{prSummaries.map((prSummary, index) => (
|
onClick={(event) => event.stopPropagation()}
|
||||||
<BoardPRSummary
|
>
|
||||||
className={cn(index > 0 && "border-t border-border pt-2")}
|
{prSummaries.length === 0 ? (
|
||||||
key={prSummary.number}
|
"no PR yet"
|
||||||
pr={prSummary}
|
) : (
|
||||||
/>
|
<div className="flex flex-col gap-1">
|
||||||
|
{groupPRsByLifecycle(prSummaries).map((group) => (
|
||||||
|
<BoardPRGroup group={group} key={group.status.label} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
"no PR yet"
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BoardPRSummary({ className, pr }: { className?: string; pr: SessionPRSummary }) {
|
type BoardPRLifecycleStatus = { label: "closed" | "open" | "draft" | "merged"; className: string };
|
||||||
const diffSummary = prDiffSummary(pr);
|
type BoardPRGroup = { status: BoardPRLifecycleStatus; prs: SessionPRSummary[] };
|
||||||
|
|
||||||
|
function BoardPRGroup({ group }: { group: BoardPRGroup }) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex min-w-0 flex-col gap-1", className)}>
|
<span
|
||||||
<span>
|
aria-label={`${group.prs.map((pr) => `#${pr.number}`).join(", ")} ${group.status.label}`}
|
||||||
PR #{pr.number} · {pr.state}
|
className="inline-flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-1"
|
||||||
|
>
|
||||||
|
<span>PR</span>
|
||||||
|
{group.prs.map((pr, index) => (
|
||||||
|
<span key={pr.number}>
|
||||||
|
<a
|
||||||
|
className="text-passive underline-offset-2 transition-colors hover:text-foreground hover:underline"
|
||||||
|
href={prBrowserUrl(pr)}
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
#{pr.number}
|
||||||
|
</a>
|
||||||
|
{index < group.prs.length - 1 ? "," : null}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
<span className={cn("font-medium", group.status.className)}>{group.status.label}</span>
|
||||||
</span>
|
</span>
|
||||||
{diffSummary ? <span className="truncate">{diffSummary}</span> : null}
|
|
||||||
<PRStatusStrip pr={pr} />
|
|
||||||
<PRAttentionPanel className="mt-1.5 pt-1.5" maxItems={2} pr={pr} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function groupPRsByLifecycle(prs: SessionPRSummary[]): BoardPRGroup[] {
|
||||||
|
const groups = new Map<BoardPRLifecycleStatus["label"], BoardPRGroup>();
|
||||||
|
for (const pr of prs) {
|
||||||
|
const status = prLifecycleStatus(pr);
|
||||||
|
const group = groups.get(status.label);
|
||||||
|
if (group) {
|
||||||
|
group.prs.push(pr);
|
||||||
|
} else {
|
||||||
|
groups.set(status.label, { status, prs: [pr] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(groups.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function prLifecycleStatus(pr: SessionPRSummary): BoardPRLifecycleStatus {
|
||||||
|
if (pr.state === "draft") return { label: "draft", className: "text-passive" };
|
||||||
|
if (pr.state === "merged") return { label: "merged", className: "text-accent" };
|
||||||
|
if (pr.state === "closed") return { label: "closed", className: "text-error" };
|
||||||
|
return { label: "open", className: "text-success" };
|
||||||
|
}
|
||||||
|
|
||||||
function sameLabel(a: string, b: string): boolean {
|
function sameLabel(a: string, b: string): boolean {
|
||||||
const normalize = (value: string) =>
|
const normalize = (value: string) =>
|
||||||
value
|
value
|
||||||
|
|
|
||||||
|
|
@ -557,7 +557,13 @@ function ProjectItem({
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{isProjectRestarting ? "Restarting…" : isSpawning ? "Spawning…" : orchestrator ? "Orchestrator" : "Spawn orchestrator"}
|
{isProjectRestarting
|
||||||
|
? "Restarting…"
|
||||||
|
: isSpawning
|
||||||
|
? "Spawning…"
|
||||||
|
: orchestrator
|
||||||
|
? "Orchestrator"
|
||||||
|
: "Spawn orchestrator"}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,12 @@ describe("useWorkspaceQuery", () => {
|
||||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||||
|
|
||||||
const [workspace] = result.current.data ?? [];
|
const [workspace] = result.current.data ?? [];
|
||||||
expect(workspace).toMatchObject({ id: "proj-1", name: "my-app", path: "/home/me/my-app", orchestratorAgent: "codex" });
|
expect(workspace).toMatchObject({
|
||||||
|
id: "proj-1",
|
||||||
|
name: "my-app",
|
||||||
|
path: "/home/me/my-app",
|
||||||
|
orchestratorAgent: "codex",
|
||||||
|
});
|
||||||
expect(workspace.sessions).toHaveLength(2);
|
expect(workspace.sessions).toHaveLength(2);
|
||||||
expect(workspace.sessions[0]).toMatchObject({
|
expect(workspace.sessions[0]).toMatchObject({
|
||||||
id: "sess-1",
|
id: "sess-1",
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,10 @@ export async function restartProjectOrchestrator({
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await refreshWorkspaceState(queryClient);
|
await refreshWorkspaceState(queryClient);
|
||||||
setOrchestratorReplacementError(projectId, error instanceof Error ? error.message : "Could not replace orchestrator");
|
setOrchestratorReplacementError(
|
||||||
|
projectId,
|
||||||
|
error instanceof Error ? error.message : "Could not replace orchestrator",
|
||||||
|
);
|
||||||
onError?.(error);
|
onError?.(error);
|
||||||
} finally {
|
} finally {
|
||||||
setProjectRestarting(projectId, false);
|
setProjectRestarting(projectId, false);
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,8 @@ describe("orchestratorHealth", () => {
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
state: "duplicates",
|
state: "duplicates",
|
||||||
message: "Multiple orchestrators are active. The newest one is used; stale ones will be cleaned up on daemon reconcile.",
|
message:
|
||||||
|
"Multiple orchestrators are active. The newest one is used; stale ones will be cleaned up on daemon reconcile.",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|
|
||||||
|
|
@ -386,7 +386,8 @@ export function orchestratorHealth(workspace: WorkspaceSummary, restarting = fal
|
||||||
if (active.length > 1) {
|
if (active.length > 1) {
|
||||||
return {
|
return {
|
||||||
state: "duplicates",
|
state: "duplicates",
|
||||||
message: "Multiple orchestrators are active. The newest one is used; stale ones will be cleaned up on daemon reconcile.",
|
message:
|
||||||
|
"Multiple orchestrators are active. The newest one is used; stale ones will be cleaned up on daemon reconcile.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const orchestrator = newestActiveOrchestrator(workspace.sessions);
|
const orchestrator = newestActiveOrchestrator(workspace.sessions);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue