+ {/* Title — its own row, bigger, can wrap */}
+
+
+ {/* Meta row: branch + PR# + diff size (simplified for merge-ready) */}
+
+
+ {secondaryText && (
+
+ )}
+
+ {/* Rate limited indicator */}
+ {rateLimited && pr?.state === "open" && (
+
+
- Merge PR #{pr.number}
-
- ) : (
-
- {alerts.map((alert) => (
-
+ PR data rate limited
+
+
+ )}
+
+ {!rateLimited && alerts.length > 0 && (
+
+
+ {alerts.slice(0, 3).map((alert) => (
+
e.stopPropagation()}
className={cn(
- "inline-flex items-center gap-1 rounded border px-2 py-0.5 text-[11px] font-medium hover:brightness-125 hover:no-underline",
+ "min-w-0 flex-1 truncate whitespace-nowrap px-2 py-0.5 font-[var(--font-mono)] text-[11px] font-medium !underline [text-decoration-skip-ink:none] [text-underline-offset:2px] hover:brightness-125",
alert.className,
)}
+ style={alert.color ? { color: alert.color } : undefined}
>
- {alert.count !== undefined && {alert.count}}
+ {alert.count !== undefined && (
+ <>
+ {alert.count}{" "}
+ >
+ )}
{alert.label}
- {alert.actionLabel && session.activity !== "active" && (
+ {alert.actionLabel && (
@@ -216,118 +516,73 @@ function SessionCardView({ session, onSend, onKill, onMerge, onRestore }: Sessio
))}
- )}
-
- )}
-
- {/* Expandable detail panel */}
- {expanded && (
-
- {session.summary && pr?.title && session.summary !== pr.title && (
-
-
- {session.summary}
-
-
- )}
-
- {session.issueUrl && (
-
-
- {session.issueLabel || session.issueUrl}
- {session.issueTitle && `: ${session.issueTitle}`}
-
-
- )}
-
- {pr && pr.ciChecks.length > 0 && (
-
-
-
- )}
-
- {pr && pr.unresolvedComments.length > 0 && (
-
-
- {pr.unresolvedComments.map((c) => (
-
- ))}
-
-
- )}
-
- {pr && (
-
-
-
- {pr.title}
-
-
- +{pr.additions}{" "}
- -{pr.deletions}
- {" · "}mergeable: {pr.mergeability.mergeable ? "yes" : "no"}
- {" · "}review: {pr.reviewDecision}
-
-
- )}
-
- {!pr && (
-
- No PR associated with this session.
-
- )}
-
-
- {isRestorable && (
-
- )}
- {!isTerminal && (
-
- )}
+ )}
+
+
+ {session.issueUrl ? (
+
+ {session.issueLabel || session.issueUrl}
+
+ ) : (
+
+ {session.activity ?? session.status}
+
+ )}
+
+ {isReadyToMerge && pr ? (
+
+ ) : !isTerminal && (
+
+ )}
- )}
+
);
}
@@ -344,25 +599,17 @@ function areSessionCardPropsEqual(prev: SessionCardProps, next: SessionCardProps
export const SessionCard = memo(SessionCardView, areSessionCardPropsEqual);
-function DetailSection({ label, children }: { label: string; children: React.ReactNode }) {
- return (
-
-
- {label}
-
- {children}
-
- );
-}
-
interface Alert {
key: string;
label: string;
className: string;
+ color?: string;
+ borderColor?: string;
url: string;
count?: number;
actionLabel?: string;
actionMessage?: string;
+ actionClassName?: string;
}
function getAlerts(session: DashboardSession): Alert[] {
@@ -379,19 +626,21 @@ function getAlerts(session: DashboardSession): Alert[] {
alerts.push({
key: "ci-unknown",
label: "CI unknown",
- className:
- "border-[rgba(245,158,11,0.3)] bg-[rgba(245,158,11,0.08)] text-[var(--color-status-attention)]",
+ className: "",
+ color: "var(--color-alert-ci-unknown)",
url: pr.url + "/checks",
});
} else {
alerts.push({
key: "ci-fail",
label: `${failCount} CI check${failCount > 1 ? "s" : ""} failing`,
- className:
- "border-[rgba(239,68,68,0.3)] bg-[rgba(239,68,68,0.08)] text-[var(--color-status-error)]",
+ className: "",
+ color: "var(--color-alert-ci)",
+ borderColor: "var(--color-alert-ci)",
url: failedCheck?.url ?? pr.url + "/checks",
actionLabel: "ask to fix",
actionMessage: `Please fix the failing CI checks on ${pr.url}`,
+ actionClassName: "bg-[var(--color-alert-ci-bg)] text-white hover:brightness-110",
});
}
}
@@ -400,19 +649,23 @@ function getAlerts(session: DashboardSession): Alert[] {
alerts.push({
key: "changes",
label: "changes requested",
- className:
- "border-[rgba(239,68,68,0.3)] bg-[rgba(239,68,68,0.08)] text-[var(--color-status-error)]",
+ className: "",
+ color: "var(--color-alert-changes)",
url: pr.url,
+ actionLabel: "ask to address",
+ actionMessage: `Please address the requested changes on ${pr.url}`,
+ actionClassName: "bg-[var(--color-alert-changes-bg)] text-white hover:brightness-110",
});
} else if (!pr.isDraft && (pr.reviewDecision === "pending" || pr.reviewDecision === "none")) {
alerts.push({
key: "review",
label: "needs review",
- className:
- "border-[rgba(245,158,11,0.3)] bg-[rgba(245,158,11,0.08)] text-[var(--color-status-attention)]",
+ className: "",
+ color: "var(--color-alert-review)",
url: pr.url,
actionLabel: "ask to post",
actionMessage: `Post ${pr.url} on slack asking for a review.`,
+ actionClassName: "bg-[var(--color-alert-review-bg)] text-white hover:brightness-110",
});
}
@@ -420,11 +673,12 @@ function getAlerts(session: DashboardSession): Alert[] {
alerts.push({
key: "conflict",
label: "merge conflict",
- className:
- "border-[rgba(239,68,68,0.3)] bg-[rgba(239,68,68,0.08)] text-[var(--color-status-error)]",
+ className: "",
+ color: "var(--color-alert-conflict)",
url: pr.url,
actionLabel: "ask to fix",
actionMessage: `Please resolve the merge conflicts on ${pr.url} by rebasing on the base branch`,
+ actionClassName: "bg-[var(--color-alert-conflict-bg)] text-white hover:brightness-110",
});
}
@@ -434,11 +688,13 @@ function getAlerts(session: DashboardSession): Alert[] {
key: "comments",
label: "unresolved comments",
count: pr.unresolvedThreads,
- className:
- "border-[rgba(239,68,68,0.3)] bg-[rgba(239,68,68,0.08)] text-[var(--color-status-error)]",
+ className: "",
+ color: "var(--color-alert-comment)",
+ borderColor: "var(--color-alert-comment)",
url: firstUrl,
actionLabel: "ask to resolve",
actionMessage: `Please address all unresolved review comments on ${pr.url}`,
+ actionClassName: "bg-[var(--color-alert-comment-bg)] text-white hover:brightness-110",
});
}
diff --git a/packages/web/src/components/SessionDetail.tsx b/packages/web/src/components/SessionDetail.tsx
index 3e3e33a92..b3d5aa377 100644
--- a/packages/web/src/components/SessionDetail.tsx
+++ b/packages/web/src/components/SessionDetail.tsx
@@ -1,13 +1,12 @@
"use client";
-import { useState, useEffect, useRef } from "react";
+import { useState, useEffect, useRef, type ReactNode } from "react";
import { useSearchParams } from "next/navigation";
import { type DashboardSession, type DashboardPR, isPRMergeReady } from "@/lib/types";
import { CI_STATUS } from "@composio/ao-core/types";
import { cn } from "@/lib/cn";
import { CICheckList } from "./CIBadge";
import { DirectTerminal } from "./DirectTerminal";
-import { ActivityDot } from "./ActivityDot";
interface OrchestratorZones {
merge: number;
@@ -35,26 +34,8 @@ const activityMeta: Record
= {
exited: { label: "Exited", color: "var(--color-status-error)" },
};
-function humanizeStatus(status: string): string {
- return status
- .replace(/_/g, " ")
- .replace(/\bci\b/gi, "CI")
- .replace(/\bpr\b/gi, "PR")
- .replace(/\b\w/g, (c) => c.toUpperCase());
-}
-
-function relativeTime(iso: string): string {
- const ms = new Date(iso).getTime();
- if (!iso || isNaN(ms)) return "unknown";
- const diff = Date.now() - ms;
- const seconds = Math.floor(diff / 1000);
- if (seconds < 60) return "just now";
- const minutes = Math.floor(seconds / 60);
- if (minutes < 60) return `${minutes}m ago`;
- const hours = Math.floor(minutes / 60);
- if (hours < 24) return `${hours}h ago`;
- const days = Math.floor(hours / 24);
- return `${days}d ago`;
+function getSessionHeadline(session: DashboardSession): string {
+ return session.issueTitle ?? session.summary ?? session.id;
}
function cleanBugbotComment(body: string): { title: string; description: string } {
@@ -75,8 +56,97 @@ function buildGitHubBranchUrl(pr: DashboardPR): string {
return `https://github.com/${pr.owner}/${pr.repo}/tree/${pr.branch}`;
}
-function buildGitHubRepoUrl(pr: DashboardPR): string {
- return `https://github.com/${pr.owner}/${pr.repo}`;
+function SessionTopStrip({
+ headline,
+ activityLabel,
+ activityColor,
+ branch,
+ pr,
+ isOrchestrator = false,
+ rightSlot,
+}: {
+ headline: string;
+ activityLabel: string;
+ activityColor: string;
+ branch: string | null;
+ pr: DashboardPR | null;
+ isOrchestrator?: boolean;
+ rightSlot?: ReactNode;
+}) {
+ return (
+
+
+
+
+ {rightSlot ?
{rightSlot}
: null}
+
+
+ );
}
async function askAgentToFix(
@@ -106,9 +176,19 @@ async function askAgentToFix(
function OrchestratorStatusStrip({
zones,
createdAt,
+ headline,
+ activityLabel,
+ activityColor,
+ branch,
+ pr,
}: {
zones: OrchestratorZones;
createdAt: string;
+ headline: string;
+ activityLabel: string;
+ activityColor: string;
+ branch: string | null;
+ pr: DashboardPR | null;
}) {
const [uptime, setUptime] = useState("");
@@ -137,52 +217,61 @@ function OrchestratorStatusStrip({
zones.merge + zones.respond + zones.review + zones.working + zones.pending + zones.done;
return (
-
-
- {/* Total count */}
-
-
- {total}
-
- agents
-
-
-
-
- {/* Per-zone pills */}
- {stats.length > 0 ? (
- stats.map((s) => (
-
-
- {s.value}
-
-
- {s.label}
+
+
+
+
+ {total}
+ agents
- ))
- ) : (
- no active agents
- )}
- {uptime && (
-
- up {uptime}
-
- )}
-
+
+
+ {/* Per-zone pills */}
+ {stats.length > 0 ? (
+ stats.map((s) => (
+
+
+ {s.value}
+
+
+ {s.label}
+
+
+ ))
+ ) : (
+
+ no active agents
+
+ )}
+
+ {uptime && (
+
+ up {uptime}
+
+ )}
+
+ }
+ />
);
}
@@ -201,11 +290,12 @@ export function SessionDetail({
label: session.activity ?? "unknown",
color: "var(--color-text-muted)",
};
+ const headline = getSessionHeadline(session);
const accentColor = "var(--color-accent)";
const terminalVariant = isOrchestrator ? "orchestrator" : "agent";
- const terminalHeight = isOrchestrator ? "calc(100vh - 240px)" : "max(440px, calc(100vh - 440px))";
+ const terminalHeight = isOrchestrator ? "clamp(560px, 76vh, 920px)" : "clamp(520px, 72vh, 860px)";
const isOpenCodeSession = session.metadata["agent"] === "opencode";
const opencodeSessionId =
typeof session.metadata["opencodeSessionId"] === "string" &&
@@ -217,233 +307,62 @@ export function SessionDetail({
: undefined;
return (
-
- {/* Nav bar — glass effect */}
-
-
- {/* Orchestrator status strip */}
+
{isOrchestrator && orchestratorZones && (
-
+
)}
-
- {/* ── Header card ─────────────────────────────────────────── */}
-
-
-
-
-
- {session.id}
-
- {/* Activity badge */}
-
-
-
- {activity.label}
-
-
-
-
- {session.summary && (
-
- {session.summary}
-
- )}
-
- {/* Meta chips */}
-
-
-
-
-
-
-
- {/* ── PR Card ─────────────────────────────────────────────── */}
- {pr &&
}
-
- {/* ── Terminal ─────────────────────────────────────────────── */}
-
-
-
+
+ {!isOrchestrator && (
+
-
- Terminal
-
-
-
-
+ )}
+
+
+
+ {pr ? (
+
+ ) : null}
+
);
}
-// ── Client-side timestamps ────────────────────────────────────────────
-
-function ClientTimestamps({
- status,
- createdAt,
- lastActivityAt,
-}: {
- status: string;
- createdAt: string;
- lastActivityAt: string;
-}) {
- const [created, setCreated] = useState
(null);
- const [lastActive, setLastActive] = useState(null);
-
- useEffect(() => {
- setCreated(relativeTime(createdAt));
- setLastActive(relativeTime(lastActivityAt));
- }, [createdAt, lastActivityAt]);
-
- return (
-
-
- {humanizeStatus(status)}
-
- {created && (
- <>
- ·
- created {created}
- >
- )}
- {lastActive && (
- <>
- ·
- active {lastActive}
- >
- )}
-
- );
-}
-
// ── PR Card ───────────────────────────────────────────────────────────
function PRCard({ pr, sessionId }: { pr: DashboardPR; sessionId: string }) {
@@ -527,7 +446,7 @@ function PRCard({ pr, sessionId }: { pr: DashboardPR; sessionId: string }) {
: "var(--color-border-default)";
return (
-
+
{/* Title row */}
·
Merged
@@ -567,7 +486,7 @@ function PRCard({ pr, sessionId }: { pr: DashboardPR; sessionId: string }) {
{/* Ready-to-merge banner */}
{allGreen ? (
-
+