Skip to content
Closed
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
2 changes: 1 addition & 1 deletion docs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Start here to find which docs apply to your task. If multiple docs are relevant,

- [Agent-loop INDEX](agent-loop/INDEX.md) — overview + endpoint map
- [Overview](agent-loop/overview.md) — the user→agent→user cycle
- [Endpoints](agent-loop/endpoints.md) — `/intent`, `/context`, `/version-patch`, `/region`, `/diff`
- [Endpoints](agent-loop/endpoints.md) — `/intent`, `/context`, `/version-patch`, `/diff`
- [Intent payload](agent-loop/intent-payload.md) — what `/intent` returns, sidecar caching, invalidation
- [Patch format](agent-loop/patch-format.md) — unified-diff conventions for `/version-patch`
- [Chips](agent-loop/chips.md) — G1 intent vocabulary (`visual` / `copy` / `behavior` / `other`)
Expand Down
2 changes: 1 addition & 1 deletion docs/agent-loop/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Silent drift in any of these endpoints breaks consumers. See the [agent-loop rul
## Read first

- [Overview](overview.md) — the cycle end-to-end with byte costs
- [Endpoints](endpoints.md) — `/context`, `/version-patch`, `/region`, `/diff`
- [Endpoints](endpoints.md) — `/context`, `/version-patch`, `/diff`
- [Uploads](uploads.md) — `POST /api/mockups`, `POST /api/mockups/[id]/version` (raw HTML + zip, size cap)
- [Patch format](patch-format.md) — unified-diff conventions for `/version-patch`

Expand Down
27 changes: 0 additions & 27 deletions docs/agent-loop/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,33 +185,6 @@ Step 4 is orchestrator-decided. Most fix cycles do not close the mockup (more an

**Rename caveat:** `name` changes the slug (the canonical URL). The owner-or-admin gate means agents can only rename mockups they themselves uploaded. If the slug changes, existing orchestrator bookmarks to `/projects/<slug>/…` break.

## `GET /api/annotations/[id]/region`

Bbox-cropped PNG of the annotation's screenshot. Sidecar-cached.

**Auth:** cookie OR Bearer.

**Response 200:**
- `Content-Type: image/png`
- `Cache-Control: private, max-age=300`
- Body: cropped PNG (typically 5–50 KB vs 200–700 KB for the full screenshot)

**Errors:**

| Status | `error` | When |
|---|---|---|
| 401 | `unauthorized` | No identity |
| 404 | `not_found` | Annotation row doesn't exist |
| 404 | `no_pin_coords` | Annotation has `pinCoords: null` (no drawn shapes) |
| 404 | `screenshot_missing` | Filesystem state corrupted |
| 500 | `invalid_pin_coords` | Stored `pinCoords` JSON is malformed |

**Bbox source:** `Annotation.pinCoords.{bboxX, bboxY, bboxW, bboxH}`, with a fixed 20px padding around the bbox clamped at image edges.

**No query params:** the bbox is fully derived from the stored pin coords. A future `?bbox=x,y,w,h` override would let agents request a different crop, but adding it would mean splitting the cache key — out of scope for v1.3.

**Caching:** sidecar `region.png`. Regenerated when `screenshot.png`'s mtime is newer than `region.png`'s. Edits to `pinCoords` (none today; pinCoords are immutable per annotation) would need a cache-key extension.

## `GET /api/mockups/[id]/diff`

Text-mode unified diff between two versions of a mockup.
Expand Down
1 change: 0 additions & 1 deletion docs/api/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ The Markup API is a set of Next.js App Router route handlers under `src/app/api/
| `POST` | `/api/mockups/[id]/annotations` | Create (JSON: body + anchors + colorIndex) |
| `GET` | `/api/annotations/[id]` | Single annotation metadata |
| `GET` | `/api/annotations/[id]/screenshot` | Full PNG screenshot |
| `GET` | `/api/annotations/[id]/region` | Bbox-cropped PNG (sidecar-cached) |
| `GET` | `/api/annotations/[id]/detail` | Aggregator for `/annotations/[id]` — annotation + screenshot dims + thread + names + mockup blurb + viewerHref |

### Agent
Expand Down
2 changes: 1 addition & 1 deletion docs/api/routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ Currently no route streams. When a route needs to stream a large payload (e.g. a

Routes that produce expensive payloads use either:

- **Sidecar files** on disk (e.g. `intent.json`, `region.png`) keyed by `(input_mtime, current_version_id)` — see [Storage](storage.md)
- **Sidecar files** on disk (e.g. `intent.json`) keyed by `(input_mtime, current_version_id)` — see [Storage](storage.md)
- **ETag headers** for in-memory aggregations (e.g. `/agent/context`)

Don't add HTTP `Cache-Control: max-age=…` to mutable resources without thinking through the invalidation path; sidecars + ETag give the same effect with explicit invalidation hooks.
7 changes: 1 addition & 6 deletions docs/api/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ ${DATA_DIR}/
│ └── annotations/
│ └── <annotationId>/
│ ├── screenshot.png # base capture (immutable per annotation)
│ ├── intent.json # sidecar cache (regenerated on read)
│ └── region.png # bbox crop (regenerated on read)
│ └── intent.json # sidecar cache (regenerated on read)
└── tmp/
└── version-<cuid>.zip # short-lived patch composition staging
```
Expand All @@ -42,10 +41,6 @@ Routes and services compose paths via these helpers — never hardcode the layou

Files derived from the primary blobs are stored as **sidecars** in the same directory. Conventions:

| Sidecar | Source | Cache key | Invalidator |
|---|---|---|---|
| `region.png` | `screenshot.png` + the annotation's `pinCoords` | `screenshot_mtime` (compared against `region.png`'s mtime) | regenerated when `screenshot.png` is newer than `region.png` |

The sidecar wrapping format for JSON caches is:

```json
Expand Down
2 changes: 1 addition & 1 deletion docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ These rules are enforced by biome + tsc + the test suite. Violating them turns t
### Agent-loop endpoints

1. **Auth via `identify(req)`** — accepts cookie OR Bearer; returns `{kind: 'user', userId} | {kind: 'agent', tokenId}` or `null`. Never re-implement auth in a route.
2. **Sidecar files are atomic-write candidates.** Writes to `intent.json` and `region.png` go directly to disk; if a future change needs concurrency safety, write to `*.tmp` and rename.
2. **Sidecar files are atomic-write candidates.** Writes to `intent.json` go directly to disk; if a future change needs concurrency safety, write to `*.tmp` and rename.
3. **Cache invalidation runs BEFORE the new write.** When a route mutates a primary blob that has derived sidecars, it deletes the stale sidecars before writing the new blob so a concurrent reader never pairs a fresh primary with a stale sidecar.
4. **The `/context` aggregator delegates to `/intent`** by importing the GET handler directly — no HTTP loopback. This keeps tests deterministic and avoids depending on `APP_URL` being reachable from the server.

Expand Down
1 change: 0 additions & 1 deletion docs/feature-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,6 @@ Surfaces that compose the agent automation cycle. These are API-driven but have
| `agent-context-read` | Single-call context aggregator: annotation + thread + inline source + diff_since_creation + project + folder_path. ETag for short-circuit | N/A (agent-only) | `GET /api/agent/context/[annotationId]` |
| `agent-version-patch` | Diff-based version update with `base_version_id`. Binary files reused by reference. 409 on conflict (stale base) | new version in `mockup-viewer-versions` | `PATCH /api/mockups/[id]/version-patch` |
| `agent-mockup-patch` | Mockup-metadata mutation. All fields (`name`, `status`, `projectId`, `folderId`, `position`) are gated by `requireOwnerOrAdmin`: the caller must be the recorded `(createdBy, createdByType)` of the mockup OR an admin. Agents can rename/move/status-change mockups they uploaded; they receive 403 `forbidden_owner` on mockups created by others. Optional close-out step after the last thread on a mockup is resolved. | `mockup-status-pill`, `mockup-actions-menu` (existing UI surfaces) | `PATCH /api/mockups/[id]` |
| `agent-region-crop` | Bbox-cropped screenshot (sidecar-cached) | N/A (agent-only) | `GET /api/annotations/[id]/region` |
| `agent-diff-text` | Text-mode unified or JSON diff between versions | used by `diff-viewer` | `GET /api/mockups/[id]/diff` |
| `agent-thread-reply` | Agent replies in thread (`authorType: 'agent'`) | `thread-timeline-message` | `POST /api/threads/[id]/reply` |
| `agent-thread-resolve` | Thread resolution | `thread-timeline-resolve-btn` | `POST /api/threads/[id]/resolve` |
Expand Down
1 change: 0 additions & 1 deletion docs/frontend/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ The route group `(app)` mounts `AppShell` once (via `(app)/layout.tsx`) so the s

- Mockup card thumbnails are served from `/api/mockups/[id]/thumbnail`. The route serves the file when ≥ 64 bytes and a valid PNG; smaller / corrupt files trigger a 404 and the card falls back to a deterministic monogram (palette-cycled hue from a 6-entry list keyed off the mockup id)
- Annotation screenshots come from `/api/annotations/[id]/screenshot` — full PNG, no transformation
- Bbox-cropped screenshots come from `/api/annotations/[id]/region` — see [`docs/agent-loop/endpoints.md`](../agent-loop/endpoints.md)

## State ownership

Expand Down
6 changes: 2 additions & 4 deletions docs/stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ Markup is a single-process Next.js application served from a Docker container. T

## Server-side image + DOM

- **`sharp`** for PNG cropping (`/api/annotations/[id]/region`)
- **`puppeteer`** (with bundled chromium, ~150 MB) for server-side DOM resolution at the bbox the user drew (`/api/annotations/[id]/intent`)
- **`diff`** + **`@types/diff`** for unified-diff apply/render (`/api/mockups/[id]/version-patch`, `/api/mockups/[id]/diff`)
- **`jszip`** for in-memory zip composition when applying patches
Expand Down Expand Up @@ -67,7 +66,7 @@ src/
app/ # Next.js App Router
api/ # API routes (route.ts files)
agent/context/[annotationId]/route.ts
annotations/[id]/{intent,region,screenshot,messages}/route.ts
annotations/[id]/{intent,screenshot,messages}/route.ts
mockups/[id]/{version,version-patch,diff,thumbnail,annotations,versions/[vid]/{source,promote}}/route.ts
threads/[id]/{reply,resolve,reopen}/route.ts
auth/{login,logout,setup}/route.ts
Expand All @@ -89,7 +88,6 @@ src/
diff/ # apply-unified, render-unified
intent/ # parser, contrast, cache, puppeteer singleton
mockup/ # service, storage, zip-extractor
region/crop.ts # sharp-based bbox crop
boot.ts, env.ts, logger.ts, prisma.ts
styles/tokens.css
prisma/
Expand All @@ -98,7 +96,7 @@ prisma/
scripts/ # one-shot maintenance scripts (tsx-run)
tests/
integration/{annotation,api,auth,lib,mockup}/*.test.ts
unit/lib/{intent,diff,region,…}/*.test.ts
unit/lib/{intent,diff,…}/*.test.ts
fixtures/mockups/*.zip
setup.ts
docs/ # this directory
Expand Down
2 changes: 1 addition & 1 deletion docs/task-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Read [`docs/INDEX.md`](INDEX.md) to find which docs apply to your task. If multiple docs are relevant, read all of them before starting. This is non-negotiable regardless of how simple the change appears.

If the change touches an agent-loop endpoint (`/intent`, `/context`, `/version-patch`, `/region`, `/diff`, or `POST /annotations`), consult [`docs/agent-loop/`](agent-loop/INDEX.md) **before** writing code. See the [agent-loop rule](../CLAUDE.md#agent-loop-rule-strict--non-negotiable).
If the change touches an agent-loop endpoint (`/intent`, `/context`, `/version-patch`, `/diff`, or `POST /annotations`), consult [`docs/agent-loop/`](agent-loop/INDEX.md) **before** writing code. See the [agent-loop rule](../CLAUDE.md#agent-loop-rule-strict--non-negotiable).

If the change replicates a `tests/fixtures/mockups/<name>.zip` fixture (lumen-coffee, helio-pricing, drone-console), follow the [mockup-replication rule](../CLAUDE.md#mockup-replication-rule-when-the-user-points-at-a-fixture).

Expand Down
10 changes: 1 addition & 9 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ return token;
App Router routes are imported and invoked as functions:

```ts
import { GET } from '@/app/api/annotations/[id]/region/route';
import { GET } from '@/app/api/annotations/[id]/detail/route';
import { POST as createMockupRoute } from '@/app/api/mockups/route';

const res = await GET(
Expand Down Expand Up @@ -118,14 +118,6 @@ const png = Buffer.from([
]);
```

For tests that need real images (`/region.png` crop, puppeteer rendering), use `sharp` to generate a buffer:

```ts
const png = await sharp({
create: { width: 200, height: 200, channels: 4, background: { r: 100, g: 200, b: 100, alpha: 1 } },
}).png().toBuffer();
```

## Fixtures

- **`tests/fixtures/mockups/valid-simple.zip`** — 28-byte `<html></html>` for tests that just need a valid zip
Expand Down
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
"react-dom": "latest",
"react-icons": "^5.6.0",
"server-only": "^0.0.1",
"sharp": "^0.34.5",
"yauzl": "latest",
"zod": "latest"
},
Expand All @@ -80,8 +79,7 @@
"better-sqlite3",
"esbuild",
"prisma",
"puppeteer",
"sharp"
"puppeteer"
]
}
}
7 changes: 3 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 0 additions & 55 deletions src/app/api/annotations/[id]/region/route.ts

This file was deleted.

25 changes: 0 additions & 25 deletions src/lib/region/crop.ts

This file was deleted.

49 changes: 25 additions & 24 deletions tests/e2e/landing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,36 @@ test.describe('Landing page', () => {

test('demo accepts a pin drop and persists across reload', async ({ page }) => {
await page.goto('/landing#demo');
await page.evaluate(() => {
window.prompt = () => 'Smoke test annotation';
});
const annotItems = page.locator('ul li[aria-label^="Annotation "]');
const before = await annotItems.count();
await page.getByRole('button', { name: /Drop pin/i }).click();
const canvas = page.locator('[role="application"][aria-label*="Mockup canvas"]');
await canvas.click({ position: { x: 200, y: 200 } });
await expect(annotItems).toHaveCount(before + 1);
const annotCards = page.locator('[data-pin-target]');
const before = await annotCards.count();

await page.getByRole('button', { name: /new annotation/i }).click();
await page
.frameLocator('iframe[title="Mockup"]')
.locator('body')
.click({ position: { x: 200, y: 200 } });
await page.getByRole('textbox', { name: /annotation body/i }).fill('Smoke test annotation');
await page.getByRole('button', { name: /^send$/i }).click();

await expect(annotCards).toHaveCount(before + 1);
await page.reload();
await expect(annotItems).toHaveCount(before + 1);
await expect(annotCards).toHaveCount(before + 1);
});

test('reset demo restores seeded state after confirmation', async ({ page }) => {
await page.goto('/landing#demo');
// window.prompt is mocked so the pin-drop flow doesn't block the test.
await page.evaluate(() => {
window.prompt = () => 'Extra annotation';
});
await expect(page.locator('ul li[aria-label^="Annotation "]')).toHaveCount(3);
const annotCards = page.locator('[data-pin-target]');
await expect(annotCards).toHaveCount(3);

// Drop a 4th pin via the UI so reset has something to clear. This avoids
// the previous approach of editing localStorage + reloading (which caused
// a React hydration mismatch because the server-rendered tree used
// seeded state while the client read the populated localStorage).
await page.getByRole('button', { name: /Drop pin/i }).click();
const canvas = page.locator('[role="application"][aria-label*="Mockup canvas"]');
await canvas.click({ position: { x: 150, y: 150 } });
await expect(page.locator('ul li[aria-label^="Annotation "]')).toHaveCount(4);
// Drop a 4th annotation via the draft flow so reset has something to clear.
await page.getByRole('button', { name: /new annotation/i }).click();
await page
.frameLocator('iframe[title="Mockup"]')
.locator('body')
.click({ position: { x: 150, y: 150 } });
await page.getByRole('textbox', { name: /annotation body/i }).fill('Extra annotation');
await page.getByRole('button', { name: /^send$/i }).click();
await expect(annotCards).toHaveCount(4);

// Two-step confirm: first click arms the confirm, second click resets.
// Match BOTH the rest-state ("Reset demo") and the armed-state
Expand All @@ -46,7 +47,7 @@ test.describe('Landing page', () => {
const reset = page.getByRole('button', { name: /Reset|Click again/i });
await reset.click();
await reset.click();
await expect(page.locator('ul li[aria-label^="Annotation "]')).toHaveCount(3);
await expect(annotCards).toHaveCount(3);
});

test('no horizontal scroll on mobile viewport', async ({ page }) => {
Expand Down
Loading
Loading