Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .env.e2e.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

# Required for LLM/vision tests (set in GitHub Actions secrets)
OPENROUTER_API_KEY=sk-or-v1-your-key-here
MODEL_NAME=google/gemini-2.5-flash-lite
MODEL_NAME=google/gemini-3-flash-preview
VISION_MODEL_NAME=google/gemini-3-flash-preview
SKETCHI_APP_NAME=sketchi
SKETCHI_APP_COMPONENT=backend
AI_GATEWAY_API_KEY=vck_your-key-here

# Optional (only needed for Browserbase rendering + Stagehand BROWSERBASE env)
Expand All @@ -19,6 +21,10 @@ STAGEHAND_TARGET_URL=http://localhost:3001
# Environment: LOCAL or BROWSERBASE (defaults to BROWSERBASE)
STAGEHAND_ENV=LOCAL

# Optional authenticated local WorkOS test user
SKETCHI_E2E_EMAIL=your-workos-test-user@maildrop.cc
SKETCHI_E2E_PASSWORD=your-workos-test-password

# Browser settings
STAGEHAND_HEADLESS=false
STAGEHAND_CHROME_PATH=
Expand Down
5 changes: 4 additions & 1 deletion .ignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Re-include agent working state for OpenCode indexing (ripgrep)
# These are gitignored but agents need to search/read them
!.sisyphus/
!.sisyphus/**
!.agents/skills
!.memory/
!.agents/skills/**
!.memory/
!.memory/**
92 changes: 18 additions & 74 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,76 +1,20 @@
## Repo CI / Deploy Note
- Vercel Preview/Prod: `NEXT_PUBLIC_CONVEX_URL` is set automatically by the Convex deploy step. If it's undefined, the Convex deploy is failing (debug that first).
- Pre-push sanity: `bun x ultracite fix`, `bun x ultracite check`, `bun run check-types`, `bun run build`, and `cd packages/backend && bunx convex codegen && bun run test`.
- don't create new branches unless asked
---

# Working Preferences

## Communication
- succinct; no filler; fragments OK
- facts first; show evidence (commands + exit codes)

## Engineering
- ship small, testable increments
- readable > clever
- skimmable code; split files at ~400 lines
- descriptive names; long OK
- docstrings: skip or "why" only
- latest packages; `bun install`

## Languages
- TypeScript first
- Go OK for utilities
- avoid Python

## Workflow
- commit + push often (small commits)
- delete obsolete docs/experiments
- use `gh` for GitHub issues/PRs instead of web UI

## Linting
- `bun x ultracite fix` format
- `bun x ultracite check` verify

## Repo
- Turborepo + bun workspaces
- `apps/web/` Next.js frontend
- `packages/backend/` Convex functions
- `bun run dev` all apps
- `bun run build` builds

## Testing
Priority: API > E2E > manual/verification > unit (last resort)

1. **API tests** - for true public APIs (none currently)
2. **E2E tests** - primary method; UI flows
3. **Manual Verification** - for fixes where CI tests add low value; use checklist + log analysis

Never mock HTTP; `convex-test` mocks Convex backend only. Verify functional intent/behavior over code coverage.

## Test Planning
Outline scenarios upfront. Create `.ts` (for E2E) or a manual checklist (for verification) with description comment - confirm approach before implementing.

For manual verification, structure the checklist and log requirements so they could be automated via an LLM (e.g. "analyze logs for X abnormality"). Add results/logs to issue comments.

## Stagehand E2E
Location: `tests/e2e/` | Stagehand 3 TS via OpenRouter

Models:
- `google/gemini-2.5-flash-lite` general
- `google/gemini-3-flash-preview` complex

Guidelines:
- prompt-first; avoid brittle selectors
- start dev server locally
- `STAGEHAND_TARGET_URL` for preview regression

## Adding Tests
1. Create `.ts` in `tests/e2e/`
2. Add scenario comment at top
3. Confirm approach first
4. Run locally before commit; preview after deploy
## Repo Constraints
- **Branches**: only one active branch other than main at a time (cleanup or recommend cleanup if found in violation)
- **Vercel**: `NEXT_PUBLIC_CONVEX_URL` is automatic. If undefined, Convex deploy failed.
- **Pre-push**: `bun x ultracite fix`, `bun run check-types`, `bun run build`, and `cd packages/backend && bun run test`.

## Preferences
- **Communication**: Succinct; fragments OK; facts first; show evidence (commands + exit codes).
- **Engineering**: `readable > clever`; long descriptive names OK; split files at ~400 lines.

## Testing Strategy
- **Priority**: API > E2E > manual/verification.
- **API (Convex)**: `packages/backend/convex/*.test.ts`. Never mock HTTP; verify functional intent.
- **E2E (Stagehand)**: Prompt-first selectors; avoid brittle CSS. Use `STAGEHAND_TARGET_URL` for previews.
- **Authenticated local E2E**: When a flow requires WorkOS sign-in, use `SKETCHI_E2E_EMAIL` and `SKETCHI_E2E_PASSWORD` from local env files instead of ad hoc credentials.
- **Local auth/editor overrides**: For local WorkOS + Convex verification, prefer `SKETCHI_ADMIN_SUBJECTS` / `SKETCHI_ICON_LIBRARY_EDITOR_SUBJECTS` in addition to email allowlists. Local Convex identities may not include email claims even when the user is signed in.
- **UI verification**: For any UI/E2E-affecting change, run a targeted local verification against the real app before finishing. Prefer `agent-browser` for the interaction path and `d3k` for browser/server log review; at minimum run the real dev server with `bun run dev` and verify the affected flow there.
- **Manual**: Checklist + log analysis (`venom.log` or Convex logs).

## Memory
- Use .memory/ directory to store any temporary artifacts.
- This directory is gitignored, so it will not be committed to the repository, but it is intentionally configured to be visible to codex.
- Use `.memory/` for temporary artifacts (gitignored but visible to tools).
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
- [What is Sketchi?](#what-is-sketchi)
- [Screenshots](#screenshots)
- [Architecture](#architecture)
- [Model Strategy](#model-strategy)
- [Features](#features)
- [Quick Start](#quick-start)
- [Development](#development)
Expand Down Expand Up @@ -120,6 +121,19 @@

---

## Model Strategy

Sketchi uses a multi-model approach via OpenRouter to balance reasoning, vision, and latency.

| Role | Model | Primary Use Case |
|------|-------|------------------|
| **Brain** | `google/gemini-3-flash-preview` | Diagram generation, structural analysis, and visual grading. |
| **Driver** | `google/gemini-2.5-flash-lite` | E2E test execution (Stagehand) and UI interaction. |
| **Experimental** | `google/gemini-3.1-flash-lite-preview` | Low-latency validation, JSON repair, and classification. |
| **Fallback** | `z-ai/glm-4.7` | High-reliability secondary model for diagram retries. |

---

## Features

### Icon Library Generator
Expand Down
4 changes: 4 additions & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ SENTRY_LOG_SAMPLE_RATE=0.1 # fraction of logs sampled
SENTRY_CONVEX_ENABLED=0 # enable Convex telemetry (1=on)
SENTRY_CONVEX_MODE=direct # direct|proxy
SKETCHI_TELEMETRY_URL=http://localhost:3001/api/telemetry # telemetry proxy endpoint
SKETCHI_ADMIN_EMAILS=admin@example.com
SKETCHI_ADMIN_SUBJECTS=
SKETCHI_ICON_LIBRARY_EDITOR_EMAILS=anand@shpit.dev
SKETCHI_ICON_LIBRARY_EDITOR_SUBJECTS=
11 changes: 9 additions & 2 deletions apps/web/src/app/library-generator/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,14 @@ export default function LibraryEditorPage({ params }: PageProps) {
<div className="flex flex-col gap-3">
<h2 className="font-semibold text-sm">Upload</h2>
<SvgUploader
isUploading={isUploading || !canEdit}
disabled={!canEdit}
isUploading={isUploading}
onUpload={handleUpload}
statusText={
canEdit
? "SVG only, max 256KB each"
: "Read-only library. Uploads require edit access."
}
/>
</div>
</aside>
Expand All @@ -317,8 +323,9 @@ export default function LibraryEditorPage({ params }: PageProps) {
</div>
<div className="flex-1 overflow-y-auto p-4">
<IconGrid
canEdit={canEdit}
icons={icons}
isBusy={isUploading || isSaving || !canEdit}
isBusy={isUploading || isSaving}
onDeleteSelected={handleDeleteSelected}
onMove={handleMove}
styleSettings={styleSettings}
Expand Down
34 changes: 33 additions & 1 deletion apps/web/src/app/library-generator/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,24 @@ import { toast } from "sonner";

import LibraryCard from "@/components/icon-library/library-card";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

export default function LibraryGeneratorPage() {
const router = useRouter();
const { user } = useAuth();
const viewer = useQuery(api.users.me, user ? {} : "skip");
const libraries = useQuery(api.iconLibraries.list);
const createLibrary = useMutation(api.iconLibraries.create);
const [libraryName, setLibraryName] = useState("");
const [libraryVisibility, setLibraryVisibility] = useState<
"private" | "public"
>("private");
const [isCreating, setIsCreating] = useState(false);
const canManagePublicIconLibraries = Boolean(
viewer?.identity.canManagePublicIconLibraries
);

let createButtonLabel = "Sign in";
if (user) {
Expand Down Expand Up @@ -71,7 +80,13 @@ export default function LibraryGeneratorPage() {

try {
const name = libraryName.trim() || "Untitled Library";
const id = await createLibrary({ name, visibility: "private" });
const id = await createLibrary({
name,
visibility:
canManagePublicIconLibraries && libraryVisibility === "public"
? "public"
: "private",
});
router.push(`/library-generator/${id}`);
} catch (error) {
const message =
Expand Down Expand Up @@ -100,6 +115,23 @@ export default function LibraryGeneratorPage() {
Sign in to create private libraries and upload icons.
</p>
)}
{user && canManagePublicIconLibraries ? (
<div className="flex items-center gap-2">
<Checkbox
checked={libraryVisibility === "public"}
id="library-visibility-public"
onCheckedChange={(checked) =>
setLibraryVisibility(checked === true ? "public" : "private")
}
/>
<Label
className="text-muted-foreground text-sm"
htmlFor="library-visibility-public"
>
Create as public library
</Label>
</div>
) : null}
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<Input
className="border-2 shadow-sm"
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/opencode/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ export default function OpenCodeDocsPage() {
<div className="size-3 rounded-full bg-[#ffbd2e]" />
<div className="size-3 rounded-full bg-[#27c93f]" />
<span className="ml-2 font-medium text-white/50 text-xs">
run command
run
</span>
</div>
<div className="flex items-center gap-2">
Expand Down
73 changes: 40 additions & 33 deletions apps/web/src/components/icon-library/icon-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface IconGridItem {
}

interface IconGridProps {
canEdit?: boolean;
icons: IconGridItem[];
isBusy?: boolean;
onDeleteSelected: (ids: string[]) => void;
Expand All @@ -22,6 +23,7 @@ interface IconGridProps {
}

export default function IconGrid({
canEdit = true,
icons,
onDeleteSelected,
onMove,
Expand Down Expand Up @@ -66,6 +68,41 @@ export default function IconGrid({
);
}

let actionControls = (
<span className="text-muted-foreground text-xs">Read-only</span>
);

if (isEditMode) {
actionControls = (
<>
<Button onClick={exitEditMode} size="sm" type="button" variant="ghost">
Cancel
</Button>
<Button
disabled={selectedIds.size === 0 || isBusy}
onClick={handleDeleteSelected}
size="sm"
type="button"
variant="destructive"
>
Delete Selected ({selectedIds.size})
</Button>
</>
);
} else if (canEdit) {
actionControls = (
<Button
disabled={isBusy}
onClick={() => setIsEditMode(true)}
size="sm"
type="button"
variant="outline"
>
Edit
</Button>
);
}

return (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-end gap-2">
Expand All @@ -91,37 +128,7 @@ export default function IconGrid({
<Plus />
</Button>
</div>
{isEditMode ? (
<>
<Button
onClick={exitEditMode}
size="sm"
type="button"
variant="ghost"
>
Cancel
</Button>
<Button
disabled={selectedIds.size === 0 || isBusy}
onClick={handleDeleteSelected}
size="sm"
type="button"
variant="destructive"
>
Delete Selected ({selectedIds.size})
</Button>
</>
) : (
<Button
disabled={isBusy}
onClick={() => setIsEditMode(true)}
size="sm"
type="button"
variant="outline"
>
Edit
</Button>
)}
{actionControls}
</div>

<div
Expand Down Expand Up @@ -178,7 +185,7 @@ export default function IconGrid({
{!isEditMode && (
<div className="flex items-center gap-1">
<Button
disabled={isBusy || !canMoveLeft}
disabled={isBusy || !canEdit || !canMoveLeft}
onClick={() => onMove(icon.id, "left")}
size="xs"
type="button"
Expand All @@ -187,7 +194,7 @@ export default function IconGrid({
</Button>
<Button
disabled={isBusy || !canMoveRight}
disabled={isBusy || !canEdit || !canMoveRight}
onClick={() => onMove(icon.id, "right")}
size="xs"
type="button"
Expand Down
Loading
Loading