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/
dist/
node_modules/
.pnpm-store/
apps/web/dist/
apps/web/.vite/
*.db
+3
View File
@@ -24,9 +24,12 @@
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^24.5.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3",
"vite": "^8.0.1"
}
+148 -51
View File
@@ -6,24 +6,65 @@ import {
createRootRoute,
createRoute,
createRouter,
useRouterState,
} 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({
component: RootLayout,
});
const indexRoute = createRoute({
const runsRoute = createRoute({
getParentRoute: () => rootRoute,
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({
routeTree,
defaultPreload: 'intent',
});
declare module '@tanstack/react-router' {
@@ -42,63 +83,119 @@ export function App() {
function RootLayout() {
return (
<div className="shell">
<header className="masthead">
<div>
<p className="eyebrow">AI Workflow Skill</p>
<Link className="brand" to="/">
<div className="relative min-h-screen overflow-hidden" data-theme="brand">
<div className="pointer-events-none fixed inset-0">
<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%)]" />
<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%)]" />
<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]" />
</div>
<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>
<p className="masthead-copy">
Phase 1 keeps the UI thin while the backend contract settles around
`orchd`.
</p>
<div className="flex flex-wrap gap-2">
<NavLink exact label="Runs overview" to="/" />
<NavLink label="Blocked queue" to="/blocked" />
</div>
</div>
</header>
<main className="content">
<main className="min-w-0 flex-1">
<Outlet />
</main>
</div>
</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 (
<section className="hero-grid">
<article className="hero-card hero-card-primary">
<p className="eyebrow">Current slice</p>
<h1>Read-only operator shell for a future multi-user web product.</h1>
<p className="lede">
The monorepo now has a dedicated React app, a Go HTTP service, and a
first API contract for runs, blocked work, and thread history.
</p>
</article>
<article className="hero-card hero-card-secondary">
<h2>Backend spine</h2>
<ul className="detail-list">
<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>
<Link
className={cn(
'inline-flex items-center gap-2 rounded-full border px-3.5 py-2 text-sm font-medium transition-colors',
'border-[color-mix(in_oklch,var(--color-border)_76%,transparent)] bg-transparent text-[var(--color-muted-foreground)]',
'hover:border-[var(--color-border-strong)] hover:bg-[color-mix(in_oklch,var(--color-surface)_78%,white_22%)] hover:text-[var(--color-foreground)]',
isActive &&
'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)]',
)}
to={to}
>
<span className="h-1.5 w-1.5 rounded-full bg-current" />
{label}
</Link>
);
}
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 {
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;
}
* {
box-sizing: border-box;
html {
min-height: 100%;
scroll-behavior: smooth;
}
body {
margin: 0;
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 {
color: inherit;
text-decoration: none;
}
@@ -35,120 +27,10 @@ a {
min-height: 100vh;
}
.shell {
min-height: 100vh;
padding: 32px 20px 48px;
.type-display {
font-family: var(--font-display);
}
.masthead {
display: grid;
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;
}
.type-mono {
font-family: var(--font-mono);
}
+8
View File
@@ -1 +1,9 @@
/// <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 { defineConfig } from 'vite';
const proxyTarget = process.env.ORCHD_PROXY_TARGET ?? 'http://127.0.0.1:8080';
export default defineConfig({
plugins: [react()],
plugins: [tailwindcss(), react()],
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': 'http://localhost:8080',
'/health': 'http://localhost:8080',
'/api': proxyTarget,
'/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
- 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
- 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`
- `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
@@ -93,9 +94,9 @@ Current implementation status:
- `Milestone 6: Waiting Primitives` is complete
- `Milestone 7: Council Review` 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
@@ -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
### Milestone 9: Web Product Phase 2 UI Foundation
### Milestone 9: Web Product Phase 2 Read-Only Operator UI
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:
- copied-in `cadence-ui` primitives and token CSS under `apps/web/src/cadence-ui`
- app-wide token style wiring in the frontend entrypoint
- any additional component installs needed as real screens land
- Tailwind v4 consumer setup so the copied-in Cadence UI source renders correctly in the app
- routed screens for runs list, run detail, blocked queue, and thread timeline
- typed frontend read helpers for the current `orchd` endpoints
Definition of done:
- `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
Status:
- in progress
- completed for the first read-only operator slice
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/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`
- `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:
- 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
## 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
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
4. install additional `cadence-ui` components on demand when those screens need them, instead of reintroducing bespoke primitives into `apps/web`
5. keep `api/openapi.yaml`, `api/events.md`, and `docs/web-product-monorepo.md` synchronized as the web surface expands
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. 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. 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
@@ -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
version: 3.5.0
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':
specifier: ^19.2.14
version: 19.2.14
@@ -55,13 +61,16 @@ importers:
version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react':
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:
specifier: ^5.9.3
version: 5.9.3
vite:
specifier: ^8.0.1
version: 8.0.1
version: 8.0.1(@types/node@24.12.0)(jiti@2.6.1)
packages:
@@ -74,6 +83,22 @@ packages:
'@emnapi/wasi-threads@1.2.0':
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':
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
@@ -403,6 +428,96 @@ packages:
'@rolldown/pluginutils@1.0.0-rc.7':
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':
resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==}
engines: {node: '>=20.19'}
@@ -439,6 +554,9 @@ packages:
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/node@24.12.0':
resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==}
'@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
@@ -484,6 +602,10 @@ packages:
detect-node-es@1.1.0:
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:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -516,10 +638,17 @@ packages:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
isbot@5.1.36:
resolution: {integrity: sha512-C/ZtXyJqDPZ7G7JPr06ApWyYoHjYexQbS6hPYD4WYCzpv2Qes6Z+CCEfTX4Owzf+1EJ933PoI2p+B9v7wpGZBQ==}
engines: {node: '>=18'}
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
@@ -590,6 +719,9 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
motion-dom@12.38.0:
resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
@@ -696,6 +828,13 @@ packages:
tailwind-merge@3.5.0:
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:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -714,6 +853,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
use-callback-ref@1.3.3:
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
engines: {node: '>=10'}
@@ -800,6 +942,25 @@ snapshots:
tslib: 2.8.1
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':
dependencies:
'@emnapi/core': 1.9.1
@@ -1061,6 +1222,74 @@ snapshots:
'@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/query-core@5.91.2': {}
@@ -1105,6 +1334,10 @@ snapshots:
tslib: 2.8.1
optional: true
'@types/node@24.12.0':
dependencies:
undici-types: 7.16.0
'@types/react-dom@19.2.3(@types/react@19.2.14)':
dependencies:
'@types/react': 19.2.14
@@ -1113,10 +1346,10 @@ snapshots:
dependencies:
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:
'@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:
dependencies:
@@ -1136,6 +1369,11 @@ snapshots:
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):
optionalDependencies:
picomatch: 4.0.3
@@ -1154,8 +1392,12 @@ snapshots:
get-nonce@1.0.1: {}
graceful-fs@4.2.11: {}
isbot@5.1.36: {}
jiti@2.6.1: {}
lightningcss-android-arm64@1.32.0:
optional: true
@@ -1205,6 +1447,10 @@ snapshots:
lightningcss-win32-arm64-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:
dependencies:
motion-utils: 12.36.0
@@ -1302,6 +1548,10 @@ snapshots:
tailwind-merge@3.5.0: {}
tailwindcss@4.2.2: {}
tapable@2.3.0: {}
tiny-invariant@1.3.3: {}
tiny-warning@1.0.3: {}
@@ -1315,6 +1565,8 @@ snapshots:
typescript@5.9.3: {}
undici-types@7.16.0: {}
use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4):
dependencies:
react: 19.2.4
@@ -1334,7 +1586,7 @@ snapshots:
dependencies:
react: 19.2.4
vite@8.0.1:
vite@8.0.1(@types/node@24.12.0)(jiti@2.6.1):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.3
@@ -1342,4 +1594,6 @@ snapshots:
rolldown: 1.0.0-rc.10
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.12.0
fsevents: 2.3.3
jiti: 2.6.1