Skip to content
Open
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
107 changes: 107 additions & 0 deletions console/src/hooks/selected-at-anchor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { describe, expect, it } from "bun:test"
import { applySelectedAtAnchor } from "./selected-at-anchor"

// Fixed "now" so test expectations don't drift with wall-clock time.
const NOW = 1_780_000_000 // unix seconds (sometime in mid-2026)
const HOUR = 3600
const MIN = 60

describe("applySelectedAtAnchor", () => {
it("no-ops when the anchor param is absent", () => {
const patch: { preset?: string; start?: number; end?: number } = {
preset: "15m",
start: NOW - 15 * MIN,
end: NOW,
}
applySelectedAtAnchor(patch, null, NOW)
expect(patch.preset).toBe("15m")
expect(patch.start).toBe(NOW - 15 * MIN)
expect(patch.end).toBe(NOW)
})

it("no-ops when the anchor is unparseable", () => {
const patch: { preset?: string; start?: number; end?: number } = {
preset: "15m",
start: NOW - 15 * MIN,
end: NOW,
}
applySelectedAtAnchor(patch, "not-a-number", NOW)
expect(patch.preset).toBe("15m")
expect(patch.start).toBe(NOW - 15 * MIN)
expect(patch.end).toBe(NOW)
})

it("no-ops when the anchor is inside the window", () => {
// Anchor 5 minutes before "now"; falls inside the 15-minute window.
const patch: { preset?: string; start?: number; end?: number } = {
preset: "15m",
start: NOW - 15 * MIN,
end: NOW,
}
applySelectedAtAnchor(patch, String(NOW - 5 * MIN), NOW)
expect(patch.preset).toBe("15m")
expect(patch.start).toBe(NOW - 15 * MIN)
expect(patch.end).toBe(NOW)
})

it("shifts a stale 15m window to bracket an anchor 30 minutes ago", () => {
// The motivating scenario: someone copied a `?preset=15m&selected_at=...`
// URL half an hour ago; the recipient opens it now.
const patch: { preset?: string; start?: number; end?: number } = {
preset: "15m",
start: NOW - 15 * MIN,
end: NOW,
}
const anchor = NOW - 30 * MIN
applySelectedAtAnchor(patch, String(anchor), NOW)
// Promoted to custom (so the URL serializes absolute start/end).
expect(patch.preset).toBe("custom")
// Duration preserved (15 minutes), end is anchor + 60s pad,
// anchor sits at the trailing edge of the window (with the pad).
expect(patch.end).toBe(anchor + 60)
expect(patch.start).toBe(anchor + 60 - 15 * MIN)
expect(patch.end! - patch.start!).toBe(15 * MIN)
})

it("treats a missing preset window as the default 1h preset", () => {
// No `preset` in URL → effective window is `[now-1h, now]` per the
// toolbar default. An anchor 2h before now is outside that.
const patch: { preset?: string; start?: number; end?: number } = {}
const anchor = NOW - 2 * HOUR
applySelectedAtAnchor(patch, String(anchor), NOW)
expect(patch.preset).toBe("custom")
expect(patch.end).toBe(anchor + 60)
expect(patch.start).toBe(anchor + 60 - HOUR)
})

it("preserves the user's chosen 1h duration when overriding", () => {
// User shared with preset=1h; recipient opens 4 hours later.
const patch: { preset?: string; start?: number; end?: number } = {
preset: "1h",
start: NOW - HOUR,
end: NOW,
}
const anchor = NOW - 5 * HOUR
applySelectedAtAnchor(patch, String(anchor), NOW)
expect(patch.preset).toBe("custom")
expect(patch.end! - patch.start!).toBe(HOUR)
// Anchor lands inside the new window with a 60s breathing pad.
expect(anchor).toBeGreaterThanOrEqual(patch.start!)
expect(anchor).toBeLessThanOrEqual(patch.end!)
})

it("handles anchors after the window end (clock skew / future-dated)", () => {
// Defensive case — an anchor in the future of the recipient's clock.
// The override should still bracket it (no assumption that anchor < now).
const patch: { preset?: string; start?: number; end?: number } = {
preset: "15m",
start: NOW - 15 * MIN,
end: NOW,
}
const anchor = NOW + 10 * MIN
applySelectedAtAnchor(patch, String(anchor), NOW)
expect(patch.preset).toBe("custom")
expect(patch.end).toBe(anchor + 60)
expect(patch.start).toBe(anchor + 60 - 15 * MIN)
})
})
60 changes: 60 additions & 0 deletions console/src/hooks/selected-at-anchor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Window-anchor override for fresh URL loads with a stale relative preset.
*
* When a list page links to an item with `?selected=<id>&selected_at=<unix_s>`
* and a recipient opens that URL hours later, the relative preset (e.g.
* `preset=15m`) bracketed to "now" would no longer contain the item. This
* helper detects that gap and shifts the window so the item lands at the
* trailing edge:
*
* - Keep the preset's *duration* — that's the "show me this much
* context" signal the original user picked.
* - Slide the window so `end = selectedAt + ANCHOR_PAD_SECONDS`,
* putting the item just past the right edge with a small breathing
* pad (so it shows up reliably in a desc-by-time list).
* - Promote `preset` to `"custom"` so the URL serializer writes absolute
* `start`/`end` back out and the shift survives subsequent navigation.
*
* No-ops when the anchor is absent, unparseable, or already inside the
* computed window.
*
* Stand-alone module (no `@/` aliases) so it's directly testable under bun
* without dragging in the toolbar store / react-router runtime deps.
*/

export const ANCHOR_PAD_SECONDS = 60

/** Duration to fall back to when the caller's patch has no preset-derived
* window. Matches the toolbar's default `1h` preset — duplicated here
* rather than imported so this module stays dependency-free. */
export const DEFAULT_FALLBACK_DURATION_SECONDS = 3600

export interface AnchorablePatch {
preset?: string
start?: number
end?: number
}

export function applySelectedAtAnchor(
patch: AnchorablePatch,
selectedAtRaw: string | null,
nowSec: number,
): void {
if (selectedAtRaw == null) return
const selectedAt = Number(selectedAtRaw)
if (!Number.isFinite(selectedAt)) return

// Treat an empty patch as the "default preset" window — 1h ending at now.
// Matches what the toolbar store falls back to when no preset param is in
// the URL.
const start = patch.start ?? nowSec - DEFAULT_FALLBACK_DURATION_SECONDS
const end = patch.end ?? nowSec
if (selectedAt >= start && selectedAt <= end) return

const duration = end - start
const newEnd = selectedAt + ANCHOR_PAD_SECONDS
const newStart = newEnd - duration
patch.preset = "custom"
patch.start = newStart
patch.end = newEnd
}
11 changes: 11 additions & 0 deletions console/src/hooks/use-url-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
isValidPreset,
} from "@/stores/toolbar"
import { getSpecForPath, type DimensionKey } from "@/stores/page-filter-specs"
import { applySelectedAtAnchor } from "./selected-at-anchor"

function nowSeconds() {
return Math.floor(Date.now() / 1000)
Expand All @@ -23,6 +24,13 @@ const P = {
refresh: "refresh",
} as const

/**
* Page-level "selected item" anchor — written by list pages alongside
* `?selected=<id>` so the recipient of a shared link can recover the
* window the item was in, not just the most-recent N minutes.
*/
const SELECTED_AT_PARAM = "selected_at"

/**
* Bidirectional sync between the toolbar Zustand store and URL search params.
* Mount once in AppLayout.
Expand Down Expand Up @@ -61,6 +69,9 @@ export function useToolbarUrlSync() {
}
}

const selectedAtRaw = searchParams.get(SELECTED_AT_PARAM)
applySelectedAtAnchor(hydratePatch, selectedAtRaw, nowSeconds())

const wireApi = searchParams.get(P.wireApi)
const model = searchParams.get(P.model)
const serverIp = searchParams.get(P.serverIp)
Expand Down
21 changes: 19 additions & 2 deletions console/src/pages/agent-turns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,23 @@ export function AgentTurnsPage() {
const agentKindFilter = agentKindStr ? agentKindStr.split(",") : []

const [selectedId, setSelectedId] = useSearchParamState("selected", "")
// Anchor (unix seconds) shared alongside `?selected` so a recipient who
// opens this URL with a stale relative preset still lands on the item's
// window — see use-url-sync.ts for the override logic.
const [, setSelectedAt] = useSearchParamState("selected_at", "")

const selectItem = useCallback(
(turnId: string, startTimeUs: number) => {
setSelectedId(turnId)
setSelectedAt(String(Math.floor(startTimeUs / 1_000_000)))
},
[setSelectedId, setSelectedAt],
)

const clearSelection = useCallback(() => {
setSelectedId("")
setSelectedAt("")
}, [setSelectedId, setSelectedAt])

const { data, isLoading, isError, error } = useAgentTurns({
page,
Expand Down Expand Up @@ -211,7 +228,7 @@ export function AgentTurnsPage() {
items.map((item) => (
<tr
key={item.turn_id}
onClick={() => setSelectedId(item.turn_id)}
onClick={() => selectItem(item.turn_id, item.start_time)}
className={cn(
"cursor-pointer border-b border-border/50 transition-colors hover:bg-muted/50",
selectedId === item.turn_id && "bg-muted",
Expand Down Expand Up @@ -283,7 +300,7 @@ export function AgentTurnsPage() {

{/* Slide-over detail panel */}
{selectedId && (
<AgentTurnDetailPanel id={selectedId} onClose={() => setSelectedId("")} />
<AgentTurnDetailPanel id={selectedId} onClose={clearSelection} />
)}
</div>
)
Expand Down
25 changes: 20 additions & 5 deletions console/src/pages/http-exchanges.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ export function HttpExchangesPage() {
const isSse = sseStr === "true" ? true : sseStr === "false" ? false : undefined

const [selectedId, setSelectedId] = useSearchParamState("selected", "")
// Anchor (unix seconds) shared alongside `?selected` so a recipient who
// opens this URL with a stale relative preset still lands on the
// exchange's window — see use-url-sync.ts for the override logic.
const [, setSelectedAt] = useSearchParamState("selected_at", "")

const { data, isLoading, isError, error } = useHttpExchanges({
page,
Expand Down Expand Up @@ -172,26 +176,37 @@ export function HttpExchangesPage() {
? items.findIndex((i) => i.id === selectedId)
: -1

const selectItemById = useCallback(
(id: string) => {
const item = items.find((i) => i.id === id)
setSelectedId(id)
// request_time is unix ms — convert to seconds for the anchor.
setSelectedAt(item ? String(Math.floor(item.request_time / 1000)) : "")
},
[items, setSelectedId, setSelectedAt],
)

const handleRowClick = useCallback(
(id: string, _index: number) => {
setSelectedId(id)
selectItemById(id)
},
[setSelectedId],
[selectItemById],
)

const handleNavigate = useCallback(
(direction: "prev" | "next") => {
const newIndex = direction === "prev" ? selectedIndex - 1 : selectedIndex + 1
if (newIndex >= 0 && newIndex < items.length) {
setSelectedId(items[newIndex].id)
selectItemById(items[newIndex].id)
}
},
[selectedIndex, items, setSelectedId],
[selectedIndex, items, selectItemById],
)

const handleClose = useCallback(() => {
setSelectedId("")
}, [setSelectedId])
setSelectedAt("")
}, [setSelectedId, setSelectedAt])

return (
<div className="relative flex h-full flex-col">
Expand Down
25 changes: 20 additions & 5 deletions console/src/pages/llm-calls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ export function LlmCallsPage() {
}, [finishReasonsData])

const [selectedId, setSelectedId] = useSearchParamState("selected", "")
// Anchor (unix seconds) shared alongside `?selected` so a recipient who
// opens this URL with a stale relative preset still lands on the call's
// window — see use-url-sync.ts for the override logic.
const [, setSelectedAt] = useSearchParamState("selected_at", "")

const { data, isLoading, isError, error } = useLlmCalls({
page,
Expand Down Expand Up @@ -185,26 +189,37 @@ export function LlmCallsPage() {
? items.findIndex((i) => i.id === selectedId)
: -1

const selectItemById = useCallback(
(id: string) => {
const item = items.find((i) => i.id === id)
setSelectedId(id)
// request_time is unix ms — convert to seconds for the anchor.
setSelectedAt(item ? String(Math.floor(item.request_time / 1000)) : "")
},
[items, setSelectedId, setSelectedAt],
)

const handleRowClick = useCallback(
(id: string, _index: number) => {
setSelectedId(id)
selectItemById(id)
},
[setSelectedId],
[selectItemById],
)

const handleNavigate = useCallback(
(direction: "prev" | "next") => {
const newIndex = direction === "prev" ? selectedIndex - 1 : selectedIndex + 1
if (newIndex >= 0 && newIndex < items.length) {
setSelectedId(items[newIndex].id)
selectItemById(items[newIndex].id)
}
},
[selectedIndex, items, setSelectedId],
[selectedIndex, items, selectItemById],
)

const handleClose = useCallback(() => {
setSelectedId("")
}, [setSelectedId])
setSelectedAt("")
}, [setSelectedId, setSelectedAt])

return (
<div className="relative flex h-full flex-col">
Expand Down
Loading