diff --git a/apps/arc-web/app/layouts/app-shell.tsx b/apps/arc-web/app/layouts/app-shell.tsx index 504191059..f191a79b7 100644 --- a/apps/arc-web/app/layouts/app-shell.tsx +++ b/apps/arc-web/app/layouts/app-shell.tsx @@ -12,6 +12,7 @@ import { Cog6ToothIcon, PlayIcon, RectangleStackIcon, + SparklesIcon, XMarkIcon, } from "@heroicons/react/24/outline"; import { Link, Outlet, useLocation, useMatches } from "react-router"; @@ -24,6 +25,7 @@ const user = { }; const navigation = [ + { name: "Start", href: "/start", icon: SparklesIcon }, { name: "Workflows", href: "/workflows", icon: RectangleStackIcon }, { name: "Runs", href: "/runs", icon: PlayIcon }, { name: "Settings", href: "/settings", icon: Cog6ToothIcon }, diff --git a/apps/arc-web/app/routes.ts b/apps/arc-web/app/routes.ts index dc0bad8ef..00baa467f 100644 --- a/apps/arc-web/app/routes.ts +++ b/apps/arc-web/app/routes.ts @@ -9,6 +9,7 @@ export default [ index("routes/redirect-home.tsx"), layout("layouts/app-shell.tsx", [ route("start", "routes/start.tsx"), + route("sessions/:sessionId", "routes/session-detail.tsx"), route("workflows", "routes/workflows.tsx"), route("workflows/:name", "routes/workflow-detail.tsx", [ index("routes/workflow-definition.tsx"), diff --git a/apps/arc-web/app/routes/pipelines.tsx b/apps/arc-web/app/routes/pipelines.tsx index 65eb466db..8c59f3b02 100644 --- a/apps/arc-web/app/routes/pipelines.tsx +++ b/apps/arc-web/app/routes/pipelines.tsx @@ -1,4 +1,6 @@ +import { useState } from "react"; import { Link } from "react-router"; +import { ChevronDownIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { columns, ciConfig } from "../data/runs"; import type { CiStatus, RunItem } from "../data/runs"; import type { Route } from "./+types/pipelines"; @@ -206,12 +208,57 @@ function BoardColumn({ column }: { column: (typeof columns)[number] }) { ); } +const allRepos = [...new Set(columns.flatMap((col) => col.items.map((item) => item.repo)))].sort(); + export default function Pipelines() { + const [query, setQuery] = useState(""); + const [repoFilter, setRepoFilter] = useState("all"); + const lowerQuery = query.toLowerCase(); + + const filteredColumns = columns.map((col) => ({ + ...col, + items: col.items.filter( + (item) => + (repoFilter === "all" || item.repo === repoFilter) && + (!query || + item.title.toLowerCase().includes(lowerQuery) || + item.repo.toLowerCase().includes(lowerQuery) || + (item.number != null && `#${item.number}`.includes(lowerQuery))), + ), + })); + return ( -
- {columns.map((col) => ( - - ))} +
+
+
+ + setQuery(e.target.value)} + className="w-full rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-9 pr-3 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40 focus:ring-0" + /> +
+
+ + +
+
+
+ {filteredColumns.map((col) => ( + + ))} +
); } diff --git a/apps/arc-web/app/routes/session-detail.tsx b/apps/arc-web/app/routes/session-detail.tsx new file mode 100644 index 000000000..fbec60f8e --- /dev/null +++ b/apps/arc-web/app/routes/session-detail.tsx @@ -0,0 +1,360 @@ +import { useState } from "react"; +import { Link, useParams } from "react-router"; +import { ChevronRightIcon } from "@heroicons/react/20/solid"; +import { + ChatBubbleLeftIcon, + PencilSquareIcon, + UserIcon, + WrenchScrewdriverIcon, +} from "@heroicons/react/24/outline"; +import type { Route } from "./+types/session-detail"; + +export const handle = { hideHeader: true, wide: true }; + +export function meta({}: Route.MetaArgs) { + return [{ title: "Session — Arc" }]; +} + +interface ToolUse { + toolName: string; + args: string; + result: string; +} + +type Turn = + | { kind: "user"; content: string } + | { kind: "assistant"; content: string } + | { kind: "tool"; tools: ToolUse[] }; + +interface Session { + id: string; + title: string; + repo: string; + time: string; + turns: Turn[]; +} + +const sessions: Record = { + s1: { + id: "s1", + title: "Add rate limiting to auth endpoints", + repo: "api-server", + time: "2h ago", + turns: [ + { + kind: "user", + 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: [ + { + toolName: "read_file", + args: `{ "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;`, + }, + { + toolName: "read_file", + args: `{ "path": "src/middleware/index.ts" }`, + result: `export { authMiddleware } from "./auth";\nexport { corsMiddleware } from "./cors";\nexport { loggingMiddleware } from "./logging";`, + }, + { + toolName: "search_files", + args: `{ "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);`, + }, + ], + }, + { + 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: [ + { + toolName: "write_file", + args: `{ "path": "src/middleware/rate-limit.ts" }`, + result: `File written: src/middleware/rate-limit.ts (47 lines)`, + }, + { + toolName: "edit_file", + args: `{ "path": "src/routes/auth.ts", "operations": [{ "type": "insert", "line": 3 }] }`, + result: `Applied 1 edit to src/routes/auth.ts`, + }, + { + toolName: "edit_file", + args: `{ "path": "src/middleware/index.ts", "operations": [{ "type": "append" }] }`, + result: `Applied 1 edit to src/middleware/index.ts`, + }, + ], + }, + { + 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", + repo: "cli-tools", + time: "4h ago", + turns: [ + { + kind: "user", + 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: [ + { + toolName: "read_file", + args: `{ "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}`, + }, + ], + }, + { + 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: [ + { + toolName: "write_file", + args: `{ "path": "src/config/parser.ts" }`, + result: `File written: src/config/parser.ts (62 lines)`, + }, + { + toolName: "run_command", + args: `{ "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`, + }, + ], + }, + { + 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", + repo: "web-dashboard", + time: "1d ago", + turns: [ + { kind: "user", 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: [ + { toolName: "search_files", args: `{ "pattern": "createBrowserRouter" }`, result: `src/main.tsx:5: import { createBrowserRouter } from "react-router-dom";\nsrc/main.tsx:8: const router = createBrowserRouter([` }, + { toolName: "read_file", args: `{ "path": "package.json" }`, result: `"react-router-dom": "^6.22.0"` }, + ], + }, + { 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 `Route.LoaderArgs` 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", + repo: "unknown", + time: "", + turns: [ + { kind: "user", content: "Hello, let's get started." }, + { kind: "assistant", content: "Sure! What would you like to work on?" }, + ], + }; +} + +interface SessionGroup { + label: string; + sessions: { id: string; title: string; repo: string; time: string }[]; +} + +const sessionGroups: SessionGroup[] = [ + { + label: "Today", + sessions: [ + { id: "s1", title: "Add rate limiting to auth endpoints", repo: "api-server", time: "2h ago" }, + { id: "s2", title: "Fix config parsing for nested values", repo: "cli-tools", time: "4h ago" }, + ], + }, + { + label: "Yesterday", + sessions: [ + { id: "s3", title: "Migrate to React Router v7", repo: "web-dashboard", time: "1d ago" }, + { id: "s4", title: "Add dark mode toggle", repo: "web-dashboard", time: "1d ago" }, + { id: "s5", title: "Update OpenAPI spec for v3", repo: "api-server", time: "1d ago" }, + ], + }, + { + label: "Previous 7 days", + sessions: [ + { id: "s6", title: "Terraform module for Redis cluster", repo: "infrastructure", time: "3d ago" }, + { id: "s7", title: "Add pipeline event types", repo: "shared-types", time: "5d ago" }, + { id: "s8", title: "Implement webhook retry logic", repo: "api-server", time: "6d ago" }, + ], + }, +]; + +function ToolRow({ tool }: { tool: ToolUse }) { + const [open, setOpen] = useState(false); + + return ( +
+ + {open && ( +
+
+
Args
+
{tool.args}
+
+
+
Result
+
{tool.result}
+
+
+ )} +
+ ); +} + +function ToolBlock({ tools }: { tools: ToolUse[] }) { + return ( +
+ {tools.map((tool, i) => ( + + ))} +
+ ); +} + +function UserBlock({ content }: { content: string }) { + return ( +
+
+ +
+
+
{content}
+
+
+ ); +} + +function AssistantBlock({ content }: { content: string }) { + return ( +
+
+ +
+
+
{content}
+
+
+ ); +} + +function SessionSidebar({ activeId }: { activeId: string }) { + return ( + + ); +} + +export default function SessionDetail() { + const { sessionId } = useParams(); + const session = sessions[sessionId ?? ""] ?? makeFallbackSession(sessionId ?? ""); + + return ( +
+ + +
+
+

{session.title}

+ {session.repo} + {session.time} +
+ +
+
+ {session.turns.map((turn, i) => { + switch (turn.kind) { + case "user": + return ; + case "assistant": + return ; + case "tool": + return
; + } + })} +
+
+
+
+ ); +} diff --git a/apps/arc-web/app/routes/start.tsx b/apps/arc-web/app/routes/start.tsx index f8bda3f42..a0b53d0a6 100644 --- a/apps/arc-web/app/routes/start.tsx +++ b/apps/arc-web/app/routes/start.tsx @@ -1,9 +1,287 @@ +import { useState, useRef, useEffect } from "react"; +import { + Listbox, + ListboxButton, + ListboxOption, + ListboxOptions, +} from "@headlessui/react"; +import { ArrowUpIcon } from "@heroicons/react/24/solid"; +import { + ChevronUpDownIcon, + FolderIcon, +} from "@heroicons/react/16/solid"; +import { + MagnifyingGlassIcon, + BoltIcon, + PencilSquareIcon, + ShieldCheckIcon, +} from "@heroicons/react/24/outline"; +import { Link } from "react-router"; import type { Route } from "./+types/start"; +export const handle = { hideHeader: true, wide: true }; + export function meta({}: Route.MetaArgs) { return [{ title: "Start — Arc" }]; } -export default function Start() { - return null; +const projects = [ + { id: "arc-web", name: "arc-web" }, + { id: "arc-attractor", name: "arc-attractor" }, + { id: "arc-cli", name: "arc-cli" }, +]; + +const branches = [ + { id: "main", name: "main" }, + { id: "develop", name: "develop" }, + { id: "feature/start-page", name: "feature/start-page" }, +]; + +const sessionGroups = [ + { + label: "Today", + sessions: [ + { id: "s1", title: "Add rate limiting to auth endpoints", repo: "api-server", time: "2h ago" }, + { id: "s2", title: "Fix config parsing for nested values", repo: "cli-tools", time: "4h ago" }, + ], + }, + { + label: "Yesterday", + sessions: [ + { id: "s3", title: "Migrate to React Router v7", repo: "web-dashboard", time: "1d ago" }, + { id: "s4", title: "Add dark mode toggle", repo: "web-dashboard", time: "1d ago" }, + { id: "s5", title: "Update OpenAPI spec for v3", repo: "api-server", time: "1d ago" }, + ], + }, + { + label: "Previous 7 days", + sessions: [ + { id: "s6", title: "Terraform module for Redis cluster", repo: "infrastructure", time: "3d ago" }, + { id: "s7", title: "Add pipeline event types", repo: "shared-types", time: "5d ago" }, + { id: "s8", title: "Implement webhook retry logic", repo: "api-server", time: "6d ago" }, + ], + }, +]; + +function BranchIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +function SessionSidebar() { + return ( + + ); +} + +export default function Start() { + const [prompt, setPrompt] = useState(""); + const [project, setProject] = useState(projects[0]); + const [branch, setBranch] = useState(branches[0]); + const textareaRef = useRef(null); + + useEffect(() => { + textareaRef.current?.focus(); + }, []); + + function autoResize() { + const el = textareaRef.current; + if (!el) return; + el.style.height = "auto"; + el.style.height = Math.min(el.scrollHeight, 280) + "px"; + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + if (prompt.trim()) handleSubmit(); + } + } + + function handleSubmit() { + if (!prompt.trim()) return; + // TODO: wire up submission + } + + return ( +
+ + +
+
+

+ What do you want to build? +

+ +
+
+ +
+