Skip to content

Create screen: BuildHero, DAM picker, nav updates#4

Merged
fluid-chey merged 5 commits into
mainfrom
createsceen-updates
Mar 13, 2026
Merged

Create screen: BuildHero, DAM picker, nav updates#4
fluid-chey merged 5 commits into
mainfrom
createsceen-updates

Conversation

@fluid-chey
Copy link
Copy Markdown
Collaborator

Summary

  • Add BuildHero create screen with tools/dimension dropdowns and DAM asset picker
  • Update LeftNav: add Create (plus) button first, rename Create → My Creations (masonry grid icon)
  • Fix landing/routing: serve app at / instead of /app/, redirect properly
  • Keep Campaigns tab active on drill-down, show campaign detail inline

Commits

  • 6d45e33 Create screen: BuildHero, tools/dimension dropdowns, DAM picker + selection
  • 19c0d78 Nav: Add Create (plus) first, rename Create to My Creations
  • 2b78c72 Fix landing: base /app/, redirect / to /app/
  • 628cb59 Make app the home/index screen: serve at / instead of /app/
  • 4f5d520 Keep Campaigns tab active when clicking a campaign; show drill-down inline

🤖 Generated with Claude Code

…ected-asset badges

- BuildHero: Tools default dropdown; format + dimension dropdowns for Social Post and Instagram Story/Video
- Remove I'm feeling lucky button; plus button opens Fluid DAM (imperative API to avoid Strict Mode destroying overlay)
- DAMPicker: FluidDAMModal passes full asset (url, name); load .env from repo root (vite envDir)
- BuildHero: selected DAM assets shown as removable badges with thumbnail/name and × to remove
- .gitignore: canvas/.env; add canvas/.env.example for VITE_FLUID_DAM_TOKEN
- Nav: Create vs My Creations; chat sidebar closed by default on Create

Made-with: Cursor
@fluid-chey fluid-chey merged commit f032df9 into main Mar 13, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants