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
68 changes: 60 additions & 8 deletions app/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -238,14 +238,66 @@
border-left: 3px solid var(--color-state-in-review);
}

/* Advisory review-finding rails in the glyph margin (B2). Dashed to read as
distinct from the solid human-comment accent, colored by severity. */
.finding-line-info {
border-left: 3px dashed var(--color-muted-foreground);
/* Advisory finding markers in the code view (agent-voice §3). A severity-colored
dot in the glyph margin makes a finding's line discoverable without opening
its view zone. Colors mirror the status tokens — severity is state. */
.finding-glyph-info,
.finding-glyph-warning,
.finding-glyph-critical {
display: flex;
align-items: center;
justify-content: center;
}
.finding-glyph-info::after,
.finding-glyph-warning::after,
.finding-glyph-critical::after {
content: "";
width: 6px;
height: 6px;
border-radius: 9999px;
}
.finding-glyph-info::after {
background-color: var(--color-muted-foreground);
}
.finding-glyph-warning::after {
background-color: var(--color-warning);
}
.finding-line-warning {
border-left: 3px dashed var(--color-warning);
.finding-glyph-critical::after {
background-color: var(--color-danger);
}
.finding-line-critical {
border-left: 3px dashed var(--color-danger);

/* Faint whole-line tint across a finding's range, so the flagged region reads at
a glance. Very low alpha of the severity color. */
.finding-line-bg-info {
background-color: color-mix(
in oklch,
var(--color-muted-foreground) 6%,
transparent
) !important;
}
.finding-line-bg-warning {
background-color: color-mix(
in oklch,
var(--color-warning) 7%,
transparent
) !important;
}
.finding-line-bg-critical {
background-color: color-mix(
in oklch,
var(--color-danger) 7%,
transparent
) !important;
}

/* Transient focus flash on jump-to-line (agent-voice §3): a stronger, brief
brand tint drawing the eye to the revealed range. Applied for ~1.2s then
removed in JS, and skipped entirely under prefers-reduced-motion. Declared
last so it wins over the faint finding tint on the same line during a flash. */
.diff-line-flash {
background-color: color-mix(
in oklch,
var(--color-brand) 22%,
transparent
) !important;
}
32 changes: 32 additions & 0 deletions app/src/components/AgentCallout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { AgentCallout } from "./AgentCallout";

describe("AgentCallout", () => {
it("renders the eyebrow, content, aside, and footer", () => {
render(
<AgentCallout
eyebrow="AGENT'S READ"
aside={<span>telemetry</span>}
footer="advisory note"
>
<p>the agent said this</p>
</AgentCallout>,
);
expect(screen.getByText("AGENT'S READ")).toBeInTheDocument();
expect(screen.getByText("the agent said this")).toBeInTheDocument();
expect(screen.getByText("telemetry")).toBeInTheDocument();
expect(screen.getByText("advisory note")).toBeInTheDocument();
});

it("omits the optional slots when not provided", () => {
render(
<AgentCallout eyebrow="LAST RUN">
<p>content only</p>
</AgentCallout>,
);
expect(screen.getByText("LAST RUN")).toBeInTheDocument();
expect(screen.getByText("content only")).toBeInTheDocument();
expect(screen.queryByText("advisory note")).not.toBeInTheDocument();
});
});
66 changes: 66 additions & 0 deletions app/src/components/AgentCallout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* The agent voice — one recognizable treatment for agent-authored content.
*
* Wherever the app shows something the agent wrote or derived (the PR intent's
* "agent's read", the last-run summary, and more), it wraps in this callout so
* the user learns a single visual cue: an HUD-teal left rail + the agent mark =
* "the agent said this". Restrained by design — a 2px brand rail and a very
* low-alpha brand tint, no new colors beyond the one owned accent. Severity and
* other status colors stay reserved for state and are never mixed in here
* (authorship is not a status).
*/

import { type ReactNode } from "react";
import { AgentMark } from "@/lib/agent-event";
import { cn } from "@/lib/utils";

interface AgentCalloutProps {
/** Mono, uppercase eyebrow label (e.g. `AGENT'S READ`, `LAST RUN`). */
readonly eyebrow: string;
/** The agent-authored content. */
readonly children: ReactNode;
/** Optional right-aligned slot in the header row (telemetry, controls). */
readonly aside?: ReactNode;
/** Optional provenance / advisory note, shown muted and italic beneath. */
readonly footer?: ReactNode;
/** Extra classes for the outer container. */
readonly className?: string;
}

/**
* Wrap agent-authored content in the shared agent-voice treatment: a brand left
* rail, a faint brand tint, and a header row carrying the agent mark and a mono
* uppercase eyebrow.
*/
export function AgentCallout({
eyebrow,
children,
aside,
footer,
className,
}: AgentCalloutProps) {
return (
<div
className={cn(
"rounded-md border-l-2 border-brand bg-brand/5 py-2 pl-3 pr-2.5",
className,
)}
>
<div className="flex items-center gap-1.5">
<AgentMark className="h-3 w-3 shrink-0 text-brand" />
<span className="font-mono text-[10px] font-semibold uppercase tracking-wide text-brand">
{eyebrow}
</span>
{aside !== undefined && (
<span className="ml-auto min-w-0">{aside}</span>
)}
</div>
<div className="mt-1">{children}</div>
{footer !== undefined && (
<div className="mt-1 text-[10px] italic text-muted-foreground">
{footer}
</div>
)}
</div>
);
}
33 changes: 17 additions & 16 deletions app/src/components/AgentPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ import {
AgentMark,
type EventPresentation,
} from "@/lib/agent-event";
import { X, Trash2, Square, FileText, History, ShieldAlert } from "lucide-react";
import { X, Trash2, Square, FileText, ShieldAlert } from "lucide-react";
import { Button } from "@/components/ui/button";
import { AgentCallout } from "./AgentCallout";
import { cn } from "@/lib/utils";

/**
Expand Down Expand Up @@ -294,17 +295,12 @@ function TrajectoryCard({ summary }: { readonly summary: TrajectorySummary }) {
});

return (
<div className="rounded-lg border border-border bg-card/60 p-4">
{/* Header: last-run label + mode + telemetry. */}
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
<span className="inline-flex items-center gap-1.5 font-display text-sm font-semibold text-foreground">
<History className="h-4 w-4 text-muted-foreground" />
Last run
</span>
<span className="font-mono text-xs text-muted-foreground">
· {summary.mode.toLowerCase()}
</span>
<span className="ml-auto flex items-center gap-3 font-mono text-[11px] tabular-nums text-muted-foreground/80">
// The last-run summary is agent-authored, so it wears the shared agent voice
// (teal rail + agent mark) exactly like the intent's "agent's read".
<AgentCallout
eyebrow="LAST RUN"
aside={
<span className="flex items-center gap-3 font-mono text-[11px] tabular-nums text-muted-foreground/80">
<span title="Wall-clock duration">
{formatElapsed(summary.duration_ms)}
</span>
Expand All @@ -316,11 +312,16 @@ function TrajectoryCard({ summary }: { readonly summary: TrajectorySummary }) {
)}
<span title="When the run ended">{endedAgo} ago</span>
</span>
</div>
}
>
{/* Mode line — a deterministic fact, kept small and muted. */}
<span className="font-mono text-xs text-muted-foreground">
{summary.mode.toLowerCase()}
</span>

{/* Commands the agent ran, each ✓/✗ with the full command in the tooltip. */}
{summary.commands.length > 0 && (
<ul className="mt-3 space-y-1 border-t border-border pt-3">
<ul className="mt-2 space-y-1 border-t border-border pt-2">
{summary.commands.map((cmd, idx) => (
<li
key={`${String(idx)}-${cmd.command}`}
Expand All @@ -346,7 +347,7 @@ function TrajectoryCard({ summary }: { readonly summary: TrajectorySummary }) {

{/* Final message, collapsed to three lines with an expand toggle. */}
{summary.final_text !== "" && (
<div className="mt-3 border-t border-border pt-3">
<div className="mt-2 border-t border-border pt-2">
<p
className={cn(
"whitespace-pre-wrap text-xs leading-relaxed text-foreground",
Expand All @@ -366,7 +367,7 @@ function TrajectoryCard({ summary }: { readonly summary: TrajectorySummary }) {
</button>
</div>
)}
</div>
</AgentCallout>
);
}

Expand Down
Loading
Loading