Merge remote-tracking branch 'origin/main' into feat/windows-platform-adapter

This commit is contained in:
Priyanshu Choudhary 2026-04-24 02:45:44 +05:30
commit 2e9eaa4f8f
9 changed files with 268 additions and 436 deletions

2
.gitignore vendored
View File

@ -54,7 +54,6 @@ id_ed25519
# Development symlinks (created per-worktree, not committed)
.claude
.sisyphus
packages/web/agent-orchestrator.yaml
# Local agent orchestrator config (may contain secrets)
@ -68,4 +67,3 @@ agent-orchestrator.yaml
# OS-specific files
.DS_Store
Thumbs.db
.gstack/

View File

@ -1,5 +0,0 @@
# Agent configuration and tracking (personal, not for repo)
# Add these to .gitignore for ALL projects with agent configs
.claude/
.opencode/

View File

@ -549,6 +549,138 @@ describe("API Routes", () => {
enrichSpy.mockRestore();
vi.useRealTimers();
});
it("uses cache-first PR enrichment with live fallback for terminal PR states", async () => {
const terminalLifecycle = createInitialCanonicalLifecycle("worker", new Date());
terminalLifecycle.session.state = "terminated";
terminalLifecycle.session.reason = "user_killed";
terminalLifecycle.session.terminatedAt = terminalLifecycle.session.lastTransitionAt;
terminalLifecycle.runtime.state = "exited";
terminalLifecycle.runtime.reason = "process_exited";
terminalLifecycle.pr.state = "merged";
terminalLifecycle.pr.reason = "merged";
const sessionsWithPRs = [
makeSession({
id: "worker-live",
status: "pr_open",
activity: "idle",
pr: {
number: 201,
url: "https://github.com/acme/my-app/pull/201",
title: "Live PR",
owner: "acme",
repo: "my-app",
branch: "feat/live-pr",
baseBranch: "main",
isDraft: false,
},
}),
makeSession({
id: "worker-killed",
status: "killed",
activity: "exited",
lifecycle: terminalLifecycle,
pr: {
number: 202,
url: "https://github.com/acme/my-app/pull/202",
title: "Terminal PR",
owner: "acme",
repo: "my-app",
branch: "feat/terminal-pr",
baseBranch: "main",
isDraft: false,
},
}),
];
(mockSessionManager.listCached as ReturnType<typeof vi.fn>).mockResolvedValue(sessionsWithPRs);
const metadataSpy = vi
.spyOn(serialize, "enrichSessionsMetadata")
.mockResolvedValue(undefined);
const enrichSpy = vi
.spyOn(serialize, "enrichSessionPR")
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(false)
.mockResolvedValueOnce(true);
const res = await sessionsGET(makeRequest("http://localhost:3000/api/sessions"));
expect(res.status).toBe(200);
expect(enrichSpy).toHaveBeenCalledTimes(3);
expect(enrichSpy.mock.calls[0]).toEqual([
expect.objectContaining({ id: "worker-live" }),
expect.anything(),
sessionsWithPRs[0]!.pr,
]);
expect(enrichSpy.mock.calls[1]).toEqual([
expect.objectContaining({ id: "worker-killed" }),
expect.anything(),
sessionsWithPRs[1]!.pr,
{ cacheOnly: true },
]);
expect(enrichSpy.mock.calls[2]).toEqual([
expect.objectContaining({ id: "worker-killed" }),
expect.anything(),
sessionsWithPRs[1]!.pr,
]);
metadataSpy.mockRestore();
enrichSpy.mockRestore();
});
it("keeps live PR refreshes for killed sessions whose PR is still open", async () => {
const runtimeTerminalLifecycle = createInitialCanonicalLifecycle("worker", new Date());
runtimeTerminalLifecycle.session.state = "terminated";
runtimeTerminalLifecycle.session.reason = "user_killed";
runtimeTerminalLifecycle.session.terminatedAt = runtimeTerminalLifecycle.session.lastTransitionAt;
runtimeTerminalLifecycle.runtime.state = "missing";
runtimeTerminalLifecycle.runtime.reason = "process_missing";
runtimeTerminalLifecycle.pr.state = "open";
runtimeTerminalLifecycle.pr.reason = "in_progress";
const sessionWithOpenPR = [
makeSession({
id: "worker-open-pr",
status: "killed",
activity: "exited",
lifecycle: runtimeTerminalLifecycle,
pr: {
number: 203,
url: "https://github.com/acme/my-app/pull/203",
title: "Open PR on killed runtime",
owner: "acme",
repo: "my-app",
branch: "feat/open-pr-runtime-dead",
baseBranch: "main",
isDraft: false,
},
}),
];
(mockSessionManager.listCached as ReturnType<typeof vi.fn>).mockResolvedValue(sessionWithOpenPR);
const metadataSpy = vi
.spyOn(serialize, "enrichSessionsMetadata")
.mockResolvedValue(undefined);
const enrichSpy = vi
.spyOn(serialize, "enrichSessionPR")
.mockResolvedValue(true);
const res = await sessionsGET(makeRequest("http://localhost:3000/api/sessions"));
expect(res.status).toBe(200);
expect(enrichSpy).toHaveBeenCalledTimes(1);
expect(enrichSpy.mock.calls[0]).toEqual([
expect.objectContaining({ id: "worker-open-pr" }),
expect.anything(),
sessionWithOpenPR[0]!.pr,
]);
metadataSpy.mockRestore();
enrichSpy.mockRestore();
});
});
describe("GET /api/runtime/terminal", () => {

View File

@ -17,6 +17,11 @@ const METADATA_ENRICH_TIMEOUT_MS = 3_000;
const PR_ENRICH_TIMEOUT_MS = 4_000;
const PER_PR_ENRICH_TIMEOUT_MS = 1_500;
function hasTerminalPRState(session: Parameters<typeof isTerminalSession>[0]): boolean {
const prState = session.lifecycle?.pr.state;
return prState === "merged" || prState === "closed";
}
function compareOrchestratorRecency(a: { lastActivityAt?: Date | null; createdAt?: Date | null; id: string }, b: { lastActivityAt?: Date | null; createdAt?: Date | null; id: string }): number {
return (
(b.lastActivityAt?.getTime() ?? 0) - (a.lastActivityAt?.getTime() ?? 0) ||
@ -151,7 +156,8 @@ export async function GET(request: Request) {
for (let i = 0; i < workerSessions.length; i++) {
const core = workerSessions[i];
if (!core?.pr) continue;
const pr = core?.pr;
if (!pr) continue;
const project = resolveProject(core, config.projects);
const scm = getSCM(registry, project);
@ -159,7 +165,14 @@ export async function GET(request: Request) {
prEnrichPromises.push(
settlesWithin(
enrichSessionPR(dashboardSessions[i], scm, core.pr),
hasTerminalPRState(core)
? enrichSessionPR(dashboardSessions[i], scm, pr, { cacheOnly: true }).then(
(cached) =>
cached
? true
: enrichSessionPR(dashboardSessions[i], scm, pr),
)
: enrichSessionPR(dashboardSessions[i], scm, pr),
PER_PR_ENRICH_TIMEOUT_MS,
),
);

View File

@ -7103,6 +7103,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active {
display: inline-flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
/* Project name + pills: inline on desktop, stacked column on mobile */
@ -7180,4 +7181,35 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active {
padding: 1px 5px;
max-width: 90px;
}
.topbar-session-pills .topbar-zone-pill {
font-size: 9px;
padding: 1px 5px;
gap: 3px;
}
/* Hide zone pill labels on mobile — count + color is enough */
.topbar-session-pills .topbar-zone-pill__label {
display: none;
}
}
/* Orchestrator agent-zone pills compact topbar variant. Replaces the
stacked status strip that used to sit above the terminal. */
.topbar-zone-pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 500;
white-space: nowrap;
color: var(--color-text-secondary);
}
.topbar-zone-pill__value { font-weight: 600; font-variant-numeric: tabular-nums; }
.topbar-zone-pill__label { opacity: 0.85; }
.topbar-zone-pill--merge { background: color-mix(in srgb, var(--color-status-ready) 12%, transparent); color: var(--color-status-ready); }
.topbar-zone-pill--respond { background: color-mix(in srgb, var(--color-status-error) 12%, transparent); color: var(--color-status-error); }
.topbar-zone-pill--review { background: color-mix(in srgb, var(--color-accent-orange) 12%, transparent); color: var(--color-accent-orange); }
.topbar-zone-pill--working { background: color-mix(in srgb, var(--color-accent-blue) 12%, transparent); color: var(--color-accent-blue); }
.topbar-zone-pill--pending { background: color-mix(in srgb, var(--color-status-attention) 12%, transparent); color: var(--color-status-attention); }
.topbar-zone-pill--done { background: color-mix(in srgb, var(--color-text-tertiary) 14%, transparent); color: var(--color-text-tertiary); }

View File

@ -192,6 +192,7 @@ export function DirectTerminal({
const [fontSize, setFontSize] = useState(getStoredFontSize());
const followOutputRef = useRef(true);
const [followOutput, setFollowOutput] = useState(true);
const programmaticScrollRef = useRef(false);
// Update URL when fullscreen changes
useEffect(() => {
@ -385,16 +386,16 @@ export function DirectTerminal({
fontsFace!.addEventListener("loadingdone", handleFontsLoadingDone);
}
// Grab viewport element for manual follow-output scroll
const viewport = terminal.element?.querySelector<HTMLElement>(".xterm-viewport") ?? null;
// Attach touch scroll for mobile — disable follow-output while user is scrolling
// Attach touch scroll for mobile — disable follow-output while user is scrolling.
// Note: onScrollTowardLatest intentionally does NOT hide the button. In normal
// buffer, terminal.onScroll fires after scrollLines() and decides based on real
// position. In alternate buffer (tmux), there's no way to detect when the user
// has truly returned to the live tail, so the button stays visible until clicked.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cleanupTouchScroll = attachTouchScroll(terminal as any, (data) => {
writeTerminal(sessionId, data);
}, {
onScrollAway: () => { followOutputRef.current = false; setFollowOutput(false); },
onScrollTowardLatest: () => { followOutputRef.current = true; setFollowOutput(true); },
});
// Set up ResizeObserver to handle flex layout changes
@ -487,31 +488,31 @@ export function DirectTerminal({
}
} else {
terminal.write(data);
if (followOutputRef.current && viewport) {
programmaticScroll = true;
viewport.scrollTop = viewport.scrollHeight;
if (followOutputRef.current) {
programmaticScrollRef.current = true;
terminal.scrollToBottom();
}
}
});
// Track whether our own write()-driven scrollTop change triggered this event
let programmaticScroll = false;
const handleViewportScroll = () => {
if (!viewport) return;
if (programmaticScroll) {
programmaticScroll = false;
// Use xterm's onScroll event (fires with new viewportY) instead of a DOM
// scroll listener — xterm v6 may update scrollTop via RAF, making DOM
// "scroll" events unreliable for detecting user-initiated scrolls.
const scrollDisposable = terminal.onScroll(() => {
if (programmaticScrollRef.current) {
programmaticScrollRef.current = false;
return;
}
const distFromBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
if (distFromBottom < 24) {
const buf = terminal.buffer.active;
const atBottom = buf.viewportY + terminal.rows >= buf.length;
if (atBottom) {
followOutputRef.current = true;
setFollowOutput(true);
} else {
followOutputRef.current = false;
setFollowOutput(false);
}
};
viewport?.addEventListener("scroll", handleViewportScroll, { passive: true });
});
// Handle window resize
const handleResize = () => {
@ -542,7 +543,7 @@ export function DirectTerminal({
if (fontsListenerAttached && fontsFace) {
fontsFace.removeEventListener("loadingdone", handleFontsLoadingDone);
}
viewport?.removeEventListener("scroll", handleViewportScroll);
scrollDisposable.dispose();
inputDisposable?.dispose();
inputDisposable = null;
unsubscribe?.();
@ -924,13 +925,21 @@ export function DirectTerminal({
<button
type="button"
onClick={() => {
followOutputRef.current = true;
setFollowOutput(true);
const t = terminalInstance.current;
if (t) {
const vp = t.element?.querySelector<HTMLElement>(".xterm-viewport");
if (vp) vp.scrollTop = vp.scrollHeight;
if (t.buffer.active.type === "normal") {
// Normal buffer: scrollback exists in xterm, use its API.
programmaticScrollRef.current = true;
t.scrollToBottom();
} else {
// Alternate buffer (tmux/vim): xterm has no scrollback to scroll
// to. The user is in tmux copy-mode (entered by attachTouchScroll
// on swipe). Send 'q' to exit copy-mode and return to live tail.
writeTerminal(sessionId, "q");
}
}
followOutputRef.current = true;
setFollowOutput(true);
}}
className="absolute bottom-3 right-3 z-20 flex h-8 w-8 items-center justify-center rounded-full border border-[var(--color-border-default)] bg-[var(--color-bg-elevated)] text-[var(--color-text-primary)] shadow-md active:scale-95"
aria-label="Jump to latest"

View File

@ -1,6 +1,6 @@
"use client";
import { useState, useEffect, useRef, useMemo, useCallback, type ReactNode } from "react";
import { useState, useEffect, useRef, useMemo, useCallback } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useMediaQuery, MOBILE_BREAKPOINT } from "@/hooks/useMediaQuery";
import {
@ -91,112 +91,32 @@ function normalizeActivityLabelForClass(activityLabel: string): string {
return activityLabel.toLowerCase().replace(/\s+/g, "-");
}
function OrchestratorTopStrip({
headline,
crumbId,
activityLabel,
activityColor,
branch,
pr,
crumbHref,
crumbLabel,
rightSlot,
}: {
headline: string;
crumbId: string;
activityLabel: string;
activityColor: string;
branch: string | null;
pr: DashboardPR | null;
crumbHref: string;
crumbLabel: string;
rightSlot?: ReactNode;
}) {
/**
* Compact agent-zone breakdown for orchestrator sessions, rendered inline in
* the topbar pill row. Replaces the previous stacked status strip that sat
* above the terminal.
*/
function OrchestratorZonePills({ zones }: { zones: OrchestratorZones }) {
const stats: Array<{ value: number; label: string; toneClass: string }> = [
{ value: zones.merge, label: "merge", toneClass: "topbar-zone-pill--merge" },
{ value: zones.respond, label: "respond", toneClass: "topbar-zone-pill--respond" },
{ value: zones.review, label: "review", toneClass: "topbar-zone-pill--review" },
{ value: zones.working, label: "working", toneClass: "topbar-zone-pill--working" },
{ value: zones.pending, label: "pending", toneClass: "topbar-zone-pill--pending" },
{ value: zones.done, label: "done", toneClass: "topbar-zone-pill--done" },
].filter((s) => s.value > 0);
if (stats.length === 0) return null;
return (
<div className="session-detail-top-strip">
{/* Breadcrumbs */}
<div className="session-detail-crumbs">
<a
href={crumbHref}
className="session-detail-crumb-back"
>
<svg
className="h-3 w-3 opacity-60"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
viewBox="0 0 24 24"
>
<path d="M15 18l-6-6 6-6" />
</svg>
{crumbLabel}
</a>
<span className="session-detail-crumb-sep">/</span>
<span className="session-detail-crumb-id">{crumbId}</span>
<span className="session-detail-mode-badge">orchestrator</span>
</div>
{/* Identity strip */}
<div className="session-detail-identity">
<div className="session-detail-identity__info">
<h1 className="session-detail-identity__title">
{headline}
</h1>
<div className="session-detail-identity__pills">
<div
className="session-detail-status-pill"
>
<span
className="session-detail-status-pill__dot"
style={{ background: activityColor }}
/>
<span className="session-detail-status-pill__label">
{activityLabel}
</span>
</div>
{branch ? (
pr ? (
<a
href={buildGitHubBranchUrl(pr)}
target="_blank"
rel="noopener noreferrer"
className="session-detail-link-pill session-detail-link-pill--branch session-detail-link-pill--branch-link hover:no-underline"
>
{branch}
</a>
) : (
<span className="session-detail-link-pill session-detail-link-pill--branch">
{branch}
</span>
)
) : null}
{pr ? (
<a
href={pr.url}
target="_blank"
rel="noopener noreferrer"
className="session-detail-link-pill session-detail-link-pill--pr hover:no-underline"
>
PR #{pr.number}
</a>
) : null}
{pr && (pr.additions > 0 || pr.deletions > 0) ? (
<span className="session-detail-link-pill session-detail-link-pill--diff">
<span className="session-detail-diff--add">+{pr.additions}</span>
{" "}
<span className="session-detail-diff--del">-{pr.deletions}</span>
</span>
) : null}
</div>
</div>
{rightSlot ? (
<div className="session-detail-identity__actions session-detail-identity__actions--custom">
{rightSlot}
</div>
) : null}
</div>
</div>
<>
{stats.map((s) => (
<span key={s.label} className={cn("topbar-zone-pill", s.toneClass)}>
<span className="topbar-zone-pill__value">{s.value}</span>
<span className="topbar-zone-pill__label">{s.label}</span>
</span>
))}
</>
);
}
@ -222,147 +142,6 @@ async function askAgentToFix(
}
}
// ── Orchestrator status strip ─────────────────────────────────────────
function _OrchestratorStatusStrip({
zones,
createdAt,
headline,
activityLabel,
activityColor,
branch,
pr,
crumbHref,
crumbLabel,
}: {
zones: OrchestratorZones;
createdAt: string;
headline: string;
activityLabel: string;
activityColor: string;
branch: string | null;
pr: DashboardPR | null;
crumbHref: string;
crumbLabel: string;
}) {
const [uptime, setUptime] = useState<string>("");
useEffect(() => {
const compute = () => {
const diff = Date.now() - new Date(createdAt).getTime();
const h = Math.floor(diff / 3_600_000);
const m = Math.floor((diff % 3_600_000) / 60_000);
setUptime(h > 0 ? `${h}h ${m}m` : `${m}m`);
};
compute();
const id = setInterval(compute, 30_000);
return () => clearInterval(id);
}, [createdAt]);
const stats: Array<{ value: number; label: string; color: string; bg: string }> = [
{
value: zones.merge,
label: "merge-ready",
color: "var(--color-status-ready)",
bg: "color-mix(in srgb, var(--color-status-ready) 10%, transparent)",
},
{
value: zones.respond,
label: "responding",
color: "var(--color-status-error)",
bg: "color-mix(in srgb, var(--color-status-error) 10%, transparent)",
},
{
value: zones.review,
label: "review",
color: "var(--color-accent-orange)",
bg: "color-mix(in srgb, var(--color-accent-orange) 10%, transparent)",
},
{
value: zones.working,
label: "working",
color: "var(--color-accent-blue)",
bg: "color-mix(in srgb, var(--color-accent-blue) 10%, transparent)",
},
{
value: zones.pending,
label: "pending",
color: "var(--color-status-attention)",
bg: "color-mix(in srgb, var(--color-status-attention) 10%, transparent)",
},
{
value: zones.done,
label: "done",
color: "var(--color-text-tertiary)",
bg: "color-mix(in srgb, var(--color-text-tertiary) 14%, transparent)",
},
].filter((s) => s.value > 0);
const total =
zones.merge + zones.respond + zones.review + zones.working + zones.pending + zones.done;
return (
<div className="mx-auto max-w-[1180px] px-5 pt-5 lg:px-8">
<OrchestratorTopStrip
headline={headline}
crumbId={headline}
activityLabel={activityLabel}
activityColor={activityColor}
branch={branch}
pr={pr}
crumbHref={crumbHref}
crumbLabel={crumbLabel}
rightSlot={
<div className="flex flex-wrap items-center gap-3 lg:justify-end">
<div className="flex items-baseline gap-1.5 mr-2">
<span className="text-[22px] font-bold leading-none tabular-nums text-[var(--color-text-primary)]">
{total}
</span>
<span className="text-[11px] text-[var(--color-text-tertiary)]">agents</span>
</div>
<div className="h-5 w-px bg-[var(--color-border-subtle)] mr-1" />
{/* Per-zone pills */}
{stats.length > 0 ? (
stats.map((s) => (
<div
key={s.label}
className="flex items-center gap-1.5 px-2.5 py-1"
style={{ background: s.bg }}
>
<span
className="text-[15px] font-bold leading-none tabular-nums"
style={{ color: s.color }}
>
{s.value}
</span>
<span
className="text-[10px] font-medium"
style={{ color: s.color, opacity: 0.8 }}
>
{s.label}
</span>
</div>
))
) : (
<span className="text-[12px] text-[var(--color-text-tertiary)]">
no active agents
</span>
)}
{uptime && (
<span className="ml-auto font-[var(--font-mono)] text-[11px] text-[var(--color-text-tertiary)]">
up {uptime}
</span>
)}
</div>
}
/>
</div>
);
}
// ── Main component ────────────────────────────────────────────────────
export function SessionDetail({
@ -404,8 +183,6 @@ export function SessionDetail({
? `/exit\nopencode --session ${opencodeSessionId}\n`
: undefined;
const dashboardHref = session.projectId ? projectDashboardPath(session.projectId) : "/";
const crumbHref = dashboardHref;
const crumbLabel = "Dashboard";
const handleKill = useCallback(async () => {
try {
@ -525,44 +302,42 @@ export function SessionDetail({
{showHeaderProjectLabel && (
<span className="dashboard-app-header__project">{headerProjectLabel}</span>
)}
{!isOrchestrator && (
<span className="dashboard-app-header__session-id topbar-mobile-only">
{session.id}
</span>
<span className="dashboard-app-header__session-id topbar-mobile-only">
{session.id}
</span>
{isOrchestrator && (
<span className="session-detail-mode-badge">orchestrator</span>
)}
</div>
{!isOrchestrator && (
<div className="topbar-session-pills">
<div className={cn("topbar-status-pill", `topbar-status-pill--${normalizeActivityLabelForClass(activity.label)}`)}>
<span className="topbar-status-pill__dot" style={{ background: activity.color }} />
<span className="topbar-status-pill__label">{activity.label}</span>
</div>
{session.branch ? (
pr ? (
<a
href={buildGitHubBranchUrl(pr)}
target="_blank"
rel="noopener noreferrer"
className="topbar-branch-pill topbar-branch-pill--link"
>
{session.branch}
</a>
) : (
<span className="topbar-branch-pill">{session.branch}</span>
)
) : null}
<div className="topbar-session-pills">
<div className={cn("topbar-status-pill", `topbar-status-pill--${normalizeActivityLabelForClass(activity.label)}`)}>
<span className="topbar-status-pill__dot" style={{ background: activity.color }} />
<span className="topbar-status-pill__label">{activity.label}</span>
</div>
)}
{session.branch ? (
pr ? (
<a
href={buildGitHubBranchUrl(pr)}
target="_blank"
rel="noopener noreferrer"
className="topbar-branch-pill topbar-branch-pill--link"
>
{session.branch}
</a>
) : (
<span className="topbar-branch-pill">{session.branch}</span>
)
) : null}
{isOrchestrator && orchestratorZones ? (
<OrchestratorZonePills zones={orchestratorZones} />
) : null}
</div>
</div>
{/* Desktop-only session title + session id.
On mobile the session id lives next to the project name (above). */}
{!isOrchestrator && (
<>
<span className="dashboard-app-header__sep topbar-desktop-only" aria-hidden="true" />
<span className="dashboard-app-header__session-title topbar-desktop-only">{headline}</span>
<span className="dashboard-app-header__session-id topbar-desktop-only">{session.id}</span>
</>
)}
<span className="dashboard-app-header__sep topbar-desktop-only" aria-hidden="true" />
<span className="dashboard-app-header__session-title topbar-desktop-only">{headline}</span>
<span className="dashboard-app-header__session-id topbar-desktop-only">{session.id}</span>
<div className="dashboard-app-header__spacer" />
<div className="dashboard-app-header__actions">
{pr ? (
@ -626,7 +401,7 @@ export function SessionDetail({
{!isOrchestrator && orchestratorHref ? (
<a
href={orchestratorHref}
className="dashboard-app-btn dashboard-app-btn--amber topbar-desktop-only"
className="dashboard-app-btn dashboard-app-btn--amber"
aria-label="Orchestrator"
>
<svg
@ -675,21 +450,6 @@ export function SessionDetail({
<div className="dashboard-main dashboard-main--desktop">
<main className="session-detail-page flex-1 min-h-0 flex flex-col bg-[var(--color-bg-base)]">
{/* Orchestrator status strip — rendered above terminal only on orchestrator pages */}
{isOrchestrator && orchestratorZones && (
<_OrchestratorStatusStrip
zones={orchestratorZones}
createdAt={session.createdAt}
headline={headline}
activityLabel={activity.label}
activityColor={activity.color}
branch={session.branch}
pr={pr}
crumbHref={crumbHref}
crumbLabel={crumbLabel}
/>
)}
{/* Terminal — fills all remaining height */}
<div className="flex-1 min-h-0 flex flex-col">
{!showTerminal ? (

View File

@ -1,50 +0,0 @@
dataDir: ~/.agent-orchestrator
worktreeDir: ~/.worktrees
port: 3000
defaults:
runtime: tmux
agent: claude-code
workspace: worktree
notifiers:
- desktop
projects:
ao-35:
repo: ComposioHQ/agent-orchestrator
path: /Users/equinox/.worktrees/ao/ao/ao-35
defaultBranch: feat/seamless-onboarding
agentRules: >-
Always run tests before pushing.
Use conventional commits (feat:, fix:, chore:, docs:, refactor:, test:).
Link issue numbers in commit messages.
Write clear commit messages that explain WHY, not just WHAT.
Use modern JavaScript (ES6+).
Prefer const over let, never use var.
Use async/await over raw Promises.
Use template literals for string interpolation.
This is a pnpm monorepo with workspaces.
Run commands from root with pnpm -r (recursive) or from specific package
directories.
Before pushing: pnpm build && pnpm typecheck && pnpm lint && pnpm test.
Always build packages before running dependent packages.
Before pushing, run these commands:
- pnpm build (build all packages)
- pnpm typecheck (type check all packages)
- pnpm lint (or pnpm lint:fix to auto-fix)

View File

@ -1,57 +0,0 @@
dataDir: ~/.agent-orchestrator
worktreeDir: ~/.worktrees
port: 3000
defaults:
runtime: tmux
agent: claude-code
workspace: worktree
notifiers:
- desktop
projects:
ao-35:
repo: ComposioHQ/agent-orchestrator
path: /Users/equinox/.worktrees/ao/ao/ao-35
defaultBranch: feat/seamless-onboarding
agentRules: >-
Always run tests before pushing.
Use conventional commits (feat:, fix:, chore:, docs:, refactor:, test:).
Link issue numbers in commit messages.
Write clear commit messages that explain WHY, not just WHAT.
Use TypeScript strict mode.
Use ESM modules with .js extensions in imports (e.g., import { foo } from
"./bar.js").
Use node: prefix for built-in modules (e.g., import { readFile } from
"node:fs").
Prefer const over let, never use var.
Use type imports for type-only imports: import type { Foo } from
"./bar.js".
No any types - use unknown with type guards instead.
This is a pnpm monorepo with workspaces.
Run commands from root with pnpm -r (recursive) or from specific package
directories.
Before pushing: pnpm build && pnpm typecheck && pnpm lint && pnpm test.
Always build packages before running dependent packages.
Before pushing, run these commands:
- pnpm build (build all packages)
- pnpm typecheck (type check all packages)
- pnpm lint (or pnpm lint:fix to auto-fix)