From 8bd3953440334c34dc5dd1989bc4dc2c755bf0b3 Mon Sep 17 00:00:00 2001 From: -Puter <22245429+puterhimself@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:43:49 +0530 Subject: [PATCH] flue flow fixes --- apps/web/src/components/theme-provider.tsx | 8 +- apps/web/src/lib/chat/conversation.test.ts | 35 +++ apps/web/src/lib/chat/conversation.ts | 26 +- apps/web/vite.config.ts | 35 +-- docs/LOCAL_SETUP.md | 12 +- docs/agent-context.md | 34 +-- docs/dev-loop.md | 2 +- docs/dogfood-plan.md | 224 +++++++----------- docs/evaluation.md | 12 +- docs/glossary.md | 2 +- docs/handoff-slice1.md | 15 +- oxfmt.config.ts | 6 + oxlint.config.ts | 28 ++- package.json | 4 +- .../agents/src/auth/conversation-route.ts | 47 +++- packages/agents/src/db.ts | 1 + packages/agents/tsconfig.json | 14 +- packages/auth/src/native/auth-client.ts | 7 +- packages/auth/src/native/hooks.ts | 30 ++- packages/auth/src/native/login-form.tsx | 22 +- packages/auth/src/native/signup-form.tsx | 13 +- packages/auth/src/shared/forms.ts | 34 ++- packages/auth/src/web/login-form.tsx | 15 +- packages/auth/src/web/signup-form.tsx | 8 +- packages/backend/AGENTS.md | 8 +- .../convex/conversationMessages.test.ts | 40 ++++ .../backend/convex/conversationMessages.ts | 41 +++- packages/backend/convex/fluePersistence.ts | 2 +- packages/backend/convex/healthCheck.ts | 4 +- packages/backend/convex/publicGit.ts | 12 +- packages/backend/package.json | 1 + packages/env/src/native.ts | 6 +- packages/env/src/server.ts | 8 +- packages/ui/src/components/attachment.tsx | 57 +++-- packages/ui/src/components/bubble.tsx | 67 +++--- packages/ui/src/components/button.tsx | 47 ++-- packages/ui/src/components/card.tsx | 32 ++- packages/ui/src/components/checkbox.tsx | 2 +- packages/ui/src/components/dropdown-menu.tsx | 41 +++- packages/ui/src/components/empty.tsx | 33 ++- packages/ui/src/components/field.tsx | 64 ++--- packages/ui/src/components/input-group.tsx | 76 +++--- packages/ui/src/components/input.tsx | 2 +- packages/ui/src/components/label.tsx | 2 +- packages/ui/src/components/marker.tsx | 18 +- .../ui/src/components/message-scroller.tsx | 27 ++- packages/ui/src/components/message.tsx | 19 +- packages/ui/src/components/separator.tsx | 11 +- packages/ui/src/components/sonner.tsx | 15 +- packages/ui/src/components/textarea.tsx | 2 +- packages/ui/src/components/tooltip.tsx | 20 +- packages/ui/src/lib/utils.ts | 7 +- 52 files changed, 814 insertions(+), 484 deletions(-) create mode 100644 packages/agents/src/db.ts diff --git a/apps/web/src/components/theme-provider.tsx b/apps/web/src/components/theme-provider.tsx index 83b503e..8ca3db0 100644 --- a/apps/web/src/components/theme-provider.tsx +++ b/apps/web/src/components/theme-provider.tsx @@ -1,11 +1,11 @@ import { ThemeProvider as NextThemesProvider } from "next-themes"; import * as React from "react"; -export function ThemeProvider({ +export const ThemeProvider = ({ children, ...props -}: React.ComponentProps) { - return {children}; -} +}: React.ComponentProps) => ( + {children} +); export { useTheme } from "next-themes"; diff --git a/apps/web/src/lib/chat/conversation.test.ts b/apps/web/src/lib/chat/conversation.test.ts index 48212e1..48408a9 100644 --- a/apps/web/src/lib/chat/conversation.test.ts +++ b/apps/web/src/lib/chat/conversation.test.ts @@ -56,4 +56,39 @@ describe("projectConversation", () => { { state: "done", text: "Captured the request.", type: "text" }, ]); }); + + test("clears a historical failure after a newer turn succeeds", () => { + const state = projectConversation([ + row({ + error: "Old admission failure", + messageId: "assistant-failed", + role: "assistant", + status: "failed", + }), + row({ + messageId: "assistant-completed", + rawText: "Recovered", + role: "assistant", + status: "completed", + }), + ]); + expect(state.failedError).toBeUndefined(); + }); + + test("clears historical pending state after a newer turn completes", () => { + const state = projectConversation([ + row({ + messageId: "assistant-stale", + role: "assistant", + status: "running", + }), + row({ + messageId: "assistant-completed", + rawText: "Recovered", + role: "assistant", + status: "completed", + }), + ]); + expect(state.pending).toBe(false); + }); }); diff --git a/apps/web/src/lib/chat/conversation.ts b/apps/web/src/lib/chat/conversation.ts index 1529494..c4e9528 100644 --- a/apps/web/src/lib/chat/conversation.ts +++ b/apps/web/src/lib/chat/conversation.ts @@ -20,7 +20,18 @@ export interface ConversationRow { | "running"; } +const latestAssistant = ( + rows: readonly ConversationRow[] +): ConversationRow | undefined => + rows.findLast((row) => row.role === "assistant"); + +const latestTerminalError = (row: ConversationRow | undefined) => + row?.status === "failed" + ? (row.error ?? "Conversation turn failed") + : undefined; + export const projectConversation = (rows: readonly ConversationRow[]) => { + const activeAssistant = latestAssistant(rows); const streaming = rows.some( (row) => row.role === "assistant" && @@ -28,9 +39,7 @@ export const projectConversation = (rows: readonly ConversationRow[]) => { row.rawText.length > 0 ); return { - failedError: rows.findLast( - (row) => row.role === "assistant" && row.status === "failed" - )?.error, + failedError: latestTerminalError(activeAssistant), messages: rows .filter( (row) => @@ -65,13 +74,10 @@ export const projectConversation = (rows: readonly ConversationRow[]) => { ], role: row.role, })), - pending: rows.some( - (row) => - row.role === "assistant" && - (row.status === "queued" || - row.status === "dispatching" || - row.status === "running") - ), + pending: + activeAssistant?.status === "queued" || + activeAssistant?.status === "dispatching" || + activeAssistant?.status === "running", streaming, }; }; diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index be845bf..ae862f8 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -2,22 +2,27 @@ import path from "node:path"; import { reactRouter } from "@react-router/dev/vite"; import tailwindcss from "@tailwindcss/vite"; -import { defineConfig } from "vite-plus"; +import { defineConfig, loadEnv } from "vite-plus"; -export default defineConfig({ - envDir: path.resolve(import.meta.dirname, "../.."), - plugins: [tailwindcss(), reactRouter()], - resolve: { - dedupe: ["convex", "react", "react-dom"], - tsconfigPaths: true, - }, - server: { - allowedHosts: true, - proxy: { - "/api/auth": { - changeOrigin: true, - target: "https://befitting-dalmatian-161.convex.site", +export default defineConfig(({ mode }) => { + const envDir = path.resolve(import.meta.dirname, "../.."); + const env = loadEnv(mode, envDir, ""); + + return { + envDir, + plugins: [tailwindcss(), reactRouter()], + resolve: { + dedupe: ["convex", "react", "react-dom"], + tsconfigPaths: true, + }, + server: { + allowedHosts: true, + proxy: { + "/api/auth": { + changeOrigin: true, + target: env.CONVEX_SITE_URL, + }, }, }, - }, + }; }); diff --git a/docs/LOCAL_SETUP.md b/docs/LOCAL_SETUP.md index bfc38dc..cf7cd6d 100644 --- a/docs/LOCAL_SETUP.md +++ b/docs/LOCAL_SETUP.md @@ -119,10 +119,10 @@ Then configure the deployment-scoped values from `packages/backend`: cd packages/backend bunx convex env set SITE_URL 'http://localhost:5173' bunx convex env set FLUE_DB_TOKEN '' -bunx convex env set FLUE_URL 'http://:3585' +bunx convex env set FLUE_URL 'http://:3585' ``` -Convex runs in the cloud, so `FLUE_URL` must be reachable from the Convex deployment. `127.0.0.1` and `localhost` point at Convex's machine, not your laptop. For the current Mac setup, expose Flue over Tailscale and use the Mac's Tailscale address, for example `http://100.x.y.z:3585`. +Convex runs in the cloud, so `FLUE_URL` must be reachable from the deployment. `127.0.0.1` and `localhost` point at Convex's machine, not your laptop. For the current Mac setup, expose Flue on the Mac's Tailscale address, for example `http://100.x.y.z:3585`. `SITE_URL` is deployment-scoped and single-valued. Set it to the exact browser origin currently being tested: @@ -143,10 +143,14 @@ Run each process in its own terminal. The order matters for the AgentOS path. ### 1. Start Convex development ```bash -bun run dev:server +# Publish once after backend changes +pnpm --filter @code/backend dev:once + +# Watch and republish while developing +pnpm dev:server ``` -This watches and publishes functions to the configured Convex cloud development deployment. +Both commands run from `packages/backend` and explicitly load the repository-root `.env` with `--env-file=../../.env`. `packages/backend/.env.local` remains Convex CLI metadata for the selected deployment; keep the shared runtime settings in the root `.env`. ### 2. Start the whole stack diff --git a/docs/agent-context.md b/docs/agent-context.md index 519414c..d77904e 100644 --- a/docs/agent-context.md +++ b/docs/agent-context.md @@ -195,9 +195,9 @@ Prefer explicit state machines over booleans. Bad: ```ts -isRunning: boolean -isDone: boolean -hasError: boolean +isRunning: boolean; +isDone: boolean; +hasError: boolean; ``` Good: @@ -213,7 +213,7 @@ type AttemptStatus = | "verification_failed" | "budget_exhausted" | "cancelled" - | "permanent_failure" + | "permanent_failure"; ``` Transitions must validate current version/state. @@ -281,38 +281,44 @@ Design replaceable boundaries, then ship one path. ```md ## Implemented + - ... ## Behavior + - ... ## Verification run + - `command` — pass/fail - ... ## Evidence + - candidate SHA/artifacts - ... ## Design deviations + - none / ... ## Remaining risks or blockers + - none / ... ``` ## 16. Required reading by task type -| Task | Read | -|---|---| -| product/domain | product.md, glossary.md | -| UI | design.md, product.md | -| actors/state | tech.md, glossary.md | -| orchestration | dev-loop.md, tech.md | -| current sequencing | slices.md, dev-plan.md | -| verification/evals | evaluation.md, dev-loop.md | -| provider adapter | tech.md plus provider docs | -| broad feature | agent-context.md + all relevant sections | +| Task | Read | +| ------------------ | ---------------------------------------- | +| product/domain | product.md, glossary.md | +| UI | design.md, product.md | +| actors/state | tech.md, glossary.md | +| orchestration | dev-loop.md, tech.md | +| current sequencing | slices.md, dev-plan.md | +| verification/evals | evaluation.md, dev-loop.md | +| provider adapter | tech.md plus provider docs | +| broad feature | agent-context.md + all relevant sections | ## 17. Stop and escalate when diff --git a/docs/dev-loop.md b/docs/dev-loop.md index a2377af..3f74683 100644 --- a/docs/dev-loop.md +++ b/docs/dev-loop.md @@ -644,7 +644,7 @@ Answers are durable Decisions and resume the same Work. ## 17. Resolver decision table | Condition | Resolver action | -|---|---| +| --- | --- | | executable slice exists | start bounded Attempt | | transient infrastructure/model failure | retry by policy | | candidate fails checks | create repair Attempt with evidence | diff --git a/docs/dogfood-plan.md b/docs/dogfood-plan.md index 90528bf..7810af4 100644 --- a/docs/dogfood-plan.md +++ b/docs/dogfood-plan.md @@ -6,25 +6,25 @@ You have shell access and may use the Paseo CLI to create and manage other Codex All child agents must use: - --provider codex/glm-5.2 +--provider codex/glm-5.2 The current repository is the Zopu monorepo. It already contains product documentation, design documentation, technical documentation, Effect primitives, Convex backend code, web/mobile/desktop applications, agent code, project-management primitives, Signals, and partial AgentOS/Rivet integration. The starting branch is expected to be: - feat/projects-backend +feat/projects-backend Verify this rather than blindly assuming it. The final integration branch should be: - dogfood/v0 +dogfood/v0 Your job is to manage the entire implementation through parallel isolated worktrees. ====================================================================== -1. PRODUCT OBJECTIVE -====================================================================== + +1. PRODUCT OBJECTIVE ====================================================================== Ship the smallest complete Zopu dogfooding loop for the Zopu repository itself: @@ -67,25 +67,11 @@ Ship the smallest complete Zopu dogfooding loop for the Zopu repository itself: The completed proof should be something like: - User: - “Add a health endpoint that exposes the current build commit.” +User: “Add a health endpoint that exposes the current build commit.” - Result: - ✓ exact message evidence stored - ✓ Signal created - ✓ Work Unit created or updated - ✓ work started - ✓ Orb provisioned - ✓ OpenCode session started - ✓ repository changed - ✓ tests executed - ✓ branch committed and pushed - ✓ pull request opened - ✓ work card updated - ✓ contextual follow-up reaches the active work +Result: ✓ exact message evidence stored ✓ Signal created ✓ Work Unit created or updated ✓ work started ✓ Orb provisioned ✓ OpenCode session started ✓ repository changed ✓ tests executed ✓ branch committed and pushed ✓ pull request opened ✓ work card updated ✓ contextual follow-up reaches the active work -====================================================================== -2. HARD SCOPE +====================================================================== 2. HARD SCOPE ====================================================================== This is a narrow dogfooding implementation. @@ -128,6 +114,7 @@ ProjectIssue may remain the backend representation of a Work Unit for this versi Project events may represent execution steps and activity. Project artifacts may represent: + - work.md; - steps.md; - agent reports; @@ -136,8 +123,7 @@ Project artifacts may represent: - commits; - pull requests. -====================================================================== -3. OPERATING RULES +====================================================================== 3. OPERATING RULES ====================================================================== Before changing anything: @@ -197,22 +183,18 @@ When an agent is blocked: Do not stop after spawning agents. You must monitor and integrate them. -====================================================================== -4. INITIAL BRANCH PREPARATION +====================================================================== 4. INITIAL BRANCH PREPARATION ====================================================================== Inspect the current branch. If currently on feat/projects-backend: - git pull --ff-only - git switch -c dogfood/v0 - git push -u origin dogfood/v0 +git pull --ff-only git switch -c dogfood/v0 git push -u origin dogfood/v0 If dogfood/v0 already exists: - git switch dogfood/v0 - git pull --ff-only +git switch dogfood/v0 git pull --ff-only If the expected branch is different, inspect the repository history and choose the branch containing the current projects backend work. Do not discard existing uncommitted work. @@ -220,23 +202,20 @@ Run the baseline checks before spawning agents. Record failures but do not spend Suggested baseline: - bun install - bun run check +bun install bun run check Also inspect package-specific scripts and use the actual commands defined by the repository. -====================================================================== -5. PASEO ORCHESTRATION +====================================================================== 5. PASEO ORCHESTRATION ====================================================================== Verify Paseo first: - paseo status - paseo ls +paseo status paseo ls If the daemon is unavailable, inspect: - tail -n 200 ~/.paseo/daemon.log +tail -n 200 ~/.paseo/daemon.log Do not continue spawning until Paseo works. @@ -245,24 +224,24 @@ For every child lane: 1. Create a worktree workspace: paseo workspace create \ - --isolation worktree \ - --mode branch-off \ - --new-branch \ - --base dogfood/v0 \ - --title "" \ - --json + --isolation worktree \ + --mode branch-off \ + --new-branch <branch> \ + --base dogfood/v0 \ + --title "<title>" \ + --json 2. Read the returned workspaceId. 3. Start the child agent: paseo run \ - --provider codex/glm-5.2 \ - --workspace <workspaceId> \ - --title "<title>" \ - --background \ - --json \ - "<full lane prompt>" + --provider codex/glm-5.2 \ + --workspace <workspaceId> \ + --title "<title>" \ + --background \ + --json \ + "<full lane prompt>" 4. Save: - branch; @@ -273,33 +252,27 @@ For every child lane: Maintain an orchestration note in a temporary file outside committed product code, for example: - /tmp/zopu-dogfood-orchestration.md +/tmp/zopu-dogfood-orchestration.md Track: -| Lane | Branch | Workspace ID | Agent ID | Status | PR | -|------|--------|--------------|----------|--------|----| +| Lane | Branch | Workspace ID | Agent ID | Status | PR | +| ---- | ------ | ------------ | -------- | ------ | --- | Use: - paseo ls - paseo inspect <agentId> - paseo logs <agentId> - paseo send <agentId> "<focused instruction>" - paseo wait <agentId> +paseo ls paseo inspect <agentId> paseo logs <agentId> paseo send <agentId> "<focused instruction>" paseo wait <agentId> Do not block waiting on one child while others are still running. Poll all first-wave agents periodically. -====================================================================== -6. SHARED CHILD-AGENT PREFIX +====================================================================== 6. SHARED CHILD-AGENT PREFIX ====================================================================== Prepend the following instructions to every child prompt: You are implementing one isolated lane of the Zopu dogfood v0 loop. -Repository base branch: - dogfood/v0 +Repository base branch: dogfood/v0 Your branch is already isolated in a Paseo Git worktree. @@ -347,29 +320,30 @@ Before finishing: - PR URL or exact PR creation instructions; - remaining limitations. -====================================================================== -7. FIRST-WAVE PARALLEL LANES +====================================================================== 7. FIRST-WAVE PARALLEL LANES ====================================================================== Spawn all four first-wave agents immediately. ----------------------------------------------------------------------- +--- + LANE A — PROJECT CHAT, SIGNALS AND WORK ROUTING ---------------------------------------------------------------------- Branch: - dogfood/zopu-orchestrator +dogfood/zopu-orchestrator Title: - Zopu Signals Orchestrator +Zopu Signals Orchestrator Prompt: Implement the project-scoped Zopu chat, Signal extraction and work-routing loop. Inspect the existing: + - Zopu agent; - Signals primitive; - ProjectIssue primitive; @@ -421,10 +395,10 @@ Rules: Acceptance examples: -Message: - “The work-unit card should expose the generated PR.” +Message: “The work-unit card should expose the generated PR.” Expected: + - exact message persisted; - Signal created; - attached to an existing UI/workspace issue if relevant; @@ -449,17 +423,18 @@ Likely ownership: - packages/backend/convex/schema* - Signal/project-issue primitives only as required. ----------------------------------------------------------------------- +--- + LANE B — ORB RUNTIME FOUNDATION ---------------------------------------------------------------------- Branch: - dogfood/orb-runtime +dogfood/orb-runtime Title: - AgentOS OpenCode Orb Runtime +AgentOS OpenCode Orb Runtime Prompt: @@ -525,6 +500,7 @@ Keep product-domain concepts separate from infrastructure leases. Do not make AgentOS VM state equal the work-unit state. Actor identity must include: + - project; - issue/work unit; - run. @@ -577,21 +553,23 @@ Document: - current limitations. Do not modify: + - Zopu chat behavior; - project-manager delegation; - web UI. ----------------------------------------------------------------------- +--- + LANE C — WEB WORKSPACE AND WORK CARDS ---------------------------------------------------------------------- Branch: - dogfood/web-workspace +dogfood/web-workspace Title: - Zopu Web Work OS +Zopu Web Work OS Prompt: @@ -613,18 +591,21 @@ Product projection: Required interface: Header: + - Zopu; - current project; - project/runtime connection status; - basic project switcher if already supported. Main surface: + - global project conversation; - active Work Unit cards; - lightweight Signals display; - persistent composer. Collapsed Work Unit card: + - title; - latest summary; - linked Signal count; @@ -635,6 +616,7 @@ Collapsed Work Unit card: - needs-input indicator when available. Expanded Work Unit: + - objective; - linked Signals and provenance; - current plan or steps; @@ -705,17 +687,18 @@ Tests: - empty state; - needs-input display. ----------------------------------------------------------------------- +--- + LANE D — SINGLE-NODE EXECUTION DEPLOYMENT ---------------------------------------------------------------------- Branch: - dogfood/runtime-deploy +dogfood/runtime-deploy Title: - Zopu Dedicated Runtime Deployment +Zopu Dedicated Runtime Deployment Prompt: @@ -794,8 +777,7 @@ The deployment must be reproducible on a clean Debian host. Do not require the developer’s MacBook to remain online after deployment. -====================================================================== -8. FIRST-WAVE MONITORING +====================================================================== 8. FIRST-WAVE MONITORING ====================================================================== After spawning all four first-wave agents: @@ -815,16 +797,15 @@ Use concise interventions. Examples: - paseo send <agentId> "Keep ProjectIssue as the v0 Work Unit. Do not introduce a second generic work-unit schema." +paseo send <agentId> "Keep ProjectIssue as the v0 Work Unit. Do not introduce a second generic work-unit schema." - paseo send <agentId> "Do not wire project-manager yet. Finish the Orb port, adapter and proof fixture only." +paseo send <agentId> "Do not wire project-manager yet. Finish the Orb port, adapter and proof fixture only." - paseo send <agentId> "Use existing Convex contracts. Avoid backend schema changes in the UI lane." +paseo send <agentId> "Use existing Convex contracts. Avoid backend schema changes in the UI lane." Wait until each first-wave agent is idle, then inspect: - paseo inspect <agentId> - paseo logs <agentId> +paseo inspect <agentId> paseo logs <agentId> Review each branch yourself. @@ -842,8 +823,7 @@ For every lane: Do not merge a failing lane merely because the child says it works. -====================================================================== -9. FIRST-WAVE MERGE PROCESS +====================================================================== 9. FIRST-WAVE MERGE PROCESS ====================================================================== Recommended merge order: @@ -855,11 +835,7 @@ Recommended merge order: Before every merge: - git switch dogfood/v0 - git pull --ff-only - git fetch origin - git log --oneline --decorate --max-count=10 origin/<branch> - git diff dogfood/v0...origin/<branch> +git switch dogfood/v0 git pull --ff-only git fetch origin git log --oneline --decorate --max-count=10 origin/<branch> git diff dogfood/v0...origin/<branch> Run lane-specific checks. @@ -867,31 +843,28 @@ Merge using the repository’s normal PR workflow when possible. If merging locally: - git merge --no-ff origin/<branch> +git merge --no-ff origin/<branch> Resolve conflicts conservatively. After each merge: - bun install - run targeted checks - git push origin dogfood/v0 +bun install run targeted checks git push origin dogfood/v0 Do not squash away useful commits unless repository policy requires squashing. -====================================================================== -10. SECOND-WAVE LANE — WIRE PROJECT MANAGER TO ORBS +====================================================================== 10. SECOND-WAVE LANE — WIRE PROJECT MANAGER TO ORBS ====================================================================== Only spawn this lane after the Orb runtime branch is integrated into dogfood/v0. Branch: - dogfood/orb-wiring +dogfood/orb-wiring Title: - Project Manager Orb Wiring +Project Manager Orb Wiring Create its Paseo workspace from the updated dogfood/v0 branch. @@ -944,20 +917,13 @@ Refactor the current Git lifecycle so it can be invoked from the Orb/application Failure mapping: -- runtime unavailable: - infrastructure failure with clear reason; -- missing user context: - needs input; -- failed tests that can be repaired: - agent continues; -- unrecoverable repeated failure: - failed with logs and evidence; -- cancellation: - terminate OpenCode and sandbox processes; -- Git rejection: - preserve commit and expose exact failure; -- PR creation failure: - retain pushed branch and retry safely. +- runtime unavailable: infrastructure failure with clear reason; +- missing user context: needs input; +- failed tests that can be repaired: agent continues; +- unrecoverable repeated failure: failed with logs and evidence; +- cancellation: terminate OpenCode and sandbox processes; +- Git rejection: preserve commit and expose exact failure; +- PR creation failure: retain pushed branch and retry safely. Idempotency: @@ -981,8 +947,7 @@ Tests: Do not add multi-agent parallel execution. -====================================================================== -11. SECOND-WAVE REVIEW AND MERGE +====================================================================== 11. SECOND-WAVE REVIEW AND MERGE ====================================================================== Monitor the orb-wiring agent closely. @@ -1007,19 +972,18 @@ Review and merge it into dogfood/v0 only after: - idempotency is demonstrated; - no secrets appear in logs or artifacts. -====================================================================== -12. FINAL LANE — END-TO-END DOGFOOD INTEGRATION +====================================================================== 12. FINAL LANE — END-TO-END DOGFOOD INTEGRATION ====================================================================== After all implementation lanes are merged, spawn the final integration agent. Branch: - dogfood/e2e-integration +dogfood/e2e-integration Title: - Zopu Dogfood End-to-End +Zopu Dogfood End-to-End Prompt: @@ -1100,7 +1064,7 @@ Verification matrix: Produce: - docs/DOGFOOD_V0.md +docs/DOGFOOD_V0.md It must contain: @@ -1119,8 +1083,7 @@ It must contain: Commit, push and open a PR targeting dogfood/v0. -====================================================================== -13. FINAL INTEGRATION RESPONSIBILITIES +====================================================================== 13. FINAL INTEGRATION RESPONSIBILITIES ====================================================================== After the final agent completes: @@ -1146,8 +1109,7 @@ If the full live path cannot complete: The final system must not claim that a PR exists unless it exists in Git. -====================================================================== -14. QUALITY GATES +====================================================================== 14. QUALITY GATES ====================================================================== A lane is not done merely because code was generated. @@ -1184,8 +1146,7 @@ The complete v0 must satisfy: - PR is created; - PR is visible in the UI. -====================================================================== -15. RESOURCE AND CONCURRENCY LIMITS +====================================================================== 15. RESOURCE AND CONCURRENCY LIMITS ====================================================================== For this v0: @@ -1201,12 +1162,11 @@ For this v0: When agents are no longer needed: - paseo archive <agentId> +paseo archive <agentId> Do not archive them until their logs and work have been reviewed. -====================================================================== -16. COMMUNICATION WITH THE USER +====================================================================== 16. COMMUNICATION WITH THE USER ====================================================================== Do not repeatedly ask the user to choose between reasonable implementation details. @@ -1230,8 +1190,7 @@ When requesting input, provide: Continue supervising all unblocked lanes while waiting. -====================================================================== -17. FINAL REPORT +====================================================================== 17. FINAL REPORT ====================================================================== When finished, provide a concise but complete report: @@ -1254,6 +1213,7 @@ When finished, provide a concise but complete report: ## Integrated branches For each branch: + - purpose; - final commit; - PR; diff --git a/docs/evaluation.md b/docs/evaluation.md index 491b25e..992043a 100644 --- a/docs/evaluation.md +++ b/docs/evaluation.md @@ -157,12 +157,12 @@ Observe real Work with human decisions, merge outcomes, incidents, and post-rele Score each candidate run 0–3 per dimension. -| Score | Meaning | -|---|---| -| 0 | unsafe/incorrect/missing | -| 1 | substantial human repair required | -| 2 | acceptable with minor correction | -| 3 | correct, complete, reviewable | +| Score | Meaning | +| ----- | --------------------------------- | +| 0 | unsafe/incorrect/missing | +| 1 | substantial human repair required | +| 2 | acceptable with minor correction | +| 3 | correct, complete, reviewable | Dimensions: diff --git a/docs/glossary.md b/docs/glossary.md index 48ace4c..77ceaea 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -3,7 +3,7 @@ > **Purpose:** prevent agents and humans from using overlapping terms inconsistently. | Term | Definition | Not the same as | -|---|---|---| +| --- | --- | --- | | Project | Durable product/repository/context boundary. | Work or chat thread | | Message | Exact conversational input/output. | Signal | | Signal | Provenanced evidence that may require attention. | Work | diff --git a/docs/handoff-slice1.md b/docs/handoff-slice1.md index 50d79cb..b03249a 100644 --- a/docs/handoff-slice1.md +++ b/docs/handoff-slice1.md @@ -1,12 +1,11 @@ # Slice 1 Handoff Notes -Date: 2026-07-27 -Branch: `master` at `cfdb2efc7` -PR: #19 (merged) +Date: 2026-07-27 Branch: `master` at `cfdb2efc7` PR: #19 (merged) ## What was built ### Core product loop + - Conversation to Signal to proposed Work with exact source provenance. - Convex owns durable data: `conversationMessages`, `signals`, `works`, `workEvents`. - Effect validation in `@code/work-os` package. @@ -15,6 +14,7 @@ PR: #19 (merged) - Mobile-first UI at `apps/web/src/components/slice-one/slice-one-page.tsx`. ### Model + - MiMo V2.5 (`xiaomi/mimo-v2.5`) via Cheaptricks AI gateway. - Provider identity `xiaomi` gives Flue correct multimodal metadata (text + image). - `AGENT_MODEL_BASE_URL` points at Cheaptricks; API key in `.env`. @@ -22,28 +22,33 @@ PR: #19 (merged) - `thinkingLevel: "medium"`. ### Reasoning traces + - Native Flue `reasoning` parts render as live "Thinking trace" (open while streaming, collapsed after). - Inline `<think>...</think>` extraction for models that embed thinking in text. - No fallback that hides model output. ### Image attachments + - Picker button + hidden file input, up to 4 images at 10 MB each. - Base64 conversion into Flue `AgentPromptImage`. - Authenticated blob-URL rendering for historical image replay. - Verified live: MiMo read exact text and background color from generated test images. ### Mobile keyboard fix + - Visual viewport hook (`use-visual-viewport.ts`). - Fixed full-screen shell; `interactive-widget=resizes-content` in viewport meta. - Header stays pinned; chat shrinks; composer follows keyboard. - Regression tests in `frontend-regressions.test.ts` and `use-visual-viewport.test.ts`. ### Rendering + - Streamdown markdown streaming with Mermaid chart support. - Tool turns hidden; reasoning-containing tool turns remain visible. - Work cards render inline in the conversation timeline with source navigation. ### Deployment + - Cheaptricks staging live at `https://zopu.cheaptricks.puter.wtf`. - Caddy reverse proxy: `/api/*` to Flue (port 3585), root to web (port 5173). - `server.allowedHosts: true` in `apps/web/vite.config.ts` for Caddy domain. @@ -52,6 +57,7 @@ PR: #19 (merged) - Deployment notes at `docs/deployment.md`. ### Repository cleanup + - 17 stale worktrees removed (Codex, Paseo, T3). - 23 stale local branches deleted. - 18 stale remote branches deleted. @@ -62,16 +68,19 @@ PR: #19 (merged) ## Current state ### Running + - Cheaptricks: web on `:5173`, Flue on `:3585`, both persistent via `nohup`. - Local Mac: servers stopped. Run `bun run dev:tailscale:agents -- --port 3585` and `bun run dev:tailscale:web` to start. ### Git + - `master` at `cfdb2efc7` on local, Cheaptricks, and remote. - Only `master` branch locally. Two remote branches remain: `origin/sai/changes`, `origin/t3code/explore-primitives-package` (not cleaned because they may be owned by other contributors/tools). ## Known issues and next steps 1. **Convex SITE_URL is single-valued.** Switch it when moving between local and staging: + ```bash cd packages/backend npx convex env set SITE_URL 'http://100.101.157.28:5173' # local diff --git a/oxfmt.config.ts b/oxfmt.config.ts index e9b8afb..e8aaa3b 100644 --- a/oxfmt.config.ts +++ b/oxfmt.config.ts @@ -3,4 +3,10 @@ import ultracite from "ultracite/oxfmt"; export default defineConfig({ ...ultracite, + ignorePatterns: [ + ...ultracite.ignorePatterns, + "**/.agents/skills/**", + "repos/**", + "scripts/**", + ], }); diff --git a/oxlint.config.ts b/oxlint.config.ts index 158b4ff..75c76b8 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -5,27 +5,43 @@ import remix from "ultracite/oxlint/remix"; export default defineConfig({ extends: [core, react, remix], - ignorePatterns: [...core.ignorePatterns, "repos/**", "scripts/**"], + ignorePatterns: [ + ...core.ignorePatterns, + "**/.agents/skills/**", + "repos/**", + "scripts/**", + ], overrides: [ + { + files: ["packages/ui/src/components/*.tsx"], + rules: { + "eslint/func-style": "off", + "jsx-a11y/click-events-have-key-events": "off", + "jsx-a11y/label-has-associated-control": "off", + "jsx-a11y/no-noninteractive-element-interactions": "off", + "jsx-a11y/prefer-tag-over-role": "off", + "sort-keys": "off", + }, + }, { files: ["**/convex/**/*.ts", "**/convex/**/*.test.ts"], rules: { - "unicorn/filename-case": "off", - // Convex targets ES2022: no toSorted/at; sequential awaits ensure atomicity - "unicorn/no-array-sort": "off", - "no-await-in-loop": "off", // Convex query builders use `any` in index callbacks "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-non-null-assertion": "off", "eslint/complexity": "off", + "no-await-in-loop": "off", + "unicorn/filename-case": "off", + // Convex targets ES2022: no toSorted/at; sequential awaits ensure atomicity + "unicorn/no-array-sort": "off", }, }, { files: ["**/convex/**/*.test.ts"], rules: { - "unicorn/no-await-expression-member": "off", "eslint/require-await": "off", "sort-keys": "off", + "unicorn/no-await-expression-member": "off", }, }, ], diff --git a/package.json b/package.json index 708560e..b4d455b 100644 --- a/package.json +++ b/package.json @@ -69,8 +69,8 @@ "build:agents": "vp run --filter @code/agents build", "docs:update": "node scripts/update-docs.ts", "subtree": "node scripts/subtree.ts", - "check": "ultracite check", - "fix": "ultracite fix" + "check": "ultracite check --disable-nested-config", + "fix": "ultracite fix --disable-nested-config" }, "devDependencies": { "@code/backend": "workspace:*", diff --git a/packages/agents/src/auth/conversation-route.ts b/packages/agents/src/auth/conversation-route.ts index dfa227b..ba1f74e 100644 --- a/packages/agents/src/auth/conversation-route.ts +++ b/packages/agents/src/auth/conversation-route.ts @@ -1,7 +1,34 @@ import { parseAgentEnv } from "@code/env/agent"; import type { AgentRouteHandler } from "@flue/runtime"; +import { ConvexHttpClient } from "convex/browser"; +import { makeFunctionReference } from "convex/server"; -import { withTurnAdmission } from "../adapters/admission-context"; +const bindTurnSubmission = makeFunctionReference< + "mutation", + { + readonly clientRequestId: string; + readonly submissionId: string; + readonly token: string; + readonly turnId: string; + }, + null +>("conversationMessages:bindTurnSubmission"); + +const readSubmissionId = async (response: Response): Promise<string | null> => { + const body: unknown = await response + .clone() + .json() + .catch(() => null); + if ( + typeof body !== "object" || + body === null || + !("submissionId" in body) || + typeof body.submissionId !== "string" + ) { + return null; + } + return body.submissionId; +}; /** Only Convex may invoke or observe the organization-scoped Flue agent. */ export const conversationRoute: AgentRouteHandler = async (context, next) => { @@ -26,5 +53,21 @@ export const conversationRoute: AgentRouteHandler = async (context, next) => { return context.json({ error: "Missing turn correlation headers" }, 400); } - return await withTurnAdmission({ clientRequestId, turnId }, () => next()); + return await next().then(async () => { + if (!context.res.ok) { + return context.res; + } + const submissionId = await readSubmissionId(context.res); + if (submissionId === null) { + return context.json({ error: "Flue admission response is invalid" }, 502); + } + const client = new ConvexHttpClient(env.CONVEX_URL); + await client.mutation(bindTurnSubmission, { + clientRequestId, + submissionId, + token: env.FLUE_DB_TOKEN, + turnId, + }); + return context.res; + }); }; diff --git a/packages/agents/src/db.ts b/packages/agents/src/db.ts new file mode 100644 index 0000000..4cdf707 --- /dev/null +++ b/packages/agents/src/db.ts @@ -0,0 +1 @@ +export { default } from "./adapters/flue-persistence"; diff --git a/packages/agents/tsconfig.json b/packages/agents/tsconfig.json index 1e01552..985f18c 100644 --- a/packages/agents/tsconfig.json +++ b/packages/agents/tsconfig.json @@ -1,14 +1,8 @@ { "extends": "@code/config/tsconfig.base.json", "compilerOptions": { - "types": [ - "bun" - ] + "types": ["bun"] }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "dist" - ] -} \ No newline at end of file + "include": ["src/**/*.ts"], + "exclude": ["dist"] +} diff --git a/packages/auth/src/native/auth-client.ts b/packages/auth/src/native/auth-client.ts index 432f65e..8dc6711 100644 --- a/packages/auth/src/native/auth-client.ts +++ b/packages/auth/src/native/auth-client.ts @@ -1,6 +1,9 @@ import { expoClient } from "@better-auth/expo/client"; import { env } from "@code/env/native"; -import { convexClient, crossDomainClient } from "@convex-dev/better-auth/client/plugins"; +import { + convexClient, + crossDomainClient, +} from "@convex-dev/better-auth/client/plugins"; import { createAuthClient } from "better-auth/react"; import Constants from "expo-constants"; import * as SecureStore from "expo-secure-store"; @@ -9,7 +12,7 @@ import { Platform } from "react-native"; const scheme = Constants.expoConfig?.scheme; if (typeof scheme !== "string") { - throw new Error("Expo auth requires a string app scheme in app.json"); + throw new TypeError("Expo auth requires a string app scheme in app.json"); } export const authClient = createAuthClient({ diff --git a/packages/auth/src/native/hooks.ts b/packages/auth/src/native/hooks.ts index 87a2c71..99684b5 100644 --- a/packages/auth/src/native/hooks.ts +++ b/packages/auth/src/native/hooks.ts @@ -1,12 +1,14 @@ import { useForm } from "@tanstack/react-form"; import { useToast } from "heroui-native"; -import { getErrorMessage, signInSchema, signUpSchema } from "../shared/forms"; +import { signInSchema, signUpSchema } from "../shared/forms"; import { authClient } from "./auth-client"; -type AuthFormOptions = { +export { getErrorMessage } from "../shared/forms"; + +interface AuthFormOptions { readonly onSuccess?: () => void; -}; +} export const useNativeSignInForm = ({ onSuccess }: AuthFormOptions = {}) => { const { toast } = useToast(); @@ -18,14 +20,17 @@ export const useNativeSignInForm = ({ onSuccess }: AuthFormOptions = {}) => { { email: value.email.trim(), password: value.password }, { onError: ({ error }) => { - toast.show({ label: error.message || "Failed to sign in", variant: "danger" }); + toast.show({ + label: error.message || "Failed to sign in", + variant: "danger", + }); }, onSuccess: () => { formApi.reset(); toast.show({ label: "Signed in successfully", variant: "success" }); onSuccess?.(); }, - }, + } ); }, validators: { onSubmit: signInSchema }, @@ -39,7 +44,11 @@ export const useNativeSignUpForm = ({ onSuccess }: AuthFormOptions = {}) => { defaultValues: { confirmPassword: "", email: "", name: "", password: "" }, onSubmit: async ({ formApi, value }) => { await authClient.signUp.email( - { email: value.email.trim(), name: value.name.trim(), password: value.password }, + { + email: value.email.trim(), + name: value.name.trim(), + password: value.password, + }, { onError: ({ error }) => { toast.show({ @@ -49,14 +58,15 @@ export const useNativeSignUpForm = ({ onSuccess }: AuthFormOptions = {}) => { }, onSuccess: () => { formApi.reset(); - toast.show({ label: "Account created successfully", variant: "success" }); + toast.show({ + label: "Account created successfully", + variant: "success", + }); onSuccess?.(); }, - }, + } ); }, validators: { onSubmit: signUpSchema }, }); }; - -export { getErrorMessage }; diff --git a/packages/auth/src/native/login-form.tsx b/packages/auth/src/native/login-form.tsx index e619593..ba81a6e 100644 --- a/packages/auth/src/native/login-form.tsx +++ b/packages/auth/src/native/login-form.tsx @@ -9,14 +9,15 @@ import { useThemeColor, } from "heroui-native"; import { useRef } from "react"; -import { Text, type TextInput, View } from "react-native"; +import { Text, View } from "react-native"; +import type { TextInput } from "react-native"; import { getErrorMessage, useNativeSignInForm } from "./hooks"; -type NativeLoginFormProps = { +interface NativeLoginFormProps { readonly onNavigateSignUp: () => void; readonly onSuccess?: () => void; -}; +} export const NativeLoginForm = ({ onNavigateSignUp, @@ -29,7 +30,14 @@ export const NativeLoginForm = ({ return ( <Surface className="rounded-xl p-5" variant="secondary"> - <Text style={{ color: foregroundColor, fontSize: 24, fontWeight: "600", marginBottom: 4 }}> + <Text + style={{ + color: foregroundColor, + fontSize: 24, + fontWeight: "600", + marginBottom: 4, + }} + > Welcome back </Text> <Text style={{ color: mutedColor, fontSize: 14, marginBottom: 20 }}> @@ -93,7 +101,11 @@ export const NativeLoginForm = ({ isDisabled={isSubmitting} onPress={() => void form.handleSubmit()} > - {isSubmitting ? <Spinner color="default" size="sm" /> : <Button.Label>Sign in</Button.Label>} + {isSubmitting ? ( + <Spinner color="default" size="sm" /> + ) : ( + <Button.Label>Sign in</Button.Label> + )} </Button> <Button onPress={onNavigateSignUp} variant="ghost"> diff --git a/packages/auth/src/native/signup-form.tsx b/packages/auth/src/native/signup-form.tsx index 1c58f10..fc5bf71 100644 --- a/packages/auth/src/native/signup-form.tsx +++ b/packages/auth/src/native/signup-form.tsx @@ -12,10 +12,10 @@ import { Text, View } from "react-native"; import { getErrorMessage, useNativeSignUpForm } from "./hooks"; -type NativeSignupFormProps = { +interface NativeSignupFormProps { readonly onNavigateSignIn: () => void; readonly onSuccess?: () => void; -}; +} export const NativeSignupForm = ({ onNavigateSignIn, @@ -27,7 +27,14 @@ export const NativeSignupForm = ({ return ( <Surface className="rounded-xl p-5" variant="secondary"> - <Text style={{ color: foregroundColor, fontSize: 24, fontWeight: "600", marginBottom: 4 }}> + <Text + style={{ + color: foregroundColor, + fontSize: 24, + fontWeight: "600", + marginBottom: 4, + }} + > Create an account </Text> <Text style={{ color: mutedColor, fontSize: 14, marginBottom: 20 }}> diff --git a/packages/auth/src/shared/forms.ts b/packages/auth/src/shared/forms.ts index dc2a305..89ff011 100644 --- a/packages/auth/src/shared/forms.ts +++ b/packages/auth/src/shared/forms.ts @@ -1,16 +1,30 @@ import { z } from "zod"; export const signInSchema = z.object({ - email: z.string().trim().min(1, "Email is required").email("Enter a valid email address"), - password: z.string().min(1, "Password is required").min(8, "Use at least 8 characters"), + email: z + .string() + .trim() + .min(1, "Email is required") + .email("Enter a valid email address"), + password: z + .string() + .min(1, "Password is required") + .min(8, "Use at least 8 characters"), }); export const signUpSchema = z .object({ confirmPassword: z.string().min(1, "Confirm your password"), - email: z.string().trim().min(1, "Email is required").email("Enter a valid email address"), + email: z + .string() + .trim() + .min(1, "Email is required") + .email("Enter a valid email address"), name: z.string().trim().min(2, "Name must be at least 2 characters"), - password: z.string().min(1, "Password is required").min(8, "Use at least 8 characters"), + password: z + .string() + .min(1, "Password is required") + .min(8, "Use at least 8 characters"), }) .refine(({ confirmPassword, password }) => confirmPassword === password, { message: "Passwords do not match", @@ -18,13 +32,19 @@ export const signUpSchema = z }); export const getErrorMessage = (error: unknown): string | null => { - if (!error) return null; - if (typeof error === "string") return error; + if (!error) { + return null; + } + if (typeof error === "string") { + return error; + } if (Array.isArray(error)) { for (const issue of error) { const message = getErrorMessage(issue); - if (message) return message; + if (message) { + return message; + } } return null; } diff --git a/packages/auth/src/web/login-form.tsx b/packages/auth/src/web/login-form.tsx index ce50f97..97ab0d0 100644 --- a/packages/auth/src/web/login-form.tsx +++ b/packages/auth/src/web/login-form.tsx @@ -38,7 +38,9 @@ export const LoginForm = ({ <Card> <CardHeader> <CardTitle>Welcome back</CardTitle> - <CardDescription>Enter your email and password to continue.</CardDescription> + <CardDescription> + Enter your email and password to continue. + </CardDescription> </CardHeader> <CardContent> <form @@ -58,7 +60,9 @@ export const LoginForm = ({ id={field.name} name={field.name} onBlur={field.handleBlur} - onChange={(event) => field.handleChange(event.target.value)} + onChange={(event) => + field.handleChange(event.target.value) + } placeholder="you@example.com" type="email" value={field.state.value} @@ -77,7 +81,9 @@ export const LoginForm = ({ id={field.name} name={field.name} onBlur={field.handleBlur} - onChange={(event) => field.handleChange(event.target.value)} + onChange={(event) => + field.handleChange(event.target.value) + } type="password" value={field.state.value} /> @@ -100,7 +106,8 @@ export const LoginForm = ({ {isSubmitting ? "Signing in…" : "Sign in"} </Button> <FieldDescription className="text-center"> - Don't have an account? <a href={signUpHref}>Sign up</a> + Don't have an account?{" "} + <a href={signUpHref}>Sign up</a> </FieldDescription> </Field> )} diff --git a/packages/auth/src/web/signup-form.tsx b/packages/auth/src/web/signup-form.tsx index 30d4950..9c2c320 100644 --- a/packages/auth/src/web/signup-form.tsx +++ b/packages/auth/src/web/signup-form.tsx @@ -35,7 +35,9 @@ export const SignupForm = ({ <Card {...props}> <CardHeader> <CardTitle>Create an account</CardTitle> - <CardDescription>Use your name, email, and password to get started.</CardDescription> + <CardDescription> + Use your name, email, and password to get started. + </CardDescription> </CardHeader> <CardContent> <form @@ -96,7 +98,9 @@ export const SignupForm = ({ type="password" value={field.state.value} /> - <FieldDescription>Use at least 8 characters.</FieldDescription> + <FieldDescription> + Use at least 8 characters. + </FieldDescription> <FieldError errors={field.state.meta.errors} /> </Field> )} diff --git a/packages/backend/AGENTS.md b/packages/backend/AGENTS.md index 85c46a7..ee37b19 100644 --- a/packages/backend/AGENTS.md +++ b/packages/backend/AGENTS.md @@ -2,12 +2,8 @@ This project uses [Convex](https://convex.dev) as its backend. -When working on Convex code, **always read -`convex/_generated/ai/guidelines.md` first** for important guidelines on -how to correctly use Convex APIs and patterns. The file contains rules that -override what you may have learned about Convex from training data. +When working on Convex code, **always read `convex/_generated/ai/guidelines.md` first** for important guidelines on how to correctly use Convex APIs and patterns. The file contains rules that override what you may have learned about Convex from training data. -Convex agent skills for common tasks can be installed by running -`npx convex ai-files install`. +Convex agent skills for common tasks can be installed by running `npx convex ai-files install`. <!-- convex-ai-end --> diff --git a/packages/backend/convex/conversationMessages.test.ts b/packages/backend/convex/conversationMessages.test.ts index 181aed0..32ac4a7 100644 --- a/packages/backend/convex/conversationMessages.test.ts +++ b/packages/backend/convex/conversationMessages.test.ts @@ -1,3 +1,4 @@ +import { env } from "@code/env/convex"; import { convexTest } from "convex-test"; import { anyApi, makeFunctionReference } from "convex/server"; import { describe, expect, test } from "vitest"; @@ -168,4 +169,43 @@ describe("conversationMessages", () => { submissionId: "submission-1", }); }); + + test("binds a Flue admission receipt exactly once", async () => { + const t = newTest(); + const organization = await ensureOrg(t, identityA); + const queued = await t + .withIdentity(identityA) + .mutation(api.conversationMessages.send, { + clientRequestId: "request-bind", + images: [], + organizationId: organization._id, + rawText: "Bind me", + }); + await t.mutation(markProcessingRef, { + attempt: 1, + leaseOwner: "worker-bind", + turnId: queued.turnId, + }); + + await t.mutation(api.conversationMessages.bindTurnSubmission, { + clientRequestId: "request-bind", + submissionId: "submission-bind", + token: env.FLUE_DB_TOKEN, + turnId: queued.turnId, + }); + await t.mutation(api.conversationMessages.bindTurnSubmission, { + clientRequestId: "request-bind", + submissionId: "submission-bind", + token: env.FLUE_DB_TOKEN, + turnId: queued.turnId, + }); + + const turn = await t.run((ctx) => ctx.db.get(queued.turnId)); + expect(turn).toMatchObject({ + status: "running", + submissionId: "submission-bind", + }); + expect(turn).not.toHaveProperty("leaseExpiresAt"); + expect(turn).not.toHaveProperty("leaseOwner"); + }); }); diff --git a/packages/backend/convex/conversationMessages.ts b/packages/backend/convex/conversationMessages.ts index 6376c48..f963aad 100644 --- a/packages/backend/convex/conversationMessages.ts +++ b/packages/backend/convex/conversationMessages.ts @@ -263,6 +263,38 @@ export const markProcessing = internalMutation({ }, }); +export const bindTurnSubmission = mutation({ + args: { + clientRequestId: v.string(), + submissionId: v.string(), + token: v.string(), + turnId: v.id("conversationTurns"), + }, + handler: async (ctx, args): Promise<null> => { + if (args.token !== env.FLUE_DB_TOKEN) { + throw new Error("Invalid agent control token"); + } + const turn = await ctx.db.get(args.turnId); + if (!turn || turn.clientRequestId !== args.clientRequestId) { + throw new Error("Turn admission correlation does not match"); + } + if (turn.status === "running" && turn.submissionId === args.submissionId) { + return null; + } + if (turn.status !== "dispatching" || turn.submissionId !== undefined) { + throw new Error("Turn is not awaiting Flue admission"); + } + await ctx.db.patch(turn._id, { + error: undefined, + leaseExpiresAt: undefined, + leaseOwner: undefined, + status: "running", + submissionId: args.submissionId, + }); + return null; + }, +}); + export const failTurn = internalMutation({ args: { attempt: v.number(), @@ -322,7 +354,7 @@ export const runTurn = internalAction({ if (!flueUrl) { throw new Error("FLUE_URL is not configured in Convex"); } - const images = await Promise.all( + const attachments = await Promise.all( turn.attachments.map(async (attachment) => { const url = await ctx.storage.getUrl(attachment.storageId); const response = url ? await fetch(url) : null; @@ -331,6 +363,7 @@ export const runTurn = internalAction({ } return { data: toBase64(await response.arrayBuffer()), + filename: attachment.filename, mimeType: attachment.mimeType, type: "image" as const, }; @@ -341,7 +374,11 @@ export const runTurn = internalAction({ `${flueUrl.replace(/\/+$/u, "")}/` ); const response = await fetch(endpoint, { - body: JSON.stringify({ images, message: turn.user.content }), + body: JSON.stringify({ + attachments, + body: turn.user.content, + kind: "user", + }), headers: { authorization: `Bearer ${env.FLUE_DB_TOKEN}`, "content-type": "application/json", diff --git a/packages/backend/convex/fluePersistence.ts b/packages/backend/convex/fluePersistence.ts index a61f4b9..194d0e5 100644 --- a/packages/backend/convex/fluePersistence.ts +++ b/packages/backend/convex/fluePersistence.ts @@ -7,7 +7,7 @@ import { mutation, query } from "./_generated/server"; import type { MutationCtx, QueryCtx } from "./_generated/server"; import { projectConversationRecords } from "./conversationProjections"; -const FLUE_SCHEMA_VERSION = "4"; +const FLUE_SCHEMA_VERSION = "5"; const DURABILITY_DEFAULT_MAX_ATTEMPTS = 10; const DURABILITY_DEFAULT_TIMEOUT_MS = 3_600_000; const LEASE_DURATION_MS = 30_000; diff --git a/packages/backend/convex/healthCheck.ts b/packages/backend/convex/healthCheck.ts index 7f9919e..f37d3f6 100644 --- a/packages/backend/convex/healthCheck.ts +++ b/packages/backend/convex/healthCheck.ts @@ -1,7 +1,5 @@ import { query } from "./_generated/server"; export const get = query({ - handler: () => - "OK" - , + handler: () => "OK", }); diff --git a/packages/backend/convex/publicGit.ts b/packages/backend/convex/publicGit.ts index 391b8ff..4f51abe 100644 --- a/packages/backend/convex/publicGit.ts +++ b/packages/backend/convex/publicGit.ts @@ -1,5 +1,13 @@ -import { CONTEXT_KINDS, contextPathForKind, MAX_CONTEXT_DOCUMENT_CHARACTERS } from '@code/primitives/project'; -import type { PreparedPublicGitSource, PublicGitImportResult, RepositoryContextDocument } from '@code/primitives/project'; +import { + CONTEXT_KINDS, + contextPathForKind, + MAX_CONTEXT_DOCUMENT_CHARACTERS, +} from "@code/primitives/project"; +import type { + PreparedPublicGitSource, + PublicGitImportResult, + RepositoryContextDocument, +} from "@code/primitives/project"; const GITHUB_HOST = "github.com"; const GITEA_HOST = "git.openputer.com"; diff --git a/packages/backend/package.json b/packages/backend/package.json index 7bed8f6..e303457 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -6,6 +6,7 @@ "author": "", "scripts": { "dev": "convex dev --env-file ../../.env", + "dev:once": "convex dev --once --env-file=../../.env", "dev:setup": "convex dev --env-file ../../.env --configure --until-success", "check-types": "tsc --noEmit -p convex/tsconfig.json", "test": "CONVEX_SITE_URL=https://convex.test FLUE_DB_TOKEN=test-token SITE_URL=https://site.test vitest run", diff --git a/packages/env/src/native.ts b/packages/env/src/native.ts index e573996..793991f 100644 --- a/packages/env/src/native.ts +++ b/packages/env/src/native.ts @@ -7,11 +7,11 @@ const convexUrlSchema = (exampleHost: string) => }); export const env = createEnv({ - clientPrefix: "EXPO_PUBLIC_", client: { - EXPO_PUBLIC_CONVEX_URL: convexUrlSchema("example.convex.cloud"), EXPO_PUBLIC_CONVEX_SITE_URL: convexUrlSchema("example.convex.site"), + EXPO_PUBLIC_CONVEX_URL: convexUrlSchema("example.convex.cloud"), }, - runtimeEnv: process.env, + clientPrefix: "EXPO_PUBLIC_", emptyStringAsUndefined: true, + runtimeEnv: process.env, }); diff --git a/packages/env/src/server.ts b/packages/env/src/server.ts index 2359302..a500041 100644 --- a/packages/env/src/server.ts +++ b/packages/env/src/server.ts @@ -7,17 +7,17 @@ const convexUrlSchema = (exampleHost: string) => }); export const env = createEnv({ + emptyStringAsUndefined: true, + runtimeEnv: process.env, server: { CONVEX_URL: convexUrlSchema("example.convex.cloud"), + DAEMON_COMMAND_LEASE_MS: z.coerce.number().int().positive().default(60_000), + DAEMON_HEARTBEAT_MS: z.coerce.number().int().positive().default(15_000), DAEMON_ID: z.string().min(1), DAEMON_NAME: z.string().min(1).optional(), DAEMON_VERSION: z.string().default("0.0.0"), - DAEMON_HEARTBEAT_MS: z.coerce.number().int().positive().default(15_000), - DAEMON_COMMAND_LEASE_MS: z.coerce.number().int().positive().default(60_000), FLUE_DB_TOKEN: z.string().min(1), RIVET_ENDPOINT: z.url().optional(), }, - runtimeEnv: process.env, skipValidation: process.env.SKIP_ENV_VALIDATION === "true", - emptyStringAsUndefined: true, }); diff --git a/packages/ui/src/components/attachment.tsx b/packages/ui/src/components/attachment.tsx index 3e0cc88..e9994bc 100644 --- a/packages/ui/src/components/attachment.tsx +++ b/packages/ui/src/components/attachment.tsx @@ -2,25 +2,26 @@ import { mergeProps } from "@base-ui/react/merge-props"; import { useRender } from "@base-ui/react/use-render"; import { Button } from "@code/ui/components/button"; import { cn } from "@code/ui/lib/utils"; -import { cva, type VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; +import type { VariantProps } from "class-variance-authority"; import * as React from "react"; const attachmentVariants = cva( "group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-none border bg-card text-card-foreground transition-colors focus-within:ring-1 focus-within:ring-ring/50 has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed", { variants: { + orientation: { + horizontal: "min-w-40 items-center", + vertical: "w-24 flex-col has-data-[slot=attachment-content]:w-30", + }, size: { default: "gap-2 text-xs has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5", sm: "gap-2.5 text-xs has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1", xs: "gap-1.5 rounded-none text-xs has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1", }, - orientation: { - horizontal: "min-w-40 items-center", - vertical: "w-24 flex-col has-data-[slot=attachment-content]:w-30", - }, }, - }, + } ); function Attachment({ @@ -41,7 +42,7 @@ function Attachment({ data-state={state} data-size={size} data-orientation={resolvedOrientation} - className={cn(attachmentVariants({ size, orientation }), className)} + className={cn(attachmentVariants({ orientation, size }), className)} {...props} /> ); @@ -50,6 +51,9 @@ function Attachment({ const attachmentMediaVariants = cva( "relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-none bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-none group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5", { + defaultVariants: { + variant: "icon", + }, variants: { variant: { icon: "", @@ -57,10 +61,7 @@ const attachmentMediaVariants = cva( "opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover", }, }, - defaultVariants: { - variant: "icon", - }, - }, + } ); function AttachmentMedia({ @@ -78,53 +79,65 @@ function AttachmentMedia({ ); } -function AttachmentContent({ className, ...props }: React.ComponentProps<"div">) { +function AttachmentContent({ + className, + ...props +}: React.ComponentProps<"div">) { return ( <div data-slot="attachment-content" className={cn( "max-w-full min-w-0 flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1", - className, + className )} {...props} /> ); } -function AttachmentTitle({ className, ...props }: React.ComponentProps<"span">) { +function AttachmentTitle({ + className, + ...props +}: React.ComponentProps<"span">) { return ( <span data-slot="attachment-title" className={cn( "block max-w-full min-w-0 truncate font-medium group-data-[state=processing]/attachment:shimmer group-data-[state=uploading]/attachment:shimmer", - className, + className )} {...props} /> ); } -function AttachmentDescription({ className, ...props }: React.ComponentProps<"span">) { +function AttachmentDescription({ + className, + ...props +}: React.ComponentProps<"span">) { return ( <span data-slot="attachment-description" className={cn( "mt-0.5 block min-w-0 truncate text-xs text-muted-foreground group-data-[state=error]/attachment:text-destructive/80", "max-w-full", - className, + className )} {...props} /> ); } -function AttachmentActions({ className, ...props }: React.ComponentProps<"div">) { +function AttachmentActions({ + className, + ...props +}: React.ComponentProps<"div">) { return ( <div data-slot="attachment-actions" className={cn( "relative z-20 flex shrink-0 items-center group-data-[orientation=vertical]/attachment:absolute group-data-[orientation=vertical]/attachment:top-3 group-data-[orientation=vertical]/attachment:right-3 group-data-[orientation=vertical]/attachment:gap-1", - className, + className )} {...props} /> @@ -160,10 +173,10 @@ function AttachmentTrigger({ defaultTagName: "button", props: mergeProps<"button">( { - type: render ? type : (type ?? "button"), className: cn("absolute inset-0 z-10 outline-none", className), + type: render ? type : (type ?? "button"), }, - props, + props ), render, state: { @@ -178,7 +191,7 @@ function AttachmentGroup({ className, ...props }: React.ComponentProps<"div">) { data-slot="attachment-group" className={cn( "flex min-w-0 scroll-fade-x snap-x snap-mandatory scroll-px-1 scrollbar-none gap-3 overflow-x-auto overscroll-x-contain py-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start", - className, + className )} {...props} /> diff --git a/packages/ui/src/components/bubble.tsx b/packages/ui/src/components/bubble.tsx index a21fc54..2333d32 100644 --- a/packages/ui/src/components/bubble.tsx +++ b/packages/ui/src/components/bubble.tsx @@ -1,7 +1,8 @@ import { mergeProps } from "@base-ui/react/merge-props"; import { useRender } from "@base-ui/react/use-render"; import { cn } from "@code/ui/lib/utils"; -import { cva, type VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; +import type { VariantProps } from "class-variance-authority"; import * as React from "react"; function BubbleGroup({ className, ...props }: React.ComponentProps<"div">) { @@ -17,28 +18,28 @@ function BubbleGroup({ className, ...props }: React.ComponentProps<"div">) { const bubbleVariants = cva( "group/bubble relative flex w-fit max-w-[80%] min-w-0 flex-col gap-1 group-data-[align=end]/message:self-end data-[align=end]:self-end data-[variant=ghost]:max-w-full", { + defaultVariants: { + variant: "default", + }, variants: { variant: { default: "*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80", - secondary: - "*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]", - muted: - "*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]", - tinted: - "*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]", - outline: - "*:data-[slot=bubble-content]:border-border *:data-[slot=bubble-content]:bg-background [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30", - ghost: - "border-none *:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50", destructive: "*:data-[slot=bubble-content]:bg-destructive/10 *:data-[slot=bubble-content]:text-destructive dark:*:data-[slot=bubble-content]:bg-destructive/20 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30", + ghost: + "border-none *:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50", + muted: + "*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]", + outline: + "*:data-[slot=bubble-content]:border-border *:data-[slot=bubble-content]:bg-background [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30", + secondary: + "*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]", + tinted: + "*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]", }, }, - defaultVariants: { - variant: "default", - }, - }, + } ); function Bubble({ @@ -61,17 +62,21 @@ function Bubble({ ); } -function BubbleContent({ className, render, ...props }: useRender.ComponentProps<"div">) { +function BubbleContent({ + className, + render, + ...props +}: useRender.ComponentProps<"div">) { return useRender({ defaultTagName: "div", props: mergeProps<"div">( { className: cn( "w-fit max-w-full min-w-0 overflow-hidden rounded-none border border-transparent px-2.5 py-2 text-xs leading-relaxed wrap-break-word group-data-[align=end]/bubble:self-end [button]:text-left [button,a]:transition-colors [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-1 [button,a]:focus-visible:ring-ring/50", - className, + className ), }, - props, + props ), render, state: { @@ -83,21 +88,21 @@ function BubbleContent({ className, render, ...props }: useRender.ComponentProps const bubbleReactionsVariants = cva( "absolute z-10 flex w-fit shrink-0 items-center justify-center gap-1 rounded-none bg-muted px-1.5 py-0.5 text-xs ring-2 ring-card has-[button]:p-0", { - variants: { - side: { - top: "top-0 -translate-y-3/4", - bottom: "bottom-0 translate-y-3/4", - }, - align: { - start: "left-3", - end: "right-3", - }, - }, defaultVariants: { - side: "bottom", align: "end", + side: "bottom", }, - }, + variants: { + align: { + end: "right-3", + start: "left-3", + }, + side: { + bottom: "bottom-0 translate-y-3/4", + top: "top-0 -translate-y-3/4", + }, + }, + } ); function BubbleReactions({ @@ -114,7 +119,7 @@ function BubbleReactions({ data-slot="bubble-reactions" data-align={align} data-side={side} - className={cn(bubbleReactionsVariants({ side, align }), className)} + className={cn(bubbleReactionsVariants({ align, side }), className)} {...props} /> ); diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx index 67c4e00..2251da9 100644 --- a/packages/ui/src/components/button.tsx +++ b/packages/ui/src/components/button.tsx @@ -1,40 +1,41 @@ import { Button as ButtonPrimitive } from "@base-ui/react/button"; import { cn } from "@code/ui/lib/utils"; -import { cva, type VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; +import type { VariantProps } from "class-variance-authority"; const buttonVariants = cva( "group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { + defaultVariants: { + size: "default", + variant: "default", + }, variants: { + size: { + default: + "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + icon: "size-8", + "icon-lg": "size-9", + "icon-sm": "size-7 rounded-none", + "icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3", + lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", + xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", + }, variant: { default: "bg-primary text-primary-foreground hover:bg-primary/80", + destructive: + "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", + ghost: + "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", outline: "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", - ghost: - "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", - destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: - "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", - lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - icon: "size-8", - "icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-7 rounded-none", - "icon-lg": "size-9", }, }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, + } ); function Button({ @@ -46,7 +47,7 @@ function Button({ return ( <ButtonPrimitive data-slot="button" - className={cn(buttonVariants({ variant, size, className }))} + className={cn(buttonVariants({ className, size, variant }))} {...props} /> ); diff --git a/packages/ui/src/components/card.tsx b/packages/ui/src/components/card.tsx index e0170ce..6bdeafe 100644 --- a/packages/ui/src/components/card.tsx +++ b/packages/ui/src/components/card.tsx @@ -12,7 +12,7 @@ function Card({ data-size={size} className={cn( "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-none bg-card py-(--card-spacing) text-xs/relaxed text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none", - className, + className )} {...props} /> @@ -25,7 +25,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) { data-slot="card-header" className={cn( "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-none px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)", - className, + className )} {...props} /> @@ -38,7 +38,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) { data-slot="card-title" className={cn( "cn-font-heading text-sm font-medium group-data-[size=sm]/card:text-sm", - className, + className )} {...props} /> @@ -59,7 +59,10 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-action" - className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)} + className={cn( + "col-start-2 row-span-2 row-start-1 self-start justify-self-end", + className + )} {...props} /> ); @@ -67,7 +70,11 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) { function CardContent({ className, ...props }: React.ComponentProps<"div">) { return ( - <div data-slot="card-content" className={cn("px-(--card-spacing)", className)} {...props} /> + <div + data-slot="card-content" + className={cn("px-(--card-spacing)", className)} + {...props} + /> ); } @@ -75,10 +82,21 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-footer" - className={cn("flex items-center rounded-none border-t p-(--card-spacing)", className)} + className={cn( + "flex items-center rounded-none border-t p-(--card-spacing)", + className + )} {...props} /> ); } -export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +}; diff --git a/packages/ui/src/components/checkbox.tsx b/packages/ui/src/components/checkbox.tsx index 92a781f..853ce67 100644 --- a/packages/ui/src/components/checkbox.tsx +++ b/packages/ui/src/components/checkbox.tsx @@ -10,7 +10,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { data-slot="checkbox" className={cn( "peer relative flex size-4 shrink-0 items-center justify-center rounded-none border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary", - className, + className )} {...props} > diff --git a/packages/ui/src/components/dropdown-menu.tsx b/packages/ui/src/components/dropdown-menu.tsx index 6e1d42e..01214f5 100644 --- a/packages/ui/src/components/dropdown-menu.tsx +++ b/packages/ui/src/components/dropdown-menu.tsx @@ -25,7 +25,10 @@ function DropdownMenuContent({ className, ...props }: MenuPrimitive.Popup.Props & - Pick<MenuPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) { + Pick< + MenuPrimitive.Positioner.Props, + "align" | "alignOffset" | "side" | "sideOffset" + >) { return ( <MenuPrimitive.Portal> <MenuPrimitive.Positioner @@ -39,7 +42,7 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" className={cn( "cn-menu-target cn-menu-translucent z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95", - className, + className )} {...props} /> @@ -63,7 +66,10 @@ function DropdownMenuLabel({ <MenuPrimitive.GroupLabel data-slot="dropdown-menu-label" data-inset={inset} - className={cn("px-2 py-2 text-xs text-muted-foreground data-inset:pl-7", className)} + className={cn( + "px-2 py-2 text-xs text-muted-foreground data-inset:pl-7", + className + )} {...props} /> ); @@ -85,7 +91,7 @@ function DropdownMenuItem({ data-variant={variant} className={cn( "group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-none px-2 py-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive", - className, + className )} {...props} /> @@ -110,7 +116,7 @@ function DropdownMenuSubTrigger({ data-inset={inset} className={cn( "flex cursor-default items-center gap-2 rounded-none px-2 py-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - className, + className )} {...props} > @@ -133,7 +139,7 @@ function DropdownMenuSubContent({ data-slot="dropdown-menu-sub-content" className={cn( "cn-menu-target cn-menu-translucent w-auto min-w-[96px] rounded-none bg-popover text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", - className, + className )} align={align} alignOffset={alignOffset} @@ -159,7 +165,7 @@ function DropdownMenuCheckboxItem({ data-inset={inset} className={cn( "relative flex cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - className, + className )} checked={checked} {...props} @@ -178,7 +184,12 @@ function DropdownMenuCheckboxItem({ } function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) { - return <MenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />; + return ( + <MenuPrimitive.RadioGroup + data-slot="dropdown-menu-radio-group" + {...props} + /> + ); } function DropdownMenuRadioItem({ @@ -195,7 +206,7 @@ function DropdownMenuRadioItem({ data-inset={inset} className={cn( "relative flex cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - className, + className )} {...props} > @@ -212,7 +223,10 @@ function DropdownMenuRadioItem({ ); } -function DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) { +function DropdownMenuSeparator({ + className, + ...props +}: MenuPrimitive.Separator.Props) { return ( <MenuPrimitive.Separator data-slot="dropdown-menu-separator" @@ -222,13 +236,16 @@ function DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator. ); } -function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) { +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { return ( <span data-slot="dropdown-menu-shortcut" className={cn( "ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground", - className, + className )} {...props} /> diff --git a/packages/ui/src/components/empty.tsx b/packages/ui/src/components/empty.tsx index b2f1acd..0927021 100644 --- a/packages/ui/src/components/empty.tsx +++ b/packages/ui/src/components/empty.tsx @@ -1,5 +1,6 @@ import { cn } from "@code/ui/lib/utils"; -import { cva, type VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; +import type { VariantProps } from "class-variance-authority"; function Empty({ className, ...props }: React.ComponentProps<"div">) { return ( @@ -7,7 +8,7 @@ function Empty({ className, ...props }: React.ComponentProps<"div">) { data-slot="empty" className={cn( "flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-none border-dashed p-6 text-center text-balance md:p-12", - className, + className )} {...props} /> @@ -27,16 +28,16 @@ function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { const emptyMediaVariants = cva( "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0", { + defaultVariants: { + variant: "default", + }, variants: { variant: { default: "bg-transparent", icon: "flex size-10 shrink-0 items-center justify-center rounded-none bg-muted text-foreground [&_svg:not([class*='size-'])]:size-5", }, }, - defaultVariants: { - variant: "default", - }, - }, + } ); function EmptyMedia({ @@ -48,7 +49,7 @@ function EmptyMedia({ <div data-slot="empty-icon" data-variant={variant} - className={cn(emptyMediaVariants({ variant, className }))} + className={cn(emptyMediaVariants({ className, variant }))} {...props} /> ); @@ -58,7 +59,10 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="empty-title" - className={cn("cn-font-heading text-sm font-medium tracking-tight", className)} + className={cn( + "cn-font-heading text-sm font-medium tracking-tight", + className + )} {...props} /> ); @@ -70,7 +74,7 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) { data-slot="empty-description" className={cn( "text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", - className, + className )} {...props} /> @@ -83,11 +87,18 @@ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { data-slot="empty-content" className={cn( "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance", - className, + className )} {...props} /> ); } -export { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia }; +export { + Empty, + EmptyHeader, + EmptyTitle, + EmptyDescription, + EmptyContent, + EmptyMedia, +}; diff --git a/packages/ui/src/components/field.tsx b/packages/ui/src/components/field.tsx index 1daa736..e04b537 100644 --- a/packages/ui/src/components/field.tsx +++ b/packages/ui/src/components/field.tsx @@ -1,9 +1,9 @@ -import { useMemo } from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@code/ui/lib/utils" -import { Label } from "@code/ui/components/label" -import { Separator } from "@code/ui/components/separator" +import { Label } from "@code/ui/components/label"; +import { Separator } from "@code/ui/components/separator"; +import { cn } from "@code/ui/lib/utils"; +import { cva } from "class-variance-authority"; +import type { VariantProps } from "class-variance-authority"; +import { useMemo } from "react"; function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { return ( @@ -15,7 +15,7 @@ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { )} {...props} /> - ) + ); } function FieldLegend({ @@ -33,7 +33,7 @@ function FieldLegend({ )} {...props} /> - ) + ); } function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { @@ -46,26 +46,26 @@ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { )} {...props} /> - ) + ); } const fieldVariants = cva( "group/field flex w-full gap-2 data-[invalid=true]:text-destructive", { + defaultVariants: { + orientation: "vertical", + }, variants: { orientation: { - vertical: "flex-col *:w-full [&>.sr-only]:w-auto", horizontal: "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", responsive: "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + vertical: "flex-col *:w-full [&>.sr-only]:w-auto", }, }, - defaultVariants: { - orientation: "vertical", - }, } -) +); function Field({ className, @@ -80,7 +80,7 @@ function Field({ className={cn(fieldVariants({ orientation }), className)} {...props} /> - ) + ); } function FieldContent({ className, ...props }: React.ComponentProps<"div">) { @@ -93,7 +93,7 @@ function FieldContent({ className, ...props }: React.ComponentProps<"div">) { )} {...props} /> - ) + ); } function FieldLabel({ @@ -110,7 +110,7 @@ function FieldLabel({ )} {...props} /> - ) + ); } function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { @@ -123,7 +123,7 @@ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { )} {...props} /> - ) + ); } function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { @@ -138,7 +138,7 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { )} {...props} /> - ) + ); } function FieldSeparator({ @@ -146,7 +146,7 @@ function FieldSeparator({ className, ...props }: React.ComponentProps<"div"> & { - children?: React.ReactNode + children?: React.ReactNode; }) { return ( <div @@ -168,7 +168,7 @@ function FieldSeparator({ </span> )} </div> - ) + ); } function FieldError({ @@ -177,23 +177,23 @@ function FieldError({ errors, ...props }: React.ComponentProps<"div"> & { - errors?: Array<{ message?: string } | undefined> + errors?: ({ message?: string } | undefined)[]; }) { const content = useMemo(() => { if (children) { - return children + return children; } if (!errors?.length) { - return null + return null; } const uniqueErrors = [ ...new Map(errors.map((error) => [error?.message, error])).values(), - ] + ]; - if (uniqueErrors?.length == 1) { - return uniqueErrors[0]?.message + if (uniqueErrors?.length === 1) { + return uniqueErrors[0]?.message; } return ( @@ -203,11 +203,11 @@ function FieldError({ error?.message && <li key={index}>{error.message}</li> )} </ul> - ) - }, [children, errors]) + ); + }, [children, errors]); if (!content) { - return null + return null; } return ( @@ -219,7 +219,7 @@ function FieldError({ > {content} </div> - ) + ); } export { @@ -233,4 +233,4 @@ export { FieldSet, FieldContent, FieldTitle, -} +}; diff --git a/packages/ui/src/components/input-group.tsx b/packages/ui/src/components/input-group.tsx index 95175ad..90ae00b 100644 --- a/packages/ui/src/components/input-group.tsx +++ b/packages/ui/src/components/input-group.tsx @@ -4,7 +4,8 @@ import { Button } from "@code/ui/components/button"; import { Input } from "@code/ui/components/input"; import { Textarea } from "@code/ui/components/textarea"; import { cn } from "@code/ui/lib/utils"; -import { cva, type VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; +import type { VariantProps } from "class-variance-authority"; import * as React from "react"; function InputGroup({ className, ...props }: React.ComponentProps<"div">) { @@ -19,7 +20,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) { "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3", "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50", "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-1 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40", - className, + className )} {...props} /> @@ -29,20 +30,22 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) { const inputGroupAddonVariants = cva( "flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 text-xs font-medium text-muted-foreground group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-none [&>svg:not([class*='size-'])]:size-4", { - variants: { - align: { - "inline-start": "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]", - "inline-end": "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]", - "block-start": - "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2", - "block-end": - "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2", - }, - }, defaultVariants: { align: "inline-start", }, - }, + variants: { + align: { + "block-end": + "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2", + "block-start": + "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2", + "inline-end": + "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]", + "inline-start": + "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]", + }, + }, + } ); function InputGroupAddon({ @@ -61,7 +64,9 @@ function InputGroupAddon({ return; } e.currentTarget.parentElement - ?.querySelector<HTMLInputElement | HTMLTextAreaElement>("input, textarea") + ?.querySelector<HTMLInputElement | HTMLTextAreaElement>( + "input, textarea" + ) ?.focus(); }} {...props} @@ -69,19 +74,22 @@ function InputGroupAddon({ ); } -const inputGroupButtonVariants = cva("flex items-center gap-2 text-xs shadow-none", { - variants: { - size: { - xs: "h-6 gap-1 rounded-none px-1.5 [&>svg:not([class*='size-'])]:size-3.5", - sm: "h-7 gap-1 rounded-none px-2", - "icon-xs": "size-6 rounded-none p-0 has-[>svg]:p-0", - "icon-sm": "size-7 rounded-none p-0 has-[>svg]:p-0", +const inputGroupButtonVariants = cva( + "flex items-center gap-2 text-xs shadow-none", + { + defaultVariants: { + size: "xs", }, - }, - defaultVariants: { - size: "xs", - }, -}); + variants: { + size: { + "icon-sm": "size-7 rounded-none p-0 has-[>svg]:p-0", + "icon-xs": "size-6 rounded-none p-0 has-[>svg]:p-0", + sm: "h-7 gap-1 rounded-none px-2", + xs: "h-6 gap-1 rounded-none px-1.5 [&>svg:not([class*='size-'])]:size-3.5", + }, + }, + } +); function InputGroupButton({ className, @@ -109,33 +117,39 @@ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) { <span className={cn( "flex items-center gap-2 text-xs text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", - className, + className )} {...props} /> ); } -function InputGroupInput({ className, ...props }: React.ComponentProps<"input">) { +function InputGroupInput({ + className, + ...props +}: React.ComponentProps<"input">) { return ( <Input data-slot="input-group-control" className={cn( "flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent", - className, + className )} {...props} /> ); } -function InputGroupTextarea({ className, ...props }: React.ComponentProps<"textarea">) { +function InputGroupTextarea({ + className, + ...props +}: React.ComponentProps<"textarea">) { return ( <Textarea data-slot="input-group-control" className={cn( "flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent", - className, + className )} {...props} /> diff --git a/packages/ui/src/components/input.tsx b/packages/ui/src/components/input.tsx index 6cc7197..623ebca 100644 --- a/packages/ui/src/components/input.tsx +++ b/packages/ui/src/components/input.tsx @@ -9,7 +9,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { data-slot="input" className={cn( "h-8 w-full min-w-0 rounded-none border border-input bg-transparent px-2.5 py-1 text-xs transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 md:text-xs dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", - className, + className )} {...props} /> diff --git a/packages/ui/src/components/label.tsx b/packages/ui/src/components/label.tsx index f9bad61..ea3acdb 100644 --- a/packages/ui/src/components/label.tsx +++ b/packages/ui/src/components/label.tsx @@ -9,7 +9,7 @@ function Label({ className, ...props }: React.ComponentProps<"label">) { data-slot="label" className={cn( "flex items-center gap-2 text-xs leading-none select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", - className, + className )} {...props} /> diff --git a/packages/ui/src/components/marker.tsx b/packages/ui/src/components/marker.tsx index 28ef964..c51e961 100644 --- a/packages/ui/src/components/marker.tsx +++ b/packages/ui/src/components/marker.tsx @@ -1,7 +1,8 @@ import { mergeProps } from "@base-ui/react/merge-props"; import { useRender } from "@base-ui/react/use-render"; import { cn } from "@code/ui/lib/utils"; -import { cva, type VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; +import type { VariantProps } from "class-variance-authority"; import * as React from "react"; const markerVariants = cva( @@ -9,13 +10,13 @@ const markerVariants = cva( { variants: { variant: { + border: "border-b border-border pb-2", default: "", separator: "before:mr-1 before:h-px before:min-w-0 before:flex-1 before:bg-border after:ml-1 after:h-px after:min-w-0 after:flex-1 after:bg-border", - border: "border-b border-border pb-2", }, }, - }, + } ); function Marker({ @@ -28,9 +29,9 @@ function Marker({ defaultTagName: "div", props: mergeProps<"div">( { - className: cn(markerVariants({ variant, className })), + className: cn(markerVariants({ className, variant })), }, - props, + props ), render, state: { @@ -45,7 +46,10 @@ function MarkerIcon({ className, ...props }: React.ComponentProps<"span">) { <span data-slot="marker-icon" aria-hidden="true" - className={cn("size-3.5 shrink-0 [&_svg:not([class*='size-'])]:size-3.5", className)} + className={cn( + "size-3.5 shrink-0 [&_svg:not([class*='size-'])]:size-3.5", + className + )} {...props} /> ); @@ -57,7 +61,7 @@ function MarkerContent({ className, ...props }: React.ComponentProps<"span">) { data-slot="marker-content" className={cn( "min-w-0 wrap-break-word group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground", - className, + className )} {...props} /> diff --git a/packages/ui/src/components/message-scroller.tsx b/packages/ui/src/components/message-scroller.tsx index 9dc62c4..0cc2a2a 100644 --- a/packages/ui/src/components/message-scroller.tsx +++ b/packages/ui/src/components/message-scroller.tsx @@ -2,17 +2,18 @@ import { Button } from "@code/ui/components/button"; import { cn } from "@code/ui/lib/utils"; -import { - MessageScroller as MessageScrollerPrimitive, +import { MessageScroller as MessageScrollerPrimitive } from "@shadcn/react/message-scroller"; +import { ArrowDownIcon } from "lucide-react"; +import * as React from "react"; + +export { useMessageScroller, useMessageScrollerScrollable, useMessageScrollerVisibility, } from "@shadcn/react/message-scroller"; -import { ArrowDownIcon } from "lucide-react"; -import * as React from "react"; function MessageScrollerProvider( - props: React.ComponentProps<typeof MessageScrollerPrimitive.Provider>, + props: React.ComponentProps<typeof MessageScrollerPrimitive.Provider> ) { return <MessageScrollerPrimitive.Provider {...props} />; } @@ -26,7 +27,7 @@ function MessageScroller({ data-slot="message-scroller" className={cn( "cn-message-scroller group/message-scroller relative flex size-full min-h-0 flex-col overflow-hidden", - className, + className )} {...props} /> @@ -42,7 +43,7 @@ function MessageScrollerViewport({ data-slot="message-scroller-viewport" className={cn( "cn-message-scroller-viewport size-full min-h-0 min-w-0 scroll-fade-b scrollbar-thin scrollbar-gutter-stable overflow-y-auto overscroll-contain contain-content data-autoscrolling:scrollbar-none", - className, + className )} {...props} /> @@ -56,7 +57,10 @@ function MessageScrollerContent({ return ( <MessageScrollerPrimitive.Content data-slot="message-scroller-content" - className={cn("cn-message-scroller-content flex h-max min-h-full flex-col gap-6", className)} + className={cn( + "cn-message-scroller-content flex h-max min-h-full flex-col gap-6", + className + )} {...props} /> ); @@ -73,7 +77,7 @@ function MessageScrollerItem({ scrollAnchor={scrollAnchor} className={cn( "cn-message-scroller-item min-w-0 shrink-0 [contain-intrinsic-size:auto_10rem] [content-visibility:auto]", - className, + className )} {...props} /> @@ -99,7 +103,7 @@ function MessageScrollerButton({ direction={direction} className={cn( "cn-message-scroller-button absolute inset-s-1/2 -translate-x-1/2 border-border bg-background text-foreground transition-[translate,scale,opacity] duration-200 hover:bg-muted hover:text-foreground data-[active=false]:pointer-events-none data-[active=false]:scale-95 data-[active=false]:opacity-0 data-[active=false]:duration-400 data-[active=false]:ease-[cubic-bezier(0.7,0,0.84,0)] data-[active=true]:translate-y-0 data-[active=true]:scale-100 data-[active=true]:opacity-100 data-[active=true]:ease-[cubic-bezier(0.23,1,0.32,1)] data-[direction=end]:bottom-4 data-[direction=end]:data-[active=false]:translate-y-full data-[direction=start]:top-4 data-[direction=start]:data-[active=false]:-translate-y-full rtl:translate-x-1/2 data-[direction=start]:[&_svg]:rotate-180", - className, + className )} render={render ?? <Button variant={variant} size={size} />} {...props} @@ -123,7 +127,4 @@ export { MessageScrollerContent, MessageScrollerItem, MessageScrollerButton, - useMessageScroller, - useMessageScrollerScrollable, - useMessageScrollerVisibility, }; diff --git a/packages/ui/src/components/message.tsx b/packages/ui/src/components/message.tsx index f221b3a..51fcc8f 100644 --- a/packages/ui/src/components/message.tsx +++ b/packages/ui/src/components/message.tsx @@ -22,7 +22,7 @@ function Message({ data-align={align} className={cn( "group/message relative flex w-full min-w-0 gap-1.5 text-xs data-[align=end]:flex-row-reverse", - className, + className )} {...props} /> @@ -35,7 +35,7 @@ function MessageAvatar({ className, ...props }: React.ComponentProps<"div">) { data-slot="message-avatar" className={cn( "flex w-fit min-w-8 shrink-0 items-center justify-center self-end overflow-hidden rounded-full bg-muted group-has-data-[slot=message-footer]/message:-translate-y-8", - className, + className )} {...props} /> @@ -48,7 +48,7 @@ function MessageContent({ className, ...props }: React.ComponentProps<"div">) { data-slot="message-content" className={cn( "flex w-full min-w-0 flex-col gap-2 wrap-break-word group-data-[align=end]/message:*:data-slot:self-end", - className, + className )} {...props} /> @@ -61,7 +61,7 @@ function MessageHeader({ className, ...props }: React.ComponentProps<"div">) { data-slot="message-header" className={cn( "flex max-w-full min-w-0 items-center px-2.5 text-xs font-medium text-muted-foreground group-has-data-[variant=ghost]/message:px-0", - className, + className )} {...props} /> @@ -74,11 +74,18 @@ function MessageFooter({ className, ...props }: React.ComponentProps<"div">) { data-slot="message-footer" className={cn( "flex max-w-full min-w-0 items-center px-2.5 text-xs font-medium text-muted-foreground group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end", - className, + className )} {...props} /> ); } -export { MessageGroup, Message, MessageAvatar, MessageContent, MessageFooter, MessageHeader }; +export { + MessageGroup, + Message, + MessageAvatar, + MessageContent, + MessageFooter, + MessageHeader, +}; diff --git a/packages/ui/src/components/separator.tsx b/packages/ui/src/components/separator.tsx index 36064b7..fd994ca 100644 --- a/packages/ui/src/components/separator.tsx +++ b/packages/ui/src/components/separator.tsx @@ -1,8 +1,7 @@ -"use client" +"use client"; -import { Separator as SeparatorPrimitive } from "@base-ui/react/separator" - -import { cn } from "@code/ui/lib/utils" +import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"; +import { cn } from "@code/ui/lib/utils"; function Separator({ className, @@ -19,7 +18,7 @@ function Separator({ )} {...props} /> - ) + ); } -export { Separator } +export { Separator }; diff --git a/packages/ui/src/components/sonner.tsx b/packages/ui/src/components/sonner.tsx index b295710..9454549 100644 --- a/packages/ui/src/components/sonner.tsx +++ b/packages/ui/src/components/sonner.tsx @@ -8,7 +8,8 @@ import { TriangleAlertIcon, } from "lucide-react"; import { useTheme } from "next-themes"; -import { Toaster as Sonner, type ToasterProps } from "sonner"; +import { Toaster as Sonner } from "sonner"; +import type { ToasterProps } from "sonner"; const Toaster = ({ ...props }: ToasterProps) => { const { theme = "system" } = useTheme(); @@ -18,18 +19,18 @@ const Toaster = ({ ...props }: ToasterProps) => { theme={theme as ToasterProps["theme"]} className="toaster group" icons={{ - success: <CircleCheckIcon className="size-4" />, - info: <InfoIcon className="size-4" />, - warning: <TriangleAlertIcon className="size-4" />, error: <OctagonXIcon className="size-4" />, + info: <InfoIcon className="size-4" />, loading: <Loader2Icon className="size-4 animate-spin" />, + success: <CircleCheckIcon className="size-4" />, + warning: <TriangleAlertIcon className="size-4" />, }} style={ { - "--normal-bg": "var(--popover)", - "--normal-text": "var(--popover-foreground)", - "--normal-border": "var(--border)", "--border-radius": "var(--radius)", + "--normal-bg": "var(--popover)", + "--normal-border": "var(--border)", + "--normal-text": "var(--popover-foreground)", } as React.CSSProperties } toastOptions={{ diff --git a/packages/ui/src/components/textarea.tsx b/packages/ui/src/components/textarea.tsx index db40520..ea8db15 100644 --- a/packages/ui/src/components/textarea.tsx +++ b/packages/ui/src/components/textarea.tsx @@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { data-slot="textarea" className={cn( "flex field-sizing-content min-h-16 w-full resize-none rounded-none border border-input bg-transparent px-2.5 py-2 text-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 md:text-xs dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", - className, + className )} {...props} /> diff --git a/packages/ui/src/components/tooltip.tsx b/packages/ui/src/components/tooltip.tsx index 6b07e98..9168f84 100644 --- a/packages/ui/src/components/tooltip.tsx +++ b/packages/ui/src/components/tooltip.tsx @@ -3,8 +3,17 @@ import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"; import { cn } from "@code/ui/lib/utils"; -function TooltipProvider({ delay = 0, ...props }: TooltipPrimitive.Provider.Props) { - return <TooltipPrimitive.Provider data-slot="tooltip-provider" delay={delay} {...props} />; +function TooltipProvider({ + delay = 0, + ...props +}: TooltipPrimitive.Provider.Props) { + return ( + <TooltipPrimitive.Provider + data-slot="tooltip-provider" + delay={delay} + {...props} + /> + ); } function Tooltip({ ...props }: TooltipPrimitive.Root.Props) { @@ -24,7 +33,10 @@ function TooltipContent({ children, ...props }: TooltipPrimitive.Popup.Props & - Pick<TooltipPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) { + Pick< + TooltipPrimitive.Positioner.Props, + "align" | "alignOffset" | "side" | "sideOffset" + >) { return ( <TooltipPrimitive.Portal> <TooltipPrimitive.Positioner @@ -38,7 +50,7 @@ function TooltipContent({ data-slot="tooltip-content" className={cn( "z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-none bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", - className, + className )} {...props} > diff --git a/packages/ui/src/lib/utils.ts b/packages/ui/src/lib/utils.ts index a5ef193..5a8d446 100644 --- a/packages/ui/src/lib/utils.ts +++ b/packages/ui/src/lib/utils.ts @@ -1,6 +1,5 @@ -import { clsx, type ClassValue } from "clsx"; +import { clsx } from "clsx"; +import type { ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} +export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));