Create screen: hero, nav, assets & ideas#5
Merged
Conversation
Made-with: Cursor
Made-with: Cursor
fluid-chey
added a commit
that referenced
this pull request
Apr 24, 2026
* tools: add OCR block-extraction pipeline
Adds tools/ocr-to-blocks.awk — post-processes tesseract TSV output (psm 3,
conf≥60, height≥20px, gap≥120px block grouping) into compact zone-annotated
block summaries for archetype layout extraction. PSM 3 selected after
validation on 3 confirmed sample images × 5 PSM modes.
Also adds archetypes/*/.ocr-notes.txt to .gitignore (working artifacts).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* validator: accept instagram-portrait 1080x1350 + meta
- Add instagram-portrait to PLATFORM_DIMS (1080×1350)
- Add MetaSchema (zod) with category/imageRole/useCases/slotCount
- Refactor getPlatformForSlug to accept parsed schema and prefer
schema.platform over slug suffix (forward-only for new archetypes)
- Require meta.category/imageRole/useCases(≥1)/slotCount when
platform === 'instagram-portrait' (strict; forward-only)
- 19 existing archetypes still pass with zero regressions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* spec: add Section 10 self-documenting metadata for instagram-portrait archetypes
Documents the forward-only meta object (category, imageRole, useCases,
slotCount + optional mood/contentDensity/imageHints/avoidCases), updates
Section 9 platform table to include instagram-portrait, and lists valid
category values for the 25 Phase 23 archetypes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* archetypes: add stat/data 4:5 archetypes (hero-stat-45, big-number-card, stat-comparison)
Three instagram-portrait (1080x1350) stat/data archetypes:
- hero-stat-45: 3-stat row + headline + body (portrait variant of hero-stat)
- big-number-card: single dominant display number for milestone posts
- stat-comparison: two-column before/after layout with vertical divider
All pass validator with full meta, 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* archetypes: add quote/testimonial 4:5 archetypes (3 archetypes)
- photocentric-quote: hero photo upper zone + pull-quote lower zone
- typographic-quote: asymmetric display-word left / body-quote right (no photo)
- book-quote-highlight: cover image + 3 annotation callouts + excerpt footer
Source-informed by Healing Quote and Book Review templates; OCR confirmed
zone-level layout structure. All pass validator, 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* archetypes: add announcement 4:5 archetypes (coming-soon-minimal, website-launch-mockup, event-promo)
- coming-soon-minimal: centered text-only countdown layout with launch date
- website-launch-mockup: bold headline + right-column screen/mockup image
- event-promo: full-bleed event photo + info band (date/time/location)
All pass validator with full meta, 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* archetypes: add photo collage 4:5 archetypes (4 archetypes)
- vintage-scrapbook: 2x2 full-canvas grid + metadata overlay
- fashion-moodboard: hero photo + typography zone + 3-image detail strip
- memory-grid-4up: asymmetric 4-panel (1 large + 1 side + 2 bottom)
- asymmetric-photo-collage: 3-photo left-dominant split + headline overlay
Source-informed by Beige Scrapbook, Moodboard Collage, and Minimalist
Aesthetic Photo Collage templates. All pass validator, 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* archetypes: add hero-photo 4:5 archetypes (photo-darken-headline, split-photo-feature)
- photo-darken-headline: full-bleed photo + gradient darkening + bottom-anchored text
- split-photo-feature: vertical split (photo left / editorial text right)
Both pass validator, 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* archetypes: add remaining 10 4:5 archetypes (tips, personal, product, motivational, carousel)
Tips/How-to: numbered-tips-cover, how-to-step-card
Personal/About: about-me-portrait, hiring-portrait-cta
Product: product-hero-backlit, product-feature-grid, product-callout-macro
Motivational: affirmation-note, handwritten-quote-photo
Carousel cover: carousel-cover-typographic
All 25 new archetypes now in place. Full suite: 44 archetypes, 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* agent-tools: extend listArchetypes with category/imageRole/platform filters + meta projection
- listArchetypes(opts) now accepts category, platform, imageRole, pageSize filters
- Returns rich meta projection: platform, category, mood, imageRole, slotCount, useCases
- Default page size 25, hard max 50
- Updates tool schema in agent.ts with filter parameter descriptions
- Existing callers (agent.ts dispatch) updated to pass optional filter inputs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* agent: Instagram portrait (4:5) becomes default; filter-first archetype guidance
- Platform Dimensions: Instagram Post now 1080×1350 (default), Instagram Square
renamed to 'legacy — only on explicit request'
- System prompt adds filter-first guidance for list_archetypes discovery
- _review.html updated: all 25 new 4:5 archetypes listed with labels/descriptions,
frame sizes corrected (portrait 270×338 vs square 270×270)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* tests: phase-23 golden tasks + archetype invariants (370 assertions, all pass)
phase-23.test.ts:
- 10 golden-task scenarios testing listArchetypes filter combinations
- Page size enforcement (default 25, hard max 50)
- All 25 new portrait archetypes appear in instagram-portrait filter
- Meta projection completeness (category, imageRole, slotCount, useCases)
invariants.test.ts:
- Every archetype has required files, valid schema, selector parity
- No brand bleed (/fluid-assets/ or external src URLs)
- background-layer/foreground-layer present in all HTML
- instagram-portrait archetypes require valid meta with all required fields
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* review: restore 9 missing legacy archetype iframes (linkedin + one-pager)
The Phase 23 rewrite dropped 9 legacy slugs from the rendered archetypes
array — they appeared only in the squareSlugs sizing set so never got an
iframe. Adds them back with appropriate labels/descriptions and introduces
two new frame sizes for the correct aspect ratios:
- linkedin (1200×627 → 360×188 at 0.3 scale): hero-stat-li, data-dashboard-li,
minimal-statement-li, split-photo-text-li, quote-testimonial-li,
article-preview-li
- onepager (612×792 → 245×317 at 0.4 scale): case-study-op,
company-overview-op, product-feature-op
Render logic now routes each slug to the correct frame class via suffix
detection (-li → linkedin, -op → onepager, explicit set → square,
default → portrait). Total: 44 iframes (19 legacy + 25 new).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* archetypes: fix meta.slotCount to match fields.length (stat-comparison, product-feature-grid)
Per spec, meta.slotCount must equal fields.length so agents can use it
for layout-fit decisions. Two mismatches flagged by the spec reviewer:
- stat-comparison: slotCount 8 → 9 (fields.length is 9)
- product-feature-grid: slotCount 10 → 14 (fields.length is 14)
Audited the other 23 new archetypes — no additional mismatches found.
Validator + eval suite still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* agent: add instagram-portrait to KNOWN_PLATFORMS + align downstream tooling
The Phase 23 system prompt defaults Instagram to 4:5 portrait and
list_archetypes advertises platform "instagram-portrait", but
normalizePlatform() would throw "Unknown platform 'instagram-portrait'"
when the agent passed that string to save_creation — a runtime break.
Changes:
- canvas/src/server/agent-tools.ts: add 'instagram-portrait' to
KNOWN_PLATFORMS; export normalizePlatform for testability; cross-reference
comment pointing at validation-hooks, dimension-check, and system prompt
- tools/dimension-check.cjs: add instagram_portrait (1080×1350) +
instagram_square (1080×1080) aliases; update 'instagram' default to
1080×1350 (matches new system-prompt default); extend autoDetectTarget
to distinguish portrait from square by dimensions
- canvas/src/server/validation-hooks.ts: map platform slugs
(instagram-portrait → instagram_portrait, instagram-square →
instagram_square) when invoking dimension-check
- canvas/src/__tests__/agent-evals/phase-23.test.ts: assert
normalizePlatform('instagram-portrait') does not throw; assert other
known platforms still work; assert unknown platforms still throw
Existing dimension-check characterization tests still pass unchanged
(clean-social-post autodetects to instagram_square via dimension match).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* tools: clean up ocr-to-blocks.awk (remove dead code + explicit empty sentinel)
Two small fixes to ocr-to-blocks.awk:
1. Remove dead variable assignments in the main record block. Lines 54-60
of the original assigned level/left/top/wid/ht/conf/text from wrong
column indices (5/6/7/8/9/10), then lines 62-70 immediately overwrote
them with correct indices (7/8/9/10/11/12). Looks like an aborted first
attempt. Now only the correct column read remains.
2. Change the empty-input emission. Previously emitted
'block 0 "(no text detected...)"' which would trip downstream consumers
that parse `block {N}` as a positive integer. Now emits `# empty` as an
explicit sentinel — distinguishable from "awk didn't run" (no output).
Verified against the 3 validation samples: text-heavy still produces the
expected 2 blocks, photo-heavy still reaches the empty path only when all
words fail the conf/height/text filters.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* agent-tools: deterministic listArchetypes + typed shapes + logged parse failures
Three related hardening changes to listArchetypes and its archetype-reading
machinery, plus a pre-existing test fix unmasked by running the full suite:
- Determinism (#4): sort dirs alphabetically before the filter loop.
readdirSync returns FS-dependent order, so pageSize truncation could
silently drop different archetypes on different platforms once the
portrait count grows past 25.
- Types (#5): introduce ArchetypeMeta, ArchetypeSchemaShape, ImageRole,
ContentDensity interfaces co-located with listArchetypes. Replaces
the two `any` escape hatches that bypassed type checking.
- Error logging (#6): a JSON.parse failure in schema.json no longer
silently swallows the error and pushes a broken archetype into results
with falsy platform/category/meta (invisible to every filter). Now logs
archetype_schema_parse_failed via logChatEvent and skips the slug.
- Testability: add FLUID_ARCHETYPES_DIR env-var override for ARCHETYPES_DIR
(matches tools/validate-archetypes.cjs pattern), required by the new
malformed-schema test.
Also fixes a pre-existing test regression in canvas/tests/agent-tools.test.ts
that asserted the old `slots` property — Phase 23 replaced it with the
richer meta projection (category, imageRole, slotCount, useCases).
New tests:
- phase-23.test.ts: determinism — results sorted alphabetically,
filtered results also sorted.
- malformed-schema.test.ts: a corrupted schema.json is skipped and
logChatEvent('archetype_schema_parse_failed', {slug}) is emitted.
Full canvas test suite: 769 passes, 0 failures. Validator: 44 archetypes,
0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* tools: install social-media-taste + gemini-social-image skills
Adds two agent guidance skill files used by Phase 24 image generation:
- social-media-taste-skill.md (231 lines): taste discipline for evaluating/writing social posts
- gemini-social-image-skill.md (282 lines): prompt architecture for Gemini image API calls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* db: add brand_assets.metadata + tool_audit_log + spend helpers
Phase 24 DB layer:
- brand_assets.metadata TEXT column (idempotent ALTER TABLE migration)
- tool_audit_log table + two indexes for Phase 24 tool-dispatch auditing
- insertGeneratedAsset: mirrors insertUploadedAsset with source='generated' + metadata
- searchBrandAssets: token-scored search over name/desc/tags, limit capped at 25
- writeToolAuditLog: inserts into tool_audit_log with nanoid + Date.now()
- dailySpendUsd: sums cost_usd_est since start of UTC day (or provided ts)
- .gitignore: assets/generated/ excluded from version control
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* harness: capabilities registry + sse-events constants + observability events
Phase 24 harness scaffolding:
- capabilities.ts: TOOL_POLICY registry with tier/costProfile/sideEffect for all 22 agent tools
- sse-events.ts: SSE_EVENTS const map + SseEventName type (existing events documented, Phase 24 additions stubs)
- observability.ts ChatEventType: adds tool_start, tool_end, permission_prompt, permission_response, cost_cap_reached, image_generated, image_gen_blocked_safety, image_gen_idempotent_hit, dam_search, archetype_schema_parse_failed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* agent: search_brand_images tool + /api/brand-assets?q= search endpoint
Phase 24 DAM-first image search:
- agent-tools.ts: searchBrandImages() function — calls searchBrandAssets(), logs dam_search event, returns scored BrandImageSearchResult[]
- agent.ts: search_brand_images tool definition (DAM-first description) + executeTool case
- watcher.ts: GET /api/brand-assets now accepts ?q=<query>&limit=<n> for scored search; existing no-q behavior preserved
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* tests: phase-24 dispatch 1 (foundation + search)
New test suite phase-24-dispatch-1.test.ts covering:
- searchBrandAssets scoring: name > desc > tags ranking, category filter, limit cap
- searchBrandImages tool: url format, required fields, limit enforcement
- TOOL_POLICY completeness: all 22 agent tools have a registry entry
- dailySpendUsd: returns 0 for empty/future log
- writeToolAuditLog integration: persists correctly, dailySpendUsd sums cost
787 tests pass (779 existing + 8 new), 0 failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* dispatch: tool-dispatch wrapper with permission tiers + cost cap + audit log
Adds tool-dispatch.ts with dispatchTool(), waitForPermissionResponse(), and
resolvePermissionResponse(). Implements always-allow/ask-first/never-allow
tiers, FLUID_DAILY_COST_CAP_USD hard/soft enforcement for image-api tools,
per-session autoApproved set, abort-aware permission waiting, and audit-log
writes on every path. Also adds generate_image policy stub to capabilities.ts
so the cost-cap tests can exercise the image-api profile ahead of dispatch 3.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chat-routes: POST /api/chats/:id/permission-response
New endpoint that resolves a pending ask-first permission prompt. Validates
chat existence, promptId, and decision (approve_once|approve_session|deny),
then calls resolvePermissionResponse() to unblock the waiting dispatchTool.
Returns 200 {success:true}, 400 for invalid body, or 404 for missing chat
or stale promptId.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* agent: route executeTool through dispatcher with session-scoped approvals
All tool calls now go through dispatchTool() instead of executeTool() directly.
Adds session-scoped autoApproved set (persists across reconnects per chatId),
trusted=true bypass via FLUID_DISPATCH_TRUSTED env var, and DispatchContext
construction in runAgentImpl. Handles all four non-ok outcomes (denied, capped,
blocked_safety, error) by sending appropriate tool_result content so the model
can continue reasoning. Existing tool_use/tool_result pair structure is
unchanged — no conversation history impact.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* env: document FLUID_DAILY_COST_CAP_USD
Adds FLUID_DAILY_COST_CAP_USD=10.00 to .env.example with a comment explaining
it caps daily image-api tool spend. The tool-dispatch wrapper blocks any
image-api call that would push cumulative daily spend past this threshold.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* tests: phase-24 dispatch 2 (tool-dispatch + permission flow + cost cap)
17 tests covering: always-allow flow with audit log, ask-first trusted bypass,
ask-first approve_once + deny flows (SSE prompt emit + executor gate),
permission timeout (50ms), cost cap hard block at 100% + soft warn at 80%,
autoApproved session persistence via approve_session, unknown-tool fallback to
always-allow, stale promptId returns false, and full HTTP endpoint integration
(404/400/success paths).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* deps: add @google/genai for Gemini 2.5 Flash Image
Install @google/genai@1.50.1 (GA, replaces EOL @google/generative-ai).
Add GEMINI_API_KEY to .env.example with single-scope AI Studio comment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* gemini: generate_image tool with idempotency + safety + provenance
- Add gemini-image.ts: Gemini 2.5 Flash Image SDK integration with typed
safety handling (SAFETY/IMAGE_SAFETY/OTHER/no_inline_data), retry backoff
on 429 (2s/4s/8s), file write to canvas/assets/generated/, and DB row insert
- Add findAssetByIdempotencyKey() and promoteAssetToLibrary() to db-api.ts
- Add ImageGenerationBlockedError and generateImageTool() to agent-tools.ts
- Add image_gen event types to observability ChatEventType union
- Classify ImageGenerationBlockedError as outcome=blocked_safety in tool-dispatch.ts
(checked by constructor name to avoid circular import)
- gitignore: add canvas/assets/generated/ alongside existing assets/generated/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* tools: promote_generated_image + read_skill + image-led workflow + taste-skill injection
- capabilities.ts: add promote_generated_image (ask-first, write-db) and
read_skill (always-allow, read) entries
- agent-system-prompt.ts: extend buildSystemPrompt(brandBrief, uiContext, activeCreationType)
with conditional social-media-taste skill injection for social creation types,
disk read cached in module-level variable; add image-led workflow guidance and
Structural Rule for photo-first backgrounds
- agent.ts: thread activeCreationType from uiContext.creationType into buildSystemPrompt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* tests: phase-24 dispatch 3 (gemini + skills + system prompt)
26 new tests covering:
- computeIdempotencyKey determinism (4 cases)
- generateImageTool idempotency hit — no Gemini call, costUsd=0, cached=true
- generateGeminiImage safety handling: SAFETY/IMAGE_SAFETY/OTHER/no_inline_data
- ImageGenerationBlockedError thrown via generateImageTool
- Success path: file write, brand_asset row insert, strict metadata shape
- promoteGeneratedImageTool: source promoted from generated→local
- readSkillTool: returns >100 lines for social-media-taste, rejects unknown names
- buildSystemPrompt: taste-skill injected for instagram, not for one-pager
- findAssetByIdempotencyKey: returns null when missing, correct shape when found
- dispatchTool cost-cap integration: capped outcome, executor not called
Also fix insertGeneratedAsset missing created_at (NOT NULL constraint).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* health: GET /api/health subsystem status + /api/chats/:id/usage-rollup
Add health-route.ts with fast local checks (no LLM calls) for anthropic,
gemini, dam, archetypes, skills, and daily spend. Wire into watcher.ts
middleware. Add usage-rollup endpoint to chat-routes.ts that sums
tool_audit_log cost_usd_est and counts agent_run_complete turns. Thread
uploadedAssetIds from message body into uiContext before calling runAgent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* store: handle tool_start/end/progress + permission_prompt + budget_warning events
Add ToolActivity, PendingPermission, BudgetWarning, UsageRollup types.
Add per-chat activeTools, pendingPermissions, budgetWarnings, usageRollups
state maps to ChatState. Wire SSE handler: tool_start pushes to activeTools,
tool_result pops it; tool_progress updates progressPct; permission_prompt
pushes PendingPermission; budget_warning sets BudgetWarning. On done event,
auto-fetch usage rollup. Add respondToPermission, dismissBudgetWarning,
fetchUsageRollup actions. sendMessage accepts optional uploadedAssetIds arg.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ui: image-attach button in chat composer + upload preview chip + budget warning + rollup footer
Add paperclip button to chat-input-form. On file select, POST raw bytes to
/api/uploads/chat-image with x-filename header, show preview chip with thumb
+ name + remove button. uploadedAssetIds threaded into sendMessage. Budget
warning banner shows remaining/cap with dismiss. Active tool chips with
spinner below messages. Usage rollup footer below composer. PermissionPrompt
cards rendered from pendingPermissions store slice.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ui: PermissionPrompt component — inline permission gate for ask-first tools
Show tool name (humanized), reason, args preview, est_cost_usd, and three
decision buttons (Deny / Approve once / Approve for this session). Calls
onDecide callback which routes to respondToPermission in the store. Card
disappears once the store removes it from pendingPermissions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* tests: phase-24 dispatch 4 (store reducers + health + rollup + upload compat)
21 new tests:
- Store reducer: tool_start/result/permission_prompt/budget_warning/dismiss
- respondToPermission removes prompt optimistically (fetch mock)
- GET /api/health: shape, key presence, env var checks (anthropic/gemini/dam)
- GET /api/chats/:id/usage-rollup: 404, zero, cost sum, turn counting
- POST /api/chats/:id/messages: backward compat (missing content → 400)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(store): remove activeTools entry on tool_end (not tool_result)
The previous D4 wiring had two bugs in the activeTools lifecycle:
1. `tool_end` had no handler, so dispatcher-started tools were pushed but
never removed — activeTools[chatId] grew unbounded within a session and
the "Generating image..." chips never disappeared.
2. `tool_result` was (incorrectly) used as the remove signal. But
`tool_result` is what agent.ts emits when streaming the Anthropic
tool_result block back to the model — a different lifecycle from
"dispatcher finished executing the tool." They co-exist, and conflating
them removed chips prematurely on cached/fast tools and missed
removal entirely when tool_result wasn't emitted (e.g. denied/capped).
Also disambiguates the `tool_start` collision: agent.ts emits
`{ toolUseId, name, input }` (→ message toolCalls list) and
tool-dispatch.ts emits `{ tool, tier, est_cost_usd, est_duration_sec }`
(→ activeTools chip). Branch on presence of `tier` vs `toolUseId` so each
handler only runs for its own payload shape. Previously every tool call
created TWO activeTools entries (one per emitter sharing the event name).
Plus: `tool_progress` now reads `pct` (dispatcher's field) instead of
`progress_pct`, matching the tool-dispatch.ts emit at
canvas/src/server/tool-dispatch.ts:194.
Test rewrite:
- Replace synthetic setState tests (which bypassed the reducer) with
real SSE-driven tests that pipe events through sendMessage + the
mocked fetch-event-source harness, exercising the actual reducer.
- New regression guards:
* tool_end removes activeTool, tool_result does NOT
* tool_result alone leaves activeTool present (guards against revert)
* agent-style tool_start (toolUseId, no tier) does NOT push to
activeTools (but still adds to message.toolCalls)
* tool_end pops OLDEST matching entry when same tool runs twice
* tool_progress with `pct` field updates progressPct
All 855 tests pass (up from 851).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* deps: install @anthropic-ai/claude-agent-sdk@0.2.119
Adds the Claude Agent SDK alongside the existing @anthropic-ai/sdk.
Phase 25 migration tooling: createSdkMcpServer + tool() for typed MCP
server definitions; query() for the agent loop migration in C3.
zod@^4.3.6 already present as a direct dep.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* agent: build SDK MCP server modules per tool group
Six factory functions in agent-mcp-servers/ that each return a
createSdkMcpServer() instance for use with query(). Every tool body
closes over dispatchCtx and delegates through dispatchTool() so the
existing permission/cost-cap/audit pipeline is preserved exactly.
Tool groups: archetypes, brand-discovery, brand-editing, visual,
context, image. Zod schemas match the existing input_schema JSON
definitions in agent.ts. Fixed zod v4 z.record() two-arg API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* agent: migrate runAgent to SDK query() + SSE translation layer
Phase 25: replaces the @anthropic-ai/sdk message loop in runAgentImpl
with @anthropic-ai/claude-agent-sdk's query() subprocess runner.
Changes:
- runAgentImpl now calls query() with all 6 MCP server modules
- Stream events (SDKAssistantMessage, SDKPartialAssistantMessage,
SDKResultMessage) are translated to the existing SSE event shapes:
text, tool_start (with toolUseId), done
- Multi-turn conversations resume via sdk_session_id column (new
migration added to db.ts)
- autoTitle() ported to query() with maxTurns:1, no tools
- Auth: SDK auto-resolves ANTHROPIC_API_KEY then claude login session;
friendly error emitted if neither is configured
- System prompt concatenated with divider; SDK provides server-side
prefix-caching (no manual cache_control needed)
- createMessageWithRetry / executeTool / loadHistory kept for
backward compat with existing test suite (guarded in dead code block)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* health: recognize claude-login auth path
The /api/health anthropic check now reports 'ok' when ANTHROPIC_API_KEY
is absent but ~/.claude/.credentials.json exists (written by `claude
login`). Falls back to 'api_key_missing' when neither is configured.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: claude login setup + env example update
Add claude login auth path documentation to AGENTS.md with both
Option A (API key) and Option B (claude login) setup instructions.
Update .env.example to document ANTHROPIC_API_KEY with a comment
explaining the dual auth path. Update tech stack entry to reflect
Claude Agent SDK migration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* tests: phase-25 (MCP server parity + SSE translation + auth paths)
14 new tests covering:
- MCP server factory parity: each of the 6 factories exposes the correct
tool names via _registeredTools introspection
- dispatchTool wrapping: list_archetypes handler calls dispatchTool
- SSE translation: sendSSE format, tool_start payload disambiguation
- agent_run_complete logging shape (input/output/cache tokens)
- Auth error regex correctly classifies auth vs non-auth errors
- Health route: claude login credentials file recognized as ok
Note: SSE translation and auth path end-to-end tests are implemented at
unit level (sendSSE shape, logChatEvent shape, regex match) because
vi.mock('@anthropic-ai/claude-agent-sdk') requires module-level hoisting
which conflicts with the test DB isolation pattern. The behavior is
covered by the regex and helper unit tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* deps: @anthropic-ai/sdk removal deferred — blocked by test dependencies
Remaining @anthropic-ai/sdk references:
- agent.ts: createMessageWithRetry and loadHistory functions (kept for
backward compat with agent-cancel.test.ts)
- agent-cancel.test.ts: 6 type casts to import('@anthropic-ai/sdk').default
- helpers/anthropic-mock.ts: Anthropic type import for mock shape
Full removal requires porting agent-cancel.test.ts to a new mock that
uses the Agent SDK's SDKMessage types, and updating anthropic-mock.ts
to not reference @anthropic-ai/sdk types directly. The Agent SDK bundles
its own copy of @anthropic-ai/sdk (v0.79.x) so both SDKs coexist safely.
Deferred to Phase 25 follow-up cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(agent): restore tool_result + creation_ready + validation_result SSE events post-SDK-migration
Phase 25 regression fix: the frontend (canvas/src/store/chat.ts) expects
three SSE events that were emitted by the pre-migration tool loop but
went silent after the Agent SDK migration:
1. tool_result — added a new `user` case to the SDK message switch in
runAgentImpl. SDKUserMessage.message.content carries tool_result
blocks with tool_use_id matching the assistant's tool_use block ids,
so the correlation is natural. A toolUseId→name map is populated
from assistant tool_use blocks and consumed in the user branch.
Payload mirrors the pre-migration shape: { toolUseId, name, hasImage,
summary|result|error }. Image base64 data stays server-side (hasImage
flag only).
2. creation_ready — re-emitted from the save_creation MCP handler in
visual.ts after dispatchTool succeeds. Same shape as pre-migration:
{ campaignId, creationId, iterationId, htmlPath }. Client refreshes
the campaign view on this event.
3. validation_result — re-emitted from both save_creation and
edit_creation handlers when the tool result carries a validation
string. Shape: { iterationId, result }.
5 new unit tests assert the SSE payload shapes and the end-to-end
emission from the visual MCP handlers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(agent): move tool permission prompts from dispatchTool to SDK canUseTool
Phase 26: permission gating for ask-first tools now runs at the Claude Agent
SDK layer via the `canUseTool` callback, not inside the in-MCP dispatchTool
wrapper. The old flow caused the SDK to consider each MCP tool "in-progress"
while dispatchTool awaited the user — so the SDK timed out, aborted the
query, and the abort propagated back to our registry and resolved the pending
permission as deny. By the time the user clicked a button, the SSE stream was
closed and the prompt was gone.
Changes:
- New canvas/src/server/permission-registry.ts holds PermissionDecision,
pendingPermissions, waitForPermissionResponse, resolvePermissionResponse —
moved verbatim from tool-dispatch.ts.
- tool-dispatch.ts no longer handles ask-first: only never-allow-by-default
short-circuits; everything else proceeds to cost-cap + executor. Re-exports
kept for backward compat.
- agent.ts passes a canUseTool callback to query() and uses
permissionMode: 'default' (was 'bypassPermissions' with
allowDangerouslySkipPermissions). Callback strips the `mcp__<ns>__` prefix,
looks up the policy, and for ask-first tools awaits the permission registry
(honoring both the chat's AbortSignal and the SDK's per-call signal).
- chat-routes.ts now imports resolvePermissionResponse from permission-registry.
- phase-24-dispatch-2 tests updated: ask-first behavior is tested at the
registry level; dispatchTool tests assert it no longer emits prompts.
- phase-26-canusetool test: mocks the SDK to capture and invoke the
canUseTool callback, exercises approve_once, approve_session, deny,
trusted, always-allow, never-allow, and abort-during-wait.
All 880 tests pass. TypeScript clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(agent): route campaignless save_creation to __standalone__ sentinel
Before this change, saveCreation with no campaignId would spawn a fresh
"Agent Campaign {date}" row per save, cluttering the campaigns list and
leaving the resulting creations unreachable from the Creations tab
(which filters on the __standalone__ sentinel).
Changes:
- Promote getOrCreateStandaloneCampaignId() into db-api.ts as a shared,
idempotent helper (single source of truth). Removes the unused local
copy that had been defined in watcher.ts.
- saveCreation now resolves cId = campaignId ?? sentinel up front and
drops the in-transaction "create new campaign" branch entirely. The
on-disk .fluid/campaigns/{cId}/... path is unchanged.
- Update the save_creation tool description + system prompt so the
agent understands campaignId is only for active-campaign contexts.
- ChatSidebar now passes activeCampaignId as an explicit null (not
undefined) when nothing is selected, so the model sees a consistent
"Active campaign: none" in every prompt.
- New standalone-save.test.ts locks in the behaviors: sentinel is
idempotent, repeat campaignless saves reuse it, "Agent Campaign ..."
rows are never created, and explicit campaignId still wins.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* cleanup: stale-iteration sweeper + 0-byte write guard + 404 placeholder
Adds preventive + corrective measures for campaign cards that show
"HTML file not found on disk" because iteration rows point to files
that never got written (or were cleaned up out of band).
- db-api: cleanupStaleIterations() + resolveIterationHtmlPath() — DB
sweep that deletes iteration rows whose html_path can't be resolved
via any of the watcher's canonical-shape strategies (1/2/3/7), with a
10-min minAgeMs guard to protect in-flight saves, and a cascade that
preserves the __standalone__ sentinel.
- watcher: POST /api/admin/cleanup-stale-creations, gated by
FLUID_ADMIN_ENABLED=true. Replaces both 404 plaintext bodies on the
iteration HTML endpoint with a self-contained dark-themed
"Creation unavailable" placeholder card so campaign iframes no longer
render raw error text.
- agent-tools.saveCreation: post-write statSync guard that throws
BEFORE the DB transaction when the HTML file is 0 bytes, so the
existing rollback path cleans up the orphan.
- tools/cleanup-stale-creations.cjs: CLI that posts to the admin
endpoint; supports --dry-run.
- tests: 8 new cases covering resolver fallbacks, minAgeMs guard,
cascade semantics, __standalone__ preservation, custom minAgeMs, and
the size-check predicate.
Baseline 887 → 895 tests passing. tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(agent-tools): apply 0-byte write guard to editCreation too
Same defensive guard as saveCreation. An empty overwrite would leave
the DB iteration row pointing at a corrupted file — exactly the "HTML
file not found" symptom Issue 3 fixes in aggregate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(render-engine): transparent-PNG fallback for unresolvable brand-asset URLs
When an /api/brand-assets/serve/{name} URL can't be resolved via DB lookup,
the previous fallback mapped to a bogus file:// path that Chromium silently
failed to load — the user saw a blank where (e.g.) a mask-image brushstroke
should have been, with no signal anywhere.
Now falls back to a 1x1 transparent PNG data URL (parses cleanly, mask
effectively no-ops, visually identical to "mask absent") and logs the
unresolvable name via logChatEvent for post-mortem visibility.
Also adds a diagnostic pre-flight scan for any /api/brand-assets/ URLs
still present after rewrite — surfaces LLM-produced reference shapes our
rewriter misses.
Refactors the URL-rewriting logic out of renderPreview into a pure,
exported helper (rewriteAssetUrls) so it can be unit-tested without
launching Chromium.
- canvas/src/server/render-engine.ts: extract rewriteAssetUrls helper,
add TRANSPARENT_PNG_DATA_URL fallback + asset_unresolvable + leftover
/api/ URL logging
- canvas/src/server/agent-system-prompt.ts: add "Brand Assets in CSS"
guidance telling the agent to verify names via list_assets before
referencing them
- canvas/src/__tests__/render-engine-mask-resolution.test.ts: 7 new
tests covering resolvable, unresolvable, mixed, no-reference, and
leftover /api/ URL cases
Test counts: 895 → 902 passing (7 new, 0 regressions).
Chromium integration test (tests/render-engine.test.ts) also passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(archetypes): strip CSS rules migrated to global.css
The CSS layer system moved global reset, .background-layer,
.foreground-layer, and body base declarations (overflow: hidden,
font-family: sans-serif) to styles/global.css. 25 archetypes still
inlined those rules; Playwright enforces removal via
canvas/e2e/css-layer-system.spec.ts.
For each file: removed the .background-layer and .foreground-layer rule
blocks, stripped overflow/font-family/margin/padding from the body rule,
and replaced one hardcoded text color with the appropriate
var(--text-*) token. Body markup (background/foreground DIVs) is
preserved. No layout, pixel, or typography changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Updates to the create screen and related canvas UI.
Changes
Made with Cursor