feat(web): build read-only operator views

This commit is contained in:
2026-03-20 11:56:29 +08:00
parent ce9061ca54
commit f58f48f0d5
12 changed files with 1997 additions and 211 deletions
+1
View File
@@ -3,6 +3,7 @@
bin/ bin/
dist/ dist/
node_modules/ node_modules/
.pnpm-store/
apps/web/dist/ apps/web/dist/
apps/web/.vite/ apps/web/.vite/
*.db *.db
+3
View File
@@ -24,9 +24,12 @@
"tailwind-merge": "^3.5.0" "tailwind-merge": "^3.5.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^24.5.2",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^8.0.1" "vite": "^8.0.1"
} }
+153 -56
View File
@@ -6,24 +6,65 @@ import {
createRootRoute, createRootRoute,
createRoute, createRoute,
createRouter, createRouter,
useRouterState,
} from '@tanstack/react-router'; } from '@tanstack/react-router';
const queryClient = new QueryClient(); import { cn } from './cadence-ui/lib/cn';
import {
BlockedQueuePage,
RunDetailPage,
RunsPage,
ThreadTimelinePage,
} from './features/operator-console';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5_000,
refetchOnWindowFocus: true,
retry: 1,
},
},
});
const rootRoute = createRootRoute({ const rootRoute = createRootRoute({
component: RootLayout, component: RootLayout,
}); });
const indexRoute = createRoute({ const runsRoute = createRoute({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
path: '/', path: '/',
component: HomePage, component: RunsPage,
}); });
const routeTree = rootRoute.addChildren([indexRoute]); const blockedQueueRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/blocked',
component: BlockedQueuePage,
});
const runDetailRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/runs/$runId',
component: RunDetailRoute,
});
const threadTimelineRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/threads/$threadId',
component: ThreadTimelineRoute,
});
const routeTree = rootRoute.addChildren([
runsRoute,
blockedQueueRoute,
runDetailRoute,
threadTimelineRoute,
]);
const router = createRouter({ const router = createRouter({
routeTree, routeTree,
defaultPreload: 'intent',
}); });
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
@@ -42,63 +83,119 @@ export function App() {
function RootLayout() { function RootLayout() {
return ( return (
<div className="shell"> <div className="relative min-h-screen overflow-hidden" data-theme="brand">
<header className="masthead"> <div className="pointer-events-none fixed inset-0">
<div> <div className="absolute inset-x-0 top-0 h-[38rem] bg-[radial-gradient(circle_at_top,oklch(0.9_0.06_174/.72),transparent_60%)]" />
<p className="eyebrow">AI Workflow Skill</p> <div className="absolute inset-y-0 right-0 w-[32rem] bg-[radial-gradient(circle_at_center,oklch(0.82_0.14_88/.18),transparent_56%)]" />
<Link className="brand" to="/"> <div className="absolute inset-0 opacity-35 [background-image:linear-gradient(to_right,color-mix(in_oklch,var(--color-border)_46%,transparent)_1px,transparent_1px),linear-gradient(to_bottom,color-mix(in_oklch,var(--color-border)_46%,transparent)_1px,transparent_1px)] [background-size:72px_72px]" />
Orch Control Plane </div>
</Link>
<div className="relative mx-auto flex min-h-screen w-full max-w-[1500px] flex-col gap-6 px-4 py-4 lg:flex-row lg:px-6 lg:py-6">
<aside className="hidden lg:flex lg:w-[18rem] lg:shrink-0">
<div className="flex h-[calc(100vh-3rem)] w-full flex-col rounded-[2rem] border border-[color-mix(in_oklch,var(--color-border)_80%,white)] bg-[color-mix(in_oklch,var(--color-card)_84%,white_16%)] p-5 shadow-[var(--shadow-md)] backdrop-blur">
<div className="space-y-4">
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.28em] text-[var(--color-primary)]">
AI Workflow Skill
</p>
<div className="space-y-3">
<Link
className="type-display text-[2.4rem] leading-[0.92] tracking-[-0.05em] text-[var(--color-foreground)]"
to="/"
>
Orch Control Plane
</Link>
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
Read-only operator surfaces for runs, blocked work, and thread history.
</p>
</div>
</div>
<nav className="mt-8 grid gap-2">
<NavLink exact label="Runs overview" to="/" />
<NavLink label="Blocked queue" to="/blocked" />
</nav>
<div className="mt-auto rounded-[1.5rem] border border-[color-mix(in_oklch,var(--color-border)_78%,transparent)] bg-[color-mix(in_oklch,var(--color-surface)_82%,white_18%)] p-4">
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-[var(--color-muted-foreground)]">
Current slice
</p>
<ul className="mt-3 grid gap-2 text-sm leading-6 text-[var(--color-foreground)]">
<li>Runs list with health counts</li>
<li>Run detail with task board</li>
<li>Global blocked queue</li>
<li>Thread timeline and artifacts</li>
</ul>
</div>
</div>
</aside>
<div className="flex min-w-0 flex-1 flex-col gap-4">
<header className="rounded-[1.75rem] border border-[color-mix(in_oklch,var(--color-border)_80%,white)] bg-[color-mix(in_oklch,var(--color-card)_84%,white_16%)] px-5 py-4 shadow-[var(--shadow-sm)] backdrop-blur lg:hidden">
<div className="flex flex-col gap-4">
<div className="space-y-2">
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.28em] text-[var(--color-primary)]">
AI Workflow Skill
</p>
<Link
className="type-display block text-[2rem] leading-[0.94] tracking-[-0.05em] text-[var(--color-foreground)]"
to="/"
>
Orch Control Plane
</Link>
</div>
<div className="flex flex-wrap gap-2">
<NavLink exact label="Runs overview" to="/" />
<NavLink label="Blocked queue" to="/blocked" />
</div>
</div>
</header>
<main className="min-w-0 flex-1">
<Outlet />
</main>
</div> </div>
<p className="masthead-copy"> </div>
Phase 1 keeps the UI thin while the backend contract settles around
`orchd`.
</p>
</header>
<main className="content">
<Outlet />
</main>
</div> </div>
); );
} }
function HomePage() { function NavLink({
exact = false,
label,
to,
}: {
exact?: boolean;
label: string;
to: string;
}) {
const pathname = useRouterState({
select: (state) => state.location.pathname,
});
const isActive = exact ? pathname === to : pathname === to || pathname.startsWith(`${to}/`);
return ( return (
<section className="hero-grid"> <Link
<article className="hero-card hero-card-primary"> className={cn(
<p className="eyebrow">Current slice</p> 'inline-flex items-center gap-2 rounded-full border px-3.5 py-2 text-sm font-medium transition-colors',
<h1>Read-only operator shell for a future multi-user web product.</h1> 'border-[color-mix(in_oklch,var(--color-border)_76%,transparent)] bg-transparent text-[var(--color-muted-foreground)]',
<p className="lede"> 'hover:border-[var(--color-border-strong)] hover:bg-[color-mix(in_oklch,var(--color-surface)_78%,white_22%)] hover:text-[var(--color-foreground)]',
The monorepo now has a dedicated React app, a Go HTTP service, and a isActive &&
first API contract for runs, blocked work, and thread history. 'border-[color-mix(in_oklch,var(--color-primary)_32%,var(--color-border))] bg-[color-mix(in_oklch,var(--color-primary)_12%,white)] text-[var(--color-primary)]',
</p> )}
</article> to={to}
>
<article className="hero-card hero-card-secondary"> <span className="h-1.5 w-1.5 rounded-full bg-current" />
<h2>Backend spine</h2> {label}
<ul className="detail-list"> </Link>
<li>`cmd/orchd` serves `chi` routes against the existing SQLite state.</li>
<li>`internal/query` shapes run, blocked-task, and thread reads.</li>
<li>`api/openapi.yaml` is the contract anchor for future typed clients.</li>
</ul>
</article>
<article className="hero-card">
<h2>Frontend posture</h2>
<p>
React, Vite, TanStack Router, and TanStack Query are present now so
Phase 2 can focus on actual operator views instead of build plumbing.
</p>
</article>
<article className="hero-card">
<h2>Next UI targets</h2>
<ul className="detail-list">
<li>Runs dashboard with task-count health signals.</li>
<li>Run detail board grouped by orchestration state.</li>
<li>Blocked queue and thread timeline wired to live refresh.</li>
</ul>
</article>
</section>
); );
} }
function RunDetailRoute() {
const { runId } = runDetailRoute.useParams();
return <RunDetailPage runId={runId} />;
}
function ThreadTimelineRoute() {
const { threadId } = threadTimelineRoute.useParams();
return <ThreadTimelinePage threadId={threadId} />;
}
File diff suppressed because it is too large Load Diff
+198
View File
@@ -0,0 +1,198 @@
export type Run = {
run_id: string;
goal: string;
summary: string;
status: string;
created_at: string;
updated_at: string;
};
export type RunListItem = {
run: Run;
task_counts: Record<string, number>;
total_tasks: number;
};
export type Task = {
run_id: string;
task_id: string;
title: string;
summary: string;
status: string;
default_to?: string;
priority: string;
acceptance_json: unknown;
latest_attempt_no?: number;
created_at: string;
updated_at: string;
};
export type TaskAttempt = {
run_id: string;
task_id: string;
attempt_no: number;
assigned_to: string;
thread_id: string;
base_ref?: string;
base_commit?: string;
branch_name?: string;
worktree_path?: string;
workspace_status?: string;
result_commit?: string;
status: string;
created_at: string;
updated_at: string;
};
export type Artifact = {
artifact_id: string;
message_id: string;
path: string;
kind: string;
metadata_json: unknown;
created_at: string;
};
export type Message = {
message_id: string;
thread_id: string;
from_agent: string;
to_agent: string;
kind: string;
summary: string;
body: string;
payload_json: unknown;
created_at: string;
artifacts: Artifact[];
};
export type Thread = {
thread_id: string;
run_id: string;
task_id: string;
subject: string;
created_by: string;
assigned_to: string;
status: string;
priority: string;
latest_message_id?: string;
created_at: string;
updated_at: string;
};
export type ThreadDetail = {
thread: Thread;
messages: Message[];
};
export type BlockedTask = {
task: Task;
attempt: TaskAttempt;
question: Message;
};
export type BlockedQueueItem = BlockedTask & {
run: Run;
};
export type RunDetail = {
run: Run;
task_counts: Record<string, number>;
total_tasks: number;
tasks: Task[];
blocked_tasks: BlockedTask[];
};
type ErrorEnvelope = {
error?: {
code?: string;
message?: string;
};
};
class ApiError extends Error {
code?: string;
status: number;
constructor(message: string, status: number, code?: string) {
super(message);
this.name = 'ApiError';
this.code = code;
this.status = status;
}
}
const apiBaseUrl = (import.meta.env.VITE_ORCH_API_BASE_URL ?? '').replace(/\/$/, '');
async function request<T>(path: string): Promise<T> {
const response = await fetch(`${apiBaseUrl}${path}`, {
cache: 'no-store',
headers: {
Accept: 'application/json',
},
});
if (!response.ok) {
let message = `Request failed with status ${response.status}`;
let code: string | undefined;
try {
const payload = (await response.json()) as ErrorEnvelope;
message = payload.error?.message ?? message;
code = payload.error?.code;
} catch {
// Ignore malformed error payloads and fall back to the HTTP status.
}
throw new ApiError(message, response.status, code);
}
return (await response.json()) as T;
}
export async function listRuns() {
const payload = await request<{ runs: RunListItem[] }>('/api/runs');
return payload.runs;
}
export async function getRunDetail(runId: string) {
const payload = await request<{ run: RunDetail }>(`/api/runs/${encodeURIComponent(runId)}`);
return payload.run;
}
export async function listRunBlocked(runId: string) {
const payload = await request<{ blocked: BlockedTask[] }>(
`/api/runs/${encodeURIComponent(runId)}/blocked`,
);
return payload.blocked;
}
export async function getThreadDetail(threadId: string) {
const payload = await request<{ thread: ThreadDetail }>(
`/api/threads/${encodeURIComponent(threadId)}`,
);
return {
...payload.thread,
messages: payload.thread.messages.map((message) => ({
...message,
artifacts: message.artifacts ?? [],
})),
};
}
export async function listBlockedQueue() {
const runs = await listRuns();
const blockedByRun = await Promise.all(
runs.map(async (item) => {
const blocked = await listRunBlocked(item.run.run_id);
return blocked.map((entry) => ({
...entry,
run: item.run,
}));
}),
);
return blockedByRun
.flat()
.sort((left, right) => Date.parse(right.question.created_at) - Date.parse(left.question.created_at));
}
+70
View File
@@ -0,0 +1,70 @@
export function formatDateTime(value: string) {
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return value;
}
return new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short',
}).format(parsed);
}
export function statusLabel(value: string) {
return value.replaceAll('_', ' ');
}
export function pluralize(count: number, singular: string) {
return `${count} ${count === 1 ? singular : `${singular}s`}`;
}
export function hasStructuredData(value: unknown): boolean {
if (value == null) {
return false;
}
if (typeof value === 'string') {
return value.trim().length > 0 && value.trim() !== '{}' && value.trim() !== '[]';
}
if (Array.isArray(value)) {
return value.length > 0;
}
if (typeof value === 'object') {
return Object.keys(value as Record<string, unknown>).length > 0;
}
return true;
}
export function formatJson(value: unknown) {
if (typeof value === 'string') {
const trimmed = value.trim();
if (!trimmed) {
return '';
}
try {
return JSON.stringify(JSON.parse(trimmed), null, 2);
} catch {
return value;
}
}
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
export function uniqueCount<T>(items: T[], select: (item: T) => string | undefined) {
const values = items
.map(select)
.filter((value): value is string => Boolean(value));
return new Set(values).size;
}
+15 -133
View File
@@ -1,33 +1,25 @@
@import "tailwindcss";
@source "./**/*.{ts,tsx}";
:root { :root {
color-scheme: light; color-scheme: light;
font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
line-height: 1.5;
font-weight: 400;
background:
radial-gradient(circle at top left, rgba(255, 196, 98, 0.32), transparent 28%),
radial-gradient(circle at bottom right, rgba(8, 145, 178, 0.16), transparent 26%),
linear-gradient(135deg, #f4efe3 0%, #f9f7f1 55%, #eef3f4 100%);
color: #172026;
--border: rgba(23, 32, 38, 0.12);
--panel: rgba(255, 255, 255, 0.82);
--panel-strong: rgba(255, 255, 255, 0.92);
--ink-soft: rgba(23, 32, 38, 0.7);
--accent: #c96f20;
--accent-deep: #0f766e;
} }
* { html {
box-sizing: border-box; min-height: 100%;
scroll-behavior: smooth;
} }
body { body {
margin: 0;
min-width: 320px; min-width: 320px;
min-height: 100vh; background:
radial-gradient(circle at top, oklch(0.92 0.06 175 / 0.86), transparent 35%),
radial-gradient(circle at right center, oklch(0.84 0.13 88 / 0.22), transparent 30%),
linear-gradient(135deg, oklch(0.975 0.012 172) 0%, oklch(0.962 0.015 164) 45%, oklch(0.988 0.006 92) 100%);
background-attachment: fixed;
} }
a { a {
color: inherit;
text-decoration: none; text-decoration: none;
} }
@@ -35,120 +27,10 @@ a {
min-height: 100vh; min-height: 100vh;
} }
.shell { .type-display {
min-height: 100vh; font-family: var(--font-display);
padding: 32px 20px 48px;
} }
.masthead { .type-mono {
display: grid; font-family: var(--font-mono);
gap: 18px;
align-items: end;
max-width: 1200px;
margin: 0 auto 28px;
}
.brand {
display: inline-block;
font-family: "Iowan Old Style", "Palatino Linotype", serif;
font-size: clamp(2rem, 4vw, 3.2rem);
letter-spacing: -0.04em;
}
.eyebrow {
margin: 0 0 10px;
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 0.78rem;
color: var(--accent-deep);
}
.masthead-copy {
max-width: 34rem;
margin: 0;
color: var(--ink-soft);
}
.content {
max-width: 1200px;
margin: 0 auto;
}
.hero-grid {
display: grid;
gap: 18px;
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.hero-card {
grid-column: span 12;
padding: 24px;
border: 1px solid var(--border);
border-radius: 24px;
background: var(--panel);
backdrop-filter: blur(12px);
box-shadow: 0 20px 50px rgba(23, 32, 38, 0.08);
}
.hero-card-primary {
background:
linear-gradient(145deg, rgba(255, 255, 255, 0.92), rgba(255, 248, 237, 0.94));
}
.hero-card-secondary {
background:
linear-gradient(145deg, rgba(240, 249, 255, 0.95), rgba(240, 253, 250, 0.9));
}
.hero-card h1,
.hero-card h2 {
margin: 0 0 12px;
font-family: "Iowan Old Style", "Palatino Linotype", serif;
letter-spacing: -0.03em;
}
.hero-card h1 {
font-size: clamp(2rem, 4.4vw, 4.4rem);
line-height: 0.94;
max-width: 12ch;
}
.hero-card h2 {
font-size: 1.5rem;
}
.lede,
.hero-card p {
margin: 0;
max-width: 48rem;
color: var(--ink-soft);
}
.detail-list {
margin: 0;
padding-left: 18px;
color: var(--ink-soft);
}
.detail-list li + li {
margin-top: 10px;
}
@media (min-width: 840px) {
.masthead {
grid-template-columns: 1.2fr 0.8fr;
}
.hero-card-primary {
grid-column: span 7;
min-height: 420px;
}
.hero-card-secondary {
grid-column: span 5;
}
.hero-card:not(.hero-card-primary):not(.hero-card-secondary) {
grid-column: span 6;
}
} }
+8
View File
@@ -1 +1,9 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_ORCH_API_BASE_URL?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
+6 -3
View File
@@ -1,14 +1,17 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
const proxyTarget = process.env.ORCHD_PROXY_TARGET ?? 'http://127.0.0.1:8080';
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [tailwindcss(), react()],
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
port: 5173, port: 5173,
proxy: { proxy: {
'/api': 'http://localhost:8080', '/api': proxyTarget,
'/health': 'http://localhost:8080', '/health': proxyTarget,
}, },
}, },
}); });
+24 -14
View File
@@ -35,6 +35,7 @@ As of now:
- `orchd` now serves a minimal read-only web API with `chi`, including `/health`, runs list/detail, run task list, blocked-task list, and thread detail endpoints backed by the existing SQLite state - `orchd` now serves a minimal read-only web API with `chi`, including `/health`, runs list/detail, run task list, blocked-task list, and thread detail endpoints backed by the existing SQLite state
- HTTP tests now cover the initial read-only `orchd` slice, and the new frontend workspace builds successfully with `pnpm run web:build` - HTTP tests now cover the initial read-only `orchd` slice, and the new frontend workspace builds successfully with `pnpm run web:build`
- Phase 2 frontend work has now started by bootstrapping `apps/web` with copied-in `cadence-ui` tokens and foundational components for button, input, textarea, dialog, form, tabs, card, badge, and alert, with the shared token stylesheet loaded from the frontend entrypoint - Phase 2 frontend work has now started by bootstrapping `apps/web` with copied-in `cadence-ui` tokens and foundational components for button, input, textarea, dialog, form, tabs, card, badge, and alert, with the shared token stylesheet loaded from the frontend entrypoint
- the first real Phase 2 read-only operator UI is now implemented in `apps/web`, including routed runs list, run detail, blocked queue, and thread timeline views backed by the existing `orchd` HTTP API, plus Tailwind v4 consumer wiring so the source-owned Cadence UI components render correctly in the app
- a repo-local `scripts/package_skill_clis.sh` packaging flow now builds bundled skill CLI assets for `inbox`, `orch`, and `council-review` - a repo-local `scripts/package_skill_clis.sh` packaging flow now builds bundled skill CLI assets for `inbox`, `orch`, and `council-review`
- `orch` now implements `run init/show`, `task add`, `dep add`, `ready`, `dispatch`, `reconcile`, `wait`, `blocked`, `answer`, `retry`, `reassign`, `cancel`, `cleanup`, and `status` - `orch` now implements `run init/show`, `task add`, `dep add`, `ready`, `dispatch`, `reconcile`, `wait`, `blocked`, `answer`, `retry`, `reassign`, `cancel`, `cleanup`, and `status`
- `orch` can create runs, gate tasks through dependencies, dispatch work through `inbox`, reconcile worker thread state back into task state, answer blocked tasks, retry or reassign work, cancel tasks or runs, clean attempt worktrees, and create per-attempt Git worktrees during strict dispatch - `orch` can create runs, gate tasks through dependencies, dispatch work through `inbox`, reconcile worker thread state back into task state, answer blocked tasks, retry or reassign work, cancel tasks or runs, clean attempt worktrees, and create per-attempt Git worktrees during strict dispatch
@@ -93,9 +94,9 @@ Current implementation status:
- `Milestone 6: Waiting Primitives` is complete - `Milestone 6: Waiting Primitives` is complete
- `Milestone 7: Council Review` is complete - `Milestone 7: Council Review` is complete
- `Milestone 8: Web Product Phase 1 Skeleton` is complete - `Milestone 8: Web Product Phase 1 Skeleton` is complete
- `Milestone 9: Web Product Phase 2 UI Foundation` is in progress - `Milestone 9: Web Product Phase 2 Read-Only Operator UI` is complete for the initial operator surface
The council review v1 surface is complete, the first web-product skeleton now exists as a separate monorepo workspace plus read-only HTTP backend slice, and Phase 2 frontend work has started on top of the internal Cadence UI component library. The council review v1 surface is complete, the first web-product skeleton now exists as a separate monorepo workspace plus read-only HTTP backend slice, and the first real operator-facing Phase 2 read-only web views now exist on top of the internal Cadence UI component library.
### Milestone 1: Go Skeleton ### Milestone 1: Go Skeleton
@@ -416,38 +417,45 @@ Remaining:
- Phase 2 should turn the frontend shell into actual run, task-board, blocked-queue, and thread-detail pages using the new HTTP contract - Phase 2 should turn the frontend shell into actual run, task-board, blocked-queue, and thread-detail pages using the new HTTP contract
### Milestone 9: Web Product Phase 2 UI Foundation ### Milestone 9: Web Product Phase 2 Read-Only Operator UI
Goal: Goal:
- bootstrap the frontend UI layer on top of the Phase 1 shell and read-only backend contract - implement the first real operator-facing read-only web UI on top of the Phase 1 shell and current `orchd` API contract
Add: Add:
- copied-in `cadence-ui` primitives and token CSS under `apps/web/src/cadence-ui` - copied-in `cadence-ui` primitives and token CSS under `apps/web/src/cadence-ui`
- app-wide token style wiring in the frontend entrypoint - Tailwind v4 consumer setup so the copied-in Cadence UI source renders correctly in the app
- any additional component installs needed as real screens land - routed screens for runs list, run detail, blocked queue, and thread timeline
- typed frontend read helpers for the current `orchd` endpoints
Definition of done: Definition of done:
- `apps/web` imports the shared Cadence token stylesheet from the frontend entrypoint - `apps/web` imports the shared Cadence token stylesheet from the frontend entrypoint
- the initial shared component set builds successfully inside the workspace - the Cadence UI source-owned components render correctly inside the consumer app
- the first routed read-only operator screens ship against the existing `orchd` contract
- future web screens can compose from `cadence-ui` primitives instead of raw one-off HTML controls - future web screens can compose from `cadence-ui` primitives instead of raw one-off HTML controls
Status: Status:
- in progress - completed for the first read-only operator slice
Completed so far: Completed so far:
- `apps/web/src/cadence-ui/` now contains copied-in Cadence UI tokens plus foundational components for button, input, textarea, dialog, form, tabs, card, badge, and alert - `apps/web/src/cadence-ui/` now contains copied-in Cadence UI tokens plus foundational components for button, input, textarea, dialog, form, tabs, card, badge, and alert
- `apps/web/package.json` now includes the required Radix, `react-hook-form`, `motion`, and utility dependencies for the copied-in components - `apps/web/package.json` now includes the required Radix, `react-hook-form`, `motion`, and utility dependencies for the copied-in components
- `apps/web/src/main.tsx` now imports `./cadence-ui/tokens/styles.css` - `apps/web/src/main.tsx` now imports `./cadence-ui/tokens/styles.css`
- `pnpm install` refreshed the workspace lockfile, and `pnpm run web:build` succeeds with the copied-in component slice - `apps/web` now includes Tailwind v4 consumer wiring in Vite and the global stylesheet so the copied-in Cadence UI utility classes render correctly
- `apps/web` now includes a typed frontend read layer for runs, run detail, blocked queue aggregation, and thread detail
- `apps/web` now ships routed runs list, run detail, blocked queue, and thread timeline pages using Cadence UI source-owned components for cards, tabs, alerts, inputs, badges, buttons, dialogs, and textareas
- the run detail view now includes grouped task boards and blocked-task summaries, while the thread timeline view now shows message payload and artifact metadata inspectors
- `pnpm run web:build` succeeds for the new operator UI, and local Vite-to-`orchd` proxy smoke checks confirm the frontend can read the seeded runs, blocked, and thread endpoints through the dev server
Remaining: Remaining:
- build the actual runs list, run detail, blocked queue, and thread timeline screens on top of the Cadence UI primitives - add operator write actions such as answer, retry, reassign, and cancel on top of the new read-only screens
- add council result/report views and realtime event handling on top of the current routed UI
- install additional `cadence-ui` components on demand as the product surface expands - install additional `cadence-ui` components on demand as the product surface expands
## Immediate Next Task ## Immediate Next Task
@@ -456,11 +464,13 @@ If a new agent is taking over now, the next concrete step should be:
1. treat `Milestone 8: Web Product Phase 1 Skeleton` as complete unless a new user request reopens the backend skeleton itself 1. treat `Milestone 8: Web Product Phase 1 Skeleton` as complete unless a new user request reopens the backend skeleton itself
2. keep the authored inbox test-plan set in `docs/tests/inbox/` synchronized if future `orch` or web work changes shared CLI-visible behavior 2. keep the authored inbox test-plan set in `docs/tests/inbox/` synchronized if future `orch` or web work changes shared CLI-visible behavior
3. continue `Milestone 9: Web Product Phase 2 UI Foundation` by implementing the first runs list, run detail, blocked queue, and thread timeline screens on top of the existing `apps/web` and `orchd` contract 3. treat `Milestone 9: Web Product Phase 2 Read-Only Operator UI` as complete for the initial operator surface and build the next web slice on top of the shipped read pages rather than replacing them
4. install additional `cadence-ui` components on demand when those screens need them, instead of reintroducing bespoke primitives into `apps/web` 4. start the next web phase by wiring operator write actions such as answer, retry, reassign, and cancel into the existing runs, blocked, and thread views
5. keep `api/openapi.yaml`, `api/events.md`, and `docs/web-product-monorepo.md` synchronized as the web surface expands 5. add council result/report screens and realtime event handling after the operator write path is clear
6. install additional `cadence-ui` components on demand when those screens need them, instead of reintroducing bespoke primitives into `apps/web`
7. keep `api/openapi.yaml`, `api/events.md`, and `docs/web-product-monorepo.md` synchronized as the web surface expands
The inbox implementation and its human-readable test-plan set are already in place, `orch` supports the main scheduler loop plus the complete council start/wait/tally/report workflow, and the web product is now past the bare frontend shell stage, so the next step should be actual Phase 2 product screens built on top of the Cadence UI foundation rather than reopening earlier milestones. The inbox implementation and its human-readable test-plan set are already in place, `orch` supports the main scheduler loop plus the complete council start/wait/tally/report workflow, and the web product now has its first real operator-facing read surfaces, so the next step should be write-capable operator workflows and council/realtime expansion rather than reopening the frontend shell or basic read pages.
## Recommended Driver Choices ## Recommended Driver Choices
@@ -0,0 +1,70 @@
# Web Phase 2 Read-Only UI
## Status
- `completed`
## Owner
- Codex
## Started At
- `2026-03-20`
## Goal
- Implement the first real Phase 2 operator UI in `apps/web` by shipping the read-only runs list, run detail, blocked queue, and thread timeline views on top of the existing `orchd` HTTP API and Cadence UI source-owned components.
## Scope
- create an execution trace for the Phase 2 read-only UI workstream
- wire the frontend to the existing read-only API contract
- implement routed views for runs list, run detail, blocked queue, and thread timeline
- reuse Cadence UI source-owned components instead of introducing new foundational UI primitives
- validate the frontend build and keep `docs/implementation-roadmap.md` synchronized
## Checklist
- [x] create the active execution roadmap for the Phase 2 read-only UI workstream
- [x] inspect the current frontend shell and backend read contract
- [x] add the frontend data layer and route structure for the Phase 2 read-only views
- [x] implement the runs list, run detail, blocked queue, and thread timeline screens
- [x] ensure Cadence UI styles render correctly in the consumer app
- [x] validate the frontend build and fix any type or integration issues
- [x] update `docs/implementation-roadmap.md`
- [x] archive this roadmap with a completion summary if the slice is fully complete
## Files
- `docs/roadmaps/archive/web-phase2-read-only-ui.md`
- `docs/implementation-roadmap.md`
- `apps/web/package.json`
- `apps/web/src/app.tsx`
- `apps/web/src/features/operator-console.tsx`
- `apps/web/src/lib/api.ts`
- `apps/web/src/lib/format.ts`
- `apps/web/src/styles.css`
- `apps/web/src/vite-env.d.ts`
- `apps/web/vite.config.ts`
- `pnpm-lock.yaml`
## Decisions
- keep the first Phase 2 UI slice read-only and route it against the existing HTTP API instead of coupling UI delivery to new backend mutations
- prefer composition from the copied-in Cadence UI source over new hand-rolled UI primitives
## Blockers
- none
## Next Step
- build the next web slice on top of the shipped read-only screens by adding operator write actions, council views, and realtime event handling without replacing the new routed operator shell
## Completion Summary
- replaced the placeholder `apps/web` landing screen with a routed read-only operator UI that now ships runs list, run detail, blocked queue, and thread timeline pages backed by the existing `orchd` API
- added a typed frontend read layer for the current HTTP contract, including client-side blocked queue aggregation across runs and thread timeline payload/artifact rendering
- wired Tailwind v4 into the consumer app so the copied-in Cadence UI source-owned components render correctly, and used those components throughout the new pages instead of introducing new foundational UI primitives
- validated the slice with `pnpm run web:build`, seeded a local demo run/thread dataset through the existing `orch` and `inbox` CLIs, and verified the Vite dev server can proxy the runs, run detail, and thread endpoints through to `orchd`
+259 -5
View File
@@ -47,6 +47,12 @@ importers:
specifier: ^3.5.0 specifier: ^3.5.0
version: 3.5.0 version: 3.5.0
devDependencies: devDependencies:
'@tailwindcss/vite':
specifier: ^4.2.2
version: 4.2.2(vite@8.0.1(@types/node@24.12.0)(jiti@2.6.1))
'@types/node':
specifier: ^24.5.2
version: 24.12.0
'@types/react': '@types/react':
specifier: ^19.2.14 specifier: ^19.2.14
version: 19.2.14 version: 19.2.14
@@ -55,13 +61,16 @@ importers:
version: 19.2.3(@types/react@19.2.14) version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.1(vite@8.0.1) version: 6.0.1(vite@8.0.1(@types/node@24.12.0)(jiti@2.6.1))
tailwindcss:
specifier: ^4.2.2
version: 4.2.2
typescript: typescript:
specifier: ^5.9.3 specifier: ^5.9.3
version: 5.9.3 version: 5.9.3
vite: vite:
specifier: ^8.0.1 specifier: ^8.0.1
version: 8.0.1 version: 8.0.1(@types/node@24.12.0)(jiti@2.6.1)
packages: packages:
@@ -74,6 +83,22 @@ packages:
'@emnapi/wasi-threads@1.2.0': '@emnapi/wasi-threads@1.2.0':
resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
'@jridgewell/remapping@2.3.5':
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@napi-rs/wasm-runtime@1.1.1': '@napi-rs/wasm-runtime@1.1.1':
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
@@ -403,6 +428,96 @@ packages:
'@rolldown/pluginutils@1.0.0-rc.7': '@rolldown/pluginutils@1.0.0-rc.7':
resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==}
'@tailwindcss/node@4.2.2':
resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==}
'@tailwindcss/oxide-android-arm64@4.2.2':
resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [android]
'@tailwindcss/oxide-darwin-arm64@4.2.2':
resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [darwin]
'@tailwindcss/oxide-darwin-x64@4.2.2':
resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==}
engines: {node: '>= 20'}
cpu: [x64]
os: [darwin]
'@tailwindcss/oxide-freebsd-x64@4.2.2':
resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [freebsd]
'@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2':
resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==}
engines: {node: '>= 20'}
cpu: [arm]
os: [linux]
'@tailwindcss/oxide-linux-arm64-gnu@4.2.2':
resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
'@tailwindcss/oxide-linux-arm64-musl@4.2.2':
resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
'@tailwindcss/oxide-linux-x64-gnu@4.2.2':
resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
'@tailwindcss/oxide-linux-x64-musl@4.2.2':
resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
'@tailwindcss/oxide-wasm32-wasi@4.2.2':
resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
bundledDependencies:
- '@napi-rs/wasm-runtime'
- '@emnapi/core'
- '@emnapi/runtime'
- '@tybys/wasm-util'
- '@emnapi/wasi-threads'
- tslib
'@tailwindcss/oxide-win32-arm64-msvc@4.2.2':
resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [win32]
'@tailwindcss/oxide-win32-x64-msvc@4.2.2':
resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==}
engines: {node: '>= 20'}
cpu: [x64]
os: [win32]
'@tailwindcss/oxide@4.2.2':
resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==}
engines: {node: '>= 20'}
'@tailwindcss/vite@4.2.2':
resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==}
peerDependencies:
vite: ^5.2.0 || ^6 || ^7 || ^8
'@tanstack/history@1.161.6': '@tanstack/history@1.161.6':
resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==} resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==}
engines: {node: '>=20.19'} engines: {node: '>=20.19'}
@@ -439,6 +554,9 @@ packages:
'@tybys/wasm-util@0.10.1': '@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/node@24.12.0':
resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==}
'@types/react-dom@19.2.3': '@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies: peerDependencies:
@@ -484,6 +602,10 @@ packages:
detect-node-es@1.1.0: detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
enhanced-resolve@5.20.1:
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
engines: {node: '>=10.13.0'}
fdir@6.5.0: fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -516,10 +638,17 @@ packages:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'} engines: {node: '>=6'}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
isbot@5.1.36: isbot@5.1.36:
resolution: {integrity: sha512-C/ZtXyJqDPZ7G7JPr06ApWyYoHjYexQbS6hPYD4WYCzpv2Qes6Z+CCEfTX4Owzf+1EJ933PoI2p+B9v7wpGZBQ==} resolution: {integrity: sha512-C/ZtXyJqDPZ7G7JPr06ApWyYoHjYexQbS6hPYD4WYCzpv2Qes6Z+CCEfTX4Owzf+1EJ933PoI2p+B9v7wpGZBQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
lightningcss-android-arm64@1.32.0: lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
@@ -590,6 +719,9 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
motion-dom@12.38.0: motion-dom@12.38.0:
resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==} resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
@@ -696,6 +828,13 @@ packages:
tailwind-merge@3.5.0: tailwind-merge@3.5.0:
resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
tailwindcss@4.2.2:
resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
tapable@2.3.0:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
tiny-invariant@1.3.3: tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -714,6 +853,9 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
use-callback-ref@1.3.3: use-callback-ref@1.3.3:
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -800,6 +942,25 @@ snapshots:
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/remapping@2.3.5':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@napi-rs/wasm-runtime@1.1.1': '@napi-rs/wasm-runtime@1.1.1':
dependencies: dependencies:
'@emnapi/core': 1.9.1 '@emnapi/core': 1.9.1
@@ -1061,6 +1222,74 @@ snapshots:
'@rolldown/pluginutils@1.0.0-rc.7': {} '@rolldown/pluginutils@1.0.0-rc.7': {}
'@tailwindcss/node@4.2.2':
dependencies:
'@jridgewell/remapping': 2.3.5
enhanced-resolve: 5.20.1
jiti: 2.6.1
lightningcss: 1.32.0
magic-string: 0.30.21
source-map-js: 1.2.1
tailwindcss: 4.2.2
'@tailwindcss/oxide-android-arm64@4.2.2':
optional: true
'@tailwindcss/oxide-darwin-arm64@4.2.2':
optional: true
'@tailwindcss/oxide-darwin-x64@4.2.2':
optional: true
'@tailwindcss/oxide-freebsd-x64@4.2.2':
optional: true
'@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2':
optional: true
'@tailwindcss/oxide-linux-arm64-gnu@4.2.2':
optional: true
'@tailwindcss/oxide-linux-arm64-musl@4.2.2':
optional: true
'@tailwindcss/oxide-linux-x64-gnu@4.2.2':
optional: true
'@tailwindcss/oxide-linux-x64-musl@4.2.2':
optional: true
'@tailwindcss/oxide-wasm32-wasi@4.2.2':
optional: true
'@tailwindcss/oxide-win32-arm64-msvc@4.2.2':
optional: true
'@tailwindcss/oxide-win32-x64-msvc@4.2.2':
optional: true
'@tailwindcss/oxide@4.2.2':
optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.2.2
'@tailwindcss/oxide-darwin-arm64': 4.2.2
'@tailwindcss/oxide-darwin-x64': 4.2.2
'@tailwindcss/oxide-freebsd-x64': 4.2.2
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2
'@tailwindcss/oxide-linux-arm64-gnu': 4.2.2
'@tailwindcss/oxide-linux-arm64-musl': 4.2.2
'@tailwindcss/oxide-linux-x64-gnu': 4.2.2
'@tailwindcss/oxide-linux-x64-musl': 4.2.2
'@tailwindcss/oxide-wasm32-wasi': 4.2.2
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.2
'@tailwindcss/oxide-win32-x64-msvc': 4.2.2
'@tailwindcss/vite@4.2.2(vite@8.0.1(@types/node@24.12.0)(jiti@2.6.1))':
dependencies:
'@tailwindcss/node': 4.2.2
'@tailwindcss/oxide': 4.2.2
tailwindcss: 4.2.2
vite: 8.0.1(@types/node@24.12.0)(jiti@2.6.1)
'@tanstack/history@1.161.6': {} '@tanstack/history@1.161.6': {}
'@tanstack/query-core@5.91.2': {} '@tanstack/query-core@5.91.2': {}
@@ -1105,6 +1334,10 @@ snapshots:
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
'@types/node@24.12.0':
dependencies:
undici-types: 7.16.0
'@types/react-dom@19.2.3(@types/react@19.2.14)': '@types/react-dom@19.2.3(@types/react@19.2.14)':
dependencies: dependencies:
'@types/react': 19.2.14 '@types/react': 19.2.14
@@ -1113,10 +1346,10 @@ snapshots:
dependencies: dependencies:
csstype: 3.2.3 csstype: 3.2.3
'@vitejs/plugin-react@6.0.1(vite@8.0.1)': '@vitejs/plugin-react@6.0.1(vite@8.0.1(@types/node@24.12.0)(jiti@2.6.1))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-rc.7 '@rolldown/pluginutils': 1.0.0-rc.7
vite: 8.0.1 vite: 8.0.1(@types/node@24.12.0)(jiti@2.6.1)
aria-hidden@1.2.6: aria-hidden@1.2.6:
dependencies: dependencies:
@@ -1136,6 +1369,11 @@ snapshots:
detect-node-es@1.1.0: {} detect-node-es@1.1.0: {}
enhanced-resolve@5.20.1:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.0
fdir@6.5.0(picomatch@4.0.3): fdir@6.5.0(picomatch@4.0.3):
optionalDependencies: optionalDependencies:
picomatch: 4.0.3 picomatch: 4.0.3
@@ -1154,8 +1392,12 @@ snapshots:
get-nonce@1.0.1: {} get-nonce@1.0.1: {}
graceful-fs@4.2.11: {}
isbot@5.1.36: {} isbot@5.1.36: {}
jiti@2.6.1: {}
lightningcss-android-arm64@1.32.0: lightningcss-android-arm64@1.32.0:
optional: true optional: true
@@ -1205,6 +1447,10 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
motion-dom@12.38.0: motion-dom@12.38.0:
dependencies: dependencies:
motion-utils: 12.36.0 motion-utils: 12.36.0
@@ -1302,6 +1548,10 @@ snapshots:
tailwind-merge@3.5.0: {} tailwind-merge@3.5.0: {}
tailwindcss@4.2.2: {}
tapable@2.3.0: {}
tiny-invariant@1.3.3: {} tiny-invariant@1.3.3: {}
tiny-warning@1.0.3: {} tiny-warning@1.0.3: {}
@@ -1315,6 +1565,8 @@ snapshots:
typescript@5.9.3: {} typescript@5.9.3: {}
undici-types@7.16.0: {}
use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4): use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4):
dependencies: dependencies:
react: 19.2.4 react: 19.2.4
@@ -1334,7 +1586,7 @@ snapshots:
dependencies: dependencies:
react: 19.2.4 react: 19.2.4
vite@8.0.1: vite@8.0.1(@types/node@24.12.0)(jiti@2.6.1):
dependencies: dependencies:
lightningcss: 1.32.0 lightningcss: 1.32.0
picomatch: 4.0.3 picomatch: 4.0.3
@@ -1342,4 +1594,6 @@ snapshots:
rolldown: 1.0.0-rc.10 rolldown: 1.0.0-rc.10
tinyglobby: 0.2.15 tinyglobby: 0.2.15
optionalDependencies: optionalDependencies:
'@types/node': 24.12.0
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 2.6.1