diff --git a/apps/web/src/app.css b/apps/web/src/app.css
index 1a5001d9e..3b8e7d256 100644
--- a/apps/web/src/app.css
+++ b/apps/web/src/app.css
@@ -1139,47 +1139,6 @@ select:disabled,
color: var(--primary);
}
-/* Mission Control — pinned above the footer rail as its own row (DESIGN.md),
- never a 7th button mixed into Routines/Files/Skills/Agents/Tools/
- Insights below. The border-bottom is what reads it as a separate band
- rather than the first row of that rail. */
-.shell-sidebar-mission-control {
- flex-shrink: 0;
- padding: 0.35rem;
- border-bottom: 1px solid var(--border);
-}
-.shell-sidebar-mission-control-row {
- display: flex;
- align-items: center;
- gap: 0.55rem;
- width: 100%;
- padding: 0.45rem 0.4rem;
- border: 0;
- border-radius: 0;
- background: transparent;
- color: var(--foreground);
- font: inherit;
- font-weight: 600;
- cursor: pointer;
- text-align: left;
-}
-.shell-sidebar-mission-control-row svg {
- width: 1.05rem;
- height: 1.05rem;
- color: var(--muted-foreground);
-}
-.shell-sidebar-mission-control-row:hover,
-.shell-sidebar-mission-control-row[data-active="true"] {
- background: color-mix(in srgb, var(--foreground) 8%, transparent);
-}
-.shell-sidebar-mission-control-row[data-active="true"] {
- box-shadow: inset 2px 0 0 0 var(--shell-accent);
-}
-.shell-sidebar-mission-control-row:focus-visible {
- outline: 1px solid var(--foreground);
- outline-offset: -1px;
-}
-
/* Footer (reference shape): a Tools row, then the account row that
anchors the pop-up menu. Both are full-width quiet rows; the account
row has no top margin of its own so the two never read as separate
@@ -3654,144 +3613,59 @@ tr.insights-row-clickable:hover {
}
}
-/* Mission Control — stat strip, then a main column
- (Needs you / In flight) beside a 320px rail (Jump back in / This week),
- the same "stack under 1100px" rule DESIGN.md gives every right rail. */
-.mission-control-layout {
- display: grid;
- grid-template-columns: minmax(0, 1fr) 20rem;
- grid-template-areas:
- "stats stats"
- "main rail";
- gap: 1rem;
- width: 100%;
-}
-.mission-control-layout > .mission-control-stats {
- grid-area: stats;
-}
-.mission-control-layout > .mission-control-panel {
- grid-area: main;
-}
-.mission-control-layout > .mission-control-panel + .mission-control-panel {
- margin-top: 1rem;
-}
-.mission-control-layout > .mission-control-rail {
- grid-area: rail;
- display: flex;
- flex-direction: column;
- gap: 1rem;
- min-width: 0;
-}
-@media (max-width: 1100px) {
- .mission-control-layout {
- grid-template-columns: 1fr;
- grid-template-areas:
- "stats"
- "main"
- "rail";
- }
-}
-
-.mission-control-panel {
+/* A workbench room's left info column: activity, artifacts, approvals and
+ participants, stacked as quiet panels beside the running chat. */
+.room-info-panel {
padding: 0.9rem 1rem;
border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
background: var(--card, var(--background));
min-width: 0;
}
-.mission-control-panel-header {
+.room-info-panel + .room-info-panel {
+ margin-top: 1rem;
+}
+.room-info-panel-header {
display: flex;
align-items: baseline;
gap: 0.6rem;
margin-bottom: 0.6rem;
}
-.mission-control-panel-header h2 {
+.room-info-panel-header h2 {
margin: 0;
font-size: 0.85rem;
font-weight: 700;
}
-.mission-control-hint {
- font-size: 0.72rem;
- color: var(--muted-foreground);
-}
-.mission-control-hint-link {
- margin-left: auto;
- font-size: 0.75rem;
- font-weight: 700;
- color: var(--primary);
-}
-.mission-control-empty-note {
+.room-info-empty-note {
margin: 0;
font-size: 0.8rem;
color: var(--muted-foreground);
}
-.mission-control-cell-primary {
+.room-info-cell-primary {
font-size: 0.85rem;
font-weight: 700;
}
-.mission-control-cell-context {
+.room-info-cell-context {
font-size: 0.7rem;
color: var(--muted-foreground);
}
-.mission-control-row-actions {
+.room-info-row-actions {
display: flex;
justify-content: flex-end;
gap: 0.4rem;
}
-.mission-control-opt {
- white-space: nowrap;
-}
-@media (max-width: 880px) {
- .mission-control-opt {
- display: none;
- }
-}
-
-.mission-control-rows {
+.room-info-approval-list,
+.room-info-artifact-list {
display: flex;
flex-direction: column;
- border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
-}
-.mission-control-jump-row {
- display: grid;
- grid-template-columns: auto 1fr auto;
- align-items: center;
gap: 0.6rem;
- width: 100%;
- padding: 0.55rem 0.7rem;
- border: 0;
- border-bottom: 1px solid color-mix(in srgb, var(--foreground) 8%, transparent);
- background: transparent;
- color: var(--foreground);
- font: inherit;
- text-align: left;
- cursor: pointer;
-}
-button.mission-control-jump-row:hover {
- background: color-mix(in srgb, var(--primary) 8%, transparent);
-}
-.mission-control-rows > .mission-control-jump-row:last-child {
- border-bottom: 0;
-}
-.mission-control-jump-row svg {
- width: 1rem;
- height: 1rem;
- color: var(--muted-foreground);
- flex-shrink: 0;
+ margin: 0;
+ padding: 0;
+ list-style: none;
}
-.mission-control-jump-body {
+.room-info-approval-row {
display: flex;
flex-direction: column;
- min-width: 0;
-}
-.mission-control-jump-when {
- font-size: 0.72rem;
- color: var(--muted-foreground);
- white-space: nowrap;
-}
-.mission-control-week-summary {
- margin: 0;
- font-size: 0.85rem;
- font-weight: 600;
+ gap: 0.4rem;
}
/* Sidebar sections: Workbenches above Chats, each with its own label. */
@@ -3861,12 +3735,46 @@ button.mission-control-jump-row:hover {
width: 100%;
}
-/* A workbench room: timeline in the stage, sub-thread replies beside it. */
+/* A workbench room: a left info column (activity, artifacts, approvals,
+ participants), the running chat in the main column, and an optional
+ sub-thread panel on the right. */
.room-layout {
display: flex;
min-height: 0;
flex: 1;
}
+.room-info-column {
+ display: flex;
+ flex-direction: column;
+ width: 18rem;
+ flex-shrink: 0;
+ min-height: 0;
+ overflow-y: auto;
+ padding: 1rem;
+ border-right: 1px solid var(--border);
+}
+@media (max-width: 1100px) {
+ .room-info-column {
+ display: none;
+ }
+}
+.room-main {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-width: 0;
+ min-height: 0;
+}
+.room-main-scroll {
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+}
+.room-main-composer {
+ flex-shrink: 0;
+ border-top: 1px solid var(--border);
+ padding: 0.75rem 0;
+}
.room-participants {
display: flex;
flex-wrap: wrap;
diff --git a/apps/web/src/chat/strings.ts b/apps/web/src/chat/strings.ts
index 5fd21767d..4b2dad1c6 100644
--- a/apps/web/src/chat/strings.ts
+++ b/apps/web/src/chat/strings.ts
@@ -26,9 +26,8 @@ export const CHAT_STRINGS = {
couldNotLoadMessages: "messages",
workbenchNotFoundTitle: "This workbench isn't here anymore",
workbenchNotFoundDescription: "It may have been deleted, or the link is out of date.",
- /** @deprecated Prefer Mission Control / New workbench recovery. */
+ /** @deprecated Prefer New workbench recovery. */
workbenchNotFoundAction: "Back to workbenches",
- workbenchNotFoundMissionControlAction: "Mission Control",
workbenchNotFoundNewWorkbenchAction: NEW_WORKBENCH_LABEL,
/** Create-workbench CTA on every mint surface. */
newWorkbenchAction: NEW_WORKBENCH_LABEL,
diff --git a/apps/web/src/pages/mission-control-page.tsx b/apps/web/src/pages/mission-control-page.tsx
deleted file mode 100644
index ce23566d5..000000000
--- a/apps/web/src/pages/mission-control-page.tsx
+++ /dev/null
@@ -1,401 +0,0 @@
-// Mission Control: the bench's dashboard — what needs a
-// decision, what's running, and a way back into recent context. A new
-// top-level route (`/mission-control`), never `/` — `/` stays the Myra
-// land-hop redirect (see routes.tsx's header comment). Every panel here is
-// backed by a query already used elsewhere in this app (pending approvals,
-// top-level runs); nothing on this page is invented. A panel with no honest
-// data source renders an empty state naming what's missing instead of a
-// fabricated number. dropped the "Runs today" / "Spend today" KPI
-// tiles and the "This week" panel — both read packages/insights routes with
-// no stock equivalent.
-
-import {
- Badge,
- Button,
- PageShell,
- RichEmptyState,
- RUN_STATUS_TONE,
- Skeleton,
- StatGrid,
- StatGridItem,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
- formatRelativeTime,
- toast,
-} from "@corbits/react-ui";
-import type { BadgeTone } from "@corbits/react-ui";
-import { ChatCircleDots, Plus, Robot } from "@/lib/icons";
-import { useMutation, useQueryClient } from "@tanstack/react-query";
-
-import { CHAT_STRINGS, type Workbench } from "@/chat";
-import { runOutcomeStatus, runStatusLabel, withListingAbandoned } from "@corbits/workflows/client";
-
-import { approveApproval, rejectApproval } from "../api";
-import { useBench } from "../bench-context";
-import { usePendingApprovals, type PendingApproval } from "../pending-approvals";
-import { tenantKeys } from "../query-client";
-import { NEW_WORKBENCH_PATH } from "../routes";
-import { useBenchActivity } from "../shell/bench-activity";
-import type { RoutineActivityItem } from "../shell/routine-activity";
-import { StageTopBar } from "../shell/stage-top-bar";
-import { workbenchPath } from "../workbench-path";
-
-function dash(value: string | number | null | undefined): string {
- if (value === null || value === undefined || value === "") return "—";
- return String(value);
-}
-
-type InFlightRow = {
- readonly key: string;
- readonly label: string;
- readonly context: string;
- readonly createdAt: string;
- readonly statusLabel: string;
- readonly statusTone: BadgeTone;
- readonly steps: string;
-};
-
-function listingFromRoutine(routine: RoutineActivityItem, now: number) {
- return withListingAbandoned(
- {
- createdAt: routine.startedAt,
- status: routine.status,
- ...(routine.endedAt !== undefined ? { endedAt: routine.endedAt } : {}),
- ...(routine.hasInFlightTurn !== undefined
- ? { hasInFlightTurn: routine.hasInFlightTurn }
- : {}),
- ...(routine.turns !== undefined ? { turns: routine.turns } : {}),
- },
- now,
- );
-}
-
-function routineInFlightRow(routine: RoutineActivityItem, now: number): InFlightRow {
- const status = runOutcomeStatus(listingFromRoutine(routine, now), now) ?? routine.status;
- return {
- key: `routine:${routine.id}`,
- label: routine.name,
- context: "routine",
- createdAt: routine.startedAt,
- statusLabel: runStatusLabel(status),
- statusTone: RUN_STATUS_TONE.running,
- // The routine feed carries no step count — an honest dash, not a guess.
- steps: "—",
- };
-}
-
-/** Every routine this bench is actively running right now, newest first. */
-export function computeInFlightRows(
- routines: readonly RoutineActivityItem[],
- now: number = Date.now(),
-): readonly InFlightRow[] {
- const rows = routines
- .filter((routine) => runOutcomeStatus(listingFromRoutine(routine, now), now) === "running")
- .map((routine) => routineInFlightRow(routine, now));
- return rows.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
-}
-
-type JumpBackRow = {
- readonly key: string;
- readonly icon: "chat" | "agent";
- readonly label: string;
- readonly context: string;
- readonly when: string;
- readonly onSelect?: () => void;
-};
-
-/** Recent conversations and agents to jump back into — real bench activity
- * (workbenches, chats, visible agent definitions), sorted by their own
- * recency field. Nothing here is invented: a workbench with no recorded
- * activity timestamp is left out rather than given a fake one. */
-export function computeJumpBackRows(
- workbenches: readonly Workbench[],
- chats: readonly Workbench[],
- agents: readonly { id: string; name: string; createdAt: string }[],
- navigate: (to: string) => void,
- limit = 4,
-): readonly JumpBackRow[] {
- const conversations = [...workbenches, ...chats]
- .filter(
- (bench): bench is Workbench & { lastActivityAt: string } =>
- bench.lastActivityAt !== undefined,
- )
- .map((bench) => ({
- key: `bench:${bench.id}`,
- icon: "chat" as const,
- label: bench.title,
- context: bench.kind === "chat" ? "chat" : "workbench",
- when: bench.lastActivityAt,
- onSelect: () => navigate(workbenchPath(bench.id)),
- }));
- const agentRows = agents.map((agent) => ({
- key: `agent:${agent.id}`,
- icon: "agent" as const,
- label: agent.name,
- context: "agent",
- when: agent.createdAt,
- }));
- return [...conversations, ...agentRows]
- .sort((a, b) => Date.parse(b.when) - Date.parse(a.when))
- .slice(0, limit);
-}
-
-function ApprovalRow({
- item,
- tenantId,
-}: {
- readonly item: PendingApproval;
- readonly tenantId: string;
-}) {
- const queryClient = useQueryClient();
- const resolveMutation = useMutation({
- mutationFn: (action: "approve" | "deny") =>
- action === "approve" ? approveApproval(tenantId, item.id) : rejectApproval(tenantId, item.id),
- onSuccess: () => {
- void queryClient.invalidateQueries({
- queryKey: tenantKeys.pendingApprovals(tenantId),
- });
- },
- onError: (cause, action) => {
- toast(
- cause instanceof Error
- ? cause.message
- : `Couldn't ${action === "approve" ? "approve" : "deny"} that request.`,
- );
- },
- });
- const pending = resolveMutation.isPending ? resolveMutation.variables : null;
-
- return (
-
-
- {item.headline}
-
- {item.agentName}
- {formatRelativeTime(item.createdAt)}
-
-
- resolveMutation.mutate("deny")}
- >
- {pending === "deny" ? "Denying…" : "Deny"}
-
- resolveMutation.mutate("approve")}
- >
- {pending === "approve" ? "Approving…" : "Approve"}
-
-
-
-
- );
-}
-
-export function MissionControlRoute({ navigate }: { readonly navigate: (to: string) => void }) {
- const { selectedTenantId: tenantId } = useBench();
- const approvalsQuery = usePendingApprovals(tenantId);
- const activity = useBenchActivity(tenantId);
-
- const pendingApprovals = approvalsQuery.kind === "ready" ? approvalsQuery.data : null;
- const oldestWaitingAt =
- pendingApprovals !== null && pendingApprovals.length > 0
- ? pendingApprovals.reduce((oldest, item) =>
- Date.parse(item.createdAt) < Date.parse(oldest.createdAt) ? item : oldest,
- ).createdAt
- : null;
-
- const inFlightRows = activity.kind === "ready" ? computeInFlightRows(activity.routines) : [];
- const activeRunsCount = activity.kind === "ready" ? inFlightRows.length : null;
-
- const jumpBackRows =
- activity.kind === "ready"
- ? computeJumpBackRows(activity.workbenches, activity.chats, activity.agents, navigate)
- : [];
-
- return (
-
-
navigate(NEW_WORKBENCH_PATH)}>
- {CHAT_STRINGS.newWorkbenchAction}
-
- }
- />
-
-
-
-
- 0 ? "live now" : "nothing running"
- }
- />
- 0}
- sub={
- oldestWaitingAt !== null
- ? `oldest ${formatRelativeTime(oldestWaitingAt)}`
- : "all caught up"
- }
- />
-
-
-
-
-
Needs you
- approvals block agents until you act
-
- {approvalsQuery.kind === "loading" ? : null}
- {approvalsQuery.kind === "error" ? (
-
- ) : null}
- {approvalsQuery.kind === "unauthenticated" ? (
-
- ) : null}
- {pendingApprovals !== null && pendingApprovals.length === 0 ? (
-
- ) : null}
- {pendingApprovals !== null && pendingApprovals.length > 0 ? (
-
-
-
- Request
- From
- Waiting
-
-
-
-
- {tenantId !== null &&
- pendingApprovals.map((item) => (
-
- ))}
-
-
- ) : null}
-
-
-
-
-
In flight
- live
-
- {activity.kind === "loading" ? : null}
- {activity.kind === "error" ? (
-
- ) : null}
- {activity.kind !== "loading" &&
- activity.kind !== "error" &&
- inFlightRows.length === 0 ? (
-
- ) : null}
- {inFlightRows.length > 0 ? (
-
-
-
- Run
- Elapsed
- Steps
-
-
-
-
- {inFlightRows.map((row) => (
-
-
- {row.label}
-
- {row.context}
-
-
- {formatRelativeTime(row.createdAt)}
-
- {row.steps}
-
- {row.statusLabel}
-
-
- ))}
-
-
- ) : null}
-
-
-
-
-
-
Jump back in
-
- {activity.kind === "loading" ? : null}
- {activity.kind !== "loading" && jumpBackRows.length === 0 ? (
- Nothing recent yet.
- ) : null}
- {jumpBackRows.length > 0 ? (
-
- {jumpBackRows.map((row) =>
- row.onSelect !== undefined ? (
-
- {row.icon === "chat" ? : }
-
- {row.label}
- {row.context}
-
-
- {formatRelativeTime(row.when)}
-
-
- ) : (
-
-
-
- {row.label}
- {row.context}
-
-
- {formatRelativeTime(row.when)}
-
-
- ),
- )}
-
- ) : null}
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/pages/run-status-tone-parity.test.ts b/apps/web/src/pages/run-status-tone-parity.test.ts
index b87b55dbf..453a185cc 100644
--- a/apps/web/src/pages/run-status-tone-parity.test.ts
+++ b/apps/web/src/pages/run-status-tone-parity.test.ts
@@ -19,9 +19,7 @@ import { describe, expect, test } from "bun:test";
import { RUN_STATUS_TONE } from "@corbits/react-ui";
import { statusTone } from "./insights-page";
-import { computeInFlightRows } from "./mission-control-page";
import { AGENT_ROSTER_STATUS_TONE } from "./agents-page";
-import type { RoutineActivityItem } from "../shell/routine-activity";
describe("run-status tone parity with react-ui's RUN_STATUS_TONE", () => {
test("Insights' statusTone agrees with canonical for every shared status", () => {
@@ -31,17 +29,6 @@ describe("run-status tone parity with react-ui's RUN_STATUS_TONE", () => {
expect(statusTone("stopped")).toBe(RUN_STATUS_TONE.stopped);
});
- test("Mission Control's in-flight rows agree with canonical for every shared status", () => {
- const routine: RoutineActivityItem = {
- id: "rtn_1",
- name: "Daily brief",
- status: "running",
- startedAt: new Date().toISOString(),
- };
- const [routineRow] = computeInFlightRows([routine]);
- expect(routineRow?.statusTone).toBe(RUN_STATUS_TONE.running);
- });
-
test("Agents' roster status tone (a genuinely different vocabulary) still agrees where it overlaps", () => {
// AgentRosterStatus is its own enum, not RunStatus — this is the one
// local map the ticket calls defensible. It only shares one name
diff --git a/apps/web/src/pages/workbench-room-page.tsx b/apps/web/src/pages/workbench-room-page.tsx
index a57212247..d437675d0 100644
--- a/apps/web/src/pages/workbench-room-page.tsx
+++ b/apps/web/src/pages/workbench-room-page.tsx
@@ -1,9 +1,20 @@
// A workbench is a child tenant, and its room is that tenant's mailbox:
// the timeline is the tenant's mail threads, participants are its
-// principals, and a message is addressed to every agent in the room.
-// Sub-threads are native in-reply-to chains, shown in a side panel.
+// principals, and a message is addressed to every agent in the room. The
+// left info column folds in what Mission Control used to show for a
+// bench overall — here scoped to this one workbench: latest activity,
+// relevant artifacts, and pending approvals with approve/deny. Sub-threads
+// are native in-reply-to chains, shown in a side panel.
-import { Button, EmptyState, PageShell, Textarea } from "@corbits/react-ui";
+import {
+ Button,
+ EmptyState,
+ PageShell,
+ Skeleton,
+ Textarea,
+ formatRelativeTime,
+ toast,
+} from "@corbits/react-ui";
import { WarningCircle } from "@/lib/icons";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
@@ -18,9 +29,12 @@ import {
type RoomMessage,
type RoomParticipant,
} from "@/chat/threads-api";
+import { approveApproval, ArtifactListPageSchema, rejectApproval, useAPIQuery } from "../api";
import { useBench } from "../bench-context";
import { createFetchStockHub } from "../needs-converge";
+import { usePendingApprovals, type PendingApproval } from "../pending-approvals";
import { roomKeys } from "../chat-path";
+import { tenantKeys } from "../query-client";
import { StageTopBar } from "../shell/stage-top-bar";
import { workbenchIdFromPath } from "../workbench-path";
@@ -113,6 +127,152 @@ function ParticipantList({ participants }: { readonly participants: readonly Roo
);
}
+function ApprovalRow({
+ item,
+ tenantId,
+}: {
+ readonly item: PendingApproval;
+ readonly tenantId: string;
+}) {
+ const queryClient = useQueryClient();
+ const resolveMutation = useMutation({
+ mutationFn: (action: "approve" | "deny") =>
+ action === "approve" ? approveApproval(tenantId, item.id) : rejectApproval(tenantId, item.id),
+ onSuccess: () => {
+ void queryClient.invalidateQueries({
+ queryKey: tenantKeys.pendingApprovals(tenantId),
+ });
+ },
+ onError: (cause, action) => {
+ toast(
+ cause instanceof Error
+ ? cause.message
+ : `Couldn't ${action === "approve" ? "approve" : "deny"} that request.`,
+ );
+ },
+ });
+ const pending = resolveMutation.isPending ? resolveMutation.variables : null;
+
+ return (
+
+
+ {item.headline}
+
+
+ {item.agentName} · {formatRelativeTime(item.createdAt)}
+
+
+
+ resolveMutation.mutate("deny")}
+ >
+ {pending === "deny" ? "Denying…" : "Deny"}
+
+ resolveMutation.mutate("approve")}
+ >
+ {pending === "approve" ? "Approving…" : "Approve"}
+
+
+
+ );
+}
+
+/** The left info column: what Mission Control used to show, scoped to this
+ * one workbench — its latest activity, its relevant artifacts, and what's
+ * pending on it — plus the participant roster already read for the room. */
+function RoomInfoColumn({
+ roomTenantId,
+ latestMessage,
+ participants,
+}: {
+ readonly roomTenantId: string;
+ readonly latestMessage: RoomMessage | undefined;
+ readonly participants: readonly RoomParticipant[];
+}) {
+ const approvalsQuery = usePendingApprovals(roomTenantId);
+ const artifactsQuery = useAPIQuery(
+ `/api/tenants/${roomTenantId}/artifacts`,
+ ArtifactListPageSchema,
+ );
+ const pendingApprovals = approvalsQuery.kind === "ready" ? approvalsQuery.data : null;
+
+ return (
+
+ );
+}
+
function Room({ roomTenantId }: { readonly roomTenantId: string }) {
const queryClient = useQueryClient();
const [openThread, setOpenThread] = useState(null);
@@ -159,6 +319,7 @@ function Room({ roomTenantId }: { readonly roomTenantId: string }) {
});
const messages = timeline.data ?? [];
+ const latestMessage = [...messages].sort((a, b) => Date.parse(b.at) - Date.parse(a.at))[0];
const opened = messages.find((message) => message.id === openThread);
const failure: unknown = timeline.error ?? participants.error;
@@ -178,26 +339,38 @@ function Room({ roomTenantId }: { readonly roomTenantId: string }) {
-
-
-
- {messages.map((message) => (
-
setOpenThread(target.id)}
+
+
+
+
+
+ {messages.map((message) => (
+ setOpenThread(target.id)}
+ />
+ ))}
+
+ {send.error === null ? null : (
+ {errorText(send.error)}
+ )}
+
+
+
+
+ send.mutate({ content: text })}
/>
- ))}
+
- {send.error === null ? null : (
-
{errorText(send.error)}
- )}
-
send.mutate({ content: text })}
- />
-
+
{opened === undefined ? null : (
diff --git a/apps/web/src/pending-approvals.test.tsx b/apps/web/src/pending-approvals.test.tsx
index 8dbb1e027..ce4624b36 100644
--- a/apps/web/src/pending-approvals.test.tsx
+++ b/apps/web/src/pending-approvals.test.tsx
@@ -13,7 +13,6 @@ import { createRoot, type Root } from "react-dom/client";
import { createChatApprovalActions } from "./approval-actions";
import { BenchContext, type BenchState } from "./bench-context";
import { NavigationProvider } from "./navigation";
-import { MissionControlRoute } from "./pages/mission-control-page";
import { usePendingApprovals } from "./pending-approvals";
import { TestQueryProvider } from "./test-query-provider";
@@ -182,11 +181,16 @@ describe("usePendingApprovals", () => {
if (path === `/api/tenants/${TENANT_ID}/approvals`) {
return json({ error: { code: "forbidden", message: "no" } }, 403);
}
- if (path.includes("/insights/activity")) return json({ days: [] });
return undefined;
});
- const el = await mount(
undefined} />);
+ function ApprovalsFailure() {
+ const approvals = usePendingApprovals(TENANT_ID);
+ if (approvals.kind === "error") return Couldn't load approvals: {approvals.message}
;
+ return Nothing waiting on you
;
+ }
+
+ const el = await mount( );
expect(el.textContent).toContain("Couldn't load approvals");
expect(el.textContent).not.toContain("Nothing waiting on you");
diff --git a/apps/web/src/routes.tsx b/apps/web/src/routes.tsx
index 4e575e8c4..4fe5dc09f 100644
--- a/apps/web/src/routes.tsx
+++ b/apps/web/src/routes.tsx
@@ -28,7 +28,6 @@ import {
Plugs,
Robot,
SlidersHorizontal,
- SquaresFour,
} from "@/lib/icons";
import { CHAT_STRINGS } from "@/chat";
import type { Slug } from "@/lib/slug";
@@ -57,9 +56,6 @@ import {
const HomeRoute = lazy(async () => ({
default: (await import("./pages/home-page")).HomeRoute,
}));
-const MissionControlRoute = lazy(async () => ({
- default: (await import("./pages/mission-control-page")).MissionControlRoute,
-}));
const NewWorkbenchPickerRoute = lazy(async () => ({
default: (await import("./pages/new-workbench-picker")).NewWorkbenchPickerRoute,
}));
@@ -116,14 +112,6 @@ export const ONBOARDING_PATH = "/onboarding";
/** Settings path — sidebar footer + settings page. */
export const SETTINGS_PATH = "/settings";
-/** Mission Control — the bench's dashboard. Pinned above
- * the sidebar's footer rail as its own row (see DESIGN.md's Shell &
- * Navigation section), reachable by direct URL and the command palette
- * like everything else, but deliberately off `NAV_ROUTES`: it isn't a
- * roster to browse, it's the one destination the sidebar always pins in
- * view. */
-export const MISSION_CONTROL_PATH = "/mission-control";
-
/** The template picker — every "+ New workbench" affordance
* (sidebar, command palette) hops here first; picking a row is what
* actually mints the workbench. Not in `NAV_ROUTES`: it has no sidebar
@@ -250,11 +238,13 @@ export const APP_ROUTES: readonly AppRoute[] = [
hasStageTopBar: false,
},
{
- path: MISSION_CONTROL_PATH,
+ // Mission Control is gone — pending approvals and activity live in the
+ // workbench view now; old links and bookmarks bounce home.
+ path: "/mission-control",
label: "Mission Control",
- icon: ,
- render: (_path: string, navigate: (to: string) => void) => (
-
+ icon: ,
+ render: (path: string, navigate: (to: string) => void) => (
+
),
},
{
diff --git a/apps/web/src/shell/sidebar.tsx b/apps/web/src/shell/sidebar.tsx
index 4d397032c..e9fb7894f 100644
--- a/apps/web/src/shell/sidebar.tsx
+++ b/apps/web/src/shell/sidebar.tsx
@@ -1,14 +1,15 @@
-// The one sidebar. Header: the brand mark, then create + search. Body:
-// Agents and Channels — nothing page-scoped ever renders here. Footer: the
-// primary rail is Artifacts, Skills, Tools, Workflows, Agents. Below the
-// rail: a single Settings row (icon + label) — Insights, the account menu
-// (avatar, name, sign out), and everything else now live inside Settings
-// itself, not as separate sidebar affordances.
+// The one sidebar. Header: the brand mark, then a create dropdown + search.
+// Body: Agents and Channels — nothing page-scoped ever renders here.
+// Footer: the primary rail is Artifacts, Skills, Tools, Workflows, Agents.
+// Below the rail: a single Settings row (icon + label) — Insights, the
+// account menu (avatar, name, sign out), and everything else now live
+// inside Settings itself, not as separate sidebar affordances.
// Always present; there is no collapse affordance and no second nav column.
// Approvals belong in the conversation, not as a standing band here.
//
// Inbox is gone (owner decision: tasks + approvals don't flow
-// into workbenches).
+// into workbenches). Mission Control is gone too — its pending-approvals
+// and activity panels now live inside each workbench's own room.
//
// No bench switcher: a workbench IS an agent conversation now,
// one per account, so there is nothing to switch between in the common
@@ -18,7 +19,16 @@
// (`command-palette-actions.ts`), which only appears once memberships
// resolve to more than one workbench.
-import { Button, SidebarPanel, SidebarPanelBody, SidebarPanelFooter } from "@corbits/react-ui";
+import {
+ Button,
+ Menu,
+ MenuContent,
+ MenuItem,
+ MenuTrigger,
+ SidebarPanel,
+ SidebarPanelBody,
+ SidebarPanelFooter,
+} from "@corbits/react-ui";
import {
FlowArrow,
FolderOpen,
@@ -27,11 +37,14 @@ import {
Plus,
Robot,
SlidersHorizontal,
- SquaresFour,
} from "@/lib/icons";
+import { useState } from "react";
+import { useBench } from "../bench-context";
import { NEW_CHAT_PATH } from "../chat-path";
-import { matchesRoute, MISSION_CONTROL_PATH, SETTINGS_PATH } from "../routes";
+import { CreateAgentPanel } from "../pages/create-agent-panel";
+import { AGENTS_PATH_PREFIX } from "../path-ids";
+import { matchesRoute, NEW_WORKBENCH_PATH, SETTINGS_PATH } from "../routes";
import { SidebarBrandMark } from "./brand-mark";
import { WorkbenchList } from "./workbench-list";
@@ -42,6 +55,9 @@ export function Sidebar({
readonly path: string;
readonly onNavigate: (to: string) => void;
}) {
+ const { selectedTenantId } = useBench();
+ const [createAgentOpen, setCreateAgentOpen] = useState(false);
+
return (
{/* Owner's shape: logo with "+" on the first row, the search box
(inside the list) below, then Agents and Channels. No
- header icon cluster — search is the box. */}
+ header icon cluster — search is the box. The "+" now opens a
+ dropdown (New Agent / New Chat / New Workbench) instead of
+ jumping straight to New chat. */}
-
onNavigate(NEW_CHAT_PATH)}
- data-tour="new-workbench-button"
- >
-
-
+
+
+
+
+
+
+
+ setCreateAgentOpen(true)}
+ >
+ New Agent
+
+ onNavigate(NEW_CHAT_PATH)}>New Chat
+ onNavigate(NEW_WORKBENCH_PATH)}>New Workbench
+
+
{/* Agents and Channels labels render inside the list, below its
search box (owner's order: logo · search · sections · rows). */}
@@ -71,21 +102,14 @@ export function Sidebar({
- {/* Mission Control is pinned above the footer rail as its own row
- (DESIGN.md's Shell & Navigation) — not a button inside the
- primary rail, which stays Artifacts/Skills/Tools/Workflows/Agents. */}
-
- onNavigate(MISSION_CONTROL_PATH)}
- >
-
- Mission Control
-
-
+ {selectedTenantId === null ? null : (
+ onNavigate(AGENTS_PATH_PREFIX)}
+ />
+ )}
{/* Footer order: Artifacts, Skills, Tools, Workflows, Agents, then