Commit Graph
392 Commits
Author SHA1 Message Date
Ray XiandGitHub 4ddfc6e447 fix(media): retry transient image generation responses (#5702) 2026-07-17 09:12:36 +00:00
lefarcenandGitHub e49d1abc42 fix: resolve media-backed memory extraction keys (#5767)
* fix: resolve media-backed memory extraction keys

Generated-By: looper 0.0.0-dev (runner=worker, agent=codex)

* fix: refine unsupported memory provider detection
2026-07-17 08:46:08 +00:00
Christopher BeaulieuandGitHub 6bee94be03 fix(deploy): honor target preview/production for Cloudflare Pages (#4483) (#4576)
* fix(deploy): honour target for Cloudflare Pages branch and URL selection (#4483)

- deployToCloudflarePages: add optional target ('preview'|'production', default 'production')
- Branch derivation: preview→'preview', production/omitted→'main'
- URL selection: preview uses per-deploy alias; production uses production root
- routes/deploy.ts: read target from request body (default 'production'), pass to deployToCloudflarePages, persist actual target (not hardcoded 'preview')
- db.ts normalizeDeployment: read target from DB row instead of hardcoding 'preview'
- contracts: widen target fields to 'preview'|'production'; add target to DeployProjectFileRequest

* test(deploy): type FormData branch capture as string | undefined (#4483)

The Phase-1 branch-capture objects were typed `{ branch?: string }` but
assigned `string | undefined`, which fails `tsc` under
`exactOptionalPropertyTypes`. Type the field as `branch: string | undefined`
so the daemon typecheck gate passes; assertions are unchanged.

* fix(deploy): reject invalid deploy target with HTTP 400 (#4576)

The deploy route coerced any non-'preview' target to 'production', so a
malformed value (typo, empty string, wrong case) silently published to the
production Cloudflare branch instead of a gated preview. Validate the supplied
target up front and return 400 for anything outside preview|production; an
omitted target still defaults to production (non-breaking).

Addresses review feedback on #4576 / #4483.

* feat(deploy): add `od deploy` CLI subcommand with --target (#4483)

Land the CLI surface for the preview/production deploy capability so it is
not API-only. `od deploy <projectId> --file <name> [--target preview|
production] [--provider vercel-self|cloudflare-pages] [--cf-zone-id]
[--cf-zone-name] [--cf-domain-prefix] [--json] [--daemon-url]` POSTs to the
same `/api/projects/:id/deploy` endpoint the web UI uses, with local
validation of `--target` before any request (omitted target = no key, server
defaults to production).

Addresses the surface-closure review feedback on #4576 by shipping the
embeddability (CLI) surface alongside the API contract. The human web-UI
preview/production selector remains tracked as a follow-up in #4577.

Tests: apps/daemon/tests/cli-deploy.test.ts (11 cases, written test-first).

* feat(deploy): add web UI target selector for Cloudflare Pages deploys

Closes the web-UI surface gap PerishCode flagged on #4576: deploy target
(preview/production) had HTTP + od CLI support but no way to choose it
from the deploy modal. Adds a target <select> to the Cloudflare Pages
deploy panel, defaulted to Preview, threaded through deployProjectFile's
new optional target argument and forwarded in the deploy POST body.

References #4576.

* fix(deploy): reject vercel-self target=production instead of silently deploying preview

POST /api/projects/:id/deploy resolved the requested target but only
threaded it into deployToCloudflarePages(); deployToVercel() never
received it and always behaved as preview. A caller explicitly
requesting target=production against the Vercel provider was silently
deployed as preview with no error (Codex P2 finding on PR #4576).

Real Vercel production-target support is out of scope for this PR, so
reject the explicit request with 400 BAD_REQUEST instead. The guard
gates on the raw requested value, not the resolved target, so an
omitted target (which defaults to 'production' for Cloudflare Pages
parity) keeps deploying Vercel as preview exactly as before.

* fix(web): default Cloudflare Pages deploy target to production

The deploy target selector defaulted to 'preview', both in its initial
useState and when syncing from the (currently placeholder,
always-'preview') GET /api/deploy/config response. A user who opened
the Cloudflare Pages deploy modal and clicked Deploy without touching
the selector silently deployed to preview instead of production,
contradicting the daemon's documented default for an omitted target
and pre-PR behavior.

Default both sites to 'production'; explicit user selection of either
target is untouched. FileViewer.deploy-target.test.tsx covers the
default and both explicit selections.

Fixes #4483
2026-07-17 08:43:50 +00:00
Ethan GuoandGitHub c110e40e8d fix(frontmatter): parse flush-left YAML sequences, quoted inline arrays, and deep block scalars (#5499) 2026-07-16 07:34:41 +00:00
265aa51630 feat(web): make Studio discovery visual and inline (#5496)
* feat(web): make Studio discovery visual and inline

Move clarification forms into the chat flow, add artifact-aware visual style choices, and keep follow-up actions hidden until discovery is complete.

* feat(web): keep clarification entirely in chat

Remove the legacy Questions workspace tab, preserve discovery analytics in the inline form, and keep Skip, Submit, and answered summaries in one conversation flow.

* docs: split Studio Chat input and response work

Frame visual discovery as the input-side initiative and record response aggregation as a separately reviewed follow-up so the current PR stays bounded.

* docs: reorganize Studio Chat experience plans

Separate the overall journey, visual discovery, and response architecture so each scope can be reviewed and delivered independently.

* docs: define first-artifact satisfaction prompt

Limit the feedback prompt to the first successful previewable artifact so it gathers a useful signal without interrupting every iteration.

* docs: keep Studio Chat plans independent

Preserve the existing discovery proposal as one document and keep response architecture as a separate review surface without an extra master layer.

* fix(web): preserve inline discovery behavior

Keep visual direction single-select while restoring custom input, optional auto-continue, analytics, and current end-to-end coverage for the inline flow.

* fix(web): preserve files when skipping discovery step

* fix(web): exclude skipped file answers

* fix(web): align inline discovery with slim prompt

Preserve the latest discovery contract while keeping inline forms, uploads, drafts, and schema-driven visual selections consistent after the rebase.

* feat(web): add style catalog preview images

* fix(web): clarify discovery form completion

* fix(web): preserve question form context

* fix(web): time out unanswered question forms

* test(web): cover inline question form next steps

* fix(web): roll back partial inline uploads

* ci: trigger validation after rebase

* fix(web): unlock blocked inline question submits

* fix(web): retain previewless visual answers

* fix(web): load visual style previews from R2

* fix(web): clean blocked inline uploads

* test(web): expect R2 style preview assets

* feat(web): show full visual style catalog

* fix(web): keep inline question answers consistent

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
2026-07-16 06:57:43 +00:00
66e4061274 fix(memory): retire chat auto-extraction and clean up its artifacts (#5708)
- chatExtractionEnabled now defaults OFF (explicit opt-in only); it gates
  the regex pack, chat-form profile capture, and LLM extractor together
  while leaving memory injection untouched
- add a one-time boot migration (memory-cleanup.ts) that deletes regex-pack
  artifacts by name+template fingerprint, prunes user_profile to the
  canonical field set the settings editor writes, and forces the legacy
  config flag off once; marker-gated so a conscious re-opt-in sticks
- persist writer provenance (frontmatter source:) on every new entry so
  future audits can tell auto-extracted facts from hand-written ones
- LLM-extracted entries are deliberately kept: on-disk they are
  indistinguishable from hand-written prose, and they are the least
  harmful of the retired pipelines' output

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
2026-07-16 02:53:44 +00:00
Nicholas-XiongandGitHub a219a933eb fix(memory): forward BYOK chat model through /api/memory/extract (#5177)
* fix: forward BYOK chat model to memory extraction (#5162)

Root cause: Two issues preventing non-OpenAI BYOK providers (e.g.
MiniMax) from working with memory-llm extraction:

1. Web side (ProjectView.tsx): The BYOK chatProvider snapshot sent
   to /api/memory/extract was missing the `model` field. Without it,
   pickProvider() fell through to PROVIDER_DEFAULTS.openai.model
   ('gpt-4o-mini'), which doesn't exist on non-OpenAI endpoints.

2. Daemon route (memory.ts): The /api/memory/extract POST handler
   did not extract body.chatModel, unlike every other memory route.
   Even when the web side passes a model, it was silently dropped.

Changes:
- ProjectView.tsx: Add `model: byokOpenCodeProvider.model` to the
  BYOK chatProvider snapshot so pickProvider() can use the user's
  actual chat model (e.g. 'MiniMax-M3').
- routes/memory.ts: Read `body.chatModel` and forward it to
  extractWithLLM options, matching the pattern used by all other
  memory routes.

Testing: typecheck passes for apps/web (affected packages). The
daemon typecheck requires pre-built @open-design/release which is
a separate toolchain concern.

* fix(contracts): add model field to ByokChatProviderConfig for BYOK memory extraction (#5177)

The web-side memory snapshot was reading byokOpenCodeProvider.model but
the BYOK config contract had no model property, causing a compile-time
type error. Add an optional model field so the daemon can forward the
user's chat-model picker choice to memory extraction.

* fix(byok): copy config.model into byokOpenCodeProvider snapshot

Address review comment on PR #5177: byokOpenCodeProviderFromConfig()
did not copy config.model into the returned ByokChatProviderConfig,
so byokChatProvider.model serialized as undefined and /api/memory/extract
fell back to the hardcoded default (gpt-4o-mini) for OpenAI-compatible
BYOK endpoints like MiniMax. Threading config.model through ensures the
chat-selected model reaches memory extraction.

Reviewed-by: mrcfps (review comments on ProjectView.tsx:5049)

* test: add model field to BYOK payload expectations in web workspace tests

* fix(daemon): forward BYOK chatProvider to post-turn memory extraction

The daemon close handler creates memoryOptions for pickProvider() but
never forwarded the per-call BYOK provider snapshot ('byokProvider'
from the chat request body). This meant post-turn LLM memory extraction
always fell through to environment/default provider detection instead
of using the user's selected BYOK provider and endpoint.

Added 'chatProvider: byokProvider ?? null' to the memoryOptions object
so pickProvider() can run 'Same as chat' extraction against the user's
actual provider/config, matching the behavior of the pre-turn
/api/memory/extract path.

Addresses reviewer feedback on PR #5177 (fix #5162).

* fix(daemon): normalize byokProvider shape for post-turn memory extraction (#5177)

The close handler forwarded byokProvider ({ protocol, ... }) directly as
chatProvider, but memory-llm.pickProvider expects { provider, ... }
with provider being a PROVIDER_DEFAULTS key. Now we map protocol → provider
the same way the web pre-turn path does (ProjectView.tsx L5894).

* fix(daemon): support keyless BYOK endpoints in post-turn memory extraction (#5177)

Three-part fix for keyless BYOK (local vLLM / Ollama / openai-compatible servers with requiresApiKey=false):

1. server.ts normalization: thread requiresApiKey through to the memoryChatProvider snapshot so pickProvider() can distinguish keyless endpoints from missing-provider cases.

2. memory-llm pickProvider() BYOK gate: relax the apiKey guard so endpoints explicitly marked with requiresApiKey=false enter the BYOK branch instead of falling through to env/media-config fallbacks (which would route to unrelated OpenAI credentials / gpt-4o-mini).

3. memory-llm callOpenAI(): conditionally emit Authorization header only when apiKey is non-empty, preventing empty Bearer from being sent to keyless local servers that would reject it.
2026-07-15 12:20:24 +00:00
c67048516c fix(web): polish preview status action (#5696)
* fix(web): polish preview status action

* fix(web): keep preview failure hint passive

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
2026-07-15 12:09:48 +00:00
167db9de20 feat(web): add preview delivery status feedback (#5557)
* feat(web): add preview delivery status feedback

* fix(web): keep delivery retry in chat

* feat(web): stage preview delivery progress

* refine(web): clarify preview delivery stages

* fix(web): preserve preview delivery feedback

* fix(web): address preview delivery review

* refine(web): keep preview delivery status passive

* Revert "refine(web): keep preview delivery status passive"

This reverts commit df5579da48.

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
2026-07-15 09:47:02 +00:00
75af2b13cf fix(branding): preserve authored seed overrides (#5613)
* fix(branding): preserve authored seed overrides

* fix(contracts): expose brand seed overrides

---------

Co-authored-by: Rasmus Lovgren <booodhi@gmail.com>
2026-07-15 09:04:54 +00:00
open-design-release-bot[bot]GitHubopen-design-release-bot[bot] <open-design-release-bot[bot]@users.noreply.github.com>
a1cbe0ec57 chore(release): bump main to 0.15.1 ahead of stable 0.15.0 (#5661)
Co-authored-by: open-design-release-bot[bot] <open-design-release-bot[bot]@users.noreply.github.com>
2026-07-15 03:30:28 +00:00
open-design-crew[bot]GitHubNJUHuaMasonopen-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
4b660237c0 feat(prompts): land the slim system-prompt line as the default charter (#5603)
* test(daemon): freeze scenario x section gating matrix for composeSystemPrompt

composeSystemPrompt splices ~30 conditional sections whose gates form an
implicit matrix (session mode x design system x skill surface x memory x
execution profile). Before any prompt-slimming refactor, this golden
snapshot freezes which sections each representative scenario receives,
so a re-gating regression surfaces as a snapshot diff instead of a
silent production behavior change.

Detection is line-anchored heading markers: discovery quotes headings
like "## Active plugin" mid-sentence, so bare substring matching would
false-positive on sections that were never spliced in. A companion
assertion fails when a marker goes stale (no scenario produces it), and
position invariants pin injection-resistance first / role-marker guard
last across every scenario.

* perf(prompts): stop shipping rare-path media/deck payloads on every run

Langfuse telemetry (24h, 2k-trace sample) puts the composed charter at
p50 96KB (~24K tokens) with a 21% stable-cache hit rate — most turns
resend it whole. Two always-on blocks serve paths most runs never take:

- MEDIA_DISPATCH_HINT (2.8KB into every non-media project) embedded a
  40-line worked bash polling loop. The loop moves to 'od media help'
  (the CLI already prints the exact next wait command on handoff), and
  the hint keeps only the dispatch rules agents historically fumbled:
  no provider REST calls, no API-key asks, POSIX not PowerShell,
  python3 not jq, fal-ai/* passthrough. 2,771 -> 1,547 chars.

- The freeform maybe-deck branch injected the full 20.7KB deck
  framework into every kind=other / no-metadata run 'just in case'.
  New detectDeckIntentSignal() scans the same request text the agent
  will see (transcript included, so a deck mention anywhere in the
  conversation stays sticky) for generous EN/CJK deck vocabulary and
  skips the framework when nothing matches: 78.5K -> 57.7K chars on
  non-deck freeform runs. False positives fall back to today's
  behavior; deck-kind projects stay unconditional. An absent signal
  (contracts/BYOK composer, tests) preserves legacy always-inject.

Also relieves the POSIX argv budget: DS-active runs sat within 4KB of
the 100KB hard cap that fails argv-bound adapters (aider/deepseek).

Baseline check: tests/media/policy-routes.test.ts (7) and
tests/connection-test.test.ts (1, local Kimi CLI version) fail
identically on the unmodified tree — no new failures from this change.

* test(daemon): refresh golden matrix snapshot for upstream memory prompt hardening

Rebase onto main picked up #4913, which reworded the memory
rule-proposal block (+230 chars in memory-bearing scenarios). Section
gating is unchanged — size-only refresh.

* feat(prompts): rewritten slim core charter behind OD_PROMPT_CORE=slim

The always-on doctrine layer (DISCOVERY_AND_PHILOSOPHY ~28K chars +
designer charter ~14K + three tail overrides that re-assert rules the
layered stack states in softer or contradictory forms) is rewritten as
ONE ~15.5K-char document: core-slim.ts. Same composed inputs drop from
65.7K to 36.0K chars (-45%); freeform non-deck runs from 57.7K to 28.9K.

Design rules of the rewrite:
- Every rule stated exactly once, under an explicit precedence ladder
  (user request > active skill > design system > memory > charter).
- Contradictions resolved instead of stacked: one verification budget
  (static pass + P0 checklist + craft scan + at most ONE render; the
  'two passes is normal' re-score loop is gone), one turn-1 form
  decision path (four skip conditions listed in one place, including
  the memory task-brief replacement), Tweaks reconciled (iteration
  surface on request, never demo controls in product files).
- Protocol markers are frozen API and survive verbatim: question-form
  shapes/ids, pick_direction/brand_spec/reference_match branch values,
  task-type routing options, data-od-id, EDITMODE markers, pinned
  React/Babel/Motion tags, artifact handoff shapes.
- Emphasis quota: no IMPORTANT/CRITICAL inflation; imperatives carry
  the weight.

Rollout: ComposeInput.promptCoreVariant ('classic' default | 'slim'),
daemon flips via OD_PROMPT_CORE=slim — zero behavior change until set,
A/B-able per daemon. Ask mode keeps the clarifying-questions tail (it
composes no core charter). Guards in tests/prompts/core-slim.test.ts:
a 16KB byte budget that fails any silent regrowth, frozen-marker
assertions, handoff profile switch, and classic-parity checks; matrix
snapshot gains slim scenarios.

Not yet covered (follow-ups): the packages/contracts BYOK composer
still ships only the classic stack; instruction-following A/B against
classic is the sign-off gate before changing the default.

* feat(prompts): shrink the slim core to an 8KB budget by evicting what a system prompt shouldn't carry

The 15.5K v1 slim charter still carried content that has no claim to an
always-on seat. v2 evicts it by category:

- Already owned elsewhere: the od-default task-type router form ships in
  plugins/_official/scenarios/od-default/SKILL.md and reaches the prompt
  as '## Active skill'; the charter now just defers to skill-owned
  turn-1 forms. An ownership test pins the form to the skill file.
- Conditional-path content: per-platform delivery contracts (breakpoint
  matrix, per-target files, native chrome) move to
  PLATFORM_CONTRACTS_BLOCK, injected only when the project declares a
  platform or multiple targets. Deck rules were already duplicated by
  the deck-gated directive - dropped from the charter entirely.
- Model-competence content: PDF extraction how-tos, attached-image
  explanations, JSON syntax reminders - deleted.
- Recital: the default discovery form example keeps only the brand
  question verbatim (its pick_direction/brand_spec/reference_match
  values are the one protocol-matched part); the other six questions
  become a one-line recipe. Option labels were always model-localized
  per language, so exact English menus were never stable anyway.
- React pins keep versions (the actual bug fix: 18.3.1 pairing +
  framer-motion React build) but drop the SRI hashes and script-tag
  boilerplate - reviewers should weigh the integrity-attribute loss.

Charter now renders at 8,187 bytes (budget test tightened 16KB -> 8KB).
Composed prompt for the common design path: 65.7K classic -> 29.6K slim
(-55%); freeform non-deck: 57.7K -> 21.5K (-63%).

* perf(prompts): evict the remaining always-on payloads from the slim stack

Follow-up to the 8KB charter: the composed slim prompt still carried
~13K of non-charter payload. Four evictions, all slim-only (classic
stays byte-stable for the A/B):

- Project metadata becomes a fact sheet ('facts' style). The classic
  block had grown seven doctrine rules plus a responsive contract and
  cross-platform rule - prose owned by the slim charter's Craft section
  and PLATFORM_CONTRACTS_BLOCK. facts keeps key-value fields, media
  workflow data, and two compact delivery lines (screen files, product
  depth). kind=other: 3.6K -> ~1.1K.
- Memory two-loop scaffolding compressed ~5.5K -> ~2.9K: identical
  headings and od-card shapes (web parses the card types; the daemon
  checks the verify-scorecard), repeated rationale prose cut.
- zh-CN locale override drops the worked quick-brief sample copy under
  slim - it mirrored the classic form verbatim, which slim recipes
  instead; the form contract already mandates localization. 2K -> 0.85K.
- MEDIA_DISPATCH_HINT gates on detectMediaIntentSignal (same
  transcript-sticky pattern as the deck signal): most conversations
  never mention media, and the hint returns for the rest of the session
  the moment one does.
- Direction library becomes a pull layer: slim composes a ~1.3K
  id+label index and the agent fetches the chosen direction's full
  palette/posture spec via the new 'od tools directions --id <id>'
  CLI (also --label / --json / bare list). One tool call on the
  direction-pick path, which runs at most once per project, replaces
  6.7K of always-on palette data. Classic keeps the inline library.

Composed slim sizes: freeform non-deck 21.5K -> 12.6K; design full
stack 29.6K -> 22.2K (fixture DS; media hint drops another 1.4K on
non-media conversations). Baseline classic: 57.7K / 65.7K.

* refactor(prompts): restructure the slim prompt as static-first layers for cache and hierarchy

Two structural defects in the slim composition, both flagged against
prompt best-practice guidance:

1. Heading hierarchy was inverted: the document opened with an H2
   ('## Security: prompt injection resistance') and the H1 charter
   appeared six sections later. The security section now lives INSIDE
   the charter as an H2 right after Precedence (single source moved to
   core-slim.ts; the classic stack imports it for its unchanged
   standalone-first position), so the composed document reads
   identity-first with a coherent H1 > H2 tree.

2. Ordering was cache-hostile: conditional blocks (mode override,
   locale, direction index, platform contracts) preceded the static
   charter — the exact 'conditional prefix sections' anti-pattern from
   the prompt-caching guidance, where every flag combination becomes a
   distinct prefix and nothing is shared. Slim now composes in
   variability tiers:

     static charter (identical across all runs — shared cacheable
     prefix) -> conversation-stable (mode, locale) -> project-stable
     (direction index, frames, platform, memory, instructions, DS,
     skill, metadata, MCP) -> turn-variable (maybe-deck and media-hint
     signal blocks, deferred to after MCP so a mid-conversation signal
     flip only invalidates the suffix) -> recency-pinned role guard.

   The charter's Precedence section now states that a session-mode
   directive appearing after it takes precedence, replacing the
   positional 'read first' authority the old ordering relied on.

Classic composition is byte-untouched (A/B baseline); ask mode keeps
the classic head. Charter budget 8KB -> 9.25KB (absorbed the ~1KB
security section; composed totals unchanged: freeform 12.7K,
full-stack 22.4K). New layered-ordering test pins the tier sequence
with line-anchored markers; the matrix position invariant now asserts
the per-variant head.

* fix(prompts): resolve five structure/expression defects from the charter review

Findings from the senior-review pass, each with a concrete failure mode:

1. Turn-ordinal section names misfire mid-session. 'Turn 1/2/3+' made
   the model treat 'now is not turn 1' as license to skip the fresh-
   brief flow when a NEW artifact is requested at turn 8 — and,
   conversely, an event-based fix alone would have re-asked questions
   the session already answered. Sections are now event-named ('On a
   fresh brief' / 'When the brand answer arrives' / 'Once direction is
   locked') AND a session-inheritance rule makes the mid-session case
   explicit: a fresh brief inherits the conversation's locked
   direction/brand/audience, asks only genuinely new unknowns, and
   builds directly when there are none — matching current production
   behavior with zero extra rounds or cache cost.

2. The handoff rule — the single highest-cost-if-violated output
   protocol (never emit <artifact> in filesystem runs) — was the only
   rule block without a heading. Promoted to '## Handoff'.

3. The form example read backwards: it showed the LAST question
   (brand) and described the earlier six in reverse ('preceded
   in-form by'), the only place in the prompt requiring the model to
   assemble structure in reverse. Now forward-order: output question
   shown in full, brand question shown in full (its values are the
   protocol-matched part), the middle questions listed as
   insert-between recipes.

4. Dangling reference: Craft said 'single family only for
   tech/utility' — a direction name defined only in the Direction
   index, which is absent whenever a design system is active.
   Reworded direction-independent.

5. direction-cards was dead vocabulary: listed as a legal control but
   every mention forbade it. Its one legitimate path is now named —
   only when the user explicitly asks to see direction options.

Charter budget 9.25KB -> 10KB: fix #3 cost ~0.7KB (restoring the full
output-question JSON), accepted deliberately — it also restores
few-shot density for weaker fleet models, and the review's own
conclusion was that the last byte-squeezing round had negative ROI.

* fix(prompts): identity-first head for slim ask mode

The last remaining hierarchy inversion: slim ask still opened with the
H2 security block before the H1 ask override. Ask now matches design
mode's structure — the ask override (the turn's whole charter) opens
the document and the security section reads as its first subsection.
Both blocks are static, so the swap is cache-neutral. Classic remains
byte-untouched.

* fix(prompts): repair five slim-vs-classic regressions from the second audit

Second senior review against the shipping classic stack surfaced two real
bugs and four wrongly-compressed classic behaviors in the slim variant:

- text_artifact/BYOK runs got the direction INDEX plus an instruction to
  run the `od tools directions` CLI they do not have; classic inlined the
  palettes. Slim now profile-switches: filesystem keeps the 1.3K pull
  index, text_artifact gets the full inline library back (+1.35K tokens
  on that path only; still ~68% below classic, and now under deepseek's
  30KB Windows argv cap, which classic never was).
- API_MODE_OVERRIDE's literal scope ('every LATER instruction … is
  overridden') inverted when slim moved the charter ahead of it: the
  charter's TodoWrite/render-check instructions escaped the override.
  Plain-stream slim runs now compose the override first again. Pure
  reorder, zero bytes.
- PLATFORM_CONTRACTS_BLOCK gated only on project metadata, missing
  freeform briefs that name a platform in text ('做个 iOS app 原型').
  New detectPlatformIntentSignal (same generous EN/CJK shape as the
  deck/media signals) ORs into the gate; server wires it.
- media-contract's 'explicit layer overrides' paragraph cross-referenced
  the classic-only layer names; reworded layer-agnostic (classic +19B).
- Restored four classic rules the compression pass misfiled as
  model-competence filler: never hot-link user-attached images by URL
  (product constraint), skill/DS per-domain precedence clause, expressive
  form-control guidance, and the modern-CSS encouragement line. Charter
  budget 10,240 -> 10,752 with rationale in the test comment.

Classic bytes untouched except the shared media-contract rewording.
Golden matrix: gating unchanged; sizes only (classic +19, slim +537).

Pre-existing failures unrelated to this change and failing on the
unmodified baseline: tests/media/policy-routes.test.ts (7), Kimi
connection test (local CLI version mismatch).

* fix(prompts): defer signal-triggered platform contracts to the turn-variable suffix

Two follow-ups from the third structure audit:

- PLATFORM_CONTRACTS_BLOCK sat in the project-stable zone while its gate
  now includes the turn-variable conversation-text signal. A mid-session
  'make it an iOS app' would insert the block early and break the cached
  prefix for every section after it (DS, skill, memory, metadata, MCP).
  The gate is now split by trigger stability: metadata triggers keep the
  early slot (fixed at project creation), signal-only triggers defer to
  slimTurnVariableParts like the deck/media signal blocks. The
  slimTurnVariableParts declaration hoists above the design section so
  the gate can reach it; no byte or ordering change for any existing
  gating combination.

- Slim ask mode on a plain stream composed CHAT -> injection -> API
  override, inverting classic's authority order (API before CHAT, so
  'overrides every rule below' covers the chat charter). The ask head
  now leads with the API override on plain streams; the later push is
  gated on !isSlimCore. Zero-byte reorder; the two overrides do not
  conflict in content, this restores parity with the shipping order.

* fix(prompts): restore form-tailoring mandate and first-message form scope

The slim-vs-classic blind A/B eval (2026-07-04, 17 pairs, double-order
opus judging) surfaced two form regressions with converging judge
rationales:

- Form quality lost 12:1:4 to classic. Mechanism: classic's discovery
  layer carries extensive per-brief form-tailoring guidance; slim
  compressed it to the single word 'tailor'. Models shipped the default
  recipe verbatim — app-platform checkboxes on landing-page briefs, the
  fundraising ask/traction buried in one textarea placeholder. Restored
  as an explicit mandate with concrete per-brief examples, a
  plausible-targets-only rule for the platform options, and a
  translate-the-labels note on the tone recipe.
- DS + detailed-brief projects skipped the turn-1 form 3/6 vs classic
  1/6: the mid-session inheritance clause generalized to first
  messages. Added the explicit scope limit: inheritance never applies
  to a project's first message.

Charter budget 10,752 -> 11,264 with the eval citation in the test
comment. Golden matrix: sizes only.

* fix(prompts): template tokens in the seed must be replaced before handoff

The 40-task x 5-turn A/B eval (2026-07-05) found slim's remaining T2
quality gap is driven by catastrophic failures, not taste: 3 of 39
pairs shipped the live-artifact skill seed with unrendered {{mustache}}
tokens (classic did this once). Mechanism: slim compressed classic's
seed-handling guidance to 'copy the seed, paste layouts', losing the
explicit fill-with-real-content step.

- Read-before-write bullet now ends with: replace EVERY template token
  with real content; a {{placeholder}} or empty section reaching the
  user is a failed delivery. (Also covers the eval's 4 thin-content
  failures.)
- Self-check static pass adds 'leftover template tokens' to its scan.

Charter 11,155B, within the existing 11,264 budget — no bump needed.

* fix(prompts): first build ships complete, no stub sections

Clean-scope T2 analysis of the 40-task eval (excluding the 10 tasks
confounded by the live-artifact skill protocol) still shows a first
build completeness gap: main 7.34 vs slim 6.10, driven by thin-content
deliveries (empty canvases, placeholder-looking blocks) on 4 of 29
pairs. The charter's 'show something visible early' line, uncushioned,
reads as license to hand off the early skeleton.

- Qualify it: '…but the turn ships the COMPLETE artifact: no stub
  sections.'
- Self-check static pass adds 'empty or stub sections' to its scan.

11,215B, within the 11,264 budget.

* refactor(prompts): split the overloaded turn-1 form section by concern

The slim charter's '## On a fresh brief' H2 bundled six distinct
concerns under one heading — trigger, default action, mid-session
inheritance, skip conditions, form-shape tailoring, and the form JSON
contract — so a maintainer could not scan the outline to find any one
rule, and the cross-cutting form contract (valid on ANY turn) was buried
under a heading scoped to fresh briefs.

Behavior-equivalent restructure (every directive's wording preserved;
only headings added and the form contract's 'any turn' note moved from a
parenthetical to the section lead):

- ## Turn 1 — the discovery form        (trigger + default action)
  - ### When to skip or inherit the form (inheritance + skip conditions)
- ## Writing a `<question-form>`         (cross-cutting: shape + tailoring + example + ordering)
  - ### Form contract                     (JSON validity + type enum + localization)

The charter outline now reads as a per-concern document. Frozen protocol
markers (form JSON, brand values, type enum, allowCustom) unchanged.
Charter 11,242 -> 11,322B; budget raised to 12,288 for readability
headroom at the maintainer's direction (budget explicitly deprioritized
for this change). Golden snapshot regenerated; 113 prompt tests green.

* refactor(prompts): readability second pass + multi-turn DS/constraint adherence

Two changes, from a critical re-read plus production feedback.

A) Readability / logic second pass (behavior-equivalent, wording preserved):
- Heading style: the two remaining imperative-tail H2s became event
  labels with the directive moved to the body lead ('## When the brand
  answer arrives', '## Once direction is locked'), so the outline reads in
  one consistent grammar. Dropping '…hand off' also fixes a heading that
  promised handoff while the rules live in the next section.
- The self-check bullet's inline (1)(2)(3)(4) run-on became a four-item
  sub-list — the least-scannable line is now the most scannable.
- The 70-word 'Read before you write' sentence split into shorter units;
  'show early / ship complete' separated into its own bullet so the two
  ideas stop reading as contradictory.
- Precedence: skill and DS collapsed into one rank split by domain (was
  numbered 2 vs 3, contradicting the 'each highest in its own domain'
  clause) — removes the only real logical seam in the ladder.

B) Multi-turn edit adherence (new '## On an edit or tweak' section):
  Production feedback reports the design system and explicit user
  constraints frequently drift during multi-turn edits. The charter was
  turn-1/first-build heavy and had no standing edit-mode invariant. Added,
  in the lifecycle between build and handoff:
  - an edit touches ONLY what the user named; untouched sections/values
    stay as they were; re-read the file and edit in place.
  - the design system binds on EVERY turn, not just first build — no
    drift, no raw hex, no palette re-pick because this turn was about
    something else.
  - every hard constraint stated this session persists across later turns
    until the user relaxes it; a new request adds to constraints, never
    silently resets them.

Charter 11,322 -> 12,029B, within the 12,288 budget (no raise). New
frozen assertion pins the adherence invariants against future
compression. 114 prompt tests + guard + typecheck green; golden snapshot
regenerated.

* refactor(prompts): two-tier charter — lifecycle spine, grouped pillars, precedence precision

From a senior word-by-word re-read. The charter was 13 flat H2s spanning
three conceptual tiers (foundations / lifecycle / reference) with no
visible spine, and the precedence wording could invert authority.

Structure (behavior-equivalent, rule wording preserved):
- 13 flat H2 -> 5 H2: two foundations (Precedence, Security) + three
  pillars, each grouping its H3s —
    ## Discovery — ask before you build   (Turn 1, skip/inherit, writing, contract)
    ## Delivery — brand → build → iterate  (brand answer, build, edit, handoff)
    ## Craft & contracts                    (craft, technical, conduct)
  The delivery lifecycle is now one connected, ordered spine — 'Editing
  an existing artifact' reads as a named phase of it, which should help
  the multi-turn drift the edit-adherence rules target.

Precision fixes:
- Precedence: 'On conflict, higher wins: 1…2…3…4' was ambiguous (which
  number is higher?) — reworded to an explicit list, 'the one higher on
  this list wins — the user's request is the highest authority, this
  charter the lowest', and the trailing pronoun tightened to 'overrides
  it wherever the two conflict'.
- 'no tool calls or file reads first' -> 'before it'; 'Resolve the
  source' -> 'Resolve the brand source'; 'restyle the whole thing' ->
  'restyle beyond the request'.
- Trimmed one of three localization restatements (kept the authoritative
  one in Form contract, now naming labels/options/placeholders) and the
  'however complete the brief reads' duplicate.

Handoff/Craft/Technical/Conduct headings demote to H3 under their
pillars (local constants, not shared with classic). Charter 12,029 ->
12,294B; budget 12,288 -> 13,312. Ordering/adherence test anchors and
golden snapshot updated. 114 tests + guard + typecheck green.

* fix(prompts): make edit-adherence forceful — do exactly what was asked, verify it landed

Production feedback and maintainer review: the edit section addressed
only scope (don't over-change) and persistence (DS/constraints), but not
the core instruction-following failure — 'the user asked for A and the
result isn't A'. The whole section was defensive; the positive imperative
that the requested change must actually happen, completely, was missing.

Strengthened '### Editing an existing artifact':
- Leads with the imperative: the delivered file must actually be A — do
  exactly what was asked, in full, in every place it applies, with
  complete-vs-partial examples ('dark green' = every element, not one).
- Forbids reinterpreting, 'improving on', partially applying, or
  substituting own taste for the user's literal words — ties to
  Precedence #1 (user's request is highest authority).
- New 'Verify before you hand off' bullet: open the file, confirm the
  change is actually present and complete and no in-force constraint
  broke; never report a change you did not make (the claim-vs-reality
  failure).
- Kept the scope / DS-binding / constraint-persistence bullets.

Charter 12,294 -> 13,122B, within the 13,312 budget. Frozen assertion
updated to pin the positive imperative and the verify bullet.

* fix(prompts): a later explicit request overrides a conflicting earlier constraint

Edge case raised in review: turn 2 'fix the color to blue, don't change
it', turn 4 'change everything to yellow'. Correct behavior is turn 4
wins (the user is changing their own mind; yellow becomes the new
standing constraint), which Precedence #1 + 'persist until the user
relaxes it' already imply — but the old bullet also said 'a new request
ADDS to those constraints', which a literal model could read as
additive-only and stall on the turn-2 'don't touch it'.

Rewrote the constraint-persistence bullet to make the override explicit:
constraints carry forward, only the user can lift or change one, and a
later explicit request overrides a conflicting earlier constraint — with
the blue->yellow turns as the in-prompt example. The anti-silent-drop
guard (model must never drop a standing constraint on its own) is kept.

Charter 13,122 -> 13,412B; budget 13,312 -> 14,336.

* fix(prompts): make the plan step agent-agnostic — TodoWrite is Claude-only

Open Design drives many local code agents; the real session mix is ~65%
non-Claude (codex, opencode, gemini, ACP family, …). The slim charter's
plan step hardcoded 'first tool call is TodoWrite' + in_progress/completed
— a Claude Code tool those agents don't have. For ~2/3 of traffic that
instruction is dead: the agent either skips planning (losing the exact
lever that helps multi-turn adherence) or hallucinates a tool it lacks.

Everything else in the slim charter was already agent-agnostic (render
checks and direction pulls go through the shell OD CLI wrapper; protocol
markup is assistant text), so TodoWrite was the one hardcoding.

- Charter plan step → generic: 'lay out a short, updatable plan; if your
  runtime has a structured plan/todo/task tool, use it, else a numbered
  list; never call a tool you don't have.'
- Composer injects a concrete note ('Your plan tool is TodoWrite; the
  host renders it as a live Todos card; mark in_progress/completed') only
  for Claude-family runs (streamFormat claude-stream-json: claude,
  codebuddy, amp) — the only agents with TodoWrite and the host's live
  Todos card. Agent-specifics live in the composer, portable doctrine in
  the charter.
- API_MODE_OVERRIDE reframed from 'TodoWrite/Read/… are unavailable' to
  'any tool call — TodoWrite/…/or whatever your runtime exposes — will
  not execute'. Keeps the specific names (issue #313 anti-pseudo-tool,
  test-pinned) while covering non-Claude runtimes.

Two frozen tests pin: charter has no TodoWrite + generic wording;
Claude-family gets the note, json-event-stream doesn't. 116 tests +
guard + typecheck green. Charter 13412 to 13543B (budget 14336).

* feat(prompts,live-artifacts): render one-level repeats, tighten slim charter craft guards

- live-artifacts/render: implement the documented `data-od-repeat="item in data.path"`
  one-level repeat directive (specified in the schema but never rendered), with
  loop-scoped `{{item.*}}` bindings, single-pass expansion so data values are never
  re-interpolated, and explicit rejection of nested repeats / non-array sources.
- live-artifact SKILL.md: state the html_template_v1 binding contract in the workflow
  (flat data.* scalars + data-od-repeat lists; no hand-loops, no bare loop vars) so
  templates stay within the renderer's capability.
- core-slim charter: "replace every template token" now yields to a skill's
  template-plus-data protocol; add an objective Layout-integrity craft rule (no
  overlap, no clipping, oversized display type must fit its column, charts use fills
  not outlines) and name those failure modes in the self-check.

* feat(prompts): add agent-agnostic production-value craft rule to slim charter

Visual-first pages regressed on slim: hero images were hand-drawn CSS boxes
instead of generated art (P1), and controls read flat/greyscale (P5). The slim
charter had dropped all of main's imagery + visual-system guidance.

Add one "Production value — feel shipped, not greyscale" craft rule: generate a
real image via the Open Design media tool (referenced through the injected
`"$OD_NODE_BIN" "$OD_BIN"` wrapper, so it is runtime-agnostic — not tied to any
one code agent) when a hero/product shot would carry the page; ship a real
product palette, colored interaction states, and elevated primary controls.

Bump SLIM_CORE_BYTE_BUDGET 14_336 -> 15_360 to seat this load-bearing guidance.

* feat(prompts): generalize the production-value imagery rule beyond hero shots

40-task eval + user review (C3 tea gift-box, P1) showed slim under-generates real
imagery on visual-first work, defaulting to hand-drawn schematic graphics. Reframe
the rule from a narrow "hero/product shot" trigger to a general default: whenever a
real picture would lift the artifact (product, place, food, person, hero, texture),
generate it via the media tool; drop to a diagram/UI mock only when that genuinely
serves the content better. Still agent-agnostic (media tool via the injected
"$OD_NODE_BIN" "$OD_BIN" wrapper).

* fix(prompts): let the production-value rule use the run's own image generation

Hard-pointing imagery at `od media generate` only works when a daemon spawned the
agent (it injects OD_PROJECT_ID); a standalone agent (eval harness, external
runtime) can't reach it, so slim silently fell back to hand-drawn placeholders
while codex-run peers used their native imagegen. Broaden the rule: generate through
whatever image generation the run offers — the Open Design media tool when wired up,
otherwise the runtime's own native image generation. Verified: C3 slim now emits a
real generated product photo instead of a schematic box.

* test(daemon): drop connected-external-mcp from the golden matrix

Upstream moved the connected-external-MCP directive out of
composeSystemPrompt into the per-turn slice (live OAuth token state must
stay out of the cached stable prefix), so it is no longer a compose
input or a gateable section.

* chore(prompts): default to the slim charter on the validation branch

VALIDATION-ONLY commit for feat/system-prompt: packaged beta builds must
exercise the rewritten slim charter, and the packaged sidecar env
allowlist does not forward OD_PROMPT_CORE, so the default flips to slim
here. OD_PROMPT_CORE=classic restores the classic stack. Do not carry
this default into main.

* perf(prompts): batch slim tool work, with quality guards restored (#5555)

* perf(prompts): batch slim tool work

* fix(prompts): restore seed-copy rule and unconditional own-browser ban

The tool-economy pass (02923153a) dropped two quality instructions as
collateral while compressing the read/preview bullets:

- 'Copy the seed and paste its layouts — don't write CSS from scratch'
  guards the visual floor when a skill ships a seed template.
- The hard 'never your own browser' ban was softened to 'no browser
  probes first', which let the candidate arm reach for Playwright after
  an export failure (2026-07-13 slim-tool-economy eval, v1_001 turn 3).

Restore both inside the batched tool-economy wording, raise the charter
byte budget 15_360 -> 15_616 following the existing restore-precedent,
and freeze both lines as regression markers.

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>

* feat(prompts): prefill question forms with a recommended default per question (#5556)

Every <question-form> the agent emits now ships a brief-inferred
recommended `default` on each question — an option value for
radio/select, an array of values for checkbox, concrete text for
free-text fields — so the user can review and submit the form unchanged
instead of answering from scratch. Applied across the slim charter form
contract, the full discovery rules, and the od-default task-type router
(whose options stay verbatim; prefill is the one permitted tailoring).
Both prompt mirrors (daemon + contracts) updated in sync, with the
example forms anchoring the pattern via "default": "pick_direction".

The web renderer already honoured defaults, with one gap: the `default`
alias only accepted scalars, so a checkbox array default was silently
dropped (parseDefaultValue). Fixed alongside, with parser coverage for
the radio/checkbox/text shapes the prompt now instructs.

Charter byte budget raised 15_616 -> 15_872 for the new bullet +
example anchors; prefill markers frozen in core-slim and
discovery-form tests.

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>

* fix(prompts): make form prefill survive streaming key order; add web search/fetch imagery fallback (#5558)

* fix(web): adopt streamed question-form defaults that arrive after the question renders

The #5556 prefill contract asks the model to ship a recommended `default`
on every <question-form> question, but whether the prefill actually showed
up depended on JSON key order: the partial-JSON parser reveals a question
as soon as its label lands, and a `default` emitted after `options`
(observed in production run fca86faa) arrived only after the reveal pass
had already parked the question on its auto-assigned empty value — which
the backfill effect then treated as "answered" and never touched again.

Fix on two sides:

- Web: QuestionFormView now tracks which questions the user actually
  touched (seeded from submitted/draft snapshots) and adopts a
  late-arriving streamed default via shouldAdoptStreamedDefault — only
  while the question is untouched AND still holds its auto-assigned empty
  value, so a late default can never clobber a real answer or an
  intentional clear (checking then unchecking a box stays cleared).
- Prompts (daemon + contracts mirrors, slim charter + full discovery
  rules): instruct models to write `default` before `options`, as the
  example forms already anchor, so the recommendation streams in with the
  question instead of trailing a long options array.

Red spec: "adopts a default that streams in after the question was
revealed" went red on the pre-fix tree and green with this change; a
companion guard pins that "untouched" means never-interacted, not
currently-empty.

* feat(prompts): fall back to web search/fetch imagery when image generation is unavailable

The production-value craft rule tells runs to reach for real imagery
through the OD media tool or the runtime's native image generation — but
a run with neither (BYOK agents without media config, runtimes without
imagegen) had no sanctioned next step and shipped the exact fallbacks the
rule bans: empty slots, schematic boxes, flat icons.

The slim charter's imagery guidance is now an explicit resolution chain:
OD media tool → native image generation → web search / web fetch. The new
final step has the run find a fitting real photo, pull it into the
project (e.g. curl), and reference it by relative path — never hot-link
the remote URL, matching the existing no-hot-link file rule so artifacts
don't rot with dead image links.

Charter byte budget raised 15_872 -> 16_128 for the fallback sentence,
documented in the budget history; a new frozen-marker test pins the three
chain steps and the no-hot-link wording so a future tool-economy pass
can't silently drop them.

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>

* chore(prompts): land #5573 form refinements on the system-prompt validation line (#5575)

* fix(web): adopt streamed question-form defaults that arrive after the question renders

The #5556 prefill contract asks the model to ship a recommended `default`
on every <question-form> question, but whether the prefill actually showed
up depended on JSON key order: the partial-JSON parser reveals a question
as soon as its label lands, and a `default` emitted after `options`
(observed in production run fca86faa) arrived only after the reveal pass
had already parked the question on its auto-assigned empty value — which
the backfill effect then treated as "answered" and never touched again.

Fix on two sides:

- Web: QuestionFormView now tracks which questions the user actually
  touched (seeded from submitted/draft snapshots) and adopts a
  late-arriving streamed default via shouldAdoptStreamedDefault — only
  while the question is untouched AND still holds its auto-assigned empty
  value, so a late default can never clobber a real answer or an
  intentional clear (checking then unchecking a box stays cleared).
- Prompts (daemon + contracts mirrors, slim charter + full discovery
  rules): instruct models to write `default` before `options`, as the
  example forms already anchor, so the recommendation streams in with the
  question instead of trailing a long options array.

Red spec: "adopts a default that streams in after the question was
revealed" went red on the pre-fix tree and green with this change; a
companion guard pins that "untouched" means never-interacted, not
currently-empty.

* feat(prompts): fall back to web search/fetch imagery when image generation is unavailable

The production-value craft rule tells runs to reach for real imagery
through the OD media tool or the runtime's native image generation — but
a run with neither (BYOK agents without media config, runtimes without
imagegen) had no sanctioned next step and shipped the exact fallbacks the
rule bans: empty slots, schematic boxes, flat icons.

The slim charter's imagery guidance is now an explicit resolution chain:
OD media tool → native image generation → web search / web fetch. The new
final step has the run find a fitting real photo, pull it into the
project (e.g. curl), and reference it by relative path — never hot-link
the remote URL, matching the existing no-hot-link file rule so artifacts
don't rot with dead image links.

Charter byte budget raised 15_872 -> 16_128 for the fallback sentence,
documented in the budget history; a new frozen-marker test pins the three
chain steps and the no-hot-link wording so a future tool-economy pass
can't silently drop them.

* feat(forms): Other-chip escape hatch, 5-question cap, form-language copy, and streamed-default truncation fix (#5573)

* feat(forms): host-owned Other chip, 5-question hard cap, and form-language host strings

Three form-UX upgrades that shipped together on the validation branch:

1. Host-owned "Other" escape hatch. Finite-choice questions no longer show
   an always-on "Custom answer" input; the host injects a localized,
   dash-bordered "Other" chip (an "Other…" entry for selects) that expands
   into the type-in field via the shared accordion motion. Opening it on a
   single-choice question deselects the fixed options; picking a fixed
   option collapses a still-empty field; checkbox custom entries coexist.
   Prompts (slim charter + discovery mirrors) now ban model-authored
   catch-all "Other …" options — they would duplicate the host's — and the
   od-default router's taskType question pins allowCustom: false in all
   three copies since its own "Other" option IS the route.

2. Hard cap: 5 questions per form. The old "keep ≤7" guidance let the
   model pad forms to the example's length. The rule is now an explicit
   count-then-cut step ("count your draft; delete the least build-critical
   until 5 remain"), and the discovery example itself was trimmed from 7
   to 5 questions so the anchor matches the cap.

3. Copy + language. Form copy anchors lead with the benefit ("Prefilled
   for you — send as is, or tweak anything first"); the localization rule
   demands native phrasing with a concrete wrong-vs-right anchor (快速确认,
   not the literal 快速简报); and forms now declare a top-level "lang"
   BCP-47 tag that the web card uses to render its own strings (Other
   chip, custom-answer copy, submit CTA) in the form's language instead of
   the UI locale — no more English "Other" chips inside a Chinese form.
   New i18n key qf.otherOption ships in all 19 locales.

Charter byte budget 15_872 → 16_896 across the two documented bumps, with
frozen-marker tests pinning the escape-hatch delegation, the hard cap, the
native-localization rule, and the lang anchor.

* fix(web): never surface a mid-stream question-form default

The streaming shaper reveals a question the moment its label parses, and
runs every question object — including the trailing, still-open one —
through mapRawQuestion. That let a MID-STREAM `default` reach the card:
the partial-JSON repair terminates it early, so "单位教育" surfaces as
"单位教" and ["历史背景与经过", "抗战精神与当代意义"] as ["历史背景与经过",
"抗战精"]. The card adopts a streamed default only while the answer is
still empty, so a truncated adoption freezes garbage that the completed
value can never overwrite — checkbox defaults (long arrays, near-certain
to be cut across delta chunks) effectively never prefilled, while short
radio scalars usually survived inside one chunk. Observed in production
run beaf2da0 (抗日主题汇报PPT): both maxSelections checkboxes rendered
with nothing selected despite the model emitting complete defaults.

Fix at the source: the trailing in-flight question exposes NO
defaultValue until its braces close. A closed object's braces are
balanced, so its default is complete; the card's empty-and-untouched
adoption rule then fills it exactly once, correctly.

Red spec reproduces the production body cut mid-default (went red on the
pre-fix tree) and pins that a closed question's default surfaces while
the next in-flight question's stays hidden.

* fix(prompts): align task-type form contract across all three copies; fit the slot menu inside the 5-question cap

Review follow-ups from PR #5573 (nettee):

- The od-default skill's task-type form now carries the same top-level
  "lang" tag as the daemon/contracts prompt copies, so host-rendered
  controls on that path key off the form language instead of falling back
  to the app locale. A new parity check reads all three copies and locks
  both contract keys ("lang" + taskType allowCustom: false) so the skill
  copy cannot drift again.

- The slim charter's default-shape recipe contradicted the new hard cap:
  "Between output and brand, in this order: platform, audience, tone.
  After brand: scale, then constraints" prescribed 7 questions in the
  same breath as "Hard cap: 5". The recipe is now 2 fixed slots plus a
  pick-AT-MOST-3 menu (same option vocabulary, same order), and the
  frozen-marker test asserts the menu wording is present while the old
  prescriptive sequence is gone — the charter can no longer carry both.

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>

* chore(prompts): land #5586 photo-overlay discipline on the system-prompt validation line (#5600)

* fix(web): adopt streamed question-form defaults that arrive after the question renders

The #5556 prefill contract asks the model to ship a recommended `default`
on every <question-form> question, but whether the prefill actually showed
up depended on JSON key order: the partial-JSON parser reveals a question
as soon as its label lands, and a `default` emitted after `options`
(observed in production run fca86faa) arrived only after the reveal pass
had already parked the question on its auto-assigned empty value — which
the backfill effect then treated as "answered" and never touched again.

Fix on two sides:

- Web: QuestionFormView now tracks which questions the user actually
  touched (seeded from submitted/draft snapshots) and adopts a
  late-arriving streamed default via shouldAdoptStreamedDefault — only
  while the question is untouched AND still holds its auto-assigned empty
  value, so a late default can never clobber a real answer or an
  intentional clear (checking then unchecking a box stays cleared).
- Prompts (daemon + contracts mirrors, slim charter + full discovery
  rules): instruct models to write `default` before `options`, as the
  example forms already anchor, so the recommendation streams in with the
  question instead of trailing a long options array.

Red spec: "adopts a default that streams in after the question was
revealed" went red on the pre-fix tree and green with this change; a
companion guard pins that "untouched" means never-interacted, not
currently-empty.

* feat(prompts): fall back to web search/fetch imagery when image generation is unavailable

The production-value craft rule tells runs to reach for real imagery
through the OD media tool or the runtime's native image generation — but
a run with neither (BYOK agents without media config, runtimes without
imagegen) had no sanctioned next step and shipped the exact fallbacks the
rule bans: empty slots, schematic boxes, flat icons.

The slim charter's imagery guidance is now an explicit resolution chain:
OD media tool → native image generation → web search / web fetch. The new
final step has the run find a fitting real photo, pull it into the
project (e.g. curl), and reference it by relative path — never hot-link
the remote URL, matching the existing no-hot-link file rule so artifacts
don't rot with dead image links.

Charter byte budget raised 15_872 -> 16_128 for the fallback sentence,
documented in the budget history; a new frozen-marker test pins the three
chain steps and the no-hot-link wording so a future tool-economy pass
can't silently drop them.

* feat(forms): Other-chip escape hatch, 5-question cap, form-language copy, and streamed-default truncation fix (#5573)

* feat(forms): host-owned Other chip, 5-question hard cap, and form-language host strings

Three form-UX upgrades that shipped together on the validation branch:

1. Host-owned "Other" escape hatch. Finite-choice questions no longer show
   an always-on "Custom answer" input; the host injects a localized,
   dash-bordered "Other" chip (an "Other…" entry for selects) that expands
   into the type-in field via the shared accordion motion. Opening it on a
   single-choice question deselects the fixed options; picking a fixed
   option collapses a still-empty field; checkbox custom entries coexist.
   Prompts (slim charter + discovery mirrors) now ban model-authored
   catch-all "Other …" options — they would duplicate the host's — and the
   od-default router's taskType question pins allowCustom: false in all
   three copies since its own "Other" option IS the route.

2. Hard cap: 5 questions per form. The old "keep ≤7" guidance let the
   model pad forms to the example's length. The rule is now an explicit
   count-then-cut step ("count your draft; delete the least build-critical
   until 5 remain"), and the discovery example itself was trimmed from 7
   to 5 questions so the anchor matches the cap.

3. Copy + language. Form copy anchors lead with the benefit ("Prefilled
   for you — send as is, or tweak anything first"); the localization rule
   demands native phrasing with a concrete wrong-vs-right anchor (快速确认,
   not the literal 快速简报); and forms now declare a top-level "lang"
   BCP-47 tag that the web card uses to render its own strings (Other
   chip, custom-answer copy, submit CTA) in the form's language instead of
   the UI locale — no more English "Other" chips inside a Chinese form.
   New i18n key qf.otherOption ships in all 19 locales.

Charter byte budget 15_872 → 16_896 across the two documented bumps, with
frozen-marker tests pinning the escape-hatch delegation, the hard cap, the
native-localization rule, and the lang anchor.

* fix(web): never surface a mid-stream question-form default

The streaming shaper reveals a question the moment its label parses, and
runs every question object — including the trailing, still-open one —
through mapRawQuestion. That let a MID-STREAM `default` reach the card:
the partial-JSON repair terminates it early, so "单位教育" surfaces as
"单位教" and ["历史背景与经过", "抗战精神与当代意义"] as ["历史背景与经过",
"抗战精"]. The card adopts a streamed default only while the answer is
still empty, so a truncated adoption freezes garbage that the completed
value can never overwrite — checkbox defaults (long arrays, near-certain
to be cut across delta chunks) effectively never prefilled, while short
radio scalars usually survived inside one chunk. Observed in production
run beaf2da0 (抗日主题汇报PPT): both maxSelections checkboxes rendered
with nothing selected despite the model emitting complete defaults.

Fix at the source: the trailing in-flight question exposes NO
defaultValue until its braces close. A closed object's braces are
balanced, so its default is complete; the card's empty-and-untouched
adoption rule then fills it exactly once, correctly.

Red spec reproduces the production body cut mid-default (went red on the
pre-fix tree) and pins that a closed question's default surfaces while
the next in-flight question's stays hidden.

* fix(prompts): align task-type form contract across all three copies; fit the slot menu inside the 5-question cap

Review follow-ups from PR #5573 (nettee):

- The od-default skill's task-type form now carries the same top-level
  "lang" tag as the daemon/contracts prompt copies, so host-rendered
  controls on that path key off the form language instead of falling back
  to the app locale. A new parity check reads all three copies and locks
  both contract keys ("lang" + taskType allowCustom: false) so the skill
  copy cannot drift again.

- The slim charter's default-shape recipe contradicted the new hard cap:
  "Between output and brand, in this order: platform, audience, tone.
  After brand: scale, then constraints" prescribed 7 questions in the
  same breath as "Hard cap: 5". The recipe is now 2 fixed slots plus a
  pick-AT-MOST-3 menu (same option vocabulary, same order), and the
  frozen-marker test asserts the menu wording is present while the old
  prescriptive sequence is gone — the charter can no longer carry both.

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>

* feat(prompts): pin photo-overlay placement discipline in the slim charter

Real-imagery production value (the strongest-texture-lever rule) kept
shipping badges and caption cards that straddle the hero image's edge or
sit on the photo's focal subject — 2026-07-14 beta feedback on a
campus-open-day landing hero showed a session-type chip half-off the
photo, covering the graduates.

New Craft bullet: an overlay on a photo pins to ONE corner with a
consistent inset, sits fully inside the image bounds, stays clear of
faces/subjects, and reads on a legible surface with separating shadow;
with no safe corner it moves beside the image. Byte budget
16_896 -> 17_408 per the in-file bump trail; markers frozen.

* chore: retrigger ci

UI P0 (entry-settings) failed on 'Use with query carries the hydrated
starter prompt' — orthogonal to this PR's prompt-text diff, green on the
base branch's identical run, and passes locally on this branch (35s).
Empty commit to re-run the shard.

* chore: drop local-build churn in next-env.d.ts

A local 'next build' rewrote the generated routes.d.ts reference path and
the merge commit swept it in via add -A; restore the mainline version —
this PR's intent touches prompts/tests only.

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>

* fix: address review findings — repeat-directive false positives, directions flag validation, router-form cap exemption

1. live-artifacts/render.ts: a data-od-repeat match now counts as a
   directive only inside an element's open tag; literal mentions in prose
   or comments render as inert content per html_template_v1 (red spec in
   tests/live-artifacts/render.test.ts).
2. cli.ts tools directions: --id/--label now fail fast on a missing
   value, a flag posing as the value, duplicates, or both flags at once
   (red spec in tests/directions-cli.test.ts) — agents call this straight
   from the prompt contract.
3. prompts/discovery.ts (+contracts mirror): the 5-question hard cap now
   names the verbatim six-field task-type router form as its one
   sanctioned exception, resolving the contradiction between two locked
   instructions; the cap itself is unchanged for authored/tailored forms.
   Marker frozen in discovery-form.test.ts; matrix snapshot regenerated.

* fix: second-round review — nested literal repeat false positive, keyboard-accessible Other chip

1. live-artifacts/render.ts: the nested-repeat guard now uses the same
   inside-open-tag detection as the top-level scan (extracted
   findRepeatDirective helper), so a literal data-od-repeat mention inside
   a repeated element's body renders inert instead of throwing; a REAL
   nested directive still throws. Red spec: reviewer's exact repro added,
   red on the previous head.
2. web QuestionForm Other chip: replaced the display:none checkbox-in-label
   with a real <button aria-pressed aria-expanded> plus focus-visible ring
   and UA-chrome reset, restoring Tab reachability for keyboard and
   screen-reader users. Existing chip tests migrated to the button role;
   new focus/toggle spec added.

* chore: retrigger ci

Web workspace tests failed on HomeView.community-filter-decouple
(aria-selected timing) — a main-side test untouched by this PR; the full
web suite passes locally on this exact head (4351/4351). Empty commit to
re-run the shard.

* fix: third-round review — media runs skip the slim design charter; token-aware repeat-directive scan

1. prompts/system.ts: resolve the media surface (image/video/audio via
   skillMode or metadata.kind) BEFORE building the prompt head, and gate
   the slim design charter on it — the media-generation contract is again
   the sole workflow authority on media runs, matching classic. The
   mid-conversation clarifying-questions block now keys off the charter's
   actual presence so media runs keep classic's clarify guidance. New spec
   proves image/video/audio slim prompts contain no discovery-form or
   HTML-handoff rules while design runs keep the charter.
2. live-artifacts/render.ts: findRepeatDirective is now quote- and
   comment-aware — a directive-looking string inside another attribute's
   quoted value or inside an HTML comment is authored text. Reviewer
   repros added as regressions (attribute-quoted literal, tag-like
   comment text); nested detection shares the same scanner.
Red-checked: 3 new specs fail with the two source files reverted.

* fix: fourth-round review — slim media head drops the Ask charter; comment-aware element-boundary scan

1. prompts/system.ts: the non-charter slim fallback head applied
   CHAT_MODE_OVERRIDE to every slim run that was not the design charter —
   including media runs, whose Ask charter ('do not create images/video/
   audio') contradicted the media contract. The head now branches
   explicitly: ask -> Ask charter, slim media (non-ask) -> classic skeleton
   (plain-stream API override + injection resistance) with the media
   contract as sole authority. Media spec extended to assert '# Ask mode'
   is absent.
2. live-artifacts/render.ts: extracted insideComment() and applied it to
   findElementEnd(), so same-tag tokens inside comments no longer move the
   depth counter (<!-- <div> --> threw 'unbalanced'; <!-- </div> --> closed
   the repeat early). findRepeatDirective shares the helper. Reviewer
   repros frozen as regressions.
Red-checked: both new specs fail with the two source files reverted.

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: Mason <jinmeihong0201@gmail.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
2026-07-14 10:21:32 +00:00
d7151b2b60 feat(analytics): link downloads and first-party browser opens (#5442)
* feat(analytics): attribute desktop downloads

* fix(analytics): initialize attribution routes after HTTP deps

* feat(analytics): bridge first-party browser opens

* feat(analytics): bridge first-party anchor opens

* fix(analytics): make attribution claims idempotent

* fix(analytics): record pending attribution results

* chore(analytics): bind landing attribution KV

* fix(ci): remove landing preview wrangler config override

* fix(landing): resolve download attribution function

* fix(analytics): harden attribution token consumption

* fix(analytics): preserve same-installation claim retries

* fix(analytics): merge recovered attribution retries

* fix(analytics): preserve attributed browser links

* fix(analytics): clear attribution on privacy reset

* fix(analytics): bridge stopped AMR console links

* fix(ci): deploy attribution schema before Pages

* fix(ci): run attribution migrations with Wrangler 4

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
Co-authored-by: PerishCode <perishcode@gmail.com>
2026-07-14 08:46:04 +00:00
lefarcenandGitHub da7c4ae051 fix(web): rank Community "Newest" by shipped publishedAt, not local install timestamps (#5597)
* fix(web): rank Community "Newest" by shipped publishedAt, not local install timestamps

The Community gallery Newest toggle sorted by the installed-record
updatedAt/installedAt. Those are stamped when a machine seeds its own
database, so a fresh install (or an upgrade that re-stamps the catalog)
writes the whole bundled catalog milliseconds apart in folder-walk
order — Newest showed reverse walk order (roughly reverse-alphabetical),
unrelated to publication recency.

Root-cause fix: bundled templates now ship a real publication date.

- packages/contracts: add optional manifest field publishedAt (ISO 8601).
- apps/web sortByNewest: freshness = publishedAt when parseable, else
  updatedAt (right signal for user-installed plugins); tie-breaks and
  stability unchanged.
- plugins/_official: backfill publishedAt into all 460 bundled manifests
  from each folder's first-appearance commit date in upstream git
  history.
- e2e: repository-resource guard requiring every bundled manifest to
  declare a parseable publishedAt, so the signal cannot silently erode.
- docs: field reference entries in plugins-spec (en + zh-CN).

Issue: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/ca0778af-97f5-4fc0-bc63-739280904979

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* worker: 模板用最新筛选,筛选出来的不是最新的

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): keep shipped appeal order inside same-publishedAt batches

Review follow-up (#5597, nettee): after the freshness tie on an equal
publishedAt, the comparator still consulted local updatedAt/installedAt,
which a fresh install stamps in folder-walk order — so every
same-publication batch (largest: 322 manifests on one catalog date)
re-sorted per machine instead of keeping the shipped visual-appeal
ranking. Same-publishedAt records now preserve their incoming position;
local timestamps break ties only when at least one side has no usable
publishedAt. Adds the requested regression spec pinning
same-publishedAt / different-updatedAt records.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): gate publishedAt freshness to bundled records; untrack oversized looper attachment

Review follow-up (#5597, nettee round 2): publishedAt was overriding
updatedAt for every visible plugin, so a user-installed github/local/
marketplace plugin carrying an old manifest publishedAt would be buried
behind bundled templates despite a newer local install. publishedAtMs
now returns null for non-bundled records — "Newest" means install
recency for user installs, catalog publication for bundled ones. Adds
the requested regression (non-bundled, old publishedAt, newer
updatedAt) and clarifies the field docs (en + zh-CN).

CI repair: commit 578f7a9 tracked .looper-attachments/bug13_1.png
(3.8MB bug screenshot), tripping the Static gate 1MiB blob guard and
the Validate workspace aggregate. Untrack it and gitignore
.looper-attachments/ so harness attachments stay out of the repo.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-14 08:43:57 +00:00
9d055a0fa6 feat(amr): add artifact-based upgrade guidance for Free users (#5591)
* feat(amr): add artifact-based upgrade guidance

* fix(amr): fail open when billing plan is unavailable

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
2026-07-14 08:11:07 +00:00
lefarcenandGitHub ebada4cac3 fix(daemon): pin mermaid dark-theme discipline into the deck framework prompt (#5595)
Dark-themed decks and pages that embed a Mermaid diagram had zero guidance
on diagram theming, so the model shipped Mermaid's default (light) theme on
dark slides: near-black edge labels, black strokes, and a transparent svg
background sitting directly on the dark slide background. Reproduced on a
dark deck slide (--bg #101014): edge labels measured ~3.3:1 contrast (below
WCAG AA) and connector lines were nearly invisible (~1.9:1) — matching the
0.14.1 acceptance report "暗黑模式下 mermaid 图表的文字与背景对比度不足".

Add a compact "Mermaid diagram theme discipline" section to
DECK_FRAMEWORK_DIRECTIVE, sibling to the data-chart discipline (#5542):
prefer deck-token-styled HTML/CSS/SVG diagrams; when embedding Mermaid,
derive the theme from the slide background at initialize time (dark
background → theme: 'dark', or theme: 'base' + themeVariables with a dark
primaryColor — darkMode alone keeps the cream node fill default); never
rely on the transparent svg backdrop; plus a drift-mode ban list and a
pre-handoff/pre-emit self-check item. Mirror the identical section into the
contracts copy so API/BYOK mode is covered too, keeping each side's
self-check heading intact.

With the disciplined init, the same dark slide renders the diagram at
≥7.9:1 (edge labels) and 10.5:1 (node text) contrast.

Anchor the change with prompt-composition tests on both sides asserting the
directive carries the mermaid-theme markers (red on main, green here).

Source: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/9cd2904d-e52c-4813-9e4f-e7a3512aaa12

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-14 06:44:01 +00:00
lefarcenandGitHub 83d67d5a62 fix(packaged): widen win32 daemon status-wait budget + back off polling (#5515)
* fix(packaged): widen win32 daemon status-wait budget + back off polling

Windows first launches are the single biggest packaged startup-crash
bucket (packaged_runtime_failed, failure_kind=unknown): ~178 events / 61
devices/day, all status-wait timeouts with no exit code, signal, or
daemon log. PostHog shows ~90% of those devices opened the app on a
later launch — the daemon was slow, not dead — because Defender scanning
freshly-written packaged binaries inflates the cold start (native
better-sqlite3 load + first SQLite open/migrate + status-pipe bind) past
the 35s wait budget.

- resolveDaemonStatusTimeoutMs is now platform-aware: win32 gets 90s of
  AV-scan headroom, other OSes keep 35s. The OD_LEGACY_DATA_DIR
  migration override still wins on every platform. The web sidecar wait
  uses the same widened baseline (it has no legacy path).
- waitForStatus backs off geometrically (150ms -> 1.5s cap) between
  connect polls instead of a flat 150ms, so a not-yet-bound pipe is not
  hammered with connects that add contention to an already-slow start.
- classifyStartupFailure tags the timeout message as `status-timeout`
  instead of `unknown`, so the bucket is measurable — the signal for
  whether the budget raise actually drained the AV-scan-slow launches.

* fix(packaged): keep timeoutMs a hard bound — cap the backoff sleep to remaining budget

Review follow-up (@nettee): with the new geometric backoff, waitForStatus could
overshoot the configured status-wait budget by a full backoff interval — a
slow/dead sidecar could sleep the whole 1.5s cap after the deadline had passed
before the while-condition re-checked it (~2.3s over, vs ~0.95s pre-change).
Since this PR intentionally widens the win32 budget, the overshoot matters.

Cap each poll sleep to the time remaining before the deadline (and break out
immediately when none remains), so timeoutMs stays a hard-ish upper bound; the
only residual overshoot is the single in-flight requestJsonIpc connect timeout.
2026-07-13 14:30:35 +00:00
lefarcenandGitHub bdc66c978e fix(daemon): pin data-chart discipline into the deck framework prompt (#5542)
Chat-generated decks that fall through to DECK_FRAMEWORK_DIRECTIVE (no-skill-seed
deck projects + the freeform slide-deck path) had zero guidance for data charts,
so the model hand-wrote div/CSS bar charts with two stable failure modes that
match the reported symptoms: bar lengths eyeballed as magic `height: NN%` numbers
that don't track the data, and value labels clipped away inside fixed-height bars.

Add a compact "Data chart discipline" section to the directive: a paste-ready
skeleton that carries each value as an inline `--v`, declares one shared `--max`
on the chart container, and computes bar length via
`calc(var(--v) / var(--max) * 100%)`; a rule that every data point renders a
visible category + value label outside the bar (never inside a clipping
fixed-height track, same root cause as the brand-deck `funnel-label-clipped`
guard); a drift-mode ban list; and two pre-handoff / pre-emit self-check items.
Mirror the identical section into the contracts copy so API/BYOK mode is covered
too, keeping each side's self-check heading intact.

Anchor the change with prompt-composition tests on both sides asserting the
directive now carries the chart-discipline markers (red on main, green here).

Source: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/34ed7e1b-5676-48d6-8195-a4be71124ad4

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-13 10:29:46 +00:00
6426847fba fix(web): separate run success from result delivery (#5525)
* fix(web): distinguish run success from result delivery

Persist delivery outcomes separately so successful design runs cannot report completion before a usable artifact is available.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): recover missing design delivery after reload

* chore(ci): rerun pull request checks

* fix(web): avoid reattaching local delivery finalization

* fix(web): retry delivery failures

* chore(ci): rerun pull request checks

* chore(ci): retrigger managed checks

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 10:16:04 +00:00
lefarcenandGitHub d4762a9522 fix(desktop): break renderer crash-reload loops with a circuit breaker (#5322)
* fix(desktop): break renderer crash-reload loops with a circuit breaker

A renderer that crashes deterministically — a GPU/V8 CHECK failure, a
corrupt profile that aborts on every load — is reloaded forever by the
desktop poll loop: load → crash → reload → crash. One 0.14.0 device did
exactly this, emitting 26,011 desktop_renderer_crash events in a single
day (96% of all renderer crashes on the release) while its window stayed
blank the whole time.

Add RendererCrashLoopBreaker (pure, unit-tested): after 5 crashes inside
a 60s rolling window it opens. The poll loop then parks on a static,
dependency-free error screen that explains recovery is automatic, rather
than cycling the crash; markRendererFailed no-ops while open. Per-crash
telemetry is suppressed once open — the tripping crash is still reported,
flagged loop_tripped — so one wedged device can't emit tens of thousands
of identical events. The breaker re-arms after a 5-minute quiet cooldown
so a transient fault still self-heals without a reinstall.

* feat(desktop): make crash-loop recovery active + observable

Review feedback: the breaker only "waited" — its cooldown re-arm reloaded
the same URL from the same cache, and the whole flow was near-invisible.

On re-arm it now actively recovers: clears the HTTP + V8 code caches
(non-destructive — no cookies or storage) before the retry reload,
targeting the most common recoverable cause (a corrupt cached bundle or
shader) instead of hoping conditions changed on their own.

And it's now observable: a console.warn when the breaker opens (parking),
a console.info per recovery attempt, and a bounded recovery-attempt
analytics signal (reason:"recovery-attempt", recovery_attempt: N, at most
one per cooldown). Triage can tell a chronic looper (index keeps
climbing) from a device that recovered (events simply stop) without
re-introducing the per-crash spam the breaker was built to kill.

* revert(desktop): drop active cache-clear recovery, keep it passive + observable

Reviewer worry: clearing caches on every cooldown could amplify churn on a
wedged device without helping the dominant crash cause (a GPU/V8 CHECK,
exit 0x80000003), i.e. pour oil on the fire. Back the breaker off to
"stop the loop + passive retry": on re-arm it reloads without mutating any
device state and lets a transient fault clear on its own.

The observability added alongside stays: a warn on breaker-open, an info
per recovery attempt, and the bounded recovery-attempt analytics signal
(recovery_attempt: N) so chronic loopers are still visible.

* feat(desktop): add report + save-logs actions to the renderer crash screen

The recovery screen was informational only. On a renderer crash the daemon
is still alive, so give the wedged user an actionable exit — reusing IPC the
preload already exposes, no new surface:

- "Report a problem" → window.__od__.openExternal() opens a prefilled GitHub
  new-issue whose title + body carry the app version, OS, and the renderer
  exit code (e.g. 0x80000003) a triager needs, and ask the user to attach
  the logs.
- "Save logs…" → window.openDesignDesktop.exportDiagnostics() runs the
  existing diagnostics-bundle export (save dialog → zip).

Neither an issue URL nor mailto can carry a file attachment, so the flow is
save-then-attach. Each button hides itself if its bridge is missing (preload
failed to load) to avoid a dead control.

* feat(desktop): add "Email us" (support@open-design.ai) to the crash screen

Alongside "Report a problem" / "Save logs…", the recovery screen now offers a
prefilled mailto to our support address for users who'd rather email than open
a GitHub account. It reuses the existing openExternal bridge; the
shell:open-external handler now also allows a mailto addressed strictly to the
support address (isSupportMailtoUrl, unit-tested) — no other scheme or address
opens, so widening past http can't be abused into arbitrary-scheme launches.
The link hides itself if the bridge is absent, like the other actions.

* style(desktop): match crash screen to the app's monochrome brand palette

The crash screen used a stray blue accent that didn't match the app. Repaint
it with the neutral design tokens from apps/web (tokens.css): warm off-white
#faf9f7 + near-black #1a1916, a monochrome near-black primary button, and a
plain underlined (not blue) email link. Dark mode inverts the pair to a light
button on the dark bg (a near-black button would vanish), mirroring the
black/white onboarding look.

* fix(desktop): address review — tighten support mailto + reveal crash screen

Two Looper findings on the crash-screen actions:

- Security: isSupportMailtoUrl validated only protocol + pathname, so
  `mailto:support@open-design.ai?bcc=attacker@example.com` (or ?to=/?cc=)
  still passed and shell.openExternal would launch the mail client with
  extra recipients/headers through the renderer-exposed bridge. Now reject
  any query key other than the crash screen's own subject/body.

- Startup lifecycle: parking on the crash screen left `revealed` false and
  the splash open when the loop tripped before revealWhenReady(), so the
  runtime's show() kept focusing the splash — a user re-focusing during a
  startup crash loop was sent back to the boot splash. Route the park
  through revealMainWindow() so the recovery screen becomes the revealed,
  focused window and the splash is torn down (no-op after normal reveal).

* feat(diagnostics): make the support bundle root-cause native renderer crashes

Audit of "Save logs…" for the crash-recovery screen: on a renderer crash the
bundle collected text logs but NOT the one artifact that can explain an opaque
native abort like 0x80000003 (a V8/Chromium CHECK) — a minidump. crashReporter
was never started (no dumps existed anywhere), the crash-report collector was
macOS-only, and there was no GPU info (the usual suspect for such crashes).

Close the gap end to end:
- apps/desktop: start crashReporter (uploadToServer:false — on-device only) and
  relocate Electron's crashDumps into <logs/desktop>/crashes, inside the log
  tree the export already bundles; snapshot GPU + feature-status +
  runtime info to <logs/desktop>/gpu-info.json at startup.
- packages/diagnostics: add a `binary` LogSource kind (copied verbatim, no text
  decode/redaction so a minidump isn't corrupted) and findCrashDumps() to
  collect .dmp files; buildDiagnosticsZip gains a `crashDumps` input.
- apps/daemon: bundle logs/desktop/gpu-info.json and the relocated crash
  minidumps (cross-platform, capped, within 14 days).

The renderer crash context (reason/exit code/url) is already captured — the
main-process console.error lands in logs/desktop/latest.log — so a bundle now
carries stack (minidump) + GPU state + context: enough to root-cause.

* fix(desktop): mkdir crashDumps dir before setPath so first-crash dumps land in the log tree

Review (PerishCode): app.setPath('crashDumps', dir) throws when the dir does
not exist; on a fresh runtime logs/desktop/crashes won't exist yet, so setPath
threw, we fell back to Electron's default crashDumps location, and the daemon
export (which only scans <logs/desktop>/crashes) missed the minidump for
exactly the first crash loop this is meant to root-cause. mkdirSync the dir
(recursive) before setPath.

* fix(desktop): reject CR/LF in the support mailto subject/body

Review (PerishCode): the key allowlist rejected to/cc/bcc keys, but a decoded
CRLF in an allowed value (subject=ok%0D%0ABcc:attacker@…) could still inject a
mail header once the mailto reaches the mail client. Reject any subject/body
value containing CR or LF.
2026-07-13 04:58:16 +00:00
lefarcenandGitHub bc5b6f0588 fix(daemon): flag runs that end with unfinished work so status never reads "Completed" early (#5470)
A design-generation run that ended its turn with TodoWrite tasks still
pending / in_progress / stopped — or was truncated by max_tokens — was
stamped `succeeded` and read "Completed" on the Pet task center and the
project pill, while the chat footer simultaneously read "Stopped with
unfinished work". Users trusted "Completed", assumed the work was done, and
re-requested, creating duplicate generations and eroding trust in the
run-state UI (#1247 / #1060).

Root cause: the daemon equates "the turn/process ended" with "succeeded" and
never consults the agent's own declared work; multiple web surfaces then map
`succeeded` -> "Completed" without any completeness signal, even though the
chat footer already distinguishes unfinished work.

Fix — one canonical signal, consumed everywhere so the surfaces cannot drift:

- packages/contracts/api/run-completeness.ts defines the canonical
  "unfinished declared work" predicate (status !== 'completed', so pending /
  in_progress / stopped all count), matching the web footer's
  unfinishedTodosFromEvents exactly.
- Daemon folds the last TodoWrite snapshot + a max_tokens truncation off the
  agent event stream (every runtime, via the single emitAgentEvent choke
  point) and derives run.endedWithUnfinishedWork once at finish().
- The project-status projection derives `incomplete` from the already
  persisted events_json — no schema migration — so it survives reload/reattach
  instead of flashing "Completed" once the in-memory run ages out.
- Contracts carry endedWithUnfinishedWork on ChatRunStatusResponse and the SSE
  end payload (backward compatible: absent = finished), plus a new
  `incomplete` ProjectDisplayStatus.
- Web surfaces (Pet task center, project pill, DesignsTab) render `incomplete`
  as amber needs-attention, never the success color; the footer now sources
  the same shared predicate.

Truncation uses the display-only option (spec Option A): the run stays
`succeeded` but is flagged incomplete, avoiding the code===0 / stdin-close
hazards of reclassifying a truncated turn to failed.

Regression coverage, red on main / green here: an HTTP-boundary e2e
(e2e/tests/dialog/task-completeness.test.ts) drives the interactive chat path
for unfinished / stopped / truncated / finished runs; daemon + contracts +
web unit tests pin the projection and the canonical predicate.

Ref: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/011f6f51-7a25-4043-ae94-182e0b8b0931

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-13 04:17:24 +00:00
9166c3bf09 feat(contracts): agent-ready capability contracts + design RFCs (#5398) (#5400)
* feat(contracts): agent-ready capability contracts + design RFCs (#5398)

Slice 1 of the "agent-ready OD" proposal (#5398): pure-TypeScript contract
shapes for the assistant's capability layer, plus the design RFCs. No behavior
code.

- packages/contracts/src/agent-tools/: descriptor (declare), actions
  (call/result), manifest (list + paged search), registry (port), task
  (cross-project AgentTask). Discriminated on `surface`; dual-track law is
  structural; action identity is a daemon-minted `invocationId`.
- docs/rfc-drafts/agent-ready.md: umbrella design + slice roadmap, the
  MCP/WebMCP-congruent capability model, tool-search options, and
  "options considered -> recommendation" for the load-bearing decisions.
- docs/rfc-drafts/agent-ready-cross-project.md: the maintainer's separate
  reviewable permissions writeup (AgentTask + permission options).

Registry runtime, endpoints, and behavior are later slices per the roadmap.

* fix(contracts): narrow AgentToolSearchQuery.surface to 'api'

Search is the PULL path over the persistent registry, which per the RFC
holds only api-surface tools — browser availability is PUSH-advertised
per-session and cannot be answered by a session-less search query. Narrow
the searchable surface to 'api' so the contract does not encode a
browser-tool discovery path the runtime would have to special-case or
break. Addresses the blocking review on #5400.

* fix(contracts): address review — narrow search result to api-only; distinct browser vs api call paths

Resolves the two remaining blocking findings on PR #5400:

- manifest.ts: narrow `AgentToolSearchResult.tools` from the full
  `AgentToolDescriptor` union to `ApiToolDescriptor[]`, enforcing the
  PULL/api-only search invariant on the response side too (the query
  `surface` was already narrowed to `'api'`). A registry impl can no
  longer type-check while leaking `browser` descriptors into a
  session-less search result.

- agent-ready.md: stop presenting `BrowserActionRequest` as the generic
  `tools/call` for every capability. The two surfaces now have distinct
  request paths: `api` tools are invoked through their declared `/api/*`
  route + `od` subcommand (no round-trip envelope), while
  `BrowserActionRequest`/`Result` is the browser-surface path only — the
  genuinely new inbound seam.

- agent-ready-cross-project.md: reconcile the slice-5 cross-project
  routing fields (`taskId?`/`targetProjectId?`) with the distinct-paths
  model — added on both the browser envelope and the api-call request,
  not bolted onto `BrowserActionRequest` for api tools.

Validation: contracts typecheck pass; pnpm guard pass.

---------

Co-authored-by: LA <la@LAs-MacBook-Pro.lan>
2026-07-11 08:38:54 +00:00
open-design-release-bot[bot]GitHubopen-design-release-bot[bot] <open-design-release-bot[bot]@users.noreply.github.com>
fb9ae974ab chore(release): bump main to 0.14.2 ahead of stable 0.14.1 (#5441)
Co-authored-by: open-design-release-bot[bot] <open-design-release-bot[bot]@users.noreply.github.com>
2026-07-10 14:50:47 +00:00
af72a6cbb5 refactor(host): split index.ts into cohesive sibling modules (part of #5165) (#5170)
Co-authored-by: LA <la@LAs-MacBook-Pro.lan>
2026-07-10 13:37:45 +00:00
5c8642f125 refactor(platform): split index.ts god-file into cohesive sibling modules (#5166)
Export-surface-stable split of packages/platform/src/index.ts (~1111 lines,
one file, ~5 unrelated concerns) into cohesive sibling modules, with index.ts
kept as a pure barrel of explicit named re-exports.

- command.ts    cross-platform command-invocation construction
- process.ts    process lifecycle, stamps, snapshots, stop escalation
- proxy-env.ts  system proxy discovery + proxy-aware env merging
- fs.ts         path containment, atomic copy, removal, log tails
- http.ts       HTTP readiness polling
- toolchain.ts  user-level toolchain bin discovery

Function bodies moved byte-identically; only import paths adjusted. Added a
@module docblock per file and JSDoc to every exported function. Tiny private
error-extraction helpers (errorCode/errorMessage) are duplicated where used to
keep the public surface identical (no new exported names). Public export set
unchanged (42 names); typecheck + 76 tests + build all green.

Tracked by #5165.

Co-authored-by: LA <la@LAs-MacBook-Pro.lan>
2026-07-10 13:37:37 +00:00
bc6a246c65 feat(slides): enhance deck project experience (#5083)
* feat(deck): enhance slide deck functionality with speaker notes and navigation improvements

- Added a switch for including speaker notes in slide decks, defaulting to true.
- Updated keyboard navigation to include 'R' for resetting to the first slide.
- Implemented click navigation for half-slide transitions.
- Enhanced the deck framework to support hidden programmatic prev/next controls.
- Improved handling of file inputs in question forms to support file submissions.

This update aims to improve user experience in creating and presenting slide decks, ensuring better accessibility and functionality.

* feat(artifacts): enforce semantic naming for user-facing deliverables

- Updated guidelines to require semantic filenames derived from user briefs for new artifacts, moving away from defaulting to `index.html`.
- Enhanced documentation to clarify naming conventions for HTML files, emphasizing the use of descriptive names like `investor-pitch-deck.html`.
- Adjusted prompts and hints across the system to reflect these changes, ensuring consistency in user-facing outputs.
- Improved test coverage to validate the new naming conventions and ensure proper implementation in various contexts.

This update aims to enhance clarity and usability in project file management, promoting better organization and user experience.

* feat(file-viewer): enhance speaker notes display and improve file workspace functionality

- Updated the speaker notes section to include a styled placeholder for empty notes, improving user experience.
- Changed the icon in the file workspace from "slides" to "panel-left" for better clarity.
- Introduced new sub-category filtering options in the file workspace, enhancing organization and navigation.
- Improved styling for the page creator search input and added visual feedback for loading states.

These changes aim to enhance usability and visual consistency across the file viewer and workspace components.

* test(web): align workspace tests with pages switcher

* test(e2e): open design files through pages menu

* fix(deck): keep source mode reachable

* fix(workspace): restore uploaded file tabs

* feat(deck-thumbnail): implement virtualized thumbnail rail for improved performance

- Introduced a new `DeckThumbnailRail` component to optimize the rendering of deck thumbnails, reducing memory usage and improving load times.
- Enhanced the `FileViewer` to integrate the new thumbnail rail, allowing for lazy loading of thumbnails based on viewport visibility.
- Added tests for the `DeckThumbnailRail` to ensure correct functionality and caching behavior.
- Updated styles for better visual consistency and performance during thumbnail rendering.

These changes aim to enhance user experience by providing a smoother and more efficient way to navigate through deck slides.

* feat(deck): enhance deck rendering with thumbnail support and keyboard shortcuts

- Integrated `DeckSlideThumbnail` for rendering individual slides in a shadow root, improving performance and reducing main-thread load.
- Updated `DeckThumbnailRail` to utilize the new thumbnail component, allowing for efficient slide navigation.
- Implemented keyboard shortcuts for deck navigation, enhancing user experience during presentations.
- Added tests for `buildDeckRenderInput` to ensure proper functionality of the deck stage fallback.
- Improved styling for loading states in the viewer, providing a smoother visual experience.

These changes aim to optimize the deck viewing experience by reducing resource consumption and enhancing interactivity.

* feat(deck): improve thumbnail loading and speaker notes editing experience

- Refactored `DeckSlideThumbnail` to enhance loading states with a new shimmer effect, improving visual feedback during thumbnail rendering.
- Updated `DeckThumbnailRail` to manage loading states for thumbnails, ensuring a smoother user experience.
- Simplified the speaker notes editing interface by removing the toggle switch, allowing direct editing from the notes body.
- Enhanced accessibility by adding roles and focus states to speaker notes elements.
- Added tests to verify the new loading states and speaker notes functionality.

These changes aim to optimize the user experience in deck navigation and speaker notes management, providing a more intuitive and responsive interface.

* feat(file-viewer): enhance version export functionality and speaker notes interaction

- Introduced new export options for file versions, including PDF and image formats, improving the ability to download and share content.
- Implemented a streamlined process for exporting version content, with progress indicators and error handling for a better user experience.
- Enhanced speaker notes editing by refining event handling to prevent unintended focus shifts, ensuring a smoother editing experience.
- Updated styles for improved visual consistency in the file version download interface.
- Added tests to verify the functionality of version exports and speaker notes interactions.

These changes aim to optimize the file viewer's usability and enhance the overall experience when managing and exporting project files.

* fix(web): polish export menus and creator filters

* fix(file-viewer): fall back to preview exports

* test(file-viewer): cover version export fallbacks

* test(file-viewer): assert loading skeleton by role

* fix(file-viewer): restore html source mode affordance

* fix(file-viewer): capture full version fallback snapshots

* docs: trim slides handoff eof

* test(exports): cover full preview snapshot requests

* test(file-viewer): account for snapshot options arg

* refactor(file-viewer, file-workspace): streamline export functions and enhance page creator dialog visibility

* test(file-workspace): enhance slide template creation and mock plugin responses

* feat(export): add support for versioned exports and enhance HTML export functionality

* feat(export): implement versioned export support across multiple formats and enhance related tests

* feat(file-viewer): enhance manual edit functionality with commit sequence tracking and add test for inline text session saving

* style(speaker-notes): update filmstrip and thumb-frame styles for improved responsiveness and add corresponding tests

* feat(file-viewer): enhance comment composer functionality with portal metrics and improve draw toolbar layout for mobile views

* feat(analytics): track slide deck viewing experience

Add analytics for the new deck/slides experience introduced in this PR:
the deck viewer surface, in-deck slide navigation, thumbnail rail, and
speaker-notes authoring. Presentation entry stays on the existing
present_popover click to avoid double-counting.

- contracts: DeckViewerSurfaceViewProps (surface_view),
  DeckViewerClickProps (ui_click), SpeakerNotesSaveResultProps
  (new speaker_notes_save_result event), wired into the event unions.
- web: track* wrappers + fire helpers in FileViewer — deck_viewer
  surface_view on deck mount, slide_prev/next/reset, thumbnail_select,
  thumbnail_rail_toggle (expand/collapse), speaker_notes_edit, and
  speaker_notes_save_result (preview + presenter surfaces).
- tests: deck analytics suite asserting the emitted (event, props) pairs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(file-viewer, file-workspace): enhance export toast functionality and improve localization for page creation

* feat(home-view, plugins-home-section, agent-skill): update default facet preference, enhance category selection handling, and improve localization support

- Set `preferDefaultFacet` to true in HomeView component for better user experience.
- Introduced `handlePickCategory` function in PluginsHomeSection to manage category selection based on card layout visibility.
- Updated PRIMARY_CATEGORIES to reflect the new order and added localization for titles and descriptions in adaptAgentSkill.
- Enhanced tests to verify the correct handling of display copy and tags from SKILL frontmatter.

* feat(plugins-home, file-workspace): introduce commercial category labels and enhance deck handling

- Added a new module for resolving and localizing commercial category labels for plugins.
- Updated the FileWorkspace and PluginCard components to display the commercial category label on deck cards.
- Enhanced the PageCreatorPresetFrame to adjust frame classes based on deck presets.
- Refactored subcategory handling in facets to align with the new commercial category taxonomy.
- Improved localization support for new category labels across multiple languages.

* feat(deck-preview): implement deck preview scaling and enhance home hero layout

- Introduced `useDeckPreviewScale` hook to manage scaling of deck previews within a fixed 1280×720 logical viewport, ensuring proportional rendering across various templates.
- Updated `FileWorkspace`, `HomeHero`, and `PluginCard` components to utilize the new scaling functionality for improved visual consistency.
- Enhanced the layout of the home hero section to accommodate new subtype chips and descriptions, including a "More" dropdown for overflow handling.
- Improved localization support for new UI elements across multiple languages.

* fix(PreviewModal): clarify sidebar prop documentation and update test description

- Updated the documentation for the `PreviewSidebar` interface to specify that the sidebar's stage-edge handles are not shown when the sidebar prop is absent.
- Modified the test suite description for `PreviewModal` to be more concise and relevant.

* test(e2e): select prototype visual card

---------

Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:41:47 +00:00
Alex LuceroandGitHub 923474ed48 Expose sanitized native-session recovery metadata in run details (#5270)
* Expose native session recovery metadata

* Update recovery metadata for resumable failures

* Fix native recovery metadata edge cases
2026-07-10 08:48:28 +00:00
edb6292f1c feat: post-update what's new card on home with release feed highlights (#5071)
* add post-update what's new card on home with release feed highlights

After an update, the home surface shows a one-time bottom-right card with
the release highlights (title, copy, optional image) and a "See what's new"
link. Content ships in the release feed metadata.json as an optional
whatsNew block sourced from tools/release/whats-new/<baseVersion>.json;
versions without highlights fall back to generic copy linking the release
notes. The card shows once per version, keyed on a locally stored
last-seen version, and never on fresh installs.

- packages/contracts: WhatsNewContent/WhatsNewResponse DTOs + analytics props
- daemon: GET /api/whats-new (channel feed fetch, 1h cache, stale/null
  degradation) + od whats-new CLI mirror
- web: WhatsNewPopup card (CSS Module, i18n chrome in 19 locales,
  surface_view/ui_click tracking), lastSeen decision logic in lib/whats-new
- release tooling: publish-metadata forwards the optional whatsNew block and
  fails loudly on malformed files; docs in tools/pack/AGENTS.md and
  tools/release/whats-new/README.md

* fix: gate whats-new card on active home view, soften Escape, validate locale overrides

Address review feedback on the post-update what's-new card:

- EntryShell passes `active={view === 'home'}` into WhatsNewPopup, and the
  component defers its fetch/show decision (and `page_name: 'home'`
  analytics) until Home is the active entry view, so direct routes like
  /projects no longer surface the card or emit home-page events.
- The document-level Escape listener now ignores already-handled events
  (`defaultPrevented`) and hides the card without marking the version as
  seen or firing the dismiss analytics event, so a stray Escape no longer
  permanently spends the once-per-version card.
- publish-metadata validates the optional `locales` block at publish time
  (plain-object shape, non-empty `title`/`body`, HTTPS `linkUrl`) so
  malformed locale overrides fail the publish loudly instead of being
  silently dropped by the daemon parser; README documents the contract.

Generated-By: looper 0.9.11 (runner=fixer, agent=claude-code)

* fix(release): validate whats-new URLs by parsing, not prefix check

Replace the startsWith("https://") checks in readWhatsNewBlock with a
shared assertHttpsUrl helper that trims, constructs new URL(...), and
requires protocol === "https:", mirroring the daemon readHttpsUrl parser
so values it would silently drop (e.g. "https://") fail the publish
loudly for imageUrl, linkUrl, and locale linkUrl.

Generated-By: looper 0.9.11 (runner=fixer, agent=claude-code)

* fix(web): stop whats-new card fetch race from swallowing the card on first Home load

The card's fetch effect armed its once-guard when the fetch STARTED, and the
effect teardown re-armed it — so React StrictMode's double-invoke (dev) or a
Home toggle mid-fetch cancelled the first fetch, the second effect run saw the
guard still set and bailed, and the resolved-but-cancelled fetch re-armed the
guard without ever showing the card. The card then never appeared until the
Home view toggled again.

Arm the guard only once a decision is actually REACHED (fetch resolved with a
payload); keep the `cancelled` flag purely as a state-update guard that no
longer re-arms the fetch. A superseded fetch drops and the surviving one
records the decision, so the card shows on first Home activation and a genuine
mid-fetch Home exit still retries on the next activation.

Red spec: apps/web/tests/components/WhatsNewPopup.test.tsx renders under
StrictMode and asserts the card appears — red on the previous effect, green now.

* feat: add silent updater preference

* fix: block posthog in beta dry runs

* refactor(whats-new): drive the home card from a dedicated R2 document

Source the post-update "what's new" card from a single hand-curated document
on a dedicated R2 bucket (https://whatsnew.open-design.ai/whats-new.json,
overridable via OD_WHATS_NEW_URL) instead of per-version release metadata.
Operators edit that one file after a release; there is no publish tooling.

- Show-once is keyed on the document's `id`, not the app version: the card
  shows whenever the id differs from the last one the client recorded, first
  sight on a fresh profile included. This removes the version gating that could
  never tell a fresh install from an existing profile upgrading across the
  feature boundary (the reviewer's blocking finding) -- content identity
  settles it instead.
- Daemon service just fetches the document and parses {id, ...content}; no
  channel feed or release-version matching. Graceful degrade to no card on any
  failure is preserved.
- Drop the now-dead release publish tooling: tools/release/whats-new/*, the
  whatsNew forwarding in publish-metadata.ts, and its tests. Ops flow documented
  in docs/whats-new.md.
- Remove the generic no-content fallback card and its two i18n keys; the card
  shows only when the document has content.

Contracts, `od whats-new` CLI, and analytics updated to the {version,id,content}
shape.

* style(whats-new): align the home card with the marketing design

Match the reference card: brand serif body copy (var(--serif), same face as
the home hero), body + CTA stacked in a left column with the illustration
stretching to the card's full height on the right so there is no dead space,
a subtle CTA, and a slightly wider, softer card. CSS-module + markup only;
no behavior change (test ids and flow are untouched).

* style(whats-new): drop the title row, float the close over the card

Per design feedback the "Design system sync" label row is removed so the serif
body copy starts at the top edge; the ✕ now floats top-right as a bordered chip
that stays legible over the illustration. The title still labels the dialog via
aria-label for screen readers. Test asserts on the body + dialog name instead.

* style(whats-new): version eyebrow + release title as the card copy

Per design feedback: the top row is the "Open Design x.y.z" version eyebrow
(left) with the close button (right); the release title renders as the main
serif line (1-2 lines) and the illustration stretches to the card height; the
long body is dropped from the card. CTA + release-notes action unchanged.

* fix(whats-new): gate the card to release channels so it can't intrude on CI

The R2 rearchitecture dropped the daemon's channel gate, so development/CI
builds fetched the live document and showed the card on Home. That broke the
P0 "settings dialog reachable" smoke (getByRole('dialog') matched both the
settings modal and the what's-new card, which also carries role="dialog") and
would have dirtied every Home visual baseline. Restore the gate: only
beta/prerelease/preview/stable fetch the document; other channels resolve to no
card and never touch the network. OD_WHATS_NEW_URL still opts any channel in
for e2e fixtures that exercise the card on purpose.

* fix(whats-new): use role="complementary" instead of "dialog" for the toast

The card is a non-modal, dismissible announcement — not a dialog. As
role="dialog" it collided with getByRole('dialog') in the P0 Settings smokes
(settings-dialog-reachable, AMR reopen, Local CLI Codex fallback), which
strict-mode-failed the moment the card rendered on Home. A complementary
landmark is the correct a11y role for supporting content and never matches a
dialog query, so those P0s pass regardless of whether the card is showing. The
daemon channel gate (5f17873e0) still keeps the card off development builds.

---------

Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
Co-authored-by: PerishCode <perishcode@gmail.com>
Co-authored-by: lefarcen <935902669@qq.com>
2026-07-10 08:38:49 +00:00
Marc ChanandGitHub 2f5c9bb218 feat: use model metadata in picker badges and bump Vela CLI (#5407)
* Use model metadata in picker badges

* fix: preserve model picker metadata fallbacks

Generated-By: looper 0.10.2 (runner=fixer, agent=codex)

* fix: pass BYOK model metadata to pickers

Generated-By: looper 0.10.2 (runner=fixer, agent=codex)

* Bump bundled Vela CLI

* Keep BYOK model permissions out of discovery

* Preserve Vela model metadata over cache

* fix: refresh nix pnpm dependency hashes
2026-07-10 06:39:02 +00:00
cd07d359df feat(chat): surface daemon failure_detail to sharpen run-error guidance (#5321)
* feat(chat): surface daemon failure_detail to sharpen run-error guidance

The daemon already classifies each run failure into failure_category /
failure_detail (run-failure-classification.ts) for retry policy and
telemetry, but kept it daemon-internal — so the chat could only key error
guidance off the coarse errorCode. That collapses distinct causes that
share one code: a hard quota and a transient 429 both arrive as
RATE_LIMITED, and a text-detected missing CLI leaks in as the opaque
AGENT_EXECUTION_FAILED.

Propagate the classification to the client (contract DTO + SSE end frame,
mirroring the proven `resumable` path) and let resolveRunFailureUi take
`detail` as a first-class input that can override a too-coarse code:

- hard_quota / workspace_credits_exhausted -> a distinct "Quota exhausted"
  card that drops the futile Retry (primaryAction 'none') and steers to the
  hosted-AMR switch card instead.
- cli_not_installed (text-detected) -> the same install-the-CLI card the
  AGENT_UNAVAILABLE code path already renders.

A transient 429 without a hard-quota detail still offers Retry, and the
Antigravity terminal switch-model flow still wins over the generic detail
override.

Stacks on #5308 (the code->guidance mapping this detail layer refines).

* chore(i18n): rebrand user-facing "AMR" to "Open Design Cloud"

The hosted model service is now Open Design Cloud, but the run-error
cards this PR sharpens (switch / auth / balance), plus settings account +
wallet labels, avatar console, onboarding gate, and the handoff website
link, still showed the old "AMR" (or bare "Open Design") name. Normalize
every user-visible brand mention to "Open Design Cloud" across all 19
locales. Value-only: no keys, agent ids, URLs, or raw daemon error
strings touched.

* fix(chat): persist daemon failure classification onto assistant message

The daemon emits the terminal `error` SSE frame from the child-close
handler before a run is finalized, so `failureCategory` / `failureDetail`
(computed at finalize) weren't known when that frame was first persisted
to the assistant message — the stored `status:error` event kept only
`{ detail, code }`. A failure persisted without the live web error
handler saving the message, or a conversation reloaded before that save
landed, therefore fell back to the coarse errorCode UI and lost the
specific fix guidance this PR adds.

Stamp the finalize-time classification onto the persisted assistant
message: enrich the last `status:error` event in place (append one only
if a failed run somehow never persisted an error frame). Covered by a
daemon regression that reads the stored assistant message via the
conversation-messages API after a failed hard-quota run.

* fix(chat): keep daemon failure classification on the live web reload/recovery paths

Two live-propagation gaps let the sharpened run-error card degrade back to
the coarse/raw UI even though the daemon now sends the classification:

- appendErrorStatusEvent() no-op'd whenever the message already had a
  trailing status:error with the same detail. In the reload/reattach race
  (reload reads the daemon-persisted error frame first, then the run
  finishes and onError arrives with the finalize-time classification) that
  dropped the new code / failureCategory / failureDetail. Merge those into
  the existing event in place instead; no-op only when nothing is added.

- The error-frame-then-status recovery probe in providers/daemon.ts broke
  out on a terminal failed status without copying failureCategory /
  failureDetail (unlike the post-loop status fallback), so markErrorRunFailure
  stamped null and the failureDetail mapping fell back to the generic card on
  the reconnect path. Mirror the two assignments before the break.

Covered by chat-events (in-place merge) and daemon-sse-reconnect (error
frame + terminal status fetch, no end frame) regressions.

* fix(web): update AMR tier-upgrade test for 3-arg resolveRunFailureUi

The main merge kept a two-argument call site that passed agentId as
detail after the signature gained an explicit detail parameter.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: re-trigger validate workspace on approved head

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: align AMR rebrand assertions with Open Design Cloud copy

Update unit and e2e selectors that still expected pre-rebrand strings
for account status, handoff website link, and switch-and-retry CTA.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 03:46:01 +00:00
323246a64c Website Clone: first-party Home entry, fidelity, real-clone example cards + tracking (#5178)
* Add Website Clone home entry with bundled web-clone skill and root-relative asset preview fix

* Retrigger CI

* Open the site entry after website-clone turns

* Harden web-clone skill asset and color fidelity

Recon now scrolls the full page and captures @font-face rules, real font/image
resource URLs, computed section palette, and resolved root variables. Asset
harvest is rebuilt on the browser network stack so cross-origin CDN images,
fonts, and media (hotlink-protected included) all land locally, with a
self-hosted fonts.css and an originalUrl→localPath manifest. Clone audit gains
--recon --strict fidelity gates (fonts self-hosted, images downloaded, key
section colors exact) that exit 2 on violations, and SKILL.md makes all three
a mandatory delivery gate.

* Default web-clone mode to faithful reproduction when unconfirmed

A real nike.com run showed the agent silently downgrading to visual-homage
mode (placeholder brand, typography blocks instead of photography, approximated
palette) because the user skipped the turn-1 confirmations and the site was a
copyright-heavy commercial SPA. Fidelity is the point of this scenario: default
to faithful reproduction, land real assets first, record trademark/copyright
replacements as a pre-deploy checklist for the user to decide in Step 6, and
never skip script recon just because the target looks bot-sensitive.

* Streamline web-clone runs: skip discovery with URL, exempt brand constraints, actionable playwright setup

Three run-path optimizations for the Website Clone scenario:

1. POST /api/projects derives metadata.skipDiscoveryBrief for web-clone
   projects whose pending prompt already carries the target URL — the URL is
   the brief, so the turn-1 question form no longer stalls the run (an
   explicit client flag still wins). Covered by
   project-create-web-clone-discovery.test.ts.
2. Prompt composition skips the design-system authoritative block and craft
   rules for intent=web-clone runs: the fidelity target is the original
   site, not the project brand; brand application belongs to the skill's
   Step 6 replacement phase.
3. The plugin's playwright-loader now also resolves from the process cwd
   (so 'npm i -D playwright' in the project root works for scripts staged
   under .od-skills/) and fails with exact fix commands; SKILL.md documents
   the staged-path resolution and forbids downgrading to eyeballing because
   of missing deps.

* Swap the copyright guardrail for faithful reproduction on web-clone runs

The official designer prompt's 'don't recreate copyrighted designs — help
the user build something original instead' bullet made web-clone agents
silently rebrand clones (Nike → placeholder brand, original SVGs instead of
real photography) even after the skill mandated faithful defaults: a system
prompt guardrail outranks skill blocks. For intent=web-clone runs the bullet
is swapped for a faithful-reproduction rule that routes trademark/copyright
caution through a pre-deploy replacement checklist (NOTES.md) the user owns.
Prompt tests guard both directions of the substitution.

Also: recon now captures scroll-feel signals (scroll-behavior, scroll-snap
rule count, sticky/fixed census) and SKILL.md makes matching the original's
scroll behavior (smooth-scroll libs, snap, sticky) a fidelity requirement.

* Extract web-clone into a shared global skill referenced by ref

The Website Clone workflow (SKILL.md + 14 recon/harvest/audit scripts +
references) moves from the example-web-clone plugin into the global skill
registry at skills/web-clone/. example-web-clone now references it via
od.context.skills:[{ref:'web-clone'}] instead of shipping its own copy, so the
upcoming per-site clone example plugins can all share one skill rather than
each duplicating a 16KB SKILL.md and its scripts.

Pinned-plugin skill resolution (server.ts) previously only loaded a plugin's
LOCAL ./SKILL.md; a {ref} skill loaded nothing, so the body never entered the
prompt and the scripts never staged. It now falls back to resolving the ref
against the global registry, loading the body and registering the skill dir for
cwd staging. Verified end-to-end in a live run: skills/web-clone stages to
.od-skills/web-clone-<hash>/ with SKILL.md + all scripts.

guard.ts residual-JS allowlist follows the scripts to their new home; a
structural test locks the manifest<->skill contract.

* Add 7 Website Clone example-prompt cards for the Home clone rail

nexu.io, open-design.ai, anthropic.com, vercel.com, resend.com, linear.app,
raycast.com — each a thin example plugin that references the shared web-clone
global skill (od.context.skills:[{ref:'web-clone'}]) so none duplicate the
workflow. Each seeds the composer with a faithful-clone prompt for its URL and
ships a self-contained example.html preview built from a real recon capture of
the target homepage, so the gallery card shows what the card clones. They
surface under the Website clone chip's example-prompt rail (verified in a live
desktop run) and inherit the chip's intent:'web-clone' metadata, so clicking one
runs through the full fidelity path (skip discovery, brand exemption, faithful
reproduction, script recon/harvest/audit).

* Polish clone example-card previews with a framed hero capture

The first pass inlined a downscaled full-page screenshot cropped mid-content —
it read as a raw screenshot dump. Each preview is now a crisp above-the-fold
hero capture (1440x900 DPR2, 16:10) presented inside a browser-window chrome
(traffic lights + host URL pill) on a brand-tinted gradient, so the gallery
card reads as an intentional site preview. Verified by rendering the previews
headless.

* Cache the plugin list so Home remounts don't grey the create rail

HomeView re-fetched + parsed the large /api/plugins payload on every remount,
leaving the create-rail chips disabled for 1-2s each time. A module-level cache
(listPluginsFresh, 10s TTL) serves warm mounts without the network round trip so
pluginsLoading clears within a frame; the first render stays cheap (empty list)
to keep the click off the critical path. Also hide the base example-web-clone
plugin from the example rail (the per-site clone-* cards are the real examples).

* Ship real self-contained clones as example-card previews + align seed prompt

Each clone-* example.html is now a real, self-contained reproduction of its
target site (inlined CSS/fonts/images via document.styleSheets serialization so
CSS-in-JS/Next.js SPAs like Vercel/Raycast capture correctly, oversized icon
sprites dropped, rasters downscaled to stay under the 5MB preview cap, plus a
scroll-reveal motion layer). Preview renders the real HTML; Remix copies it into
a project as index.html. Manifests reuse the base web-clone useCase.query with a
{{targetUrl}} default per site, so picking a card seeds the same preset prompt
with the URL filled in instead of the marketing description.

* Shrink clone example previews ~33% (dedup + drop EOT + WebP)

Three lossless-to-near-lossless passes on the self-contained clones:
- Dedup repeated data-URIs (same image used in <img> + css url() N times): store
  each once as a :root CSS var, point url() at var(), hydrate img[src] from the
  var via a tiny script. Resend alone shed 1.8MB.
- Drop leftover EOT/fontobject font data-URIs (IE-only, redundant with woff2).
- Re-encode inlined raster images to WebP (~50% smaller than JPEG at equivalent
  quality) via cwebp q72.
Total across the 7 cards: ~23MB → ~15.5MB, every card now under 3MB (was up to
5MB). Image hydration + fidelity verified by headless render.

* Lead create rail with Website clone; seed example cards with just the URL

- Move 'web-clone' to the front of CREATE_RAIL_ORDER so Website clone is the
  first create-rail card, ahead of Slides (deck).
- Website-clone example cards (Clone Nexu, Clone Vercel, …) now seed the
  composer with the same short 'clone this site: <url>' scaffold the chip uses
  (localized webClonePromptSeed prefix + the card's targetUrl), e.g.
  'Website URL to clone: https://nexu.io', instead of the multi-paragraph
  build spec. Picking the chip with an empty composer already seeds the bare
  prefix for the user to paste a URL after.
- Fix a test-only regression from the Home plugin-list cache: the module-level
  visible-plugins cache leaked a case's mocked /api/plugins payload into later
  cases via listPluginsFresh, greying the rail / stranding scenarios on
  'Template None'. Add resetPluginsCache() and clear it in a global afterEach.

* Track web-clone example-card picks (example_prompt / example_open_project)

The Website-clone example cards (Clone Nexu / Vercel / …) had no analytics on
pick or remix, so the site-clone funnel could not be broken down per example.
Wire the two clicks the contracts already type (埋点文档 row 116):

- useExamplePlugin → ui_click area=chat_composer element=example_prompt
- duplicateExamplePlugin (Remix) → element=example_open_project

Both carry chip_id (web-clone) + plugin_id (example-clone-<site>) + plugin_type
for per-site breakdown. The created project's own project_kind=web_clone (already
derived from metadata.intent via projectKindToTracking) still rides
project_create_result / run_created, so 'how many projects are website clones' is
answerable without extra work. No contracts change — the element enum and the
web_clone project_kind already existed. Adds a behavioral test asserting both
events fire with the right triplet + attribution.

* Mirror web-clone copyright-guardrail swap into contracts/BYOK prompt

PR #5178 blocking review (nettee/looper): the daemon prompt special-cases
intent='web-clone' (faithful-reproduction rule) but the contracts prompt stack —
which apps/web uses for API/BYOK runs — imported OFFICIAL_DESIGNER_PROMPT directly
and never routed web-clone, so BYOK website-clone runs kept the original 'build
something original' guardrail. Daemon and BYOK now behave identically:
- contracts/prompts/official-system.ts: add COPYRIGHT_GUARDRAIL_BULLET,
  WEB_CLONE_COPYRIGHT_GUARDRAIL_BULLET, renderOfficialDesignerPrompt (mirrors the
  daemon renderer; contracts base has no execution placeholders so it only swaps
  the bullet).
- contracts/prompts/system.ts: composeSystemPrompt routes the charter through
  renderOfficialDesignerPrompt({ webCloneFidelity: metadata.intent==='web-clone' }).
- Add system-prompt-web-clone.test.ts covering both modes + the byte-identical
  bullet guard.

* Complete web-clone BYOK prompt parity + rebase relative JS asset refs

Two remaining blocking issues from PR #5178 looper re-review:

1. contracts composeSystemPrompt still appended the active-design-system
   'authoritative' block + visual-direction override unconditionally, while the
   daemon suppresses both for intent==='web-clone'. A BYOK web-clone run with an
   active design system was still told the design system wins, fighting faithful
   reproduction. Gate both sections behind !isWebCloneRun; add contracts tests
   asserting web-clone BYOK prompts drop the design-system guidance.

2. rewriteInlinedScriptAssetRefs only handled root-relative literals. When
   js/app.js is inlined into the HTML entry, a sibling lookup like
   fetch('./data.json') / new Worker('./worker.js') / import('./chunk.js')
   resolved against the HTML root instead of js/. Take the source script path and
   rebase confirmed relative refs against it (mirrors the CSS pass), membership-
   confirmed; add unit cases for ./sibling, ../parent, and escaping/missing.

* Website-clone examples: plain text prompt cards, drop embedded HTML + Remix

The 7 site example cards embedded a full self-contained clone of each target site
(anthropic.com, vercel.com, …) as example.html and offered a one-click Remix that
copied that markup into a project. Shipping verbatim copies of other companies'
sites is an IP risk, so replace them with plain text prompt cards:

- Delete plugins/_official/examples/clone-* (7 dirs, incl. the example.html
  reproductions and the Remix-enabling preview.entry).
- Add a 'web-clone' entry to HOME_PROMPT_EXAMPLES (en + zh-CN, others fall back to
  en): 7 text cards that fill the composer with 'Website URL to clone: <url>'.
  With the site plugins gone, the web-clone chip's preset rail is empty so these
  static text cards render instead. Clicking one fills the URL prompt and (via
  the existing usePromptExample) still fires ui_click element=example_prompt
  chip_id=web-clone — no plugin_id / no Remix / no example_open_project.
- Remove the now-dead webCloneCardTargetUrl seed override; refresh stale comments.
- Rewrite HomeView.web-clone-tracking test for the text-card path (asserts text
  cards render, no plugin-preset / no remix affordance, and the pick still tracks).

The base example-web-clone skill + web-clone run behavior (intent, fidelity
prompt, discovery skip) are unchanged.

* Give web-clone example cards a site logo + domain, not raw prompt text

The 7 Website-clone examples rendered as near-identical white boxes each showing
'Website URL to clone: https://<site>' — visually flat and hard to scan. Render
them as a logo gallery instead: each card shows the site's own favicon (resolved
at render time via a public favicon service — no third-party brand assets are
bundled) in a rounded tile above the bare domain (nexu.io, vercel.com, …). A
blocked/failed icon fetch falls back to a lettered monogram tile. Click behavior
is unchanged: the full 'Website URL to clone: <url>' prompt is still what fills
the composer and fires ui_click element=example_prompt. Non-URL prompt examples
(other chips) keep the plain text card.

* Web-clone examples: keep only open-design, add kimi

Legal review flagged brand-copy risk in the example rail. Drop the
nexu / anthropic / vercel / resend / linear / raycast site prompts and
keep just open-design.ai plus kimi.com (official Kimi partnership).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 03:28:06 +00:00
57386e4ef1 fix(analytics): attribute byok opencode providers (#5356)
Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-07-10 02:56:02 +00:00
81b20dc6a2 fix(web): avoid missing design-system preview assets (#5381)
Co-authored-by: lefarcen <935902669@qq.com>
2026-07-09 14:34:45 +00:00
Marc ChanGitHubopen-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com>
b3356807b6 fix(amr): improve unavailable model gating (#5335)
* fix(amr): honor full model list metadata

* fix(amr): classify tier entitlement failures

* fix(amr): refine unavailable model upgrade affordance

* fix(web): restore stale AMR model fallback

Generated-By: looper 0.10.1 (runner=fixer, agent=codex)

* fix(web): surface AMR tier upgrade failures

Generated-By: looper 0.10.1 (runner=fixer, agent=codex)

* fix(amr): mirror chat upgrade attribution

Generated-By: looper 0.10.1 (runner=fixer, agent=codex)

* fix(amr): skip disabled default models

Generated-By: looper 0.10.1 (runner=fixer, agent=codex)

* fix(amr): honor catalog default model

* chore(pack): bump vela cli to 0.0.20

* fix(amr): align locked catalog fallback state

* chore(nix): refresh pnpm deps hash

* fix(amr): refresh entitlement-aware model state

* fix(amr): refresh models after upgrade return

* fix(amr): preserve default model analytics

* fix(amr): tighten model gating follow-ups

Generated-By: looper 0.10.1 (runner=fixer, agent=codex)

* fix(amr): repair default and refresh follow-ups

Generated-By: looper 0.10.1 (runner=fixer, agent=codex)

* fix(amr): resolve explicit default before spawn

Generated-By: looper 0.10.1 (runner=fixer, agent=codex)

* fix(amr): concretize default model in smoke paths

Generated-By: looper 0.10.1 (runner=fixer, agent=codex)

* fix(amr): repair default model gating

Generated-By: looper 0.10.1 (runner=fixer, agent=codex)

* fix(amr): share connection-test model cache

Generated-By: looper 0.10.1 (runner=fixer, agent=codex)

---------

Co-authored-by: open-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com>
2026-07-09 10:37:26 +00:00
4b9072f01b feat(web): onboarding PR-C — first-loop completion funnel (+ stage dims) (#5160)
* feat(web): first-loop ledger + onboarding_completed funnel event

Tracks the first-generation loop of a recommendation-started project
(prompt_sent -> generated -> artifact_viewed -> delivered) in a session
ledger keyed by the created project id, and fires onboarding_completed
exactly once, when the loop closes with a delivery (share option picked /
export succeeded) IN THAT SAME project, carrying every step reached in
order (spec S8.3 loop, S11.1).

Project-scoped so a delivery in an unrelated later project can never
close another project's loop; the ledger is dropped on completion so
post-completion activity can't re-open or grow it. Delivery taps live in
FileViewer's share/export path (which knows the project id); the earlier
steps ride the existing ProjectView funnel points. Also adds the optional
studio_stage / feature_id dims to the studio hint events (spec S11.1)
ahead of the stage-layer and S8.7 feature-hint work, which stay blocked
on the in-flight NextStepActions branch.

Rebased onto main after #5111 merged; adapted to main's project-keyed
onboarding-entry guards and the split contracts analytics/events barrel.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web): fire onboarding delivered only on export/share success

Move the first-loop `delivered` tap from the share-option click to the
export/share success callback in all three export flows (programmatic,
template, image). Closing the onboarding loop now means the artifact was
actually delivered, not merely that the user opened an export option.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 10:48:52 +00:00
Tom HuangandGitHub 956b6607f5 feat(preview): add powered WebGL previews and examples (#5266)
* feat(powered-preview): implement cross-origin isolation for WebGL and Worker artifacts

- Introduced a new powered preview feature that allows rendering HTML artifacts in a cross-origin-isolated iframe, enabling the use of Web Workers, WASM, and SharedArrayBuffer.
- Added API endpoint `/api/preview/isolation` to report the daemon's directly-reachable HTTP origin for rendering.
- Implemented logic in the file viewer to determine if an artifact requires powered preview based on its content.
- Updated the file serving routes to support powered preview, ensuring proper headers are set for cross-origin isolation.
- Enhanced the user interface to include new scenarios for WebGL and Worker visualizations in the home hero section, with corresponding translations and descriptions.

This feature addresses the limitations of the opaque-origin preview sandbox, allowing for more complex GPU and compute applications to function correctly.

* feat(preview): add WebGL templates and lazy HTML previews

* test(web): stabilize project back routing assertion

* fix(preview): address powered preview review blockers

* fix(preview): isolate powered preview origin
2026-07-08 08:58:51 +00:00
YOMXXXandGitHub e8ea684795 fix: persist plain-stream artifact tags (#4731)
* fix: persist plain stream artifact tags

* fix: address plain-stream review feedback

* fix: ignore indented fenced artifact examples

* fix: summarize non-html persisted artifacts

* chore: rerun flaky web ci

* fix: ignore inline artifact examples

* fix: harden plain artifact persistence

* fix: align plain stream fence parsing

* fix: harden plain artifact recovery

* fix(web): content-check recovered non-html artifacts

* fix(daemon): align bare artifact parsing
2026-07-08 07:35:35 +00:00
3124cfe9c8 Add Tom Modern Design System (#4918)
* Add Tom Modern Design System

New design system: Tom Modern Design
- Editorial-technical design family
- Neutral #fafafa canvas, #ff5a00 vermillion accent
- Geist Sans/Mono typography
- 0px corners, hard offset shadows
- 26 CSS component blocks
- Premium token card and code window components
- Full 9-section DESIGN.md specification

Source: https://github.com/Tom-Opencart/tom-modern-html-style-rule

* fix: address review feedback for tom-modern design system

- Remove unsupported 'swatches' key from manifest.json
- Add license and attribution to manifest source
- Add all shared schema tokens (A1, A2, B-slot) to tokens.css
- Add '--tm-' prefix to BRAND_EXTENSION_PREFIXES
- Inline canonical tokens in components.html fixture
- Replace broken Geist CDN links with working @font-face declarations
- Use shared token names throughout components.css

* fix: remove invalid source keys from manifest.json

license and attribution are not part of the v1 design-system project
schema for bundled sources. Upstream repo (Tom-Opencart/tom-modern-html-style-rule)
currently has no license file.

* fix: use shared token names in DESIGN.md, scope --tm- to tom-modern brand

- Replace --tm-accent/--tm-bg/--tm-surface/--tm-text etc. with shared
  schema names (--accent, --bg, --surface, --fg, --border, etc.)
- Move --tm- from global BRAND_EXTENSION_PREFIXES to
  BRAND_EXTENSIONS['tom-modern'] with explicit per-brand list

* docs: add Tom Modern attribution note

* docs: add Geist loading guidance for generated artifacts

* docs: clarify Tom Modern authorship and permission

* Add palette and type-scale fixture section for Tom Modern preview

* Add Tom Modern palette preview screenshot

* Add Tom Modern component preview screenshot

---------

Co-authored-by: Tom OC <141485734+TrueTaragane@users.noreply.github.com>
2026-07-08 07:19:27 +00:00
Alex LuceroandGitHub fe5624a0d9 Define shared native session-map contract for resumable agents (#5268)
* Define agent session map contract

* Allow unknown session matrix rows
2026-07-08 06:59:49 +00:00
4f5bdeed1d feat(web): new-user personalized onboarding — Home recommendation + Studio first-gen hint (#5111)
* feat(web): personalized first-run recommendation on Home + Studio handoff

After a new user finishes the first-run questionnaire, Home shows one
compact, personalized starting point (role/use-case -> product path via a
static mapping) instead of the generic empty entry. Clicking it opens
Studio with the recommended first request pre-filled in the composer
(not auto-sent); the generic quick-start cards are suppressed while the
composer carries content. Session-only, no persistence: the recommendation
is memory-scoped and gone on refresh or for returning users.

Adds the first-generation funnel events (onboarding_first_prompt_sent,
onboarding_first_generation_completed) plus the recommendation
surface_view / ui_click, all via the v2 analytics convention. i18n keys
across all 19 locales.

Spec: OD new-user personalized onboarding SS6-7, S11.

* feat(web): one-time first-generation hint in Studio

When a new user's first previewable artifact appears in Studio, show a
single lightweight, dismissible note pointing them at view / edit /
export. Shown at most once ever (localStorage), non-modal, in the preview
corner so it never stacks with the post-turn next-step card (spec S8.5,
one guide at a time). Fires onboarding_first_artifact_hint surface_view /
ui_click via the v2 analytics convention.

Spec: OD new-user personalized onboarding S8.3.

* style(web): use real tokens in onboarding CSS modules

--accent-contrast and --shadow don't exist in the token set; the fallbacks
were silently in effect. Use the repo's plain-white-on-accent convention and
the real --shadow-md elevation token (light/dark adaptive) instead.

* feat(web): onboarding_prompt_prefilled event + is_first_session super property

Completes the spec S11.1 funnel middle step: fire
onboarding_prompt_prefilled once when Studio mounts with the recommended
first request actually seeded in the composer (confirming the Home ->
Studio handoff), carrying role / use_cases / product_type /
recommendation_id through the session-only entry slot. Register
is_first_session as a PostHog super property, pinned to the install's
first-ever session id in localStorage.

* fix(web): recommendation strip hover states + give browse-all a destination

The global button:hover:not(:disabled) rule in primitives.css out-specifies
plain .class:hover module rules, repainting the primary CTA to --bg-subtle
(white label on near-white) and forcing a gray wash on the text actions.
Match its specificity per the existing onboarding-cloud pattern: primary
keeps its accent fill on hover; the text actions get a soft white wash
instead of gray, and the underline is dropped.

Browse-all previously just dismissed the strip with no feedback. It now
also opens the New-project template picker — the concrete 'all types'
catalogue the label promises (spec S7.4 通用选择).

* fix(web): unify hover treatment of the two text actions on the recommendation strip

换一个 hovered orange while 浏览全部类型 stayed neutral — as peers with the
same pill wash they now share one hover behavior (white wash + text
darkens); only their resting text color carries the hierarchy.

* fix(web): recommendation strip — card width, placed under the composer

Full-page width below the template section read as an auxiliary notice bar
at the bottom of Home — the one surface a brand-new user is meant to see
first was the hardest to find. HomeHero gains an optional recommendationSlot
rendered directly under the composer card (before the template section);
the strip is constrained to the composer column (720px, centered) so it
reads as a card in the same visual stack.

* feat(web): path-scoped starter templates in Studio for recommendation projects

The empty conversation above a pre-filled composer hid the quick-start
cards entirely, leaving dead space; and the generic three cards are
unrelated to what the user just told us they want. A recommendation-started
project now shows its own product path's starters (e.g. prototype /
component / low-fi for product_ui) as one-click composer replacements —
clicking overwrites the draft. Generic projects and the general fallback
keep the existing set. Supersedes the hide-when-prefilled gate from spec
S8.5: with replace-on-click semantics the cards complement the pre-filled
request instead of competing with it, so onDraftEmptyChange plumbing and
its regression test are removed. Starter copy moves to a shared module
(onboarding/starter-copy.ts) reused by the Home strip.

* fix(web): icon-only cycle button on the recommendation strip

The text 换一个试试 button crowded the 720px card and pushed 浏览全部类型
onto its own wrapped line. It is now an icon-only refresh button with the
label in title/aria-label (zh copy shortened to 换一换); all three actions
fit one row.

* fix(web): tighten recommendation strip copy across locales

Primary CTA drops the Studio jargon (在 Studio 中开始 -> 开始创作 / Start
creating) and the fallback shortens to 全部类型 / All types; both updated
across all 19 locales.

* fix(web): primary CTA sits rightmost on the recommendation strip

Action order is now cycle icon, 全部类型, then 开始创作 — the standard
primary-on-the-right reading order.

* feat(web): attention entrance for the recommendation card

The old 200ms fade rode in with the page load and read as furniture. The
card now enters 350ms after mount — motion-after-stillness, the only thing
moving once Home settles — then a one-time accent glint sweeps it twice,
mirroring HomeHero's attention-sheen invitation language. Both animations
respect prefers-reduced-motion.

* fix(web): recommendation card motion + hierarchy — push-down reveal, CTA glint, tinted primary

Entrance is now the repo's canonical accordion reveal: the slot stays
collapsed for 600ms, then expands (grid-rows 0fr -> 1fr) and pushes the
template section down — displacement as the attention cue, replacing the
card-wide sheen that read oddly on an optional surface. A one-time tinted
glint then sweeps the CTA (the same click-me-next language home-hero puts
on the send button).

开始创作 demotes from a solid accent fill to the tinted accent-secondary
(NextStepActions Share vocabulary): the composer's 发送 stays the screen's
one filled primary; first-time attention is carried by motion, not fill
weight. The shown->clicked funnel can re-promote it later if data says so.

* fix(web): drop the hover state on starter-card return glyph

The accent-filled circle on hover reads as 'this will send' — the cards
only fill the composer. Keep the glyph static; the card hover itself is
the affordance.

* fix(web): spend the first-artifact hint budget on dismiss, not on show

Marking seen at mount meant any parent-gate flicker (files refresh /
streaming blip unmounting and remounting the component) burned the
once-ever hint before anyone could read it — reproduced live in the
desktop app. The budget is now spent by the user's own close action;
remounts before dismissal simply show the hint again, matching the spec's
'visible until closed or used'.

* fix(web): anchor the first-artifact hint under the preview toolbar

Bottom-right corner had zero directionality (and sat on the pet widget) —
the hint talks about view/edit/export but pointed at nothing. It now sits
directly below the artifact toolbar with an upward caret aimed at the
share/download cluster, and slides down from it on entry. Offsets track
the stable chrome heights, not preview internals.

* feat(web): settle-then-bob entrance for the first-artifact hint

600ms delay lets the freshly-opened preview settle; the slide-down then
ends in two decaying bobs (4px, 2px) — a nudge that says 'I appeared'
without a glint, sized for a persistent hint rather than a toast.

* feat(web): motion-driven drop-and-knock entrance for the first-artifact hint

Fully opaque throughout — the 600ms settle window is a delayed mount, not
an opacity fade (a see-through frame made the copy unreadable). The card
then drops 28px from the toolbar and sets down (easeOut), holds a beat,
and gives two equal 4px knocks with a short pause between them (easeInOut
segments). Driven by motion/react (already a dependency; ProjectView uses
AnimatePresence) with useReducedMotion opting the whole sequence out.

* test(web): export useReducedMotion from the motion/react mock

FirstArtifactHint is the first component to call it; without the export
every jsdom test whose project fixtures include an .html file crashed the
whole ProjectView render.

* fix(web): first-artifact hint must never occlude or intercept the viewer toolbar

The hardcoded top offset landed the card ON the preview's action toolbar in
layouts with taller tab chrome, blocking comment/zoom clicks (caught by the
Playwright critical suite). Two independent fixes: the card now anchors
below the lowest visible .viewer-toolbar, measured imperatively via ref
(state-driven repositioning re-rendered mid-flight and froze the motion
keyframes) with re-measures while the project chrome settles; and the card
is pointer-events: none (dismiss button excepted), so even a transient
overlap can never swallow toolbar clicks.

* fix(web): first-artifact hint copy leads with editing

At the first-artifact moment the loop's next step is modify, not deliver
(spec S8.3 orders 查看 -> 修改 -> 导出; export already has the next-step
card's Share and the export-ready nudge). Body copy now leads with edit /
annotate and closes with export-when-happy, matching the caret that points
at the view/edit toolbar. All 19 locales.

* fix(web): address onboarding review feedback (PR #5111)

- contracts: re-add `amr_auth_result` / `file_version_restore_result` to the
  `AnalyticsEventName` union (they were present in `AnalyticsEventPayload` but
  missing from the name union) and add a compile-time drift guard so the two
  can never diverge again.
- studio: gate the one-time first-generation hint on the consumed
  `onboardingEntryRef` so it only shows for projects started from the Home
  recommendation, not for returning users opening an existing HTML project.
- analytics: make `isFirstSession()` a pure read and defer pinning the
  first-session marker to the consented analytics-init path
  (`pinFirstSessionForCapture`), so an install that boots with analytics off
  and opts in later still records its real first analytics session as first.
- onboarding: stash the Home→Studio handoff entry only for a successful
  create/navigation; clear it when the create fails or is aborted so a later
  unrelated project mount can't consume the stale slot and mis-attribute
  itself as recommendation-started.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): address onboarding review follow-ups (PR #5111)

- analytics: centralize the first-analytics-session pin in `getAnalyticsClient`
  (the single consent gate every caller funnels through), so the opt-in-later
  path — booting with metrics off then enabling them mid-session via
  `setConsent(true)` — also records the real first analytics session as first.
  Adds `analytics-first-session-consent` regression coverage.
- onboarding: canonicalize `useCases` against questionnaire display order in
  `resolveProductType` / `buildRecommendation`, so a set of answers resolves to
  the same product bucket (and echoes the same telemetry) regardless of the
  chip click order. Adds selection-order-independence tests.
- onboarding: create the project FIRST in `handleRecommendationStart` and only
  dismiss the Home recommendation once it actually opened; a failed/aborted
  create now keeps the recommendation mounted so the user can retry instead of
  dropping to a dead-end generic Home. Adds `RecommendedStartRegion.handoff`
  reject-path coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): report has_prefilled_prompt from real send state (PR #5111)

The onboarding_first_prompt_sent funnel event hard-coded has_prefilled_prompt
to true for every recommendation-started project, but the user can edit, clear,
replace, or starter-swap the prefilled composer before sending. Snapshot the
seed prompt at mount and compare it against the actually-sent prompt via a new
`sentPrefilledPrompt` invariant helper, so the send-through split reflects
whether the suggestion was really used. Adds unit coverage for the
unmodified / edited / cleared / no-seed cases.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): pin first analytics session only after client init succeeds

pinFirstSessionForCapture() ran right after the config check but before
import('posthog-js') / posthog.init(), so a boot where init threw returned null
from the catch while the marker was already burned — a later real session would
then report is_first_session=false despite never capturing. Move the pin to the
point where init is known to have succeeded (after client = posthog, before
return), and add a "config enabled but client init fails" regression case.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): key onboarding handoff by project id; gate first-gen on artifact

Address two review findings on PR #5111:

- The Home→Studio onboarding entry was stashed in a single global
  sessionStorage slot before create resolved, racing any concurrent
  project navigation: an unrelated project mounting mid-create could
  consume the slot and steal the personalized starter/hint/funnel
  context. Key the handoff by the created project id and move the stash
  into the create success path (App.handleCreateProject), where the id is
  finally known. A failed/aborted create now leaves nothing behind, and a
  concurrent mount reads its own (absent) key.

- onboarding_first_generation_completed fired on any succeeded run, even
  one producing only text or a clarifying question, overstating the funnel
  and diverging from the first-artifact hint. Gate it on the same
  artifact-producing condition the hint uses (a produced previewable
  .html), via a new producedPreviewableArtifact invariant.

Tests: keyed per-project isolation (race regression), handoff no longer
touches sessionStorage, and producedPreviewableArtifact incl. a succeeded
run with no artifact.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): StrictMode-safe onboarding handoff, visible start failure, gate hint impression

Address three review findings on PR #5111:

- consumeOnboardingEntryForProject is a destructive read called from
  ProjectView render; under reactStrictMode the throwaway first mount
  consumed the sessionStorage slot and the committed remount saw null,
  dropping every recommendation-only behavior in dev. Cache the first
  parsed entry per project id in the helper so both StrictMode passes (and
  later remounts) get the value while the slot is still cleared on first
  read. Adds a double-consume regression test.

- EntryShell.handleRecommendationStart swallowed create failures in a
  catch and RecommendedStartRegion discarded the promise, so a failed
  "Start creating" was a silent no-op. Let the failure propagate; HomeView
  now routes it into the same error channel other entry actions use
  (setError + new home.recommendation.startFailed string in all locales),
  and RecommendedStartRegion awaits with a pending state that re-enables
  for retry on failure.

- FirstArtifactHint fired its surface_view impression before `settled`,
  while the card still rendered null, counting mounts removed during the
  600ms settle window as impressions the user never saw. Gate the effect
  on settled. Adds an impression-timing regression test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): make onboarding funnel guards project-scoped and gate first-prompt after pre-run gates

Address two review findings on PR #5111:

- The once-per-project funnel guards (first_prompt_sent /
  first_generation_completed) were mount-local refs. Now that the
  onboarding entry survives ProjectView remounts via the project-id cache,
  reopening a project reset the refs while the entry kept returning, so
  both events could re-fire on a later conversation/run. Move the guards
  into project-keyed module flags beside the entry cache so they hold
  across remounts (within the session, which is the entry's lifetime).

- onboarding_first_prompt_sent was emitted before the queue-only / busy /
  AMR balance pre-run gates, which can abort the send without creating a
  run. A hard AMR gate then showed the sign-in/recharge dialog while the
  event had already fired and the once-only guard suppressed the real
  retry. Move the tracking below those gates, to the point the send is
  committed to creating a run.

Adds regression coverage for the project-scoped guards (survive remount,
per-id isolation).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): cache onboarding seed prompt with the entry so it survives reopen-before-send

Address review on PR #5111: onboardingSeedPromptRef was rebuilt only from
project.pendingPrompt, which onClearPendingPrompt wipes on the first mount.
Since the onboarding entry now survives ProjectView remounts via the
project-id cache, a user who accepts a recommendation, opens Studio, leaves
before sending, then reopens the same project in-session kept the entry but
lost the seed — so onboarding_first_prompt_sent reported
has_prefilled_prompt=false even when they sent the original recommended draft,
undercounting the accepted-recommendation path.

Cache the seed prompt WITH the entry (new OnboardingEntry.seedPrompt, stashed
at create time in App.handleCreateProject). ProjectView prefers the cached
seed and falls back to pendingPrompt. Adds a reopen-before-send regression.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 06:09:26 +00:00
open-design-release-bot[bot]GitHubopen-design-release-bot[bot] <open-design-release-bot[bot]@users.noreply.github.com>
38c20ef6fa chore(release): bump main to 0.14.1 ahead of stable 0.14.0 (#5287)
Co-authored-by: open-design-release-bot[bot] <open-design-release-bot[bot]@users.noreply.github.com>
2026-07-08 06:06:47 +00:00
9b554ccbc1 feat(analytics): instrument plan mode & Excalidraw sketch flows (#4862) (#5243)
* feat(analytics): instrument plan mode and Excalidraw sketch flows

Adds PostHog tracking for the two features shipped in #4862.

Plan mode: wire the (previously defined-but-never-called)
`trackComposerSessionModeClick` at the composer/home session-mode toggle
so switching into `plan` (and finally ask/design too) emits
`session_mode_toggle` with mode_before/mode_after. Run-level plan usage is
already covered via `session_mode='plan'` on run_created/run_finished.

Sketch: add `sketch_save_result` (explicit Save button, not autosave) and
`sketch_export_result` (PNG export written into the project — the sketch's
real output) to complete the flow that starts at `new_sketch`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(analytics): track open_sketch re-engagement entry

Per acceptance-doc review: add open_sketch (opening an existing sketch
from the Design Files list) alongside new_sketch. Keeps sketch_save_result
and the sketch_editor area per the same review; run-source attribution on
sketch export is deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 04:37:07 +00:00
9a7ff9bf8f refactor(sidecar): split index.ts into cohesive sibling modules (part of #5165) (#5168)
Co-authored-by: LA <la@LAs-MacBook-Pro.lan>
2026-07-08 03:28:05 +00:00
c74320a13f refactor(download): split index.ts into cohesive sibling modules (part of #5165) (#5167)
Co-authored-by: LA <la@LAs-MacBook-Pro.lan>
2026-07-08 03:11:29 +00:00
8ca68bbfe6 feat(web): pre-run Open Design Cloud balance gate with subscription dialog (#5103)
* feat(web): Open Design Cloud pre-run balance gate — two-tier dialogs with auto-resume

Gate every AMR chat send before any run spawns. HARD tier (definitive
empty wallet <= $0, or signed out) blocks with a subscription-invitation
dialog: distinct copy and CTA per cause (console wallet page vs in-app
sign-in), benefits card, and auto-resume — a completed sign-in or a
recharge observed by a bounded wallet watch resumes the parked task on
its own. SOFT tier (<= $4) shows a dismissible reminder with top-up /
start-anyway / persisted don't-ask-again (never silences the hard tier).

Blocked project sends park in the conversation queue with their full
payload and pause auto-drain until a send passes the gate; home submits
hold the create open (continuation promise) so drafts and staged context
are never lost, and the home decision carries into the project's first
auto-send to avoid double-prompting. Fail-open contract: unknown or
unparseable wallet answers never block; hard blocks are confirmed with
refresh=1 before firing. New amr_entry sources cover both surfaces and
tiers; nine chat.amrBalanceGate.* / chat.amrLowBalance.* i18n key groups
ship in all 19 locales.

* fix(web): a stale wallet-refresh echo must not confirm a hard block

A failed refresh returns the previous cached snapshot with stale=true and
an error; treating it as a fresh definitive $0 (or signed-out) answer
violated the gate's fail-open contract and could strand a user who just
topped up while the wallet endpoint hiccuped. (Review feedback.)

* chore: retrigger ci

* test(e2e): align AMR UI checks with balance gate

* fix(web): preserve AMR gated sends across conversation switches

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: Amy <1184569493@qq.com>
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
2026-07-07 09:20:54 +00:00
Marc ChanandGitHub 24206f7c11 refactor(daemon): split server helper modules (#4659)
* Split daemon run helper modules

* refactor(daemon): split remaining server helpers

* fix(daemon): restore routine run context import

Generated-By: looper 0.9.11 (runner=fixer, agent=codex)

* fix(daemon): preserve null analytics project kind

Generated-By: looper 0.9.11 (runner=fixer, agent=codex)

* fix(daemon): move run analytics helper under runtimes

Generated-By: looper 0.9.11 (runner=fixer, agent=codex)

* fix(web): handle diagnostic persisted events

* test(daemon): gate media policy capture on run env

Generated-By: looper 0.0.0-dev (runner=fixer, agent=codex)
2026-07-07 08:17:47 +00:00
df3c2e76d1 feat: label cloud model cost tiers (#5216)
* feat: label cloud model cost tiers

* fix: order cloud models predictably

* fix: expose model badges as real text

* fix: keep model option names stable

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
2026-07-07 07:40:58 +00:00
0ac3f04851 feat(analytics): add per-project project_turn_index run counter (#5215)
* feat(analytics): add per-project project_turn_index run counter

turn_index counts runs session-wide across all projects (sessionStorage,
resets each browser session). Add a project-scoped sibling
project_turn_index (localStorage keyed by project id, project-lifetime per
device) so we can analyse "within THIS project, which prompt / follow-up is
this run?" without the session-global counter conflating separate projects.

- identity.ts: claimProjectTurnIndex(projectId)
- contracts: projectTurnIndex hint + project_turn_index run prop
- ProjectView: claim alongside the session turn at the create-run dispatch
- daemon: stamp project_turn_index (mirrors the turn_index passthrough)

Existing turn_index is unchanged. New unit test pins per-project isolation,
cross-session persistence, and independence from the session counter.

* feat(analytics): stamp session/project turn index on the BYOK-OpenCode run path

Per review of the tracking-acceptance doc: the BYOK-OpenCode send path
previously carried neither turn_index nor project_turn_index. Claim both
(plus is_first_run / has_existing_artifact) there too, so run_created /
run_finished report the session-global and project-scoped run sequence
uniformly across every runtime (local cli / amr / byok).

* chore: retrigger ci

---------

Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
Co-authored-by: Amy <1184569493@qq.com>
2026-07-07 07:17:50 +00:00
lefarcenandGitHub ba6435f6a4 feat(packaged): attach crash-scene evidence to packaged_runtime_failed (#5224)
* feat(packaged): attach crash-scene evidence to packaged_runtime_failed

The pre-daemon startup crash class (`packaged_runtime_failed`, #4696) reports
only structured buckets today: failure_kind, exit_code, error_name, and a
log-tail-parsed error_code/missing_module. That is enough to see THAT startup
failed, but not WHY on a given machine:

- The mac `daemon-start` failures are better-sqlite3 ERR_MODULE_NOT_FOUND, yet
  the shipped 0.13.0 DMG's `better_sqlite3.node` is verified present, arm64,
  signed, notarized and resolvable — so this is a machine-side subset, not a
  build defect, and we have no per-machine signal to explain the subset.
- The Windows `unknown` bucket (the single largest slice) carries no exit code
  and no daemon log to parse, so today it is a dead end.

Enrich the event with on-machine crash evidence: the scrubbed + truncated
top-level error message/stack (the only signal the `unknown` bucket has), and a
best-effort probe of the daemon's better-sqlite3 binding on THIS machine
(present + size), which separates "file missing" from "file present but
unloadable" (arch mismatch / quarantine / AV). All paths and free-form text are
run through the existing `scrubUserPaths` and length-capped before send.

- startup-telemetry.ts: add `error_message`, `error_stack`,
  `native_module_present/size/path`; new `nativeModulePath` arg + injectable
  `statNativeModule` dep.
- index.ts: pass the packaged binding path
  (`getAppPath()/node_modules/better-sqlite3/build/Release/better_sqlite3.node`,
  layout verified against the shipped DMG) through the fatal-exit report.
- contracts: extend `PackagedRuntimeFailedProps` with the optional fields.
- tests: red-spec first — assert scrubbed message/stack and the native probe
  (present/missing) via the injected `statNativeModule`.

* fix(packaged): scrub full Windows profile segment even with spaces

Review (PerishCode/Looper): the new free-form error_message/error_stack fields
run through scrubUserPaths, but its Windows branch matched `[^\\\s]+` — it
stopped at the first whitespace. A Windows profile dir can contain a space
("C:\Users\John Doe\..."), so it produced "C:\Users\<redacted> Doe\..." and
leaked the rest of the segment, breaking this PR's stated privacy bound.

Consume the whole segment up to the next backslash (`[^\\]+`). POSIX home
segments cannot contain spaces and file:// URLs percent-encode them, so only
this backslash form needs the whitespace-tolerant boundary. Add fixture
coverage for the spaced profile — both standalone and embedded in a crash
message — asserting the surname never survives.

* fix(packaged): make Windows-home scrub separator-agnostic

Review round 2 (PerishCode/Looper): the prior fix covered the backslash form,
but slash-normalized Windows paths ("C:/Users/John Doe/...", which JS/Electron/
Node diagnostics commonly emit) still leaked the surname — the POSIX "/Users/"
rule matched them and stopped at the first space, producing
"C:/Users/<redacted> Doe/...".

Replace the backslash-only Windows rule with a separator-agnostic one that
matches `<drive>:[\\/]Users[\\/]` and consumes the whole segment up to the next
slash OR backslash (spaces allowed), running before the POSIX rule. The `\r\n`
in the class keeps it from running across lines in a multi-line stack. POSIX
segments can't contain spaces, so that rule is left with its whitespace
boundary. Fixtures added for the slash form (standalone + embedded) and a
multi-line no-over-redaction case.
2026-07-06 16:45:21 +00:00
dde6228121 feat(daemon): add OD_ALLOWED_INTERNAL_HOSTS allowlist for internal provider endpoints (#4250)
* feat(daemon): add OD_ALLOWED_INTERNAL_HOSTS allowlist for internal provider endpoints

Fixes #3225. The 0.8.0 SSRF hardening blocks provider base URLs that
resolve to private/internal ranges, with no way to opt a trusted host
out -- breaking internally-hosted LiteLLM/Ollama gateways on VPN-only
RFC1918 addresses.

Add an operator opt-in: OD_ALLOWED_INTERNAL_HOSTS exempts listed hosts
from the internal-IP guard, threaded only into user-configured provider
endpoints (connection test, model discovery, BYOK chat). The
attacker-controllable asset-download guard stays strict. Malformed and
CIDR entries are dropped with a warning rather than silently trusted.
The Settings base-URL field no longer rejects internal-IP URLs
client-side, deferring the authoritative decision to the daemon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(e2e): update BYOK base-URL P0 for relaxed internal-IP validation (#3225)

The P0 spec used an internal-IP URL as its 'invalid' example to assert the
client-side error; that URL is now accepted (the daemon owns the allowlist
decision). Switch to a non-http scheme, which is still rejected client-side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(web): defer internal-IP base URL to the daemon in BYOK draft validation (#3225)

isValidApiBaseUrl was relaxed, but the Test and model-fetch actions gate
on validateByokDraft -> blockingByokDraftIssues, which still treated any
validateBaseUrl error (including a forbidden internal IP) as a blocking
base_url_invalid. So the Test button appeared but the action stayed
blocked client-side and never reached the daemon allowlist. Treat a
syntactically-valid forbidden URL as non-blocking here too; genuinely
malformed / non-http URLs still block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 14:35:28 +00:00