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
32 changes: 3 additions & 29 deletions apps/server/src/client/pages/container-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
Clock,
Code,
Copy,
CurrencyDollar,
ListBullets,
PaperPlaneTilt,
Robot,
Expand Down Expand Up @@ -682,15 +681,13 @@ function SessionSidebarItem({
status,
messageCount,
agent,
cost,
isActive,
onClick,
}: {
session: SessionInfo
status: string
messageCount: number
agent: string | null
cost: number
isActive: boolean
onClick: () => void
}) {
Expand All @@ -717,7 +714,6 @@ function SessionSidebarItem({
<span>
{messageCount} msg{messageCount !== 1 ? "s" : ""}
</span>
{cost > 0 && <span>${cost.toFixed(4)}</span>}
</div>
</div>
</button>
Expand Down Expand Up @@ -964,18 +960,10 @@ export default function ContainerDetailPage() {
const repoName = parsed ? `${parsed.owner}/${parsed.repo}` : chatRepo
const activeSummary = summarizeSession(activeSession, serverMessages)

// Summary. Cost is derived per-session with a message fallback so pending
// placeholder sessions (no cost on the session object) still report a total.
// When the messages live under a different key than the session id (pending
// placeholders), the per-session sum is 0, so fall back to every message.
const perSessionCost = sessions.reduce((sum, s) => {
const msgs = Array.isArray(messages[s.id]) ? messages[s.id] : []
return sum + summarizeSession(s, msgs).cost
}, 0)
const totalCost = perSessionCost > 0 ? perSessionCost : summarizeSession(sessions[0], allMessages).cost
const totalMessages = allMessages.length
// Tool calls are a far more meaningful "how much work happened" signal than
// cost, which the Flue runtime does not report per message (so it reads 0).
// Tool calls are the "how much work happened" signal. The Flue runtime never
// reports token usage/cost in the history we read, so we don't surface a
// cost figure at all rather than show a perpetually-empty one.
const totalTools = allMessages.reduce((sum, m) => sum + (m.parts?.filter(isToolPart).length ?? 0), 0)
const overallStatus =
detail.status ??
Expand Down Expand Up @@ -1065,14 +1053,6 @@ export default function ContainerDetailPage() {
<Wrench className="size-3" />
{totalTools}
</span>
{/* The Flue runtime doesn't report token usage/cost, so most runs
have nothing to total — show a figure only when one is present
rather than a confusing "usage not tracked" placeholder. */}
{totalCost > 0 && (
<span className="inline-flex items-center gap-1" title="Model cost reported for this run">
<CurrencyDollar className="size-3" />${totalCost.toFixed(4)}
</span>
)}
{observedAtIso && (
<span className="inline-flex items-center gap-1">
<Clock className="size-3" />
Expand Down Expand Up @@ -1168,7 +1148,6 @@ export default function ContainerDetailPage() {
status={sessionStatus[s.id]?.type ?? "unknown"}
messageCount={sessionMessages.length}
agent={itemSummary.agent}
cost={itemSummary.cost}
isActive={s.id === effectiveSessionId}
onClick={() => {
setActiveSessionId(s.id)
Expand Down Expand Up @@ -1248,11 +1227,6 @@ export default function ContainerDetailPage() {
<span className="truncate">{activeSummary.model}</span>
</span>
)}
{activeSummary.cost > 0 && (
<span className="inline-flex items-center gap-1">
<CurrencyDollar className="size-3" />${activeSummary.cost.toFixed(4)}
</span>
)}
</div>
</div>
<span className="hidden shrink-0 font-mono text-[10px] text-muted-foreground/40 sm:inline">
Expand Down
223 changes: 135 additions & 88 deletions apps/server/src/client/pages/sessions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
CaretLeft,
CaretRight,
ChatsCircle,
CurrencyDollar,
MagnifyingGlass,
Robot,
Stack,
Expand Down Expand Up @@ -142,7 +141,7 @@ export default function SessionsPage() {

return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-lg font-semibold">Agent runs</h1>
<p className="text-sm text-muted-foreground">
Expand All @@ -151,7 +150,7 @@ export default function SessionsPage() {
: "Recent agent conversations"}
</p>
</div>
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-2">
<NewChatDialog />
<ButtonGroup aria-label="Clear agent runs">
<Button
Expand Down Expand Up @@ -219,15 +218,15 @@ export default function SessionsPage() {
</div>
</div>

<div className="flex items-center gap-3">
<div className="relative flex items-center">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<div className="relative flex w-full items-center sm:w-72">
<MagnifyingGlass className="absolute left-2 size-3.5 text-muted-foreground" />
<input
type="text"
placeholder="Search by entity, title, or agent..."
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
className="h-7 w-72 border border-input bg-background pl-7 pr-7 text-xs outline-none placeholder:text-muted-foreground focus:border-ring"
className="h-7 w-full border border-input bg-background pl-7 pr-7 text-xs outline-none placeholder:text-muted-foreground focus:border-ring"
/>
{searchInput && (
<button
Expand All @@ -239,7 +238,7 @@ export default function SessionsPage() {
</button>
)}
</div>
<div className="ml-auto flex items-center gap-1.5 text-xs text-muted-foreground">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground sm:ml-auto">
<span>Per page:</span>
{PAGE_SIZES.map((s) => (
<Button key={s} variant={limit === s ? "secondary" : "ghost"} size="xs" onClick={() => setLimit(s)}>
Expand Down Expand Up @@ -279,95 +278,143 @@ export default function SessionsPage() {
)}
</div>
) : (
<div className="w-full overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[240px]">Entity</TableHead>
<TableHead className="min-w-[140px]">Agent</TableHead>
<TableHead className="w-[90px] text-center">Status</TableHead>
<TableHead className="w-[80px] text-center">
<span className="inline-flex items-center gap-1">
<Stack className="size-3" /> Sessions
</span>
</TableHead>
<TableHead className="w-[80px] text-center">
<span className="inline-flex items-center gap-1">
<ChatsCircle className="size-3" /> Msgs
</span>
</TableHead>
<TableHead className="w-[80px] text-right">
<span className="inline-flex items-center gap-1">
<CurrencyDollar className="size-3" /> Cost
</span>
</TableHead>
<TableHead className="w-[100px] text-right">Updated</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((session: SessionListItem) => {
const ghUrl = entityGitHubUrl(session.entityKey, "issues")
const parsed = parseEntityKey(session.entityKey)
const chatRepo = chatEntityRepo(session.entityKey)
const repoName = parsed ? `${parsed.owner}/${parsed.repo}` : chatRepo
<>
{/* Mobile: stacked cards (the wide table doesn't fit a phone). */}
<ul className="divide-y md:hidden">
{filtered.map((session: SessionListItem) => {
const chatRepo = chatEntityRepo(session.entityKey)

return (
<TableRow
key={session.entityKey}
className="cursor-pointer"
return (
<li key={session.entityKey}>
<button
type="button"
className="flex w-full flex-col gap-2 px-4 py-3 text-left transition-colors hover:bg-muted/40"
onClick={() => navigate(`/containers/detail?key=${encodeURIComponent(session.entityKey)}`)}
>
<TableCell>
<div className="space-y-0.5">
<div className="flex items-center gap-2 font-mono text-sm">
{ghUrl ? <GitHubLink href={ghUrl}>{session.entityKey}</GitHubLink> : session.entityKey}
{chatRepo && (
<Badge variant="secondary" className="font-sans">
Chat
</Badge>
)}
</div>
{(session.title || repoName) && (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{session.title && <span className="truncate">{session.title}</span>}
{repoName && (
<GitHubLink href={repoGitHubUrl(repoName)}>
<span className="text-[10px]">{repoName}</span>
</GitHubLink>
)}
</div>
<div className="flex items-start justify-between gap-2">
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 font-mono text-sm">
<span className="break-all">{session.entityKey}</span>
{chatRepo && (
<Badge variant="secondary" className="font-sans">
Chat
</Badge>
)}
</div>
</TableCell>
<TableCell>
<StatusIndicator status={session.status} />
</div>
{(session.title || session.agent) && (
<div className="space-y-0.5">
<div className="text-sm">{session.agent ?? "-"}</div>
{session.model && (
<div className="truncate text-[11px] text-muted-foreground">{session.model}</div>
{session.title && (
<p className="line-clamp-2 text-xs text-muted-foreground">{session.title}</p>
)}
{session.agent && (
<p className="text-[11px] text-muted-foreground">
{session.agent}
{session.model ? ` · ${session.model}` : ""}
</p>
)}
</div>
</TableCell>
<TableCell className="text-center">
<StatusIndicator status={session.status} />
</TableCell>
<TableCell className="text-center font-mono text-sm tabular-nums">
{session.sessionCount}
</TableCell>
<TableCell className="text-center font-mono text-sm tabular-nums">
{session.messageCount}
</TableCell>
<TableCell className="text-right font-mono text-sm tabular-nums">
{session.totalCost > 0 ? `$${session.totalCost.toFixed(2)}` : "-"}
</TableCell>
<TableCell className="text-right text-muted-foreground">
{formatTimeAgo(session.updatedAt)}
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</div>
)}
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-muted-foreground">
<span className="inline-flex items-center gap-1">
<Stack className="size-3" />
{session.sessionCount} session{session.sessionCount !== 1 ? "s" : ""}
</span>
<span className="inline-flex items-center gap-1">
<ChatsCircle className="size-3" />
{session.messageCount} msg{session.messageCount !== 1 ? "s" : ""}
</span>
<span className="ml-auto">{formatTimeAgo(session.updatedAt)}</span>
</div>
</button>
</li>
)
})}
</ul>

{/* Desktop: full table. */}
<div className="hidden w-full overflow-x-auto md:block">
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[240px]">Entity</TableHead>
<TableHead className="min-w-[140px]">Agent</TableHead>
<TableHead className="w-[90px] text-center">Status</TableHead>
<TableHead className="w-[80px] text-center">
<span className="inline-flex items-center gap-1">
<Stack className="size-3" /> Sessions
</span>
</TableHead>
<TableHead className="w-[80px] text-center">
<span className="inline-flex items-center gap-1">
<ChatsCircle className="size-3" /> Msgs
</span>
</TableHead>
<TableHead className="w-[100px] text-right">Updated</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((session: SessionListItem) => {
const ghUrl = entityGitHubUrl(session.entityKey, "issues")
const parsed = parseEntityKey(session.entityKey)
const chatRepo = chatEntityRepo(session.entityKey)
const repoName = parsed ? `${parsed.owner}/${parsed.repo}` : chatRepo

return (
<TableRow
key={session.entityKey}
className="cursor-pointer"
onClick={() => navigate(`/containers/detail?key=${encodeURIComponent(session.entityKey)}`)}
>
<TableCell>
<div className="space-y-0.5">
<div className="flex items-center gap-2 font-mono text-sm">
{ghUrl ? <GitHubLink href={ghUrl}>{session.entityKey}</GitHubLink> : session.entityKey}
{chatRepo && (
<Badge variant="secondary" className="font-sans">
Chat
</Badge>
)}
</div>
{(session.title || repoName) && (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{session.title && <span className="truncate">{session.title}</span>}
{repoName && (
<GitHubLink href={repoGitHubUrl(repoName)}>
<span className="text-[10px]">{repoName}</span>
</GitHubLink>
)}
</div>
)}
</div>
</TableCell>
<TableCell>
<div className="space-y-0.5">
<div className="text-sm">{session.agent ?? "-"}</div>
{session.model && (
<div className="truncate text-[11px] text-muted-foreground">{session.model}</div>
)}
</div>
</TableCell>
<TableCell className="text-center">
<StatusIndicator status={session.status} />
</TableCell>
<TableCell className="text-center font-mono text-sm tabular-nums">
{session.sessionCount}
</TableCell>
<TableCell className="text-center font-mono text-sm tabular-nums">
{session.messageCount}
</TableCell>
<TableCell className="text-right text-muted-foreground">
{formatTimeAgo(session.updatedAt)}
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</div>
</>
)}
</CardContent>
</Card>
Expand Down
Loading