Skip to content
Closed
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
20 changes: 14 additions & 6 deletions apps/studio/frontend/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Metadata } from "next";
import type { ReactNode } from "react";
import { NextIntlClientProvider } from "next-intl";
import { getLocale, getMessages, getTranslations } from "next-intl/server";
import Shell from "@/components/Shell";
import { ToastProvider } from "@/components/Toast";
import "./globals.css";
Expand All @@ -9,13 +11,17 @@ export const metadata: Metadata = {
description: "Lion Studio orchestration observability",
};

export default function RootLayout({
export default async function RootLayout({
children,
}: Readonly<{
children: ReactNode;
}>) {
const locale = await getLocale();
const messages = await getMessages();
const t = await getTranslations({ locale, namespace: "common" });

return (
<html lang="en" suppressHydrationWarning>
<html lang={locale} suppressHydrationWarning>
<head>
{/* Prevent FOUC: read localStorage before paint, default to light */}
<script
Expand All @@ -29,11 +35,13 @@ export default function RootLayout({
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:z-[100] focus:m-2 focus:rounded focus:border focus:border-edge focus:bg-surface-nav focus:px-4 focus:py-2 focus:text-content-primary"
>
Skip to main content
{t("skipToMain")}
</a>
<ToastProvider>
<Shell>{children}</Shell>
</ToastProvider>
<NextIntlClientProvider messages={messages}>
<ToastProvider>
<Shell>{children}</Shell>
</ToastProvider>
</NextIntlClientProvider>
</body>
</html>
);
Expand Down
82 changes: 50 additions & 32 deletions apps/studio/frontend/app/runs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Link from "next/link";
import { Fragment, Suspense, useEffect, useMemo, useState } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import Button from "@/components/Button";
import Duration from "@/components/Duration";
import PageHeader from "@/components/PageHeader";
Expand All @@ -11,7 +12,6 @@
import { listRuns } from "@/lib/api";
import type { RunListResponse } from "@/lib/api";
import type { RunSummary } from "@/lib/types";
import { empty, errors } from "@/lib/copy";

// ADR-0025: six-value session vocabulary (running/completed/failed/
// timed_out/aborted/cancelled). "done" stays as an alias for completed
Expand All @@ -26,6 +26,17 @@
"cancelled",
] as const;

// Maps raw status code to the catalog key under runs.filters.status.*
const STATUS_KEY_MAP: Record<string, string> = {
pending: "filters.status.pending",
running: "filters.status.running",
done: "filters.status.done",
failed: "filters.status.failed",
timed_out: "filters.status.timedOut",
aborted: "filters.status.aborted",
cancelled: "filters.status.cancelled",
};

type ViewMode = "sessions" | "invocations";

interface InvocationGroup {
Expand Down Expand Up @@ -112,16 +123,18 @@

function StatusFilterChip({
value,
label,
active,
onClick,
}: {
value: string;
label: string;
active: boolean;
onClick: () => void;
}) {
return (
<Button variant="toggle" size="sm" active={active} onClick={onClick}>
{value}
{label}
</Button>
);
}
Expand Down Expand Up @@ -150,6 +163,7 @@
now: number;
indent?: boolean;
}) {
const t = useTranslations("runs");
const prov = provenanceLabel(run);
const durSec = durationSeconds(run, now);
return (
Expand All @@ -166,7 +180,10 @@
{run.project && (
<span
className="rounded px-1 py-0.5 font-mono text-[10px] border border-edge text-content-muted"
title={`Project: ${run.project} (${run.project_source ?? "unknown"})`}
title={t("row.projectTitle", {
project: run.project,
source: run.project_source ?? "unknown",
})}
>
{run.project}
</span>
Expand All @@ -191,7 +208,7 @@
<Duration value={durSec} />
<span
className="rounded px-1 py-0.5 font-mono text-[10px] border border-edge text-content-muted"
title={`Source: ${run.source_kind ?? "live"}`}
title={t("row.sourceTitle", { source: run.source_kind ?? "live" })}
>
{prov}
</span>
Expand All @@ -205,6 +222,7 @@
}

function RunsPageInner() {
const t = useTranslations("runs");
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
Expand Down Expand Up @@ -280,7 +298,7 @@
}
}
} catch {
if (active) setError(errors.loadRuns);
if (active) setError(t("errors.load"));
} finally {
if (active) setLoading(false);
}
Expand Down Expand Up @@ -318,7 +336,7 @@
});
}

const runs = data?.runs ?? [];

Check warning on line 339 in apps/studio/frontend/app/runs/page.tsx

View workflow job for this annotation

GitHub Actions / frontend

The 'runs' logical expression could make the dependencies of useMemo Hook (at line 345) change on every render. To fix this, wrap the initialization of 'runs' in its own useMemo() Hook
const total = data?.total ?? 0;
const totalPages = data?.total_pages ?? 1;

Expand All @@ -330,13 +348,13 @@
return (
<main className="mx-auto flex w-full max-w-7xl flex-col gap-5 px-4 py-6 animate-page-enter">
<PageHeader
title="Runs"
subtitle="Live and completed agent sessions"
title={t("title")}
subtitle={t("subtitle")}
density="tight"
badges={
!loading ? (
<span className="text-meta text-content-muted tabular-nums">
{total} run{total !== 1 ? "s" : ""}
{t("count", { total })}
</span>
) : null
}
Expand All @@ -352,15 +370,15 @@
active={viewMode === "sessions"}
onClick={() => setViewMode("sessions")}
>
Sessions
{t("view.sessions")}
</Button>
<Button
size="sm"
variant="toggle"
active={viewMode === "invocations"}
onClick={() => setViewMode("invocations")}
>
Invocations
{t("view.invocations")}
</Button>
</div>
{/* ADR-0026: project filter chips */}
Expand All @@ -372,7 +390,7 @@
active={!project}
onClick={() => setQuery({ project: "", page: 1 })}
>
All
{t("filters.project.all")}
</Button>
{knownProjects.map((p) => (
<Button
Expand All @@ -392,14 +410,14 @@
<div className="flex items-center gap-1.5">
<input
type="text"
placeholder="Filter by playbook..."
placeholder={t("filters.playbook.placeholder")}
value={playbookInput}
onChange={(e) => setPlaybookInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && applyPlaybookFilter()}
className="h-7 rounded border border-edge bg-surface-raised px-2.5 text-meta text-content-primary placeholder:text-content-muted focus:border-interactive-primary focus:outline-none"
/>
<Button size="sm" variant="secondary" onClick={applyPlaybookFilter}>
Search
{t("filters.playbook.search")}
</Button>
{playbook && (
<Button
Expand All @@ -410,7 +428,7 @@
setQuery({ playbook: "", page: 1 });
}}
>
Clear
{t("filters.playbook.clear")}
</Button>
)}
</div>
Expand All @@ -419,6 +437,7 @@
<StatusFilterChip
key={s}
value={s}
label={t(STATUS_KEY_MAP[s] as Parameters<typeof t>[0])}
active={statuses.includes(s)}
onClick={() => toggleStatus(s)}
/>
Expand All @@ -438,13 +457,13 @@
<thead>
<tr className="border-b border-edge bg-surface-overlay text-meta uppercase tracking-[0.06em] text-content-muted">
<th className="px-3 py-2.5 font-medium">
{viewMode === "invocations" ? "Invocation" : "Run"}
{viewMode === "invocations" ? t("table.invocation") : t("table.run")}
</th>
<th className="px-3 py-2.5 font-medium">Status</th>
<th className="px-3 py-2.5 font-medium">{t("table.status")}</th>
<th className="px-3 py-2.5 font-medium">
{viewMode === "invocations" ? "Sessions" : "Activity"}
{viewMode === "invocations" ? t("table.sessions") : t("table.activity")}
</th>
<th className="px-3 py-2.5 font-medium">Updated</th>
<th className="px-3 py-2.5 font-medium">{t("table.updated")}</th>
</tr>
</thead>
<tbody>
Expand All @@ -458,9 +477,9 @@
invocationGroups.length === 0 && ungroupedRuns.length === 0 ? (
<tr>
<td colSpan={6} className="px-3 py-14 text-center text-body text-content-muted">
<span className="block mb-1 text-[11px]">{empty.runs}</span>
<span className="block mb-1 text-[11px]">{t("empty.none")}</span>
{(statuses.length > 0 || playbook) && (
<span className="text-meta">Try adjusting your filters.</span>
<span className="text-meta">{t("empty.adjustFilters")}</span>
)}
</td>
</tr>
Expand All @@ -481,7 +500,10 @@
role="button"
tabIndex={0}
aria-expanded={isExpanded}
aria-label={`Invocation ${group.invocation_id.slice(-8)}, ${group.sessions.length} session${group.sessions.length !== 1 ? "s" : ""}`}
aria-label={t("invocation.ariaLabel", {
id: group.invocation_id.slice(-8),
count: group.sessions.length,
})}
className="border-b border-edge-subtle bg-surface-overlay/50 text-content-primary cursor-pointer transition-colors duration-100 hover:bg-surface-overlay"
onClick={() => toggleExpand(group.invocation_id)}
onKeyDown={(e) => {
Expand All @@ -500,7 +522,7 @@
{isExpanded ? "▼" : "▶"}
</span>
<span className="font-medium">
Invocation{" "}
{t("invocation.label")}{" "}
<span className="font-mono text-meta text-content-muted">
{group.invocation_id.slice(-8)}
</span>
Expand All @@ -519,8 +541,7 @@
)}
</td>
<td className="px-3 py-2 text-content-secondary">
{group.sessions.length} session
{group.sessions.length !== 1 ? "s" : ""}
{t("invocation.sessionCount", { count: group.sessions.length })}
</td>
<td className="px-3 py-2 text-meta text-content-muted">
<Timestamp value={latestUpdated} />
Expand All @@ -541,9 +562,9 @@
) : runs.length === 0 ? (
<tr>
<td colSpan={4} className="px-3 py-14 text-center text-body text-content-muted">
<span className="block mb-1 text-[11px]">{empty.runs}</span>
<span className="block mb-1 text-[11px]">{t("empty.none")}</span>
{(statuses.length > 0 || playbook) && (
<span className="text-meta">Try adjusting your filters.</span>
<span className="text-meta">{t("empty.adjustFilters")}</span>
)}
</td>
</tr>
Expand All @@ -557,26 +578,23 @@
{/* Pagination — hidden in invocations mode (full dataset loaded client-side) */}
{viewMode === "sessions" && (
<div className="flex items-center justify-between text-meta text-content-muted">
<span>
Page {page} of {totalPages || 1} &mdash; {total} run
{total !== 1 ? "s" : ""}
</span>
<span>{t("pagination.summary", { page, totalPages: totalPages || 1, total })}</span>
<div className="flex gap-2">
<Button
size="sm"
variant="secondary"
disabled={!data?.has_prev}
onClick={() => setQuery({ page: page - 1 })}
>
Previous
{t("pagination.previous")}
</Button>
<Button
size="sm"
variant="secondary"
disabled={!data?.has_next}
onClick={() => setQuery({ page: page + 1 })}
>
Next
{t("pagination.next")}
</Button>
</div>
</div>
Expand Down
Loading
Loading