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
62 changes: 62 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13369,3 +13369,65 @@ the full Vitest gate passed 1,320 tests with one intentional live-connector
skip; the production build succeeded; the affected mobile and multi-workspace
specs passed 13 journeys; and the complete serial Playwright gate passed all 47
desktop, mobile, accessibility, and application journeys.

---

## 2026-07-15 — Mission Control agent surface scroll repair

Reproduced the fleet regression against live data at a 900×650 desktop
viewport. The `/agents` and `/agents/[id]` route shells opted into clipped
overflow but did not establish the flex-column parent required by their nested
scroll regions, leaving lower profiles and profile controls outside the
reachable viewport.

Restored the intended flex-column scroll contract on both routes and added
stable fleet/profile scroll-region hooks. The Mission Control lifecycle E2E
journey now constrains the viewport and proves that both surfaces overflow and
can scroll before continuing through profile binding and removal.

Verification: live-data browser checks confirmed wheel scrolling and clickable
top/bottom actions without mutating production records; lint and typecheck
passed (existing repository warnings only); the isolated Vitest gate passed
1,320 tests with one intentional live-connector skip; and the production-built
Mission Control Playwright suite passed both journeys.

---

## 2026-07-15 — Mission Control attention surfaces

Audited Mission Control, the cross-workspace Inbox, Activity, and top-bar
controls against live data at the operator's 1164×698 viewport. The Activity
pill's numeric keyboard hint read like a stuck notification count; the bell and
help controls were inert; assigned work lacked enough context to triage in
place; raw action-request events flooded Activity; and the capacity metric
reported agent presence rather than concurrent run slots.

Replaced the fake Activity count with an accessible shortcut description and
wired the help control into the shared keyboard guide. The global bell now uses
the same persisted notification and since-visit inbox lifecycle as workspace
alerts, exposes one combined unread count, and opens a responsive right-side
attention panel with assignments, operational alerts, recent activity, and one
mark-all-read action.

The global Inbox now excludes terminal assignments and provides a persistent
issue inspector with description, status, priority, project, labels, agent/run
state, latest comment, and canonical workspace links. Global Activity now uses
the workspace timeline interpreter, turns raw events into human-readable rows,
groups repeated action-request/watchdog updates by subject, and provides a
source inspector. Mission Control reports active runs against configured
finite slots while naming unlimited-capacity agents explicitly.

Verification: live-data browser comparison confirmed all four updated surfaces
and both top-bar controls; lint and typecheck passed (existing repository
warnings only); the complete Vitest gate passed 1,323 tests with one intentional
live-connector skip; focused notification, event grouping, and capacity
coverage passed 12/12; and the production-built multi-workspace Playwright
journeys passed 8/8 after making the empty-data assertion deterministic.

Follow-up: the global top bar's breadcrumb trail was visually suggestive but
rendered as inert text. Global pages now declare explicit parent destinations,
the shared shell renders a semantic breadcrumb landmark with keyboard-focusable
parent links, and the current page remains non-interactive with `aria-current`.
The live-data 1164×698 comparison preserved the existing visual hierarchy while
proving Activity → Mission Control navigation; a fresh production build and the
focused Playwright journey passed.
2 changes: 1 addition & 1 deletion src/app/(app)/activity/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default async function GlobalActivityPage() {
user={data.user}
workspaces={data.workspaces}
activePath="/activity"
crumbs={["Forge", "Activity"]}
crumbs={[{ label: "Forge", href: "/" }, { label: "Activity" }]}
title="Activity"
subtitle="Live run & event feed across all your workspaces"
eyebrow="Across workspaces"
Expand Down
9 changes: 7 additions & 2 deletions src/app/(app)/agents/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ export default async function AgentFleetDetailPage({
user={data.user}
workspaces={data.workspaces}
activePath="/agents"
crumbs={["Forge", "Mission Control", "Agents", "Profile"]}
crumbs={[
{ label: "Forge", href: "/" },
{ label: "Mission Control", href: "/" },
{ label: "Agents", href: "/agents" },
{ label: "Profile" },
]}
scope="control"
contentClass="overflow-hidden"
contentClass="flex flex-col overflow-hidden"
>
<AgentDetailContent
id={id}
Expand Down
8 changes: 6 additions & 2 deletions src/app/(app)/agents/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ export default async function AgentFleetPage({
user={data.user}
workspaces={data.workspaces}
activePath="/agents"
crumbs={["Forge", "Mission Control", "Agents"]}
crumbs={[
{ label: "Forge", href: "/" },
{ label: "Mission Control", href: "/" },
{ label: "Agents" },
]}
title="Agent fleet"
subtitle="Define identities, connect execution, bind workspaces, and monitor live use."
eyebrow="Mission Control"
scope="control"
contentClass="overflow-hidden"
contentClass="flex flex-col overflow-hidden"
>
<AgentsContent
isInstanceAdmin={data.user.instanceRole === "INSTANCE_ADMIN"}
Expand Down
2 changes: 1 addition & 1 deletion src/app/(app)/inbox/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default async function GlobalInboxPage() {
user={data.user}
workspaces={data.workspaces}
activePath="/inbox"
crumbs={["Forge", "Inbox"]}
crumbs={[{ label: "Forge", href: "/" }, { label: "Inbox" }]}
title="Inbox"
subtitle="Assignments across all your workspaces"
eyebrow="Across workspaces"
Expand Down
2 changes: 2 additions & 0 deletions src/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { redirect } from "next/navigation";
import { auth } from "@/server/auth";
import { TrpcProvider } from "@/lib/trpc-provider";
import { PushNotificationProvider } from "@/components/push-notification-provider";
import { KeyboardHelp } from "@/components/keyboard-help";

/**
* Outer authenticated layout. Purely auth-gating + a slug-aware tRPC
Expand All @@ -20,6 +21,7 @@ export default async function AppLayout({ children }: { children: React.ReactNod
<TrpcProvider>
<PushNotificationProvider />
{children}
<KeyboardHelp />
</TrpcProvider>
);
}
2 changes: 1 addition & 1 deletion src/app/(app)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default async function MissionControlPage() {
user={data.user}
workspaces={data.workspaces}
activePath="/"
crumbs={["Forge", "Mission Control"]}
crumbs={[{ label: "Forge", href: "/" }, { label: "Mission Control" }]}
title="Mission Control"
subtitle="Across all workspaces"
eyebrow="Home"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ export function AgentDetailContent({
"Identity-level agent profile — applies in every workspace you bind it to."
}
/>
<div className="min-h-0 flex-1 overflow-y-auto">
<div className="min-h-0 flex-1 overflow-y-auto" data-testid="agent-profile-scroll-region">
<div className="mx-auto max-w-4xl space-y-6 p-6">
<div>
<Link
Expand Down
2 changes: 1 addition & 1 deletion src/app/(app)/settings/agents/agents-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export function AgentsContent({

return (
<>
<div className="min-h-0 flex-1 overflow-y-auto">
<div className="min-h-0 flex-1 overflow-y-auto" data-testid="agent-fleet-scroll-region">
<div className="mx-auto max-w-4xl space-y-5 p-6">
<div className="grid gap-2 sm:grid-cols-4" aria-label="Agent lifecycle">
{[
Expand Down
2 changes: 0 additions & 2 deletions src/app/(app)/w/[slug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Sidebar } from "@/components/sidebar";
import { TopBar } from "@/components/top-bar";
import { CommandPalette } from "@/components/command-palette";
import { QuickCreate } from "@/components/quick-create";
import { KeyboardHelp } from "@/components/keyboard-help";
import { RealtimeProvider } from "@/components/realtime-provider";
import RealtimeToaster from "@/components/realtime-toaster";
import { TrpcProvider } from "@/lib/trpc-provider";
Expand Down Expand Up @@ -150,7 +149,6 @@ export default async function WorkspaceShellLayout({
</div>
<CommandPalette />
<QuickCreate />
<KeyboardHelp />
<TimeTrackerWidget />
<RealtimeProvider workspaceId={workspace.id} />
<RealtimeToaster />
Expand Down
2 changes: 1 addition & 1 deletion src/app/(app)/whats-new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default async function WhatsNewPage() {
user={data.user}
workspaces={data.workspaces}
activePath="/whats-new"
crumbs={["Forge", "What's New"]}
crumbs={[{ label: "Forge", href: "/" }, { label: "What's New" }]}
title="What's New"
subtitle="Shipped changes, newest first"
eyebrow="Release notes"
Expand Down
139 changes: 105 additions & 34 deletions src/components/global-shell/global-activity.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
"use client";

import { Activity as ActivityIcon } from "lucide-react";
import { useState } from "react";
import Link from "next/link";
import { Activity as ActivityIcon, ArrowUpRight, CircleDot } from "lucide-react";
import { Spinner, EmptyState } from "@/components/ui";
import { activityActorName, activityActorOwnerTitle } from "@/lib/activity-actor";
import { trpc } from "@/lib/trpc";
import { relativeTime } from "@/lib/utils";
import { cn, relativeTime } from "@/lib/utils";
import { WsChip } from "./mission-control-home";

/**
* Global activity feed — recent events across every workspace the user
* belongs to (the full-page counterpart to the Activity dock). Read-only;
* each row carries its workspace chip and the acting human/agent.
* Cross-workspace activity with a compact stream and a persistent inspector.
* The server owns event interpretation so this page, Mission Control, and
* future notification surfaces all use the same human-readable copy + links.
*/
export function GlobalActivity() {
const activity = trpc.global.activity.useQuery();
const rows = activity.data ?? [];
const [selectedId, setSelectedId] = useState<string | null>(null);
const selected = rows.find((row) => row.id === selectedId) ?? rows[0] ?? null;

return (
<div className="mx-auto w-full max-w-[900px] px-8 py-6">
<div className="mx-auto w-full max-w-[1200px] px-4 py-4 sm:px-8 sm:py-6">
{activity.isLoading ? (
<div className="flex items-center justify-center py-16">
<Spinner />
Expand All @@ -29,35 +32,103 @@ export function GlobalActivity() {
description="Runs, comments, and decisions across your workspaces appear here as they happen."
/>
) : (
<div className="overflow-hidden rounded-lg border border-border bg-card/40">
{rows.map((e, i) => {
const actor = activityActorName(e);
const actorTitle = activityActorOwnerTitle(e);
const kind = e.kind.split(".").slice(1).join(".") || e.kind;
return (
<div
key={e.id}
className={`flex items-center gap-3 px-3 py-2.5 ${i > 0 ? "border-t border-border/60" : ""}`}
>
<WsChip ws={e.workspace} dense />
<span className="inline-flex h-4 items-center rounded bg-subtle px-1 text-[9px] font-medium uppercase tracking-wider text-muted-foreground">
{kind}
</span>
<span className="flex-1 truncate text-sm">
<span className="text-foreground/90" title={actorTitle}>
{actor}
</span>{" "}
<span className="text-muted-foreground">· {e.subjectType}</span>
</span>
<span
className="text-meta tabular-nums text-muted-foreground/70"
style={{ minWidth: 52, textAlign: "right" }}
<div className="grid items-start gap-3 lg:grid-cols-[minmax(0,1fr)_22rem]">
<section className="overflow-hidden rounded-lg border border-border bg-card/40">
<header className="flex items-center justify-between border-b border-border/70 px-3.5 py-3">
<div>
<h2 className="text-sm font-semibold">Recent changes</h2>
<p className="text-meta mt-0.5 text-muted-foreground">
Repeated watchdog updates are grouped by subject.
</p>
</div>
<span className="text-meta text-muted-foreground">{rows.length} events</span>
</header>
<div className="divide-y divide-border/60">
{rows.map((event) => {
const active = selected?.id === event.id;
return (
<button
key={event.id}
type="button"
aria-pressed={active}
onClick={() => setSelectedId(event.id)}
className={cn(
"focus-ring grid w-full grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3 px-3.5 py-3 text-left hover:bg-subtle",
active && "bg-subtle/80",
)}
>
<WsChip ws={event.workspace} dense />
<span className="min-w-0">
<span className="block truncate text-[0.8125rem] font-medium">
{event.title}
</span>
{event.detail && (
<span className="text-meta mt-0.5 block truncate text-muted-foreground">
{event.detail}
</span>
)}
</span>
<span className="flex items-center gap-1.5">
{event.occurrences > 1 && (
<span className="rounded bg-background px-1 font-mono text-[10px] text-muted-foreground">
×{event.occurrences}
</span>
)}
<span className="text-meta shrink-0 tabular-nums text-muted-foreground/70">
{relativeTime(event.createdAt)}
</span>
</span>
</button>
);
})}
</div>
</section>

{selected && (
<aside className="rounded-lg border border-border bg-card/50 lg:sticky lg:top-4">
<header className="border-b border-border/70 px-4 py-3">
<div className="flex items-center gap-2">
<WsChip ws={selected.workspace} />
<span className="rounded bg-subtle px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-wider text-muted-foreground">
{selected.category}
</span>
</div>
<h2 className="mt-3 text-base font-semibold leading-snug">{selected.title}</h2>
{selected.detail && (
<p className="text-meta mt-1.5 leading-relaxed text-muted-foreground">
{selected.detail}
</p>
)}
</header>
<dl className="grid grid-cols-[5rem_minmax(0,1fr)] gap-x-3 gap-y-2 px-4 py-3 text-[0.75rem]">
<dt className="text-muted-foreground">Actor</dt>
<dd className="truncate">{selected.actor.label}</dd>
<dt className="text-muted-foreground">Subject</dt>
<dd className="truncate">{selected.subject.label}</dd>
<dt className="text-muted-foreground">Event</dt>
<dd className="truncate">{selected.kind.replaceAll("_", " ").toLowerCase()}</dd>
<dt className="text-muted-foreground">When</dt>
<dd>{relativeTime(selected.createdAt)}</dd>
{selected.occurrences > 1 && (
<>
<dt className="text-muted-foreground">Grouped</dt>
<dd>{selected.occurrences} related updates</dd>
</>
)}
</dl>
<div className="border-t border-border/70 p-3">
<Link
href={selected.href}
className="focus-ring inline-flex min-h-10 w-full items-center justify-center gap-1.5 rounded-md bg-ember px-3 text-xs font-semibold text-ember-foreground hover:bg-ember/90 sm:min-h-9"
>
{relativeTime(e.createdAt)}
</span>
Open source <ArrowUpRight className="h-3.5 w-3.5" />
</Link>
<p className="text-meta mt-2 flex items-center gap-1.5 text-muted-foreground">
<CircleDot className="h-3 w-3" /> Mission Control remains read-only.
</p>
</div>
);
})}
</aside>
)}
</div>
)}
</div>
Expand Down
Loading
Loading