Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions apps/web/components/agents-view.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";

const viewUrl = new URL("./agents-view.tsx", import.meta.url);

describe("Agent directory", () => {
it("describes agents, not humans", async () => {
const source = await readFile(viewUrl, "utf8");
expect(source).toContain(
'description="Permission-scoped agents with governed learning"',
);
expect(source).not.toContain("human collaborators");
});

it("offers no affordance for agent creation, which has no API", async () => {
const source = await readFile(viewUrl, "utf8");
expect(source).not.toContain("New agent");
});
});

describe("Agent detail", () => {
it("routes work assignment to the operations board", async () => {
const source = await readFile(viewUrl, "utf8");
expect(source).toContain('href="/operations"');
expect(source).toContain("Assign work");
expect(source).not.toContain("Invoke");
});
Comment on lines +21 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cover the readiness-dependent title contract.

This test checks the route and label but not the new title behavior. A regression could remove either the ready prompt or agent.readiness.reason while the test still passes; assert both branches or render the component and verify the resulting title.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/components/agents-view.test.ts` around lines 21 - 27, Extend the
“routes work assignment to the operations board” test to cover the
readiness-dependent title contract: assert the ready-state prompt and the
not-ready title using agent.readiness.reason. Prefer rendering the component and
verifying the resulting title, or add assertions for both branches while
preserving the existing route and label checks.


it("keeps every remaining disabled control tied to live state", async () => {
const source = await readFile(viewUrl, "utf8");
for (const match of source.matchAll(/disabled(?:={([^}]*)})?/g)) {
expect(match[1], "permanently disabled control").toBeTruthy();
}
Comment on lines +29 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Parse disabled JSX attributes more precisely.

The regex accepts disabled={true} as valid, misses valid whitespace such as disabled = {state}, and can match unrelated text. Use a JSX-aware parser or at least reject boolean literals and support whitespace around the assignment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/components/agents-view.test.ts` around lines 29 - 33, Update the
disabled-attribute scan in the test “keeps every remaining disabled control tied
to live state” to parse JSX attributes precisely: support whitespace around the
assignment, reject boolean literals such as disabled={true}, and avoid matching
unrelated text. Prefer a JSX-aware parser; otherwise tighten the regex and
assertions to enforce these cases.

});
});
21 changes: 8 additions & 13 deletions apps/web/components/agents-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { OpsShell } from "@/components/ops-shell";
import { PageHeader } from "@/components/page-header";
import { Avatar } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Button, buttonVariants } from "@/components/ui/button";

type EvidenceState = "reported" | "unavailable" | "unknown";
type AgentReadiness = {
Expand Down Expand Up @@ -130,13 +130,7 @@ export function AgentsView() {
<PageHeader
eyebrow="Workforce"
title="Agent directory"
description="Permission-scoped human collaborators with governed learning"
actions={
<Button disabled title="Agent creation is not available yet">
<Bot />
New agent
</Button>
}
description="Permission-scoped agents with governed learning"
/>
<div className="flex items-center gap-2 border-b bg-[var(--color-paper-2)] p-3">
<label className="flex h-9 min-w-0 max-w-md flex-1 items-center gap-2 rounded-md border bg-background px-3">
Expand Down Expand Up @@ -300,17 +294,18 @@ export function AgentDetailView({
title={agent.name}
description={`${agent.configuredRuntime} · ${agent.configuredModel} · owned by ${agent.owner}`}
actions={
<Button
disabled
<Link
href="/operations"
className={buttonVariants({ variant: "default" })}
title={
agent.readiness.state === "ready"
? "Assign work from Tasks"
? "Create a task on the operations board and dispatch it here"
: agent.readiness.reason
}
>
<Bot />
Invoke
</Button>
Assign work
</Link>
}
/>
<div className="flex items-center gap-3 border-b bg-[var(--color-paper-2)] px-4 py-3">
Expand Down
6 changes: 4 additions & 2 deletions apps/web/components/os/company-os-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ describe("Company OS shell foundation", () => {
}
});

it("keeps organisation switcher non-authoritative and disabled until multi-org exists", async () => {
it("states the organisation instead of offering a dead switcher", async () => {
const source = await readFile(shellUrl, "utf8");
expect(source).toContain("organisations.length > 1");
expect(source).toContain('id="org-switcher"');
expect(source).toContain("disabled");
expect(source).toContain('<span className="sr-only">Organisation: </span>');
expect(source).not.toContain("not available yet");
expect(source).toContain("localStorage.setItem(\"muster-theme\"");
expect(source).not.toMatch(/localStorage\.setItem\([^\)]*organisation/i);
expect(source).not.toMatch(/localStorage\.setItem\([^\)]*approval/i);
Expand Down
46 changes: 32 additions & 14 deletions apps/web/components/os/company-os-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ export function CompanyOsShell({ children }: { children: ReactNode }) {
const [mobileOpen, setMobileOpen] = useState(false);
const [paletteOpen, setPaletteOpen] = useState(false);
const [theme, setTheme] = useState<"dark" | "light">("dark");
const [chosenOrganisationId, setChosenOrganisationId] = useState("");

useEffect(() => {
const current =
Expand All @@ -266,6 +267,10 @@ export function CompanyOsShell({ children }: { children: ReactNode }) {
const pendingApprovals = command.data?.pendingApprovalCount ?? 0;
const overallHealth = toHealthState(command.data?.overallHealth ?? "unknown");
const org = session.data?.organisation;
const organisations = session.data?.organisations ?? [];
// The switcher only earns its interactivity once a second membership exists;
// with one organisation the top bar states it instead of offering a choice.
const selectedOrganisationId = chosenOrganisationId || org?.id || "";
const actor = session.data?.actor;
const environment = session.data?.environment ?? "unknown";

Expand Down Expand Up @@ -324,20 +329,33 @@ export function CompanyOsShell({ children }: { children: ReactNode }) {

<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<label className="sr-only" htmlFor="org-switcher">
Organisation
</label>
<select
id="org-switcher"
className="max-w-[12rem] truncate rounded-md border border-border bg-background px-2 py-1 text-xs font-medium"
value={org?.id ?? ""}
disabled
title="Multi-organisation membership is not available yet"
>
<option value={org?.id ?? ""}>
{org?.name ?? (session.isLoading ? "Loading…" : "Organisation")}
</option>
</select>
{organisations.length > 1 ? (
<>
<label className="sr-only" htmlFor="org-switcher">
Organisation
</label>
<select
id="org-switcher"
className="max-w-[12rem] truncate rounded-md border border-border bg-background px-2 py-1 text-xs font-medium"
value={selectedOrganisationId}
onChange={(event) =>
setChosenOrganisationId(event.target.value)
}
Comment on lines +341 to +343

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Wire organisation selection to an authoritative context switch.

This only updates local UI state. The supplied session builder currently always returns one organisation, so the selector is unreachable; if multi-memberships are later returned, selecting one will change the label while the session and loaded data remain scoped to the previous organisation. Persist and verify the membership change server-side, then refresh/invalidate organisation-scoped data before showing the new selection. Replace the source-only assertion with a behavior test for that flow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/components/os/company-os-shell.tsx` around lines 341 - 343, Update
the organisation selector’s onChange flow to persist and verify the chosen
membership server-side, then refresh or invalidate organisation-scoped session
and data before committing the new UI selection. Replace the source-only
assertion with a behavior test covering selection, server confirmation, data
refresh, and the resulting organisation context.

Source: Coding guidelines

>
{organisations.map((membership) => (
<option key={membership.id} value={membership.id}>
{membership.name}
</option>
))}
</select>
</>
) : (
<p className="max-w-[12rem] truncate text-xs font-medium">
<span className="sr-only">Organisation: </span>
{org?.name ??
(session.isLoading ? "Loading…" : "Organisation")}
</p>
)}
{session.data?.customer ? (
<Badge className="bg-muted text-muted-foreground">
Customer: {session.data.customer.name}
Expand Down
Loading