diff --git a/apps/fabro-web/app/data/retros.ts b/apps/fabro-web/app/data/retros.ts deleted file mode 100644 index cb28ce4ba..000000000 --- a/apps/fabro-web/app/data/retros.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { formatDurationSecs } from "../lib/format"; - -export type SmoothnessRating = "effortless" | "smooth" | "bumpy" | "struggled" | "failed"; - -type LearningCategory = "repo" | "code" | "workflow" | "tool"; - -export interface Learning { - category: LearningCategory; - text: string; -} - -type FrictionKind = "retry" | "timeout" | "wrong_approach" | "tool_failure" | "ambiguity"; - -export interface FrictionPoint { - kind: FrictionKind; - description: string; - stage_id?: string; -} - -type OpenItemKind = "tech_debt" | "follow_up" | "investigation" | "test_gap"; - -export interface OpenItem { - kind: OpenItemKind; - description: string; -} - -export interface StageRetro { - stage_id: string; - stage_label: string; - status: string; - duration_ms: number; - retries: number; - cost?: number; - notes?: string; - failure_reason?: string; - files_touched: string[]; -} - -export interface AggregateStats { - total_duration_ms: number; - total_cost?: number; - total_retries: number; - files_touched: string[]; - stages_completed: number; - stages_failed: number; -} - -export interface Retro { - run_id: string; - workflow_name: string; - goal: string; - timestamp: string; - smoothness?: SmoothnessRating; - stages: StageRetro[]; - stats: AggregateStats; - intent?: string; - outcome?: string; - learnings?: Learning[]; - friction_points?: FrictionPoint[]; - open_items?: OpenItem[]; -} - -export const smoothnessConfig: Record = { - effortless: { label: "Effortless", bg: "bg-emerald-500/15", text: "text-emerald-400", dot: "bg-emerald-400" }, - smooth: { label: "Smooth", bg: "bg-mint/15", text: "text-mint", dot: "bg-mint" }, - bumpy: { label: "Bumpy", bg: "bg-amber/15", text: "text-amber", dot: "bg-amber" }, - struggled: { label: "Struggled", bg: "bg-orange-500/15", text: "text-orange-400", dot: "bg-orange-400" }, - failed: { label: "Failed", bg: "bg-coral/15", text: "text-coral", dot: "bg-coral" }, -}; - -export const learningCategoryConfig: Record = { - repo: { label: "Repo", text: "text-teal-400" }, - code: { label: "Code", text: "text-sky-400" }, - workflow: { label: "Workflow", text: "text-violet-400" }, - tool: { label: "Tool", text: "text-amber" }, -}; - -export const frictionKindConfig: Record = { - retry: { label: "Retry", text: "text-amber" }, - timeout: { label: "Timeout", text: "text-coral" }, - wrong_approach: { label: "Wrong Approach", text: "text-orange-400" }, - tool_failure: { label: "Tool Failure", text: "text-coral" }, - ambiguity: { label: "Ambiguity", text: "text-violet-400" }, -}; - -export const openItemKindConfig: Record = { - tech_debt: { label: "Tech Debt", text: "text-orange-400" }, - follow_up: { label: "Follow-up", text: "text-teal-400" }, - investigation: { label: "Investigation", text: "text-sky-400" }, - test_gap: { label: "Test Gap", text: "text-coral" }, -}; - -function formatDurationMs(ms: number): string { - return formatDurationSecs(Math.floor(ms / 1000)); -} - -export { formatDurationMs }; diff --git a/apps/fabro-web/app/data/verifications.ts b/apps/fabro-web/app/data/verifications.ts deleted file mode 100644 index 40aeb1bc4..000000000 --- a/apps/fabro-web/app/data/verifications.ts +++ /dev/null @@ -1,96 +0,0 @@ -export type VerificationResult = "pass" | "fail" | "skip" | "na"; - -export type VerificationType = "ai" | "automated" | "analysis" | "ai-analysis"; - -export interface Criterion { - name: string; - description: string; - type: VerificationType | null; - status: VerificationResult; -} - -export interface VerificationCategory { - name: string; - question: string; - status: VerificationResult; - criteria: Criterion[]; -} - -export const statusConfig = { - pass: { - label: "Pass", - color: "text-mint", - bg: "bg-mint/15", - dot: "bg-mint", - border: "border-l-mint/50", - }, - fail: { - label: "Fail", - color: "text-coral", - bg: "bg-coral/15", - dot: "bg-coral", - border: "border-l-coral/50", - }, - skip: { - label: "Skip", - color: "text-fg-muted", - bg: "bg-overlay", - dot: "bg-fg-muted", - border: "border-l-fg-muted/50", - }, - na: { - label: "N/A", - color: "text-fg-muted", - bg: "bg-overlay", - dot: "bg-fg-muted", - border: "border-l-fg-muted/50", - }, -} as const satisfies Record< - VerificationResult, - { label: string; color: string; bg: string; dot: string; border: string } ->; - -export const typeConfig = { - ai: { label: "AI", color: "text-teal-300", bg: "bg-teal-500/10" }, - automated: { label: "Automated", color: "text-mint", bg: "bg-mint/10" }, - analysis: { label: "Analysis", color: "text-amber", bg: "bg-amber/10" }, - "ai-analysis": { label: "AI + Analysis", color: "text-teal-300", bg: "bg-teal-500/10" }, -} as const satisfies Record< - VerificationType, - { label: string; color: string; bg: string } ->; - -export type VerificationMode = "active" | "evaluate" | "disabled"; - -export interface CriterionPerformance { - f1: number | null; - passAt1: number | null; - mode: VerificationMode; - evaluations: VerificationResult[]; -} - -export const modeConfig = { - active: { label: "Active", color: "text-mint", bg: "bg-mint/10" }, - evaluate: { label: "Evaluate", color: "text-amber", bg: "bg-amber/10" }, - disabled: { label: "Disabled", color: "text-fg-muted", bg: "bg-overlay" }, -} as const satisfies Record< - VerificationMode, - { label: string; color: string; bg: string } ->; - -export function getCriteriaSummary(criteria: readonly Criterion[]) { - return { - passing: criteria.filter((c) => c.status === "pass").length, - failing: criteria.filter((c) => c.status === "fail").length, - na: criteria.filter((c) => c.status === "na").length, - total: criteria.length, - }; -} - -export function slugify(name: string): string { - return name - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/(^-|-$)/g, ""); -} - diff --git a/apps/fabro-web/app/layouts/app-shell.tsx b/apps/fabro-web/app/layouts/app-shell.tsx index 5f9d250ed..72a26cf13 100644 --- a/apps/fabro-web/app/layouts/app-shell.tsx +++ b/apps/fabro-web/app/layouts/app-shell.tsx @@ -11,8 +11,6 @@ import { Bars3Icon, BeakerIcon, ChartBarIcon, - CheckBadgeIcon, - LightBulbIcon, MoonIcon, PlayIcon, RectangleStackIcon, @@ -30,8 +28,6 @@ export async function loader() { const navigation = [ { name: "Workflows", href: "/workflows", icon: RectangleStackIcon }, { name: "Runs", href: "/runs", icon: PlayIcon }, - { name: "Verification", href: "/verification/criteria", icon: CheckBadgeIcon }, - { name: "Retros", href: "/retros", icon: LightBulbIcon }, { name: "Insights", href: "/insights", icon: ChartBarIcon }, ]; diff --git a/apps/fabro-web/app/lib/time.ts b/apps/fabro-web/app/lib/time.ts index decaf06fe..1d8a818d4 100644 --- a/apps/fabro-web/app/lib/time.ts +++ b/apps/fabro-web/app/lib/time.ts @@ -22,46 +22,3 @@ export function timeUntil(iso: string): string { return relativeTime(Math.floor((new Date(iso).getTime() - Date.now()) / 1000), false); } -/** - * Return a human-readable date label for grouping (e.g. "Today", "Yesterday", "Previous 7 days"). - */ -function dateLabel(iso: string): string { - const now = new Date(); - const date = new Date(iso); - const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const startOfYesterday = new Date(startOfToday.getTime() - 86_400_000); - const startOf7DaysAgo = new Date(startOfToday.getTime() - 7 * 86_400_000); - - if (date >= startOfToday) return "Today"; - if (date >= startOfYesterday) return "Yesterday"; - if (date >= startOf7DaysAgo) return "Previous 7 days"; - return "Older"; -} - -interface SessionItem { - id: string; - title: string; - created_at: string; -} - -interface SessionGroup { - label: string; - sessions: SessionItem[]; -} - -/** - * Group a flat list of sessions (already sorted newest-first) into date-labeled groups. - */ -export function groupSessionsByDate(sessions: SessionItem[]): SessionGroup[] { - const groups: SessionGroup[] = []; - let current: SessionGroup | undefined; - for (const s of sessions) { - const label = dateLabel(s.created_at); - if (!current || current.label !== label) { - current = { label, sessions: [] }; - groups.push(current); - } - current.sessions.push(s); - } - return groups; -} diff --git a/apps/fabro-web/app/router.tsx b/apps/fabro-web/app/router.tsx index b085bc59d..d7041cedb 100644 --- a/apps/fabro-web/app/router.tsx +++ b/apps/fabro-web/app/router.tsx @@ -12,7 +12,6 @@ import * as Setup from "./routes/setup"; import * as SetupComplete from "./routes/setup-complete"; import * as AuthLogin from "./routes/auth-login"; import * as Start from "./routes/start"; -import * as SessionDetail from "./routes/session-detail"; import * as Workflows from "./routes/workflows"; import * as WorkflowDetail from "./routes/workflow-detail"; import * as WorkflowDefinition from "./routes/workflow-definition"; @@ -26,12 +25,6 @@ import * as RunSettings from "./routes/run-settings"; import * as RunGraph from "./routes/run-graph"; import * as RunFiles from "./routes/run-files"; import * as RunUsage from "./routes/run-usage"; -import * as RunRetro from "./routes/run-retro"; -import * as VerificationCriteria from "./routes/verification-criteria"; -import * as VerificationCriterion from "./routes/verification-criterion"; -import * as VerificationControls from "./routes/verification-controls"; -import * as VerificationControl from "./routes/verification-control"; -import * as Retros from "./routes/retros"; import * as Insights from "./routes/insights"; import * as InsightsEditor from "./routes/insights-editor"; import * as InsightsNew from "./routes/insights-new"; @@ -97,7 +90,6 @@ export const routes: RouteObject[] = [ }), children: [ route("start", Start), - route("sessions/:sessionId", SessionDetail), route("workflows", Workflows), route("workflows/:name", WorkflowDetail, { children: [ @@ -115,14 +107,8 @@ export const routes: RouteObject[] = [ route("graph", RunGraph), route("files", RunFiles), route("usage", RunUsage), - route("retro", RunRetro), ], }), - route("verification/criteria", VerificationCriteria), - route("verification/criteria/:id", VerificationCriterion), - route("verification/controls", VerificationControls), - route("verification/controls/:id", VerificationControl), - route("retros", Retros), route("insights", Insights, { children: [ indexRoute(InsightsEditor), diff --git a/apps/fabro-web/app/routes/retros.tsx b/apps/fabro-web/app/routes/retros.tsx deleted file mode 100644 index 8f0b0cad0..000000000 --- a/apps/fabro-web/app/routes/retros.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { useState } from "react"; -import { useNavigate } from "react-router"; -import { MagnifyingGlassIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; -import { smoothnessConfig, formatDurationMs } from "../data/retros"; -import type { SmoothnessRating } from "../data/retros"; -import { apiJson } from "../api"; -import type { PaginatedRetroList } from "@qltysh/fabro-api-client"; - -interface RetroRow { - run_id: string; - workflow_name: string; - goal: string; - timestamp: string; - smoothness?: SmoothnessRating; - total_duration_ms: number; - friction_point_count: number; -} - -export async function loader({ request }: any) { - const { data: apiRetros } = await apiJson("/retros", { request }); - const retros: RetroRow[] = apiRetros.map((r) => ({ - run_id: r.run.id, - workflow_name: r.workflow.slug, - goal: r.run.title, - timestamp: r.timestamp, - smoothness: r.smoothness as SmoothnessRating | undefined, - total_duration_ms: r.stats.total_duration_ms, - friction_point_count: r.friction_point_count, - })); - return { retros }; -} - -export function meta({}: any) { - return [{ title: "Retros \u2014 Fabro" }]; -} - -const smoothnessOptions: Array<{ value: SmoothnessRating; label: string }> = [ - { value: "effortless", label: "Effortless" }, - { value: "smooth", label: "Smooth" }, - { value: "bumpy", label: "Bumpy" }, - { value: "struggled", label: "Struggled" }, - { value: "failed", label: "Failed" }, -]; - -function SmoothnesssBadge({ smoothness }: { smoothness: SmoothnessRating | undefined }) { - if (!smoothness) { - return --; - } - const config = smoothnessConfig[smoothness]; - return ( - - - {config.label} - - ); -} - -function formatTimestamp(ts: string): string { - const date = new Date(ts); - return date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - hour: "numeric", - minute: "2-digit", - }); -} - -function truncate(text: string, maxLength: number): string { - if (text.length <= maxLength) return text; - return text.slice(0, maxLength) + "\u2026"; -} - -export default function Retros({ loaderData }: any) { - const { retros } = loaderData; - const navigate = useNavigate(); - const [query, setQuery] = useState(""); - const [smoothnessFilter, setSmoothnessFilter] = useState("all"); - - if (retros.length === 0) { - return

No retrospectives yet.

; - } - - const lowerQuery = query.toLowerCase(); - const filtered = retros.filter( - (r) => - (smoothnessFilter === "all" || r.smoothness === smoothnessFilter) && - (r.goal.toLowerCase().includes(lowerQuery) || - r.workflow_name.toLowerCase().includes(lowerQuery)), - ); - - return ( -
-
-
- - setQuery(e.target.value)} - className="w-full rounded-md border border-line bg-panel/80 py-2 pl-9 pr-3 text-sm text-fg-2 placeholder-fg-muted outline-none transition-colors focus:border-focus focus:ring-0" - /> -
-
- - -
-
- -
- - - - - - - - - - - - - {filtered.map((retro) => ( - navigate(`/runs/${retro.run_id}/retro`)}> - - - - - - - - ))} - -
WorkflowGoalSmoothnessDurationFrictionsWhen
- {retro.workflow_name} - - {truncate(retro.goal, 60)} - - - - {formatDurationMs(retro.total_duration_ms)} - - {retro.friction_point_count} - - {formatTimestamp(retro.timestamp)} -
-
-
- ); -} diff --git a/apps/fabro-web/app/routes/run-detail.tsx b/apps/fabro-web/app/routes/run-detail.tsx index 2372d1ded..13ec550a2 100644 --- a/apps/fabro-web/app/routes/run-detail.tsx +++ b/apps/fabro-web/app/routes/run-detail.tsx @@ -11,7 +11,6 @@ const tabs = [ { name: "Overview", path: "", count: null }, { name: "Stages", path: "/stages/detect-drift", count: null }, { name: "Files Changed", path: "/files", count: null }, - { name: "Retro", path: "/retro", count: null }, { name: "Usage", path: "/usage", count: null }, ]; diff --git a/apps/fabro-web/app/routes/run-retro.tsx b/apps/fabro-web/app/routes/run-retro.tsx deleted file mode 100644 index 70af52de8..000000000 --- a/apps/fabro-web/app/routes/run-retro.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import { Link } from "react-router"; -import { - smoothnessConfig, - learningCategoryConfig, - frictionKindConfig, - openItemKindConfig, - formatDurationMs, -} from "../data/retros"; -import type { Retro } from "../data/retros"; -import { apiJson } from "../api"; - -export async function loader({ request, params }: any) { - const retro = await apiJson(`/runs/${params.id}/retro`, { request }); - return { retro }; -} - -export function meta({ data }: any) { - const retro = data?.retro; - return [{ title: retro ? `Retro: ${retro.goal} \u2014 Fabro` : "Retro \u2014 Fabro" }]; -} - -function formatCost(cost: number | undefined): string { - if (cost == null) return "--"; - return `$${cost.toFixed(2)}`; -} - -export default function RunRetro({ loaderData }: any) { - const { retro } = loaderData; - - if (!retro) { - return

No retrospective found for this run.

; - } - - const smoothness = retro.smoothness ? smoothnessConfig[retro.smoothness] : null; - - return ( -
- {/* Smoothness + Summary Header */} -
- {smoothness && ( - - - {smoothness.label} - - )} -
-

{retro.goal}

-

- {retro.workflow_name} · {new Date(retro.timestamp).toLocaleString()} -

-
-
- - {/* Aggregate Stats */} -
- - - 0} /> - -
- - {/* Intent + Outcome */} - {(retro.intent ?? retro.outcome) && ( -
- {retro.intent && ( -
-

Intent

-

{retro.intent}

-
- )} - {retro.outcome && ( -
-

Outcome

-

{retro.outcome}

-
- )} -
- )} - - {/* Learnings */} - {retro.learnings && retro.learnings.length > 0 && ( -
-

Learnings

-
- {retro.learnings.map((learning, i) => { - const config = learningCategoryConfig[learning.category]; - return ( -
- - {config.label} - -

{learning.text}

-
- ); - })} -
-
- )} - - {/* Friction Points */} - {retro.friction_points && retro.friction_points.length > 0 && ( -
-

Friction Points

-
- {retro.friction_points.map((fp, i) => { - const config = frictionKindConfig[fp.kind]; - return ( -
- - {config.label} - -
-

{fp.description}

- {fp.stage_id && ( -

- Stage: {retro.stages.find((s) => s.stage_id === fp.stage_id)?.stage_label ?? fp.stage_id} -

- )} -
-
- ); - })} -
-
- )} - - {/* Open Items */} - {retro.open_items && retro.open_items.length > 0 && ( -
-

Open Items

-
- {retro.open_items.map((item, i) => { - const config = openItemKindConfig[item.kind]; - return ( -
- - {config.label} - -

{item.description}

-
- ); - })} -
-
- )} - - {/* Stage Breakdown */} -
-

Stage Breakdown

-
- - - - - - - - - - - - - {retro.stages.map((stage) => ( - - - - - - - - - ))} - -
StageStatusDurationRetriesCostFiles
- - {stage.stage_label} - - {stage.notes && ( -

{stage.notes}

- )} - {stage.failure_reason && ( -

{stage.failure_reason}

- )} -
- - - {formatDurationMs(stage.duration_ms)} - - 0 ? "text-amber" : "text-fg-3"}> - {stage.retries} - - - {formatCost(stage.cost)} - - {stage.files_touched.length} -
-
-
-
- ); -} - -function StatCard({ label, value, warn }: { label: string; value: string; warn?: boolean }) { - return ( -
-

{label}

-

- {value} -

-
- ); -} - -function StageStatusBadge({ status }: { status: string }) { - const styles: Record = { - completed: "text-mint", - running: "text-teal-500", - pending: "text-fg-muted", - failed: "text-coral", - cancelled: "text-fg-muted", - }; - const colorClass = styles[status] ?? "text-fg-3"; - return ( - - {status} - - ); -} diff --git a/apps/fabro-web/app/routes/session-detail.tsx b/apps/fabro-web/app/routes/session-detail.tsx deleted file mode 100644 index 33cc4b521..000000000 --- a/apps/fabro-web/app/routes/session-detail.tsx +++ /dev/null @@ -1,448 +0,0 @@ -import { useState } from "react"; -import { Link, useParams } from "react-router"; -import { - ChatBubbleLeftIcon, - ClipboardDocumentIcon, - CheckIcon, - PencilSquareIcon, - UserIcon, -} from "@heroicons/react/24/outline"; -import { ToolRow, ToolBlock } from "../components/tool-use"; -import type { ToolUse } from "../components/tool-use"; -import { timeAgo, groupSessionsByDate } from "../lib/time"; -import { apiJson } from "../api"; -import type { SessionDetail as ApiSessionDetail, PaginatedSessionList } from "@qltysh/fabro-api-client"; - -export const handle = { hideHeader: true, wide: true }; - -export function meta({}: any) { - return [{ title: "Session — Fabro" }]; -} - -export async function loader({ request, params }: any) { - const [apiSession, { data: apiSessions }] = await Promise.all([ - apiJson(`/sessions/${params.sessionId}`, { request }), - apiJson("/sessions", { request }), - ]); - const session: Session = { - id: apiSession.id, - title: apiSession.title, - model: apiSession.model.id, - created_at: apiSession.created_at, - updated_at: apiSession.updated_at, - turns: apiSession.turns.map((t): Turn => { - switch (t.kind) { - case "tool": - return { - kind: "tool", - tools: t.tools.map((tu) => ({ - id: tu.id, - toolName: tu.tool_name, - input: tu.input, - result: tu.result, - isError: tu.is_error, - durationMs: tu.duration_ms, - })), - }; - case "user": - return { kind: "user", content: t.content, created_at: t.created_at }; - case "assistant": - return { kind: "assistant", content: t.content }; - } - }), - }; - const sessionGroups = groupSessionsByDate( - apiSessions.map((s) => ({ id: s.id, title: s.title, created_at: s.created_at })) - ); - return { session, sessionGroups }; -} - -type Turn = - | { kind: "user"; content: string; created_at?: string } - | { kind: "assistant"; content: string } - | { kind: "tool"; tools: ToolUse[] }; - -interface Session { - id: string; - title: string; - model: string; - created_at: string; - updated_at: string; - turns: Turn[]; -} - -// Keep hardcoded sessions as fallback -const sessions: Record = { - s1: { - id: "s1", - title: "Add rate limiting to auth endpoints", - model: "Opus 4.6", - created_at: "2026-03-06T14:30:00Z", - updated_at: "2026-03-06T15:45:00Z", - turns: [ - { - kind: "user", - created_at: "2026-02-28T10:00:00Z", - content: "Add rate limiting to the auth endpoints. We're getting hit with brute force attempts on /api/auth/login and /api/auth/register. Use a sliding window approach with Redis, 10 requests per minute per IP.", - }, - { - kind: "assistant", - content: "I'll implement sliding window rate limiting using Redis. Let me first look at the existing auth routes and middleware setup.", - }, - { - kind: "tool", - tools: [ - { - id: "toolu_s1_01", - toolName: "read_file", - input: `{ "path": "src/routes/auth.ts" }`, - result: `import { Router } from "express";\nimport { loginHandler } from "../handlers/auth";\nimport { registerHandler } from "../handlers/auth";\n\nconst router = Router();\nrouter.post("/login", loginHandler);\nrouter.post("/register", registerHandler);\n\nexport default router;`, - isError: false, - durationMs: 45, - }, - { - id: "toolu_s1_02", - toolName: "read_file", - input: `{ "path": "src/middleware/index.ts" }`, - result: `export { authMiddleware } from "./auth";\nexport { corsMiddleware } from "./cors";\nexport { loggingMiddleware } from "./logging";`, - isError: false, - durationMs: 38, - }, - { - id: "toolu_s1_03", - toolName: "search_files", - input: `{ "pattern": "redis", "path": "src/" }`, - result: `src/lib/redis.ts:1: import Redis from "ioredis";\nsrc/lib/redis.ts:3: export const redis = new Redis(process.env.REDIS_URL);`, - isError: false, - durationMs: 210, - }, - ], - }, - { - kind: "assistant", - content: "Good — there's already a Redis client at `src/lib/redis.ts`. I'll create a rate limiting middleware using the sliding window algorithm and apply it to the auth routes.\n\nHere's the plan:\n1. Create `src/middleware/rate-limit.ts` with sliding window logic\n2. Apply it to `/login` and `/register` routes\n3. Return `429 Too Many Requests` when the limit is exceeded", - }, - { - kind: "tool", - tools: [ - { - id: "toolu_s1_04", - toolName: "write_file", - input: `{ "path": "src/middleware/rate-limit.ts" }`, - result: `File written: src/middleware/rate-limit.ts (47 lines)`, - isError: false, - durationMs: 65, - }, - { - id: "toolu_s1_05", - toolName: "edit_file", - input: `{ "path": "src/routes/auth.ts", "operations": [{ "type": "insert", "line": 3 }] }`, - result: `Applied 1 edit to src/routes/auth.ts`, - isError: false, - durationMs: 42, - }, - { - id: "toolu_s1_06", - toolName: "edit_file", - input: `{ "path": "src/middleware/index.ts", "operations": [{ "type": "append" }] }`, - result: `Applied 1 edit to src/middleware/index.ts`, - isError: false, - durationMs: 55, - }, - ], - }, - { - kind: "assistant", - content: "Done. I've created the rate limiter and wired it up:\n\n- **`src/middleware/rate-limit.ts`** — Sliding window rate limiter using Redis sorted sets. Each request adds a timestamped entry, expired entries are pruned, and the count is checked against the limit.\n- **`src/routes/auth.ts`** — Applied `rateLimit({ windowMs: 60_000, max: 10 })` to both `/login` and `/register`.\n- **`src/middleware/index.ts`** — Re-exported the new middleware.\n\nWhen the limit is exceeded, clients get a `429` response with a `Retry-After` header indicating seconds until the window resets.", - }, - ], - }, - s2: { - id: "s2", - title: "Fix config parsing for nested values", - model: "Sonnet 4.6", - created_at: "2026-03-06T12:30:00Z", - updated_at: "2026-03-06T13:15:00Z", - turns: [ - { - kind: "user", - created_at: "2026-02-28T10:00:00Z", - content: "The CLI crashes when parsing nested TOML config values like [database.connection]. Can you debug and fix this?", - }, - { - kind: "assistant", - content: "Let me look at the config parser to understand how nested keys are handled.", - }, - { - kind: "tool", - tools: [ - { - id: "toolu_s2_01", - toolName: "read_file", - input: `{ "path": "src/config/parser.ts" }`, - result: `export function parseConfig(raw: string): Record {\n const result: Record = {};\n for (const line of raw.split("\\n")) {\n const match = line.match(/^(\\w+)\\s*=\\s*(.+)$/);\n if (match) result[match[1]] = match[2].trim();\n }\n return result;\n}`, - isError: false, - durationMs: 52, - }, - ], - }, - { - kind: "assistant", - content: "Found the issue. The parser uses a flat `Record` and the regex `^(\\w+)` only matches simple keys — it can't handle TOML section headers like `[database.connection]` or dotted keys like `database.host`.\n\nI'll rewrite the parser to support nested sections and dotted keys, producing a properly nested object.", - }, - { - kind: "tool", - tools: [ - { - id: "toolu_s2_02", - toolName: "write_file", - input: `{ "path": "src/config/parser.ts" }`, - result: `File written: src/config/parser.ts (62 lines)`, - isError: false, - durationMs: 78, - }, - { - id: "toolu_s2_03", - toolName: "run_command", - input: `{ "command": "npm test -- --grep 'config parser'" }`, - result: `PASS src/config/__tests__/parser.test.ts\n config parser\n ✓ parses flat keys (2ms)\n ✓ parses section headers (1ms)\n ✓ parses dotted keys (1ms)\n ✓ handles nested sections (1ms)\n\nTest Suites: 1 passed\nTests: 4 passed`, - isError: false, - durationMs: 2150, - }, - ], - }, - { - kind: "assistant", - content: "Fixed. The parser now:\n\n1. Tracks the current section header (e.g. `[database.connection]`)\n2. Constructs nested objects from dotted paths\n3. Handles both `key = value` within sections and bare `dotted.key = value` syntax\n\nAll 4 tests pass, including the new nested section case.", - }, - ], - }, - s3: { - id: "s3", - title: "Migrate to React Router v7", - model: "Opus 4.6", - created_at: "2026-03-05T10:00:00Z", - updated_at: "2026-03-05T11:30:00Z", - turns: [ - { kind: "user", created_at: "2026-02-26T10:00:00Z", content: "Help me migrate our app from React Router v6 to v7. We're using createBrowserRouter with data loaders." }, - { kind: "assistant", content: "I'll audit your current router setup and identify what needs to change for v7. Let me scan the codebase." }, - { - kind: "tool", - tools: [ - { id: "toolu_s3_01", toolName: "search_files", input: `{ "pattern": "createBrowserRouter" }`, result: `src/main.tsx:5: import { createBrowserRouter } from "react-router-dom";\nsrc/main.tsx:8: const router = createBrowserRouter([`, isError: false, durationMs: 180 }, - { id: "toolu_s3_02", toolName: "read_file", input: `{ "path": "package.json" }`, result: `"react-router-dom": "^6.22.0"`, isError: false, durationMs: 35 }, - ], - }, - { kind: "assistant", content: "You're on React Router 6.22. The migration to v7 involves:\n\n1. Replace `react-router-dom` with `react-router` (single package)\n2. Update `createBrowserRouter` import path\n3. Migrate data loaders to the new `any` type\n4. Update any `useLoaderData` calls to be type-safe\n\nLet me make these changes." }, - ], - }, -}; - -// Fallback for unknown session IDs -function makeFallbackSession(id: string): Session { - return { - id, - title: "Session", - model: "Opus 4.6", - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - turns: [ - { kind: "user", created_at: "2026-02-28T10:00:00Z", content: "Hello, let's get started." }, - { kind: "assistant", content: "Sure! What would you like to work on?" }, - ], - }; -} - -interface SessionGroupType { - label: string; - sessions: { id: string; title: string; created_at: string }[]; -} - -const sessionGroups: SessionGroupType[] = [ - { - label: "Today", - sessions: [ - { id: "s1", title: "Add rate limiting to auth endpoints", created_at: "2026-03-06T14:30:00Z" }, - { id: "s2", title: "Fix config parsing for nested values", created_at: "2026-03-06T12:30:00Z" }, - ], - }, - { - label: "Yesterday", - sessions: [ - { id: "s3", title: "Migrate to React Router v7", created_at: "2026-03-05T10:00:00Z" }, - { id: "s4", title: "Add dark mode toggle", created_at: "2026-03-05T09:00:00Z" }, - { id: "s5", title: "Update OpenAPI spec for v3", created_at: "2026-03-05T08:00:00Z" }, - ], - }, - { - label: "Previous 7 days", - sessions: [ - { id: "s6", title: "Terraform module for Redis cluster", created_at: "2026-03-03T15:00:00Z" }, - { id: "s7", title: "Add pipeline event types", created_at: "2026-03-01T11:00:00Z" }, - { id: "s8", title: "Implement webhook retry logic", created_at: "2026-02-28T09:00:00Z" }, - ], - }, -]; - -function CopyButton({ text }: { text: string }) { - const [copied, setCopied] = useState(false); - - function handleCopy() { - navigator.clipboard.writeText(text).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }); - } - - return ( - - ); -} - -function UserBlock({ content, created_at }: { content: string; created_at?: string }) { - return ( -
-
-
- -
-
-
{content}
-
-
-
- - {created_at != null && {timeAgo(created_at)}} -
-
- ); -} - -function AssistantBlock({ content, showCopy }: { content: string; showCopy: boolean }) { - return ( -
-
-
- -
-
-
{content}
-
-
- {showCopy && ( -
- -
- )} -
- ); -} - -function SessionSidebar({ activeId, groups }: { activeId: string; groups: SessionGroupType[] }) { - return ( - - ); -} - -export default function SessionDetail({ loaderData }: any) { - const { session, sessionGroups: loaderGroups } = loaderData; - - return ( -
- - -
-
-

{session.title}

- {timeAgo(session.created_at)} - {session.model} -
- -
-
- {session.turns.map((turn, i) => { - switch (turn.kind) { - case "user": - return ; - case "assistant": { - const next = session.turns[i + 1]; - const showCopy = next?.kind !== "tool"; - return ; - } - case "tool": - return
; - } - })} -
-
- -
-
-
-