diff --git a/.changeset/late-lions-share.md b/.changeset/late-lions-share.md index 3a7a0db3a..26f584514 100644 --- a/.changeset/late-lions-share.md +++ b/.changeset/late-lions-share.md @@ -4,3 +4,8 @@ --- Allow workers to report non-terminal PR workflow events like `pr-created`, `draft-pr-created`, and `ready-for-review` with optional PR URL/number metadata, while keeping merged and closed PR state SCM-owned. + +**Migration:** `Session` now carries canonical lifecycle truth in `session.lifecycle` +and explicit activity-evidence metadata in `session.activitySignal`. Third-party +callers that construct `Session` objects directly must populate those fields or +route through the core session helpers that synthesize them. diff --git a/.changeset/pr1300-review-followups.md b/.changeset/pr1300-review-followups.md index cf4a1a7f5..b5022bc84 100644 --- a/.changeset/pr1300-review-followups.md +++ b/.changeset/pr1300-review-followups.md @@ -4,4 +4,4 @@ "@aoagents/ao-web": patch --- -Tighten the session lifecycle review follow-ups by debouncing report-watcher reactions, restoring the shared Geist/JetBrains font setup, wiring recovery validation to real agent activity probes, adding direct coverage for `ao report`, activity-signal classification, and dashboard lifecycle audit panels, and fixing the remaining lifecycle-state regressions around legacy merged-session rehydration and malformed canonical payload parsing. +Tighten the session lifecycle review follow-ups by debouncing report-watcher reactions, restoring the shared Geist/JetBrains font setup, wiring recovery validation to real agent activity probes, adding direct coverage for `ao report`, activity-signal classification, and dashboard lifecycle audit panels, fixing the remaining lifecycle-state regressions around legacy merged-session rehydration and malformed canonical payload parsing, making agent-report metadata writes atomic, persisting canonical payloads for legacy sessions on read, stabilizing detecting evidence hashes, and removing the remaining inline-style cleanup debt from the session detail view. diff --git a/packages/core/src/__tests__/lifecycle-status-decisions.test.ts b/packages/core/src/__tests__/lifecycle-status-decisions.test.ts index b8a823e69..9aa7727d2 100644 --- a/packages/core/src/__tests__/lifecycle-status-decisions.test.ts +++ b/packages/core/src/__tests__/lifecycle-status-decisions.test.ts @@ -24,6 +24,17 @@ describe("hashEvidence", () => { const hash2 = hashEvidence("evidence B"); expect(hash1).not.toBe(hash2); }); + + it("ignores activity labels and timestamps when hashing probe evidence", () => { + const active = hashEvidence( + "signal_disagreement runtime=alive process=unknown activity_signal=valid via_native activity=active at=2026-04-18T10:00:00.000Z", + ); + const blocked = hashEvidence( + "signal_disagreement runtime=alive process=unknown activity_signal=valid via_native activity=blocked at=2026-04-18T10:01:00.000Z", + ); + + expect(active).toBe(blocked); + }); }); describe("isDetectingTimedOut", () => { diff --git a/packages/core/src/__tests__/session-manager/query.test.ts b/packages/core/src/__tests__/session-manager/query.test.ts index 0b9c397a2..cb65a951a 100644 --- a/packages/core/src/__tests__/session-manager/query.test.ts +++ b/packages/core/src/__tests__/session-manager/query.test.ts @@ -88,6 +88,34 @@ describe("list", () => { expect(repaired!["status"]).toBe("working"); }); + it("persists canonical lifecycle payloads for legacy session metadata on read", async () => { + writeMetadata(sessionsDir, "app-legacy", { + worktree: "/tmp/legacy", + branch: "feat/legacy", + status: "working", + project: "my-app", + createdAt: "2025-01-01T00:00:00.000Z", + }); + + const oldTime = new Date("2026-01-02T00:00:00.000Z"); + utimesSync(join(sessionsDir, "app-legacy"), oldTime, oldTime); + + const sm = createSessionManager({ config, registry: mockRegistry }); + const sessions = await sm.list("my-app"); + const legacy = sessions.find((session) => session.id === "app-legacy"); + + expect(legacy).toBeDefined(); + expect(legacy!.lastActivityAt.getTime()).toBe(oldTime.getTime()); + + const repaired = readMetadataRaw(sessionsDir, "app-legacy"); + expect(repaired?.["stateVersion"]).toBe("2"); + expect(repaired?.["statePayload"]).toBeTruthy(); + + const payload = JSON.parse(repaired!["statePayload"]); + expect(payload.session.startedAt).toBe("2025-01-01T00:00:00.000Z"); + expect(payload.session.lastTransitionAt).toBe("2025-01-01T00:00:00.000Z"); + }); + it("filters by project ID", async () => { // In hash-based architecture, each project has its own directory // so filtering is implicit. This test verifies list(projectId) only diff --git a/packages/core/src/agent-report.ts b/packages/core/src/agent-report.ts index 747930be9..ffb94f4fc 100644 --- a/packages/core/src/agent-report.ts +++ b/packages/core/src/agent-report.ts @@ -26,8 +26,8 @@ import type { SessionId, SessionStatus, } from "./types.js"; -import { updateCanonicalLifecycle, updateMetadata, readMetadataRaw } from "./metadata.js"; -import { deriveLegacyStatus } from "./lifecycle-state.js"; +import { mutateMetadata, readMetadataRaw } from "./metadata.js"; +import { buildLifecycleMetadataPatch, cloneLifecycle, deriveLegacyStatus, parseCanonicalLifecycle } from "./lifecycle-state.js"; import { parsePrFromUrl } from "./utils/pr.js"; import { validateStatus } from "./utils/validation.js"; @@ -402,7 +402,13 @@ export function applyAgentReport( let legacyStatus: SessionStatus | null = null; let previousLegacyStatus: SessionStatus | null = null; - const nextLifecycle = updateCanonicalLifecycle(dataDir, sessionId, (current) => { + const nextMetadata = mutateMetadata(dataDir, sessionId, (existing) => { + const current = cloneLifecycle( + parseCanonicalLifecycle(existing, { + sessionId, + status: validateStatus(existing["status"]), + }), + ); previousLegacyStatus = deriveLegacyStatus(current, validateStatus(raw["status"])); before = buildAuditSnapshot(current, previousLegacyStatus); const validation = validateAgentReportTransition(current, input.state); @@ -454,36 +460,42 @@ export function applyAgentReport( current.session.startedAt = now; } legacyStatus = deriveLegacyStatus(current, previousLegacyStatus); - return current; + const next = { ...existing }; + Object.assign( + next, + buildLifecycleMetadataPatch(current, previousLegacyStatus), + { + [AGENT_REPORT_METADATA_KEYS.STATE]: input.state, + [AGENT_REPORT_METADATA_KEYS.AT]: now, + }, + ); + if (trimmedNote) { + next[AGENT_REPORT_METADATA_KEYS.NOTE] = trimmedNote; + } else { + next[AGENT_REPORT_METADATA_KEYS.NOTE] = ""; + } + if (isPRWorkflowReport(input.state)) { + if (trimmedPrUrl) { + next[AGENT_REPORT_METADATA_KEYS.PR_URL] = trimmedPrUrl; + } + if (prNumber !== undefined) { + next[AGENT_REPORT_METADATA_KEYS.PR_NUMBER] = String(prNumber); + } + if (prIsDraft !== undefined) { + next[AGENT_REPORT_METADATA_KEYS.PR_IS_DRAFT] = prIsDraft ? "true" : "false"; + } + } + return next; }); - if (!nextLifecycle || !before || !previousState || !nextState || !legacyStatus) { + if (!nextMetadata || !before || !previousState || !nextState || !legacyStatus) { throw new Error(`Failed to apply agent report for session ${sessionId}`); } - // Persist report metadata alongside the lifecycle patch. - const metadataUpdates: Record = { - [AGENT_REPORT_METADATA_KEYS.STATE]: input.state, - [AGENT_REPORT_METADATA_KEYS.AT]: now, - }; - if (trimmedNote) { - metadataUpdates[AGENT_REPORT_METADATA_KEYS.NOTE] = trimmedNote; - } else { - // Clear stale notes from previous reports so they don't mislead humans. - metadataUpdates[AGENT_REPORT_METADATA_KEYS.NOTE] = ""; - } - if (isPRWorkflowReport(input.state)) { - if (trimmedPrUrl) { - metadataUpdates[AGENT_REPORT_METADATA_KEYS.PR_URL] = trimmedPrUrl; - } - if (prNumber !== undefined) { - metadataUpdates[AGENT_REPORT_METADATA_KEYS.PR_NUMBER] = String(prNumber); - } - if (prIsDraft !== undefined) { - metadataUpdates[AGENT_REPORT_METADATA_KEYS.PR_IS_DRAFT] = prIsDraft ? "true" : "false"; - } - } - updateMetadata(dataDir, sessionId, metadataUpdates); + const nextLifecycle = parseCanonicalLifecycle(nextMetadata, { + sessionId, + status: validateStatus(nextMetadata["status"]), + }); const after = buildAuditSnapshot(nextLifecycle, legacyStatus); const auditEntry: AgentReportAuditEntry = { diff --git a/packages/core/src/lifecycle-manager.ts b/packages/core/src/lifecycle-manager.ts index b11844016..7bb09f4fc 100644 --- a/packages/core/src/lifecycle-manager.ts +++ b/packages/core/src/lifecycle-manager.ts @@ -568,8 +568,17 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan if (terminalOutput) { await agent.recordActivity(session, terminalOutput); } - } catch { - // Best effort only. + } catch (error) { + observer?.recordOperation?.({ + metric: "lifecycle_poll", + operation: "activity.record", + outcome: "failure", + correlationId: createCorrelationId("lifecycle-poll"), + projectId: session.projectId, + sessionId: session.id, + reason: error instanceof Error ? error.message : String(error), + level: "warn", + }); } } @@ -716,8 +725,17 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan const sessionsDir = getSessionsDir(config.configPath, project.path); updateMetadata(sessionsDir, session.id, { pr: detectedPR.url }); } - } catch { - // Retry next poll. + } catch (error) { + observer?.recordOperation?.({ + metric: "lifecycle_poll", + operation: "scm.detect_pr", + outcome: "failure", + correlationId: createCorrelationId("lifecycle-poll"), + projectId: session.projectId, + sessionId: session.id, + reason: error instanceof Error ? error.message : String(error), + level: "warn", + }); } } @@ -789,8 +807,17 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan activityEvidence, }), ); - } catch { - // Keep current status on SCM failure. + } catch (error) { + observer?.recordOperation?.({ + metric: "lifecycle_poll", + operation: "scm.poll_pr", + outcome: "failure", + correlationId: createCorrelationId("lifecycle-poll"), + projectId: session.projectId, + sessionId: session.id, + reason: error instanceof Error ? error.message : String(error), + level: "warn", + }); } } diff --git a/packages/core/src/lifecycle-status-decisions.ts b/packages/core/src/lifecycle-status-decisions.ts index bea13bf47..24ba6cdac 100644 --- a/packages/core/src/lifecycle-status-decisions.ts +++ b/packages/core/src/lifecycle-status-decisions.ts @@ -43,8 +43,18 @@ interface LifecycleDecision { * Create a short hash of evidence string for detecting unchanged evidence. * Used to prevent counter reset when the same weak evidence re-presents. */ +function normalizeEvidenceForHash(evidence: string): string { + return evidence + .replace(/\sactivity=[^\s]+/g, "") + .replace(/\sat=[^\s]+/g, "") + .trim(); +} + export function hashEvidence(evidence: string): string { - return createHash("sha256").update(evidence).digest("hex").slice(0, 12); + return createHash("sha256") + .update(normalizeEvidenceForHash(evidence)) + .digest("hex") + .slice(0, 12); } interface OpenPRDecisionInput { diff --git a/packages/core/src/metadata.ts b/packages/core/src/metadata.ts index 5946719c2..4214b7a56 100644 --- a/packages/core/src/metadata.ts +++ b/packages/core/src/metadata.ts @@ -169,26 +169,50 @@ export function updateMetadata( sessionId: SessionId, updates: Partial>, ): void { + mutateMetadata(dataDir, sessionId, (existing) => { + return applyMetadataUpdates(existing, updates); + }, { createIfMissing: true }); +} + +function applyMetadataUpdates( + existing: Record, + updates: Partial>, +): Record { + let next = { ...existing }; + // Merge updates — remove keys set to empty string + for (const [key, value] of Object.entries(updates)) { + if (value === undefined) continue; + if (value === "") { + const { [key]: _removed, ...rest } = next; + void _removed; + next = rest; + } else { + next[key] = value; + } + } + return next; +} + +export function mutateMetadata( + dataDir: string, + sessionId: SessionId, + updater: (existing: Record) => Record, + options: { createIfMissing?: boolean } = {}, +): Record | null { const path = metadataPath(dataDir, sessionId); let existing: Record = {}; if (existsSync(path)) { existing = parseKeyValueContent(readFileSync(path, "utf-8")); + } else if (!options.createIfMissing) { + return null; } - // Merge updates — remove keys set to empty string - for (const [key, value] of Object.entries(updates)) { - if (value === undefined) continue; - if (value === "") { - const { [key]: _, ...rest } = existing; - existing = rest; - } else { - existing[key] = value; - } - } + const next = updater({ ...existing }); mkdirSync(dirname(path), { recursive: true }); - atomicWriteFileSync(path, serializeMetadata(existing)); + atomicWriteFileSync(path, serializeMetadata(next)); + return next; } export function readCanonicalLifecycle( diff --git a/packages/core/src/session-manager.ts b/packages/core/src/session-manager.ts index 519caeb5a..4f725b8a4 100644 --- a/packages/core/src/session-manager.ts +++ b/packages/core/src/session-manager.ts @@ -494,6 +494,24 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM const duplicatePRAttachments = new Map(); for (const record of repaired) { + if (record.raw["stateVersion"] !== "2" || !record.raw["statePayload"]) { + const lifecycle = cloneLifecycle( + parseCanonicalLifecycle(record.raw, { + sessionId: record.sessionName, + status: validateStatus(record.raw["status"]), + createdAt: record.raw["createdAt"] ? new Date(record.raw["createdAt"]) : undefined, + }), + ); + const canonicalUpdates = lifecycleMetadataUpdates(record.raw, lifecycle); + updateMetadataPreservingMtime( + sessionsDir, + record.sessionName, + canonicalUpdates, + record.modifiedAt, + ); + record.raw = applyMetadataUpdatesToRaw(record.raw, canonicalUpdates); + } + if (isOrchestratorSessionRecord(record.sessionName, record.raw, sessionPrefix)) { record.raw = repairSingleSessionMetadataOnRead(sessionsDir, record, sessionPrefix).raw; continue; diff --git a/packages/plugins/scm-github/test/index.test.ts b/packages/plugins/scm-github/test/index.test.ts index 5c32d4021..0d0cf4492 100644 --- a/packages/plugins/scm-github/test/index.test.ts +++ b/packages/plugins/scm-github/test/index.test.ts @@ -428,6 +428,12 @@ describe("scm-github plugin", () => { expect(ghMock).not.toHaveBeenCalled(); }); + it("returns null when project has no repo configured", async () => { + const result = await scm.detectPR(makeSession(), { ...project, repo: undefined }); + expect(result).toBeNull(); + expect(ghMock).not.toHaveBeenCalled(); + }); + it("returns null on gh CLI error", async () => { mockGhError("gh: not found"); const result = await scm.detectPR(makeSession(), project); diff --git a/packages/plugins/scm-gitlab/test/index.test.ts b/packages/plugins/scm-gitlab/test/index.test.ts index 1d7da50da..601553b0c 100644 --- a/packages/plugins/scm-gitlab/test/index.test.ts +++ b/packages/plugins/scm-gitlab/test/index.test.ts @@ -356,6 +356,12 @@ describe("scm-gitlab plugin", () => { expect(glabMock).not.toHaveBeenCalled(); }); + it("returns null when project has no repo configured", async () => { + const result = await scm.detectPR(makeSession(), { ...project, repo: undefined }); + expect(result).toBeNull(); + expect(glabMock).not.toHaveBeenCalled(); + }); + it("returns null and warns on glab CLI error", async () => { mockGlabError("glab: not found"); const result = await scm.detectPR(makeSession(), project); diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css index 0039422e2..59711e6fa 100644 --- a/packages/web/src/app/globals.css +++ b/packages/web/src/app/globals.css @@ -1841,6 +1841,37 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { height: 6px; border-radius: 999px; flex-shrink: 0; + background: currentColor; +} + +.session-detail-tone--working { + color: var(--color-status-working); + background: var(--color-status-working); +} + +.session-detail-tone--ready { + color: var(--color-status-ready); + background: var(--color-status-ready); +} + +.session-detail-tone--attention { + color: var(--color-status-attention); + background: var(--color-status-attention); +} + +.session-detail-tone--error { + color: var(--color-status-error); + background: var(--color-status-error); +} + +.session-detail-tone--muted { + color: var(--color-text-muted); + background: var(--color-text-muted); +} + +.session-detail-tone--accent { + color: var(--color-accent); + background: var(--color-accent); } .session-detail-status-pill--active { @@ -1939,6 +1970,69 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { background: color-mix(in srgb, var(--color-bg-elevated) 92%, transparent); } +.session-detail-height--worker { + height: clamp(380px, 48vh, 560px); +} + +.session-detail-height--orchestrator { + height: clamp(400px, 52vh, 620px); +} + +.session-detail-height--full { + height: 100%; +} + +.session-detail-zone-pill { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 999px; +} + +.session-detail-zone-pill__value { + font-size: 15px; + font-weight: 700; + line-height: 1; + font-variant-numeric: tabular-nums; +} + +.session-detail-zone-pill__label { + font-size: 10px; + font-weight: 500; + opacity: 0.8; +} + +.session-detail-zone-pill--merge { + color: var(--color-status-ready); + background: color-mix(in srgb, var(--color-status-ready) 10%, transparent); +} + +.session-detail-zone-pill--respond { + color: var(--color-status-error); + background: color-mix(in srgb, var(--color-status-error) 10%, transparent); +} + +.session-detail-zone-pill--review { + color: var(--color-accent-orange); + background: color-mix(in srgb, var(--color-accent-orange) 10%, transparent); +} + +.session-detail-zone-pill--working { + color: var(--color-accent-blue); + background: color-mix(in srgb, var(--color-accent-blue) 10%, transparent); +} + +.session-detail-zone-pill--pending { + color: var(--color-status-attention); + background: color-mix(in srgb, var(--color-status-attention) 10%, transparent); +} + +.session-detail-zone-pill--done { + color: var(--color-text-tertiary); + background: color-mix(in srgb, var(--color-text-tertiary) 14%, transparent); +} + /* ── Session cards — Emil design vision ──────────────────────────────── */ .session-card { @@ -5012,6 +5106,31 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { border-radius: 10px; } + .session-detail__status-pill--active { + color: var(--color-status-working); + background: color-mix(in srgb, var(--color-status-working) 12%, transparent); + } + + .session-detail__status-pill--ready { + color: var(--color-status-ready); + background: color-mix(in srgb, var(--color-status-ready) 12%, transparent); + } + + .session-detail__status-pill--waiting { + color: var(--color-status-attention); + background: color-mix(in srgb, var(--color-status-attention) 12%, transparent); + } + + .session-detail__status-pill--error { + color: var(--color-status-error); + background: color-mix(in srgb, var(--color-status-error) 12%, transparent); + } + + .session-detail__status-pill--idle { + color: var(--color-text-muted); + background: color-mix(in srgb, var(--color-accent) 12%, transparent); + } + .session-detail__time { margin-left: auto; font-family: var(--font-mono); @@ -5094,6 +5213,22 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { flex-shrink: 0; } + .session-detail-ci-tone--neutral { + background: var(--color-text-tertiary); + } + + .session-detail-ci-tone--pass { + background: var(--color-accent-green); + } + + .session-detail-ci-tone--fail { + background: var(--color-accent-red); + } + + .session-detail-ci-tone--pending { + background: var(--color-status-attention); + } + /* ── Mobile PR Dense Rows enhancements ─────────────────────────────── */ .mobile-pr-mobile-layout { diff --git a/packages/web/src/components/SessionDetail.tsx b/packages/web/src/components/SessionDetail.tsx index 4e8ee42b9..d83e7f498 100644 --- a/packages/web/src/components/SessionDetail.tsx +++ b/packages/web/src/components/SessionDetail.tsx @@ -72,13 +72,6 @@ function formatTimeCompact(isoDate: string | null): string { return `${Math.floor(diffHours / 24)}d ago`; } -function getCiDotBg(pr: DashboardPR): string { - if (isPRRateLimited(pr) || isPRUnenriched(pr)) return "var(--color-text-tertiary)"; - if (pr.ciStatus === "passing") return "var(--color-accent-green)"; - if (pr.ciStatus === "failing") return "var(--color-accent-red)"; - return "var(--color-status-attention)"; -} - function getCiShortLabel(pr: DashboardPR): string { if (isPRRateLimited(pr) || isPRUnenriched(pr)) return "CI"; if (pr.ciStatus === "passing") return "CI passing"; @@ -132,6 +125,39 @@ function activityStateClass(activityLabel: string): string { return "session-detail-status-pill--neutral"; } +function activityToneClass(activityColor: string): string { + switch (activityColor) { + case "var(--color-status-working)": + return "session-detail-tone--working"; + case "var(--color-status-ready)": + return "session-detail-tone--ready"; + case "var(--color-status-attention)": + return "session-detail-tone--attention"; + case "var(--color-status-error)": + return "session-detail-tone--error"; + default: + return "session-detail-tone--muted"; + } +} + +function mobileStatusPillClass(activityLabel: string): string { + const normalized = activityLabel.toLowerCase(); + if (normalized === "active") return "session-detail__status-pill--active"; + if (normalized === "ready") return "session-detail__status-pill--ready"; + if (normalized === "waiting for input") return "session-detail__status-pill--waiting"; + if (normalized === "blocked" || normalized === "exited") { + return "session-detail__status-pill--error"; + } + return "session-detail__status-pill--idle"; +} + +function ciToneClass(pr: DashboardPR): string { + if (isPRRateLimited(pr) || isPRUnenriched(pr)) return "session-detail-ci-tone--neutral"; + if (pr.ciStatus === "passing") return "session-detail-ci-tone--pass"; + if (pr.ciStatus === "failing") return "session-detail-ci-tone--fail"; + return "session-detail-ci-tone--pending"; +} + function SessionTopStrip({ headline, crumbId, @@ -199,8 +225,10 @@ function SessionTopStrip({ )} > {activityLabel} @@ -344,42 +372,36 @@ function _OrchestratorStatusStrip({ return () => clearInterval(id); }, [createdAt]); - const stats: Array<{ value: number; label: string; color: string; bg: string }> = [ + const stats: Array<{ value: number; label: string; toneClass: string }> = [ { value: zones.merge, label: "merge-ready", - color: "var(--color-status-ready)", - bg: "color-mix(in srgb, var(--color-status-ready) 10%, transparent)", + toneClass: "session-detail-zone-pill--merge", }, { value: zones.respond, label: "responding", - color: "var(--color-status-error)", - bg: "color-mix(in srgb, var(--color-status-error) 10%, transparent)", + toneClass: "session-detail-zone-pill--respond", }, { value: zones.review, label: "review", - color: "var(--color-accent-orange)", - bg: "color-mix(in srgb, var(--color-accent-orange) 10%, transparent)", + toneClass: "session-detail-zone-pill--review", }, { value: zones.working, label: "working", - color: "var(--color-accent-blue)", - bg: "color-mix(in srgb, var(--color-accent-blue) 10%, transparent)", + toneClass: "session-detail-zone-pill--working", }, { value: zones.pending, label: "pending", - color: "var(--color-status-attention)", - bg: "color-mix(in srgb, var(--color-status-attention) 10%, transparent)", + toneClass: "session-detail-zone-pill--pending", }, { value: zones.done, label: "done", - color: "var(--color-text-tertiary)", - bg: "color-mix(in srgb, var(--color-text-tertiary) 14%, transparent)", + toneClass: "session-detail-zone-pill--done", }, ].filter((s) => s.value > 0); @@ -414,19 +436,12 @@ function _OrchestratorStatusStrip({ stats.map((s) => (
- + {s.value} - + {s.label}
@@ -479,12 +494,14 @@ export function SessionDetail({ }; const headline = getSessionTitle(session); - const accentColor = "var(--color-accent)"; const terminalVariant = isOrchestrator ? "orchestrator" : "agent"; const terminalHeight = isOrchestrator ? "clamp(400px, 52vh, 620px)" : "clamp(380px, 48vh, 560px)"; + const terminalHeightClass = isOrchestrator + ? "session-detail-height--orchestrator" + : "session-detail-height--worker"; const isOpenCodeSession = session.metadata["agent"] === "opencode"; const opencodeSessionId = typeof session.metadata["opencodeSessionId"] === "string" && @@ -659,17 +676,26 @@ export function SessionDetail({