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
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ const ROW_ESTIMATE = {
tool: 180,
"lane-open": 44,
"lane-close": 34,
parallel: 46,
"parallel-turns": 46,
parallel: 34,
"parallel-turns": 34,
structure: 30,
note: 86,
divider: 60,
Expand Down Expand Up @@ -156,12 +156,6 @@ export function SessionTranscript({
useAnimationFrameWithResizeObserver: true,
})

const indexByKey = useMemo(() => new Map(rows.map((row, index) => [row.key, index])), [rows])
const jumpTo = (key: string) => {
const index = indexByKey.get(key)
if (index !== undefined) virtualizer.scrollToIndex(index, { align: "start" })
}

// A pasted `?span=` link lands on the block it names. Once, on mount — after
// that the URL follows the reader rather than leading them. Not before the
// scroller exists, though: the list element attaches in a layout effect, so
Expand Down Expand Up @@ -215,7 +209,6 @@ export function SessionTranscript({
selected={"span" in row && row.span.spanId === selectedSpanId}
onSelectSpan={onSelectSpan}
onOpenTraceView={onOpenTraceView}
onJump={jumpTo}
/>
</div>
)
Expand All @@ -237,7 +230,6 @@ interface BlockProps {
selected: boolean
onSelectSpan: (spanId: string | undefined) => void
onOpenTraceView: () => void
onJump: (key: string) => void
}

/**
Expand Down Expand Up @@ -387,19 +379,6 @@ function TurnChapter({
</span>
{turn.agentName !== undefined && <AgentPill name={turn.agentName} />}
{turn.failed && <Pill tone="error">Failed</Pill>}
{/* The cluster's banner and the indentation carry the explanation; the
pill is the glanceable trace of it that survives collapse. Bounded,
unlike the old per-turn jump chips, whose row of unshrinkable
buttons was one of the things pushing the page sideways. */}
{row.parallelWith.length > 0 && (
<span
className="flex shrink-0 items-center gap-1 rounded-sm bg-primary/12 px-1.5 py-px font-mono text-[10px] text-primary uppercase tracking-[0.08em]"
title={`ran in parallel with ${row.parallelWith.map((ref) => turnOrdinal(ref.turn)).join(", ")}`}
>
<BranchForkIcon size={10} className="shrink-0" />
parallel
</span>
)}
{/* Shrinkable, unlike its neighbours: the summary joins every tool
name in the turn, so it truncates rather than widening the page. */}
{collapsed && <span className={META}>{summariseTurn(row)}</span>}
Expand Down Expand Up @@ -595,7 +574,11 @@ function SystemBlock({
<div className="min-w-0 grow">
<ClampedText
text={row.text}
body={raw ? undefined : <MessageResponse className="text-sm">{row.text}</MessageResponse>}
body={
raw ? undefined : (
<MessageResponse className="text-sm">{row.text}</MessageResponse>
)
}
expanded={disclosed(openRows, textKey, false)}
onToggleExpanded={() => onToggleRow(textKey)}
/>
Expand Down Expand Up @@ -1073,7 +1056,7 @@ function LaneOpen({
timePadding="pt-2"
className="pt-2.5"
>
<div className="flex flex-wrap items-center gap-2.5 py-1.5">
<div className="flex items-center gap-2.5 py-1.5">
<FaceRobotIcon size={14} className="shrink-0 text-chart-1" />
<span className={cn(LABEL, "text-chart-1")}>
{row.laneKind === "subagent" ? "Subagent" : "Agent"}
Expand All @@ -1092,17 +1075,6 @@ function LaneOpen({
: `· trace ${row.span.traceId.slice(0, 8)}`}{" "}
· {row.spanCount} spans · {formatDuration(row.span.durationMs)}
</span>
{/* The fork banner above already lists every sibling lane; this is the
glanceable trace of it that survives past that marker scrolling out. */}
{row.parallelWith.length > 0 && (
<span
className="flex shrink-0 items-center gap-1 rounded-sm bg-primary/12 px-1.5 py-px font-mono text-[10px] text-primary uppercase tracking-[0.08em]"
title={`ran in parallel with ${row.parallelWith.map((ref) => ref.agentName).join(", ")}`}
>
<BranchForkIcon size={10} className="shrink-0" />
parallel
</span>
)}
<span aria-hidden className="h-px grow bg-border" />
</div>
{/* The handoff's own payload: the `execute_tool task` span this block
Expand Down Expand Up @@ -1165,53 +1137,26 @@ function LaneClose({
}

/**
* The fork banner both parallel markers share: a bordered, tinted block whose
* every line WRAPS. The old markers were one flex line of unshrinkable text and
* chips, which is exactly the shape that pushes a page into sideways clipping —
* a marker about concurrency must never cost the reader the right edge.
* The fork marker both parallel kinds share: one rule across the column, in the
* same shape as the page's other structural lines. WHAT forked is already on
* the rows right below it — all the marker has to say is that they did not run
* in sequence, and when they were open together.
*/
function ParallelBanner({
title,
description,
refs,
onJump,
}: {
title: string
description: string
/** The forked threads, as jump chips. */
refs: readonly { key: string; label: string }[]
onJump: (key: string) => void
}) {
function ParallelRule({ label, range }: { label: string; range: string }) {
return (
<div className="flex min-w-0 flex-col gap-1.5 rounded-md border border-primary/25 bg-primary/6 px-3 py-2.5">
<div className="flex items-center gap-2.5">
<BranchForkIcon size={14} className="shrink-0 text-primary" />
<span className={cn(LABEL, "text-primary")}>{title}</span>
</div>
<p className="min-w-0 break-words pl-6 text-muted-foreground text-xs leading-relaxed">
{description}
</p>
<div className="flex flex-wrap items-center gap-1.5 pl-6">
{refs.map((ref) => (
<button
key={ref.key}
type="button"
onClick={() => onJump(ref.key)}
title={ref.label}
className="max-w-72 cursor-pointer truncate rounded-sm bg-primary/12 px-2 py-0.5 font-mono text-[11px] text-primary hover:bg-primary/20"
>
{ref.label}
</button>
))}
</div>
<div className="flex items-center gap-2.5 py-1.5">
<BranchForkIcon size={13} className="shrink-0 text-primary" />
<span className={cn(LABEL, "text-primary")}>{label}</span>
<span className={cn(META, "shrink-0")}>{range}</span>
<span aria-hidden className="h-px grow bg-primary/25" />
</div>
)
}

/** Only a window every member shared is reported as an overlap. A chain of
* pairwise overlaps has none, and the sentence then reports the run's extent
* pairwise overlaps has none, and the marker then reports the run's extent
* instead of inventing one. */
function overlapSentence(
function overlapWindow(
row: {
startMs: number
endMs: number
Expand All @@ -1221,8 +1166,8 @@ function overlapSentence(
timeZone: string,
): string {
return row.overlapStartMs !== undefined && row.overlapEndMs !== undefined
? `they overlap ${clockOf(row.overlapStartMs, timeZone)} → ${clockOf(row.overlapEndMs, timeZone)}`
: `their runs interleave between ${clockOf(row.startMs, timeZone)} and ${clockOf(row.endMs, timeZone)}`
? `overlap ${clockOf(row.overlapStartMs, timeZone)} → ${clockOf(row.overlapEndMs, timeZone)}`
: `interleaved ${clockOf(row.startMs, timeZone)} ${clockOf(row.endMs, timeZone)}`
}

/**
Expand All @@ -1232,21 +1177,18 @@ function overlapSentence(
function ParallelMarker({
row,
timeZone,
onJump,
}: BlockProps & { row: Extract<TranscriptRow, { kind: "parallel" }> }) {
return (
<Row
time={clockOf(row.startMs, timeZone)}
depth={row.depth}
timePadding="pt-3"
className="pt-5"
timePadding="pt-2.5"
className="pt-4"
flush
>
<ParallelBanner
title={`${row.lanes.length} lanes in parallel`}
description={overlapSentence(row, timeZone)}
refs={row.lanes.map((lane) => ({ key: lane.key, label: lane.agentName }))}
onJump={onJump}
<ParallelRule
label={`${row.lanes.length} lanes in parallel`}
range={overlapWindow(row, timeZone)}
/>
</Row>
)
Expand All @@ -1265,24 +1207,18 @@ function ParallelMarker({
function ParallelTurnsMarker({
row,
timeZone,
onJump,
}: BlockProps & { row: Extract<TranscriptRow, { kind: "parallel-turns" }> }) {
return (
<Row
time={clockOf(row.startMs, timeZone)}
depth={row.depth}
timePadding="pt-3"
className="pt-5"
timePadding="pt-2.5"
className="pt-4"
flush
>
<ParallelBanner
title={`${row.turns.length} turns in parallel`}
description={overlapSentence(row, timeZone)}
refs={row.turns.map((ref) => ({
key: ref.key,
label: `${turnOrdinal(ref.turn).toUpperCase()}${ref.turn.agentName === undefined ? "" : ` ${ref.turn.agentName}`}`,
}))}
onJump={onJump}
<ParallelRule
label={`${row.turns.length} turns in parallel`}
range={overlapWindow(row, timeZone)}
/>
</Row>
)
Expand Down
74 changes: 18 additions & 56 deletions apps/web/src/lib/agent-sessions/session-transcript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,18 +237,13 @@ describe("buildTranscript — lanes and parallel markers", () => {
])
})

it("marks overlapping lanes with reciprocal jump links", () => {
it("opens one marker over the lanes that overlapped", () => {
const rows = transcript(twoLanes)
const opens = rows.filter((row) => row.kind === "lane-open")
expect(opens.map((row) => row.agentName)).toEqual(["db-lane", "trace-lane"])
expect(opens[0]!.parallelWith.map((ref) => ref.agentName)).toEqual(["trace-lane"])
expect(opens[1]!.parallelWith.map((ref) => ref.agentName)).toEqual(["db-lane"])
// The links point at the other lane's own row key.
expect(opens[0]!.parallelWith[0]!.key).toBe(opens[1]!.key)

const marker = findRow(rows, "parallel")
expect(marker.forkedBy).toBe("planner-agent")
expect(marker.lanes).toHaveLength(2)
expect(marker.lanes.map((ref) => ref.agentName)).toEqual(["db-lane", "trace-lane"])
// The fork runs forwards, and both lanes really were open together.
expect(marker.startMs).toBeLessThan(marker.endMs)
expect(marker.overlapStartMs).toBe(T0 + 2 * SECOND)
Expand Down Expand Up @@ -1108,7 +1103,7 @@ describe("buildTranscript — parallel clustering", () => {
}),
]

it("keeps a staggered run in one cluster and links only the pairs that overlapped", () => {
it("keeps a staggered run in one cluster", () => {
const rows = transcript(chain)
const marker = findRow(rows, "parallel")
expect(marker.lanes.map((lane) => lane.agentName)).toEqual(["a-lane", "b-lane", "c-lane"])
Expand All @@ -1119,13 +1114,6 @@ describe("buildTranscript — parallel clustering", () => {
// than reporting one that runs backwards.
expect(marker.overlapStartMs).toBeUndefined()
expect(marker.overlapEndMs).toBeUndefined()

const opens = findRows(rows, "lane-open")
expect(opens.map((row) => row.parallelWith.map((ref) => ref.agentName))).toEqual([
["b-lane"],
["a-lane", "c-lane"],
["b-lane"],
])
})

// The lane that breaks the run under a "previous member" rule: it ends before
Expand Down Expand Up @@ -1159,15 +1147,8 @@ describe("buildTranscript — parallel clustering", () => {
const rows = transcript(nested)
expect(findRows(rows, "parallel")).toHaveLength(1)
const marker = findRow(rows, "parallel")
expect(marker.lanes).toHaveLength(3)
expect(marker.lanes.map((lane) => lane.agentName)).toEqual(["long-lane", "x-lane", "y-lane"])
expect(marker.startMs).toBeLessThan(marker.endMs)

const opens = findRows(rows, "lane-open")
expect(opens.map((row) => row.parallelWith.map((ref) => ref.agentName))).toEqual([
["x-lane", "y-lane"],
["long-lane"],
["long-lane"],
])
})
})

Expand Down Expand Up @@ -1689,39 +1670,25 @@ describe("buildTranscript — parallel turns", () => {
for (const row of findRows(rows, "assistant")) expect(row.depth).toBe(0)
})

it("gives each member turn header reciprocal jump data", () => {
const rows = transcript(twoTurns)
const headers = findRows(rows, "turn")
expect(headers.map((row) => row.turn.agentName)).toEqual(["log-lane", "metric-lane"])
expect(headers[0]!.parallelWith.map((ref) => ref.turn.agentName)).toEqual(["metric-lane"])
expect(headers[1]!.parallelWith.map((ref) => ref.turn.agentName)).toEqual(["log-lane"])
// The links point at the other turn's own header row key.
expect(headers[0]!.parallelWith[0]!.key).toBe(headers[1]!.key)
expect(headers[1]!.parallelWith[0]!.key).toBe(headers[0]!.key)
})

// The marker's jump targets have to be row keys, not turn-shaped lookalikes:
// a key that resolves to nothing scrolls nowhere and reports no error.
it("resolves every jump key to a row that is on the page", () => {
// The marker counts its members, so a member it names has to be a turn that
// is really on the page — a phantom would be counted into the headline.
it("names every member turn with the key its header renders under", () => {
const rows = transcript(twoTurns)
const keys = new Set(rows.map((row) => row.key))
const marker = findRow(rows, "parallel-turns")
expect(marker.turns.map((ref) => ref.turn.agentName)).toEqual(["log-lane", "metric-lane"])
for (const ref of marker.turns) expect(keys.has(ref.key)).toBe(true)
for (const header of findRows(rows, "turn")) {
for (const ref of header.parallelWith) expect(keys.has(ref.key)).toBe(true)
}
})

it("keeps the marker and the chip data on a collapsed member", () => {
it("keeps the marker over a collapsed member", () => {
const rows = transcript(twoTurns, { collapsedTurns: new Set(["conversation:conv-a"]) })
// Collapse hides a turn's body, never its header — so the chapter-level
// concurrency survives it.
expect(kinds(rows)).toEqual(["parallel-turns", "turn", "turn", "assistant"])
const collapsed = findRows(rows, "turn")[0]!
expect(collapsed.parallelWith.map((ref) => ref.turn.agentName)).toEqual(["metric-lane"])
expect(findRows(rows, "turn")[0]!.depth).toBe(1)
})

it("keeps a staggered run in one cluster and links only the pairs that overlapped", () => {
it("keeps a staggered run in one cluster", () => {
const staggered = [
...conversationTurn({ id: "a", agentName: "a-lane", startMs: 0, durationMs: 10 * SECOND }),
...conversationTurn({
Expand All @@ -1748,10 +1715,8 @@ describe("buildTranscript — parallel turns", () => {
// than reporting one that runs backwards.
expect(marker.overlapStartMs).toBeUndefined()
expect(marker.overlapEndMs).toBeUndefined()

expect(
findRows(rows, "turn").map((row) => row.parallelWith.map((ref) => ref.turn.agentName)),
).toEqual([["b-lane"], ["a-lane", "c-lane"], ["b-lane"]])
// Every member of the chain hangs off the one marker.
for (const header of findRows(rows, "turn")) expect(header.depth).toBe(1)
})

// The turn that breaks the run under a "previous member" rule: it ends before
Expand Down Expand Up @@ -1781,10 +1746,7 @@ describe("buildTranscript — parallel turns", () => {
// The two short turns never met each other, so only the long one is shared.
expect(marker.overlapStartMs).toBeUndefined()
expect(marker.overlapEndMs).toBeUndefined()

expect(
findRows(rows, "turn").map((row) => row.parallelWith.map((ref) => ref.turn.agentName)),
).toEqual([["x-lane", "y-lane"], ["long-lane"], ["long-lane"]])
for (const header of findRows(rows, "turn")) expect(header.depth).toBe(1)
})

it("says nothing about turns that ran one after the other", () => {
Expand All @@ -1799,7 +1761,7 @@ describe("buildTranscript — parallel turns", () => {
]
const rows = transcript(sequential)
expect(kinds(rows)).not.toContain("parallel-turns")
for (const header of findRows(rows, "turn")) expect(header.parallelWith).toStrictEqual([])
for (const header of findRows(rows, "turn")) expect(header.depth).toBe(0)
})

// An `empty-turn` stub is HTTP/DB work with no agent activity in it. Pairing
Expand All @@ -1819,15 +1781,15 @@ describe("buildTranscript — parallel turns", () => {
]
const rows = transcript(spans)
expect(kinds(rows)).toEqual(["turn", "assistant", "empty-turn"])
expect(findRow(rows, "turn").parallelWith).toStrictEqual([])
expect(findRow(rows, "turn").depth).toBe(0)
})

// Structural chrome, like the lane markers: the filtered view no longer has
// the ordering the marker describes.
it("drops the marker and the chips while a query is active", () => {
it("drops the marker and its indentation while a query is active", () => {
const rows = transcript(twoTurns, { query: "metric-lane" })
expect(kinds(rows)).toEqual(["turn", "assistant"])
expect(findRow(rows, "turn").parallelWith).toStrictEqual([])
expect(findRow(rows, "turn").depth).toBe(0)
})

it("marks the lab fixture's dispatched fan-out", () => {
Expand Down
Loading
Loading