diff --git a/console/src/components/layout/sidebar.tsx b/console/src/components/layout/sidebar.tsx
index 8881d04..ea1123c 100644
--- a/console/src/components/layout/sidebar.tsx
+++ b/console/src/components/layout/sidebar.tsx
@@ -10,11 +10,11 @@ import {
MessagesSquare,
Network,
PanelLeftClose,
- PanelLeftOpen,
Settings,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { useSidebarStore } from "@/stores/sidebar"
+import { Logo } from "@/components/ui/logo"
/** Toolbar-level param keys that should be preserved across page navigation */
const TOOLBAR_KEYS = ["preset", "start", "end", "wire_api", "model", "server_ip", "refresh"]
@@ -54,13 +54,36 @@ export function Sidebar() {
expanded ? "w-[200px]" : "w-[44px]",
)}
>
-
-
- {expanded ? : }
-
+
+ {expanded ? (
+ <>
+
+
+
+
+ >
+ ) : (
+ // Collapsed: the icon doubles as the expand affordance — saves a
+ // row and is the most discoverable place to put the toggle when
+ // there's no room for a second button.
+
+
+
+ )}
diff --git a/console/src/components/ui/logo.tsx b/console/src/components/ui/logo.tsx
new file mode 100644
index 0000000..35fcce3
--- /dev/null
+++ b/console/src/components/ui/logo.tsx
@@ -0,0 +1,71 @@
+/**
+ * TokenScope brand mark. Two variants share the same icon glyph (rounded
+ * square "scope frame" containing three decreasing horizontal lines —
+ * abstracted tokens viewed through the lens), so the icon-only and the
+ * wordmark line up visually when the sidebar collapses.
+ *
+ * Stroke + fills use `currentColor` so the mark inherits the surrounding
+ * text colour and respects light/dark themes without extra CSS.
+ */
+
+import { cn } from "@/lib/utils"
+
+interface LogoProps {
+ variant: "icon" | "wordmark"
+ className?: string
+}
+
+export function Logo({ variant, className }: LogoProps) {
+ if (variant === "icon") {
+ return (
+
+
+
+
+
+
+ )
+ }
+
+ // Wordmark: same icon glyph at the left + "TokenScope" set in a
+ // system-stack semi-bold. SVG renders crisply at any DPI and
+ // tints with currentColor; we accept the (tiny) variance across OS
+ // font choices in exchange for not shipping a webfont.
+ return (
+
+
+
+
+
+
+
+
+ TokenScope
+
+
+ )
+}
diff --git a/console/src/pages/agent-turns.tsx b/console/src/pages/agent-turns.tsx
index 755d8c7..14ca7da 100644
--- a/console/src/pages/agent-turns.tsx
+++ b/console/src/pages/agent-turns.tsx
@@ -14,17 +14,22 @@ const STATUS_OPTIONS = ["in_progress", "complete", "incomplete"]
const PAGE_SIZES = [20, 50, 100] as const
+// Identity columns (Agent / Client) sit immediately after Time; coarse
+// shape (Calls / Status) and per-turn token counters (In / Out) follow
+// — that's the order operators reach for first when triaging a turn.
+// Less-frequently-scanned dimensions (Model / Wire API / Server) and
+// the long preview column trail.
const columns = [
{ key: "start_time", label: "Time", width: "w-[210px]", sortable: true, align: "left" as const },
- { key: "wire_api", label: "Wire API", width: "w-[120px]", sortable: false, align: "left" as const },
- { key: "primary_model", label: "Model", width: "w-[180px]", sortable: false, align: "left" as const },
{ key: "agent_kind", label: "Agent", width: "w-[100px]", sortable: false, align: "left" as const },
{ key: "client_ip", label: "Client", width: "w-[130px]", sortable: false, align: "left" as const },
- { key: "server_ip", label: "Server", width: "w-[130px]", sortable: false, align: "left" as const },
- { key: "status", label: "Status", width: "w-[100px]", sortable: false, align: "left" as const },
{ key: "call_count", label: "Calls", width: "w-[60px]", sortable: true, align: "right" as const },
+ { key: "status", label: "Status", width: "w-[100px]", sortable: false, align: "left" as const },
{ key: "total_input_tokens", label: "In", width: "w-[70px]", sortable: true, align: "right" as const },
{ key: "total_output_tokens", label: "Out", width: "w-[70px]", sortable: true, align: "right" as const },
+ { key: "primary_model", label: "Model", width: "w-[180px]", sortable: false, align: "left" as const },
+ { key: "wire_api", label: "Wire API", width: "w-[120px]", sortable: false, align: "left" as const },
+ { key: "server_ip", label: "Server", width: "w-[130px]", sortable: false, align: "left" as const },
{ key: "duration_ms", label: "Duration", width: "w-[90px]", sortable: true, align: "right" as const },
{ key: "preview", label: "User Input", width: "", sortable: false, align: "left" as const },
] as const
diff --git a/console/src/pages/models.tsx b/console/src/pages/models.tsx
index fb44472..7f0e132 100644
--- a/console/src/pages/models.tsx
+++ b/console/src/pages/models.tsx
@@ -27,6 +27,13 @@ function getErrorRate(m: MetricsModelRow): number {
function getSortValue(m: MetricsModelRow, key: SortKey): number | string {
if (key === "error_rate") return getErrorRate(m)
if (key === "model" || key === "wire_api") return m[key]
+ // Underlying field is tpot_avg (ms/token, lower = faster) but the column
+ // surfaces it as TPS = 1000/tpot_avg. Invert the sort value so "desc"
+ // gives fastest first — matches what a user clicking "Generation TPS"
+ // expects to see.
+ if (key === "tpot_avg") {
+ return m.tpot_avg != null && m.tpot_avg > 0 ? 1000 / m.tpot_avg : 0
+ }
return (m[key] as number) ?? 0
}
@@ -146,7 +153,7 @@ export function ModelsPage() {
-
+
@@ -197,7 +204,9 @@ export function ModelsPage() {
{formatMs(m.e2e_avg)}
{formatMs(m.e2e_p95)}
- {m.tpot_avg != null ? `${m.tpot_avg.toFixed(1)} ms/tok` : "—"}
+ {m.tpot_avg != null && m.tpot_avg > 0
+ ? `${(1000 / m.tpot_avg).toFixed(1)} tok/s`
+ : "—"}
{formatNumber(m.total_input_tokens)}
{formatNumber(m.total_output_tokens)}
diff --git a/console/src/pages/overview.tsx b/console/src/pages/overview.tsx
index 2df597e..a13fe96 100644
--- a/console/src/pages/overview.tsx
+++ b/console/src/pages/overview.tsx
@@ -99,9 +99,13 @@ export function OverviewPage() {
subtext={`${formatNumber(summary?.total_input_tokens)} in / ${formatNumber(summary?.total_output_tokens)} out`}
/>
0
+ ? `${(1000 / summary.tpot_avg).toFixed(1)} tok/s`
+ : "—"
+ }
+ subtext="streaming only · generation speed"
/>