Skip to content

Commit d86120e

Browse files
fixed usages, and stats... noice
1 parent 7664ab6 commit d86120e

12 files changed

Lines changed: 330 additions & 166 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "hlid",
33
"description": "Hliðskjálf - Watcher of Worlds",
4-
"version": "0.0.68",
4+
"version": "0.0.69",
55
"private": true,
66
"type": "module",
77
"imports": {

src/components/ledger/SessionsLedger.tsx

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { ChevronLeft, ChevronRight, Pencil, X } from "lucide-react";
22
import { type KeyboardEvent, useRef, useState } from "react";
33
import { ConfirmAction } from "#/components/ConfirmAction";
4+
import { statusDotClass } from "#/components/nav/SystemStatusDot";
45
import { PrivacyMask } from "#/components/PrivacyMask";
56
import type { SessionRow } from "#/db";
7+
import type { LiveStats, WsStatus } from "#/hooks/wsStore";
68
import { fmt, fmtDate } from "#/lib/formatters";
79

810
const THIRTY_DAYS_S = 30 * 86_400;
@@ -14,11 +16,21 @@ function SessionItem({
1416
onDelete,
1517
onNavigate,
1618
onRename,
19+
isActive,
20+
wsStatus,
21+
sessionState,
22+
hasPendingPermissions,
23+
liveStats,
1724
}: {
1825
session: SessionRow;
1926
onDelete: (id: string) => void;
2027
onNavigate: (id: string) => void;
2128
onRename: (id: string, label: string) => void;
29+
isActive?: boolean;
30+
wsStatus?: WsStatus;
31+
sessionState?: "idle" | "running" | "error";
32+
hasPendingPermissions?: boolean;
33+
liveStats?: LiveStats;
2234
}) {
2335
const [editing, setEditing] = useState(false);
2436
const [editValue, setEditValue] = useState("");
@@ -81,6 +93,11 @@ function SessionItem({
8193
onClick={() => onNavigate(session.id)}
8294
className="flex items-center gap-3 flex-1 min-w-0 px-4 py-2.5 text-left"
8395
>
96+
{isActive && wsStatus && sessionState && (
97+
<span
98+
className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${statusDotClass(wsStatus, sessionState, hasPendingPermissions ?? false)}`}
99+
/>
100+
)}
84101
<div className="flex-1 min-w-0">
85102
<PrivacyMask className="text-[11px] tracking-wider text-foreground/80 truncate">
86103
{session.label ?? "untitled"}
@@ -92,12 +109,18 @@ function SessionItem({
92109
</div>
93110
<div className="text-right shrink-0">
94111
<PrivacyMask className="text-[11px] tabular-nums text-[var(--data)]/70">
95-
${(session.total_cost ?? 0).toFixed(4)}
112+
$
113+
{(isActive && liveStats && liveStats.queries > 0
114+
? liveStats.cost
115+
: (session.total_cost ?? 0)
116+
).toFixed(4)}
96117
</PrivacyMask>
97118
<PrivacyMask className="text-[9px] tabular-nums text-muted-foreground/40 mt-0.5">
98119
{fmt(
99-
(session.total_input_tokens ?? 0) +
100-
(session.total_output_tokens ?? 0),
120+
isActive && liveStats && liveStats.queries > 0
121+
? liveStats.input_tokens + liveStats.output_tokens
122+
: (session.total_input_tokens ?? 0) +
123+
(session.total_output_tokens ?? 0),
101124
)}{" "}
102125
tok
103126
</PrivacyMask>
@@ -150,6 +173,11 @@ export function SessionsLedger({
150173
onRename,
151174
onNavigate,
152175
onCleanup,
176+
activeSessionId,
177+
wsStatus,
178+
sessionState,
179+
hasPendingPermissions,
180+
liveStats,
153181
}: {
154182
data: { sessions: SessionRow[]; total: number };
155183
page: number;
@@ -163,6 +191,11 @@ export function SessionsLedger({
163191
onRename: (id: string, label: string) => void;
164192
onNavigate: (id: string) => void;
165193
onCleanup: (days: number) => void;
194+
activeSessionId?: string | null;
195+
wsStatus?: WsStatus;
196+
sessionState?: "idle" | "running" | "error";
197+
hasPendingPermissions?: boolean;
198+
liveStats?: LiveStats;
166199
}) {
167200
const [jumpInput, setJumpInput] = useState("");
168201

@@ -248,6 +281,11 @@ export function SessionsLedger({
248281
onDelete={onDelete}
249282
onRename={onRename}
250283
onNavigate={onNavigate}
284+
isActive={activeSessionId != null && s.id === activeSessionId}
285+
wsStatus={wsStatus}
286+
sessionState={sessionState}
287+
hasPendingPermissions={hasPendingPermissions}
288+
liveStats={liveStats}
251289
/>
252290
))
253291
)}
Lines changed: 191 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { X } from "lucide-react";
2+
import { useEffect, useRef, useState } from "react";
13
import { Bar, BarChart, Cell, Tooltip, XAxis, YAxis } from "recharts";
2-
import type { TopToolCall } from "#/db";
4+
import type { ToolErrorEntry, TopToolCall } from "#/db";
5+
import { getToolErrorsFn } from "#/lib/serverFns";
36
import { ChartCard } from "../ChartCard";
47

58
const AXIS_TICK = {
@@ -17,7 +20,113 @@ function shortToolName(name: string): string {
1720
return parts.length > 1 ? parts[parts.length - 1] : name;
1821
}
1922

23+
/**
24+
* Strip `<tool_use_error>...</tool_use_error>` wrapper if present, then trim.
25+
*/
26+
function cleanErrorText(raw: string): string {
27+
return raw
28+
.replace(/^<tool_use_error>\s*/i, "")
29+
.replace(/\s*<\/tool_use_error>$/i, "")
30+
.trim();
31+
}
32+
33+
// ─── ErrorModal ───────────────────────────────────────────────────────────────
34+
35+
function ErrorModal({
36+
toolName,
37+
onClose,
38+
}: {
39+
toolName: string;
40+
onClose: () => void;
41+
}) {
42+
const [errors, setErrors] = useState<ToolErrorEntry[] | null>(null);
43+
const dialogRef = useRef<HTMLDivElement>(null);
44+
45+
useEffect(() => {
46+
dialogRef.current?.focus();
47+
getToolErrorsFn({ data: toolName })
48+
.then(setErrors)
49+
.catch(() => setErrors([]));
50+
}, [toolName]);
51+
52+
const displayName = shortToolName(toolName);
53+
54+
return (
55+
// biome-ignore lint/a11y/useKeyWithClickEvents: Escape handled by inner div
56+
// biome-ignore lint/a11y/noStaticElementInteractions: modal backdrop pattern
57+
<div
58+
className="fixed inset-0 z-50 bg-background/70 backdrop-blur-sm flex items-center justify-center p-4"
59+
onClick={onClose}
60+
>
61+
<div
62+
ref={dialogRef}
63+
tabIndex={-1}
64+
role="dialog"
65+
aria-modal="true"
66+
aria-label={`Errors for ${displayName}`}
67+
className="relative bg-card border border-border shadow-lg w-full max-w-md max-h-[70vh] flex flex-col focus:outline-none"
68+
onClick={(e) => e.stopPropagation()}
69+
onKeyDown={(e) => {
70+
if (e.key === "Escape") onClose();
71+
}}
72+
>
73+
{/* Header */}
74+
<div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
75+
<div>
76+
<div className="text-xs font-medium text-foreground font-mono">
77+
{displayName}
78+
</div>
79+
{toolName !== displayName && (
80+
<div className="text-[9px] text-muted-foreground font-mono truncate max-w-[300px]">
81+
{toolName}
82+
</div>
83+
)}
84+
</div>
85+
<button
86+
type="button"
87+
onClick={onClose}
88+
aria-label="Close"
89+
className="text-muted-foreground hover:text-foreground transition-colors p-1 -mr-1"
90+
>
91+
<X className="w-3.5 h-3.5" />
92+
</button>
93+
</div>
94+
95+
{/* Body */}
96+
<div className="overflow-y-auto flex-1 p-4">
97+
{errors === null ? (
98+
<div className="text-[10px] text-muted-foreground animate-pulse">
99+
Loading…
100+
</div>
101+
) : errors.length === 0 ? (
102+
<div className="text-[10px] text-muted-foreground">
103+
No error details found.
104+
</div>
105+
) : (
106+
<div className="space-y-2">
107+
{errors.map((e, i) => (
108+
// biome-ignore lint/suspicious/noArrayIndexKey: stable order from DB, no reorder
109+
<div key={i} className="flex gap-3 items-start">
110+
<span className="shrink-0 text-[9px] tabular-nums text-muted-foreground pt-0.5 min-w-[24px] text-right">
111+
{e.count}×
112+
</span>
113+
<span className="text-[10px] font-mono text-foreground/80 leading-relaxed break-all">
114+
{cleanErrorText(e.text)}
115+
</span>
116+
</div>
117+
))}
118+
</div>
119+
)}
120+
</div>
121+
</div>
122+
</div>
123+
);
124+
}
125+
126+
// ─── TopToolsChart ────────────────────────────────────────────────────────────
127+
20128
export function TopToolsChart({ data }: { data: TopToolCall[] }) {
129+
const [selectedTool, setSelectedTool] = useState<string | null>(null);
21130
const empty = data.length === 0;
22131
const rows = data.map((d) => ({
23132
name: shortToolName(d.name),
@@ -30,67 +139,89 @@ export function TopToolsChart({ data }: { data: TopToolCall[] }) {
30139
const height = empty ? 140 : Math.max(140, 26 * rows.length + 20);
31140

32141
return (
33-
<ChartCard
34-
title="Top tool calls"
35-
subtitle={empty ? undefined : `Top ${data.length} by count`}
36-
height={height}
37-
empty={empty ? "No tool events yet" : undefined}
38-
>
39-
<BarChart
40-
data={rows}
41-
layout="vertical"
42-
margin={{ top: 4, right: 12, bottom: 0, left: 8 }}
142+
<>
143+
<ChartCard
144+
title="Top tool calls"
145+
subtitle={empty ? undefined : `Top ${data.length} by count`}
146+
height={height}
147+
empty={empty ? "No tool events yet" : undefined}
43148
>
44-
<XAxis
45-
type="number"
46-
tick={AXIS_TICK}
47-
axisLine={false}
48-
tickLine={false}
49-
/>
50-
<YAxis
51-
type="category"
52-
dataKey="name"
53-
tick={AXIS_TICK}
54-
axisLine={false}
55-
tickLine={false}
56-
width={84}
57-
/>
58-
<Tooltip
59-
cursor={{ fill: "color-mix(in oklch, var(--data) 8%, transparent)" }}
60-
content={({ active, payload }) => {
61-
if (!active || !payload?.length) return null;
62-
const r = payload[0].payload as (typeof rows)[number];
63-
return (
64-
<div className="text-[9px] tabular-nums bg-background/95 border border-border px-2 py-1 rounded shadow-sm text-foreground/80 space-y-0.5">
65-
<div className="text-foreground">{r.fullName}</div>
66-
<div>{r.count} calls</div>
67-
<div className="text-muted-foreground">
68-
{(r.errorRate * 100).toFixed(1)}% errors
149+
<BarChart
150+
data={rows}
151+
layout="vertical"
152+
margin={{ top: 4, right: 12, bottom: 0, left: 8 }}
153+
>
154+
<XAxis
155+
type="number"
156+
tick={AXIS_TICK}
157+
axisLine={false}
158+
tickLine={false}
159+
/>
160+
<YAxis
161+
type="category"
162+
dataKey="name"
163+
tick={AXIS_TICK}
164+
axisLine={false}
165+
tickLine={false}
166+
width={84}
167+
/>
168+
<Tooltip
169+
cursor={{
170+
fill: "color-mix(in oklch, var(--data) 8%, transparent)",
171+
}}
172+
content={({ active, payload }) => {
173+
if (!active || !payload?.length) return null;
174+
const r = payload[0].payload as (typeof rows)[number];
175+
return (
176+
<div className="text-[9px] tabular-nums bg-background/95 border border-border px-2 py-1 rounded shadow-sm text-foreground/80 space-y-0.5">
177+
<div className="text-foreground">{r.fullName}</div>
178+
<div>{r.count} calls</div>
179+
<div className="text-muted-foreground">
180+
{(r.errorRate * 100).toFixed(1)}% errors
181+
{r.errorRate > 0 && (
182+
<span className="ml-1 opacity-60">
183+
— click for details
184+
</span>
185+
)}
186+
</div>
69187
</div>
70-
</div>
71-
);
72-
}}
188+
);
189+
}}
190+
/>
191+
<Bar
192+
dataKey="count"
193+
radius={[0, 2, 2, 0]}
194+
isAnimationActive={false}
195+
onClick={(barData) => {
196+
const r = barData as unknown as (typeof rows)[number];
197+
if (r.errorRate > 0) setSelectedTool(r.fullName);
198+
}}
199+
style={{ cursor: "pointer" }}
200+
>
201+
{rows.map((r) => {
202+
// Defensive clamp: errorRate is contract-bounded to [0,1] but
203+
// guard against bad upstream data so opacity stays valid.
204+
const er = Math.max(0, Math.min(1, r.errorRate));
205+
// Min opacity 0.7 keeps low-error bars readable on the dark
206+
// theme background (--destructive is too muddy at <0.7).
207+
return (
208+
<Cell
209+
key={r.fullName}
210+
fill={er > 0 ? "var(--chart-error)" : "var(--data)"}
211+
fillOpacity={er > 0 ? 0.7 + er * 0.3 : 0.85}
212+
/>
213+
);
214+
})}
215+
</Bar>
216+
</BarChart>
217+
</ChartCard>
218+
219+
{selectedTool && (
220+
<ErrorModal
221+
toolName={selectedTool}
222+
onClose={() => setSelectedTool(null)}
73223
/>
74-
{/* Single bar per tool. Cell fill ramps from --data (no errors)
75-
toward --destructive as errorRate rises so high-error tools
76-
visually pop instead of fading away. */}
77-
<Bar dataKey="count" radius={[0, 2, 2, 0]} isAnimationActive={false}>
78-
{rows.map((r) => {
79-
// Defensive clamp: errorRate is contract-bounded to [0,1] but
80-
// guard against bad upstream data so opacity stays valid.
81-
const er = Math.max(0, Math.min(1, r.errorRate));
82-
// Min opacity 0.7 keeps low-error bars readable on the dark
83-
// theme background (--destructive is too muddy at <0.7).
84-
return (
85-
<Cell
86-
key={r.fullName}
87-
fill={er > 0 ? "var(--chart-error)" : "var(--data)"}
88-
fillOpacity={er > 0 ? 0.7 + er * 0.3 : 0.85}
89-
/>
90-
);
91-
})}
92-
</Bar>
93-
</BarChart>
94-
</ChartCard>
224+
)}
225+
</>
95226
);
96227
}

0 commit comments

Comments
 (0)