Commit Graph
2084 Commits
Author SHA1 Message Date
lefarcenandGitHub cb5fd3baab fix(web): auto-fit wide html previews (#5751)
* fix(web): auto-fit wide html previews

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

* test(e2e): allow auto-fit preview zoom

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

* fix(web): gate preview auto-fit on content width

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

* fix: measure preview width through iframe bridge

* fix: bridge powered preview sizing
2026-07-17 06:16:04 +00:00
1b2eace97f fix(updater): harden update handoff flow (#5766)
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
2026-07-17 03:04:19 +00:00
PerishFireandGitHub 5766fe2cc1 fix(updater): defer locked launcher cleanup (#5780) 2026-07-17 02:38:55 +00:00
3a6221a54b fix(landing-page): stop download function 404-shadowing localized catalog pages (#5770)
* fix(landing-page): emit localized plugins/templates/[kind] pages in production

The `[locale]/plugins/templates/[kind]` route imported `getStaticPaths`
from the sibling English `[kind]` page and re-invoked it. That cross-page
`getStaticPaths` re-export resolved to zero paths in the CI (Linux)
production build, so every `/<locale>/plugins/templates/<kind>/` URL
(deck, prototype, image, video, audio, live-artifact, hyperframes) 404'd
in production for all non-English locales — while the English routes and
the localized category index built fine. The language switcher on the
English category pages links straight into these 404s.

Derive the locale x category paths directly from PLUGIN_CATEGORIES (the
same source the English page's own getStaticPaths uses), mirroring the
self-contained `[locale]/plugins/[slug]` wrapper, so the route no longer
depends on the fragile cross-page getStaticPaths import.

* fix(landing-page): stop download function shadowing localized catalog pages

The attributed-download function `functions/[os]/[arch]/[token]/[asset].ts`
has a file-path route (`/[os]/[arch]/[token]/[asset]`) that structurally
matches ANY four-segment path. Pages Functions run before static-asset
serving, so it was intercepting the localized plugin catalog pages — e.g.
`/zh/plugins/templates/deck/` bound os=zh, arch=plugins, token=templates,
asset=deck — and returning `{"error":"download_not_found"}` (404) instead
of the real page. Every `/<locale>/plugins/templates/<kind>/` URL (the only
four-segment paths on the site) 404'd for all non-English locales, and the
language switcher on the English category pages links straight into them.

Real download URLs are minted only as `/{windows|macos|linux}/{arch}/{token}
/{asset}` (see `functions/api/attribution/mint.ts`), so gate the function on
the platform segment: anything whose first segment isn't a download platform
falls through to the static asset via `context.next()`. Download requests
are unaffected. Adds `next` to the local `PagesFunctionContext` type.

Paired with the localized `templates/[kind]` getStaticPaths fix in the same
branch, which makes those static pages exist in the first place.

---------

Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
2026-07-16 15:28:09 +00:00
3117a902ec careers: show apply email as plain text, drop mailto jumps (#5758)
The /careers/ page turned the apply address and every CTA into mailto:
links (hero button, the email itself, each role card, the closing button).
Clicking them yanked the visitor into their mail client, which felt abrupt.

Make the apply email a display-only element instead:
- hero + closing: render join@open-design.ai as a plain <span> (user-select:
  all so one click selects the whole address to copy), no mailto
- role cards: <a mailto> → non-clickable <div>; drop the "Apply →" affordance
  and the interactive hover lift
- enlarge the email (clamp(20px,2.6vw,26px), 700) so it reads as the CTA
- remove the now-dead copy fields (applyCta / applySubjectPrefix / roleApply)
  from the interface and all 12 locales

astro check passes (0 errors/warnings); /careers/, /zh/careers/, /ja/careers/
render with zero mailto links.

Co-authored-by: joeylee12629-star <joeylee12629@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:20:50 +00:00
264e0f25fa Fix AMR opencode resume EOF recovery (#5744)
Generated-By: looper 0.10.1 (runner=fixer, agent=codex)

Co-authored-by: Amy <1184569493@qq.com>
2026-07-16 09:39:19 +00:00
235c0f8d23 fix(landing): show the 0.14.0/0.15.0 release covers on the blog index (#5742)
The blog index resolves card art from the `postImages` map, not from the
`/blog/<id>-cover.webp` convention the detail page uses. The two most
recent release posts had cover images but no map entry, so their cards
fell back to the generated line-art placeholder instead of their warm
editorial covers. Register both so the index shows the real artwork.

Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
2026-07-16 09:06:27 +00:00
12816e5d52 feat(landing): guide signed-out visitors to desktop download (#5737)
* feat(landing): guide guests to desktop download

* fix(landing): handle tablet download guidance

* test(landing): cover tablet download guidance

* chore(ci): retrigger PR checks

---------

Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
Co-authored-by: Amy <1184569493@qq.com>
2026-07-16 08:59:39 +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
9d6a340d02 fix: defuse script tags in stored prefetch captures (#5503)
prefetch/page.html was saved verbatim, so previewing a capture executed
the source site's live JavaScript from the local origin. Sites that
ship Google One Tap (Behance, Medium, LinkedIn, ...) made Chrome pop a
"127.0.0.1 — Sign in with Google" prompt whenever the capture or any
file built from it rendered in the studio; analytics beacons and
consent SDKs replayed the same way.

previewablePrefetchHtml now leads every script tag with a non-JS type
attribute (on duplicate attributes the HTML parser keeps the first, so
an original type="module" further right is inert). The markup stays
readable as extraction evidence; only execution is disabled.

Co-authored-by: wiggdevin <wiggdevin@gmail.com>
2026-07-16 07:19:22 +00:00
Nicholas-XiongandGitHub ba92a075b9 fix(web): portal deploy success toast above modal backdrop (#5326) (#5455)
The deploySavedToast was rendered inline inside the component tree, so it
ended up underneath the deploy modal backdrop (z-index 1700). Portal it to
document.body like the adjacent deployActionToast, and raise the placement-top
toast z-index from 1200 to 1800 so toasts always render above active modals.
2026-07-16 07:15:02 +00:00
mehmet turacandGitHub 8a1eb3deb8 Fix large HTML preview fallback (#5710)
* Fix large HTML preview fallback

* fix: refresh promoted large HTML previews
2026-07-16 07:14:33 +00:00
Ray XiandGitHub c83f60a892 fix daemon BYOK OpenCode Not Found classification (#5726)
* fix daemon BYOK OpenCode Not Found classification

* fix daemon upstream client error resumability

Exclude non-transient upstream client errors from session resume after committed work, while preserving transient upstream resume behavior.

Generated-By: looper 0.10.4 (runner=fixer, agent=codex)
2026-07-16 06:58:28 +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
e70f0eeefc fix(web): contain absolute tool paths to the project root when resolving touched files (#5352) (#5577)
attribution, restricted to agent-touched files resolved through
findTouchedProjectFile. Two residual gaps in that matching:

- A ROOT-level project file (candidate contains no '/') never
  suffix-matches an absolute event path like /workspace/index.html, so a
  mixed turn that rewrites index.html plus any resolvable file drops the
  rewritten HTML from the touched set and nothing auto-opens.
- An out-of-root absolute path sharing a tail with a project file
  (/tmp/site/index.html, or a '..' escape resolving outside the root)
  CAN suffix-match and mis-attribute an external write.

findTouchedProjectFile now takes the project's resolvedDir: absolute
paths are lexically normalized ('.'/'..') and must sit under the root,
in which case they match by their project-relative form; out-of-root
paths (including '..' escapes) are rejected before any suffix matching.
Paths whose '..' climbs above their own anchor are always rejected.
Callers without a usable root keep the legacy behavior.

Red on main / green here:
apps/web/tests/components/ProjectView.touched-path-containment.test.ts
(6 of 10 cases fail on main: both root-file misses and out-of-root
false positives).


Claude-Session: https://claude.ai/code/session_017peXvs7Q3sA5dhZ6vsYYwN

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 06:47:50 +00:00
lefarcenandGitHub c8bad6bcb4 fix(web): keep report-only Design runs off the ARTIFACT_NOT_FOUND path (#5724)
A successful Design turn with zero produced files is only a missing
deliverable when the run actually attempted to mutate project files.
Image-analysis and report-only audit turns answer in prose without ever
calling a write tool; classify them as report_only and let them finish
as normal text results instead of appending ARTIFACT_NOT_FOUND.
2026-07-16 06:40:05 +00:00
mehmet turacandGitHub b6fbde0ba2 Surface provider errors before connection timeout (#5713) 2026-07-16 03:47:25 +00:00
6e38058dcf feat(web): add agent-assisted home headline (#5699)
* feat(web): add time-aware home greeting

* feat(web): add contextual greeting emoji

* feat(web): use Agent-focused home headline

* fix(web): refine agent-focused home copy

* fix(web): keep German home copy informal

---------

Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
2026-07-16 03:40:39 +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
04236af504 fix(daemon): scan user-authored text only and latch intent signals per conversation (#5709)
* fix(daemon): scan user-authored text only and latch intent signals per conversation

The slim charter gates three stable-region prompt blocks on per-turn regex
signals scanned over (message, currentPrompt). For transcript-resending
clients `message` embeds assistant turns, so the turn-1 discovery form's own
option copy ("Slide deck / pitch", 幻灯/路演, iOS/Android/响应式) flipped
deck+platform at t2 on every form-driven conversation, and agent-switch
history trims flipped signals back OFF — each flip changes
stableInstructionFingerprint and re-sends the ~17K stable block
(stable-prompt-changed miss). Production: stablePromptCacheHit fell from
32.6% (0.14.1) to 20.4% (0.15.0).

- extractUserAuthoredSignalText: scan only `## user` section bodies of a
  packed transcript (assistant sections and the context-warning block are
  dropped); plain messages scan whole. `[form answers — *]` blocks narrow
  to the value part of each answer line — the real task-type form's own
  question label carries deck vocabulary.
- latchConversationIntentSignals + conversations.intent_signals_json
  (guarded migration): effective = stored OR fresh, monotonic ON for the
  life of the conversation.
- OD_INTENT_SIGNAL_MODE=legacy restores the pre-hotfix scan as an escape
  hatch.

A genuine mid-conversation activation now costs exactly one
stable-prompt-changed miss; form/assistant-copy false flips cost zero.

* fix(daemon): run the intent-signal latch under BEGIN IMMEDIATE

Review hardening (#5709): take the write lock before the read so the
read+merge+write cannot lose a previously latched bit to another writer.
Within one daemon process the sequence was already non-interleavable
(better-sqlite3 is synchronous, no await between read and write), so this
pins the monotonic guarantee against future refactors and hypothetical
multi-connection writers. Adds a db-level contract suite for the latch:
monotonic merge (never replace), cross-connection merge on the same
database file, unparsable-JSON fallback, and missing-row passthrough.

* fix(daemon): accept the full form-answer header grammar in signal narrowing

Review fix (#5709): the narrowing header matcher only recognized the
canonical em-dash form, but the repo's shared grammar
(packages/contracts/src/artifacts/od-card.ts parseFormAnswers, the server
turn-2 override, and the CLI docs) also accepts hyphen / colon separators
and the bare [form answers] header, case-insensitively. A CLI-driven
hyphen-form payload skipped narrowing, so echoed question labels could
still flip intent signals. The matcher now mirrors the shared grammar and
the answer-line pattern aligns with the shared parser; parity across all
variants is pinned red→green in intent-signal-user-text.test.ts.

* fix(daemon): tolerate CRLF and quoted role headings in the signal extractor

Review fix (#5709, third pass): extractUserAuthoredSignalText switched
into transcript mode off any standalone role heading and only recognized
exact-LF marker lines, so two valid inputs extracted an empty body and
silenced every intent signal: a plain prompt that merely quotes a
`## assistant` heading, and a CRLF-packed transcript (multiline `$`
matches before `\r`, but the line-equality marker check does not).
Transcript mode now requires a real `## user` marker line, and marker
comparison strips a trailing CR. Both shapes are pinned red→green in
intent-signal-user-text.test.ts.

* fix(daemon): require a packed-transcript shape before role-scoped scanning

Review fix (#5709, fourth pass): a plain prompt that quotes a `## user`
heading mid-text still entered transcript mode, dropping everything
before the quote as pre-marker text and silencing explicit deck/media/
platform requests. Transcript mode now additionally requires the first
content line to be a role marker or the context-warning header — the
shape buildDaemonTranscript always emits — while transcripts that begin
with a trimmed `## assistant` section keep parsing as transcripts. Both
pinned red→green in intent-signal-user-text.test.ts.

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
2026-07-16 02:53:04 +00:00
f9c191c2eb feat(landing): add 0.14.0 and 0.15.0 release blog posts (+ i18n audit fixes) (#5704)
* feat(landing): add 0.14.0 and 0.15.0 release blog posts

Cover the two most recent minor releases the blog was missing:
0.14.0 "Inspiration Time Machine" (plan mode, HTML version history,
design-system source context, model cost tiers) and 0.15.0 "Cost Less.
Ship Faster." (a leaner Design System Prompt — 49.5% faster to first
token, decks that drive like a workspace, website clone from a URL).

Each post follows the established release-note format: English body,
per-locale bodyHtml for all 10 shipped non-English locales, and cover
plus inline feature illustrations in the site's line-art house style.

* fix(landing): repair blog localization gaps found in an audit

A full-blog i18n audit surfaced pre-existing gaps in the localized
bodies (localizedBodyHtml fully replaces the Markdown body, so any gap
is invisible to English readers but real for everyone else):

- osaka-kyoto-meetup was English-only — add full 10-locale i18n.
- 0.9.0 translations dropped the inline figure the English body has —
  restore it (localized figcaption) in all 10 locales.
- what-is-vibe-design, vibe-design-tools/-vs-/-with-stitch, and
  open-source-alternative had their translated headings one level too
  deep (## rendered as h3/h4) — normalize to match the English levels.

* style(landing): restyle 0.14.0/0.15.0 release art to warm editorial covers

The two new release posts shipped with the near-white line-art cover
style, which clashed with the warm editorial illustration covers used by
the rest of the recent blog cluster on the index. Regenerate both
covers and their inline feature figures in the warm sage-green editorial
style so the blog index reads as one consistent set, and update the
figure alt text to match the new artwork.

---------

Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
2026-07-16 02:29:47 +00:00
0f76c2a086 feat(landing-page): fold Agent into a Product mega menu (#5705)
The top nav listed Product and Agent as sibling tabs. Product is really the
umbrella for what Open Design ships, so promote it to a multi-column mega
menu whose columns are top-level categories: a Products column (Open Design /
HTML Anything / HTML Video) and an Agent column (the coding agents, header
linking to the /agents/ hub). This declutters the bar and leaves room to add
more category columns (e.g. Feature) later without another top-level tab.

- Product trigger opens `.nav-dropdown-mega`: flex columns, each a category
  with a small-caps head + list; the long Agent list caps its own height and
  scrolls so the panel never runs off short viewports.
- Product now lights up for /agents/ pages too, since Agent lives inside it.
- Remove the standalone Agent top-level tab.
- Mobile drawer flattens the mega to stacked full-width categories and drops
  the Agent scroll cap so the list flows naturally.

No new i18n keys — reuses the existing `product` / `agent` labels, so all 11
locales are covered. `astro check` passes with 0 errors.

Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
2026-07-16 02:15:48 +00:00
Ray XiandGitHub 94a5bd2e08 fix BYOK OpenCode permission bypass (#5701) 2026-07-15 14:44:22 +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
cdffb1b635 fix(daemon): block SSRF in library ingest remote fetch (#5529)
* fix(daemon): block SSRF in library ingest remote fetch

POST /api/library/ingest fetches a client-supplied URL via
`fetchRemoteBytes`, which used a raw `fetch(url, { redirect: 'follow' })`
with no host validation. The sibling brand-asset path already routes
every external fetch through `assertPublicBrandUrl` (rejects
cloud-metadata 169.254.169.254, loopback, RFC1918/CGNAT, link-local, and
re-validates each redirect hop with redirect:'manual') — ingest just
missed it.

That let any local caller make the privileged daemon issue GET requests
to arbitrary internal services (127.0.0.1:*, 169.254.169.254, RFC1918)
and read the response bytes back out through the unauthenticated
`GET /api/library/assets/:id/raw` route — SSRF with full response
exfiltration (e.g. cloud-metadata IAM credentials). The ingest auth gate
is satisfied by a `chrome-extension://` Origin header, which a
non-browser local process (or a prompt-injected agent — this product
spawns external code-agent CLIs on untrusted content) sets trivially, so
no token is required. Same class as #5478.

Route `fetchRemoteBytes` through `fetchExternalBrandAsset` so the guard
and per-hop redirect re-validation apply. Drop-in; the size cap and mime
handling are unchanged.

Regression test drives a real daemon + production HTTP API against a
loopback canary standing in for an internal metadata service: pre-fix the
daemon fetched the canary and the secret was read back via /raw; post-fix
the internal URL is refused and never touched.

* fix(daemon): pin brand-asset SSRF guard to the connection (DNS rebinding)

Addresses the review blocker on the library-ingest SSRF fix:
`assertPublicBrandUrl` validated the host with its own DNS lookup, but
the subsequent `fetch` re-resolved the name independently. An
attacker-controlled hostname could answer a public address for the
validation and a private one for the actual connection (DNS rebinding /
TOCTOU), so the ingest route — which persists the response and exposes it
through /raw — stayed bypassable.

Pin the validation to the socket: `fetchExternalBrandAsset` now installs
a connection-time `lookup` (createValidatingLookup) on an undici Agent
dispatcher that refuses the connection when the resolved address is
non-public, using the same `isNonPublicHost` predicate as the pre-check
so the two block sets can't drift. The address we validate IS the address
the socket connects to, and the same dispatcher re-binds every redirect
hop. Same pattern the repo already uses in plugins/plugin-asset-cache.ts.
`assertPublicBrandUrl` stays as a cheap URL-level pre-check.

This hardens the shared brand-fetch guard, so both the brand-extraction
callers and the library-ingest path are covered. Adds a rebinding
regression test exercising the pinned connect-time lookup directly.

* fix(daemon): reuse a long-lived guarded dispatcher (do not close per-request)

Review follow-up: the per-request `Agent` was closed in a `finally` right
after `fetchExternalBrandAsset` returned the response. Closing an undici
Agent waits for its in-flight request, which only completes once the
caller consumes the body — so the successful path could not complete
(the caller reads `arrayBuffer()` after the function returns).

Use a single module-level `brandAssetDispatcher` reused across calls
(same lifetime pattern as plugins/plugin-asset-cache.ts) and never close
it per-request. The pinned validating lookup still refuses non-public
resolved addresses on every connection. Dropped the now-unused
per-call `lookupImpl` param; the rebinding test drives
`createValidatingLookup` directly.

---------

Co-authored-by: tomsen-ai <230283659+tomsen-ai@users.noreply.github.com>
2026-07-15 12:20:03 +00:00
Ethan GuoandGitHub 05cb03c8ab fix(web): sandbox the speaker-notes presenter deck iframes (#5571) 2026-07-15 12:19:46 +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
Chanhyo JungandGitHub de4ca325d3 fix(web): keep model lock affordances visible (#5533) 2026-07-15 12:08:39 +00:00
c92b4aea44 fix(daemon): accept --prompt-file for od media generate (#5534)
* fix(daemon): accept --prompt-file for od media generate

AGENTS.md's capability-exposure contract requires every od action that
takes a prompt to accept long-form prompts via --prompt-file <path|->
(so jobs piping through heredoc / jq / an external agent stay clean).
`od media generate` was the one prompt-taking action that missed it: it
read `flags.prompt` directly, while od run / od brand / od automation /
od files version-create all route through the shared readPromptFromFlags
(inline --prompt, else file, else stdin `-`). A long or multi-line media
prompt (detailed image/video descriptions, program-generated prompts)
therefore couldn't be fed from a file or a pipe, and could hit the argv
length limit.

Add 'prompt-file' to MEDIA_GENERATE_STRING_FLAGS and read the prompt via
readPromptFromFlags; --prompt behavior is unchanged. Restores the
consistency an agent driving `od` expects.

Regression test spawns the real CLI against a fake daemon and asserts a
long prompt reaches /api/projects/:id/media/generate from both
--prompt-file <file> and --prompt-file - (stdin). Red on main, green here.

* test(daemon): assert od media generate exits cleanly in the prompt-file spec

Review follow-up (non-blocking): the runCli helper resolved on any close
and discarded stderr, so the spec could pass even if the CLI exited
non-zero after the POST (during polling / result handling). Capture the
exit code + stderr and assert exit 0 (the fake daemon resolves both
/media/generate and the /media/tasks poll), so a post-POST failure fails
the spec instead of passing on the recorded request alone.

---------

Co-authored-by: tomsen-ai <230283659+tomsen-ai@users.noreply.github.com>
2026-07-15 11:55:22 +00:00
zhanghuiyingandGitHub a795322b63 feat: add SiliconFlow Global BYOK preset (#5108) 2026-07-15 11:15:06 +00:00
lefarcenandGitHub 24c7876b37 fix(web): preserve delivery for in-place HTML edits (#5689) 2026-07-15 10:48:26 +00:00
Chanhyo JungandGitHub 59fd5dd599 fix(daemon): ingest call-prefixed Codex images (#5582) 2026-07-15 10:09:20 +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
huqiandGitHub 03617d1296 feat(daemon): add AtomCode CLI as an agent runtime (#5654)
* feat(daemon): add AtomCode CLI as an agent runtime

Add a declarative RuntimeAgentDef for AtomCode CLI (open-source, Rust,
MIT, v4.26.0), making it invocable from both the web UI and the `od` CLI
alongside the existing 26 agent adapters.

AtomCode's headless mode (`atomcode -p "..."`) streams the assistant
reply on stdout — Claude Code `-p` style. The adapter reuses the same
`promptViaFile: true` + `streamFormat: 'plain'` infrastructure that
grok-build already exercises: the daemon stages the composed prompt in
a temp file and passes it via `--prompt-file`, staying clear of argv
length limits. `-y` auto-approves tool calls in the non-interactive
headless context. Authentication is CLI-owned (`atomcode login` /
`~/.atomcode/config.toml`), matching the grok-build / qoder pattern.

Closes nexu-io/open-design#5653

* fix(daemon): address AtomCode adapter review feedback
2026-07-15 09:41:40 +00:00
Chanhyo JungandGitHub 27f8421851 fix(web): reset entry scroll on route changes (#5607) 2026-07-15 09:32:05 +00:00
coyaSONGandGitHub 4c1344f8b8 fix(daemon): read Pi model list from stdout (#5637) 2026-07-15 09:14:07 +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
lefarcenandGitHub 358ac21a0f fix(packaged): force-stop a lingering skewed desktop on relaunch (#5677)
An updated daemon can connect to an old desktop main process that never
quit and still holds the fixed `desktop.sock`. The old desktop rejects
newer sidecar messages (e.g. `render-slides`, added in 0.13.0) as
"unknown sidecar message", which surfaces as the version-skew PPTX export
failure.

The launcher relaunch path only ever waited for the old process to leave
on its own: `waitForLauncherAfterQuit` warned and continued on timeout,
and `inspectExistingDesktopForLauncher` gave up (returned `exit`) when a
running-but-stale-sidecar desktop ignored SHUTDOWN — leaving the user
pinned to the skewed process. Meanwhile daemon/web children already get a
SIGTERM->SIGKILL escalation via `stopProcesses` in `closeManagedChild`.

Close that gap: escalate the same `stopProcesses` helper against the
lingering desktop in both launcher spots, then restart fresh so the
relaunched app binds a matching-version desktop on the socket.

Scope: the JS launcher paths only. The mac/win deferred-installer scripts
(`macDeferredInstallerScript` / `windowsDeferredInstallerScript`) still
give up on timeout and are left as a follow-up for desktop infra review.
2026-07-15 08:54:01 +00:00
76604a862b fix(daemon): dedup concurrent library ingests instead of 500-ing the hash-race loser (#5662)
* fix(daemon): dedup concurrent library ingests instead of 500-ing the race loser

registerLibraryAsset is documented as idempotent by content hash — the same
bytes seen twice collapse to one asset and it must never throw into a caller's
main flow. But for owned storage it does a bare findLibraryAssetByHash SELECT,
then `await mkdir` + `await writeFile`, then insertLibraryAsset against a
UNIQUE(content_hash) column with no transaction or ON CONFLICT. The awaits
yield the event loop between the check and the insert, so two concurrent
ingests of identical bytes both pass the not-found SELECT, both write the
content-addressed file, then both INSERT — the loser hits "UNIQUE constraint
failed: library_assets.content_hash", which the route surfaces as 500
INGEST_FAILED and, worse, skips that upload's addLibraryAssetSource/tag-merge
so the asset's back-link graph silently loses an edge.

Let the UNIQUE constraint be the arbiter instead of the pre-await SELECT: wrap
the INSERT in try/catch, and on a UNIQUE(content_hash) violation re-run
findLibraryAssetByHash and fold into the existing dedup branch (extracted as
dedupIntoExistingAsset) — appending the loser's source/tags and returning
deduped:true. The content-addressed file both requests wrote is byte-identical,
so the loser's write is a harmless idempotent overwrite. No lock, no schema
change.

Red spec (tests/library-ingest-concurrent-hash-race.test.ts): 12 concurrent
identical POST /api/library/ingest must all return 200 and dedup to one asset.
Red on main (4-5/12 return 500 UNIQUE constraint failed); green with the fix.

* test(daemon): assert the deduped asset preserves every concurrent ingest's tag

Review follow-up (nettee): the regression only exercised the 500-elimination
path — every request was identical and untagged, so a regression that returns
200 while dropping the loser's source/tag merge (the silent data-integrity half
of this bug) would still pass. Tighten it:

- each of the 12 concurrent ingests now carries a distinct tag; after they
  converge, fetch the asset via GET /api/library/assets/:id and assert every
  tag survived the dedup merge (none dropped).
- assert each 200 carries a non-empty asset.id and a boolean `deduped`, and
  that exactly one request is the fresh insert (deduped=false) while the rest
  fold in — so the test proves complete success, not just absence of a 500.

Still red on origin/main (4/12 return 500 UNIQUE constraint failed), green with
the fix (3x). Also transitively verifies the concurrent tag-merge itself
doesn't lose an update.

---------

Co-authored-by: tomsen-ai <230283659+tomsen-ai@users.noreply.github.com>
2026-07-15 08:53:26 +00:00
lefarcenandGitHub e38f5d1c24 fix(web): stamp chunk_id on exception frames so PostHog can symbolicate (#5679)
* fix(web): stamp chunk_id on exception frames so PostHog can symbolicate

Every packaged front-end exception ships with minified frames (`?:?`,
mangled function names) even though tools-pack faithfully injects chunk
ids and uploads the browser sourcemaps to PostHog on every release. In
production, 0 frames ever carried a chunk id and 5,140 frames in 7 days
failed with "had no source url or chunk id".

Root cause: exception capture is owned by our own error-tracking.ts (it
runs before consent + before posthog-js loads, via a direct ingest
fetch), and client.ts sets `capture_exceptions: false`. The chunk id
that @posthog/cli bakes into each chunk is stamped onto frames by
posthog-js's native capture — which we bypass. Packaged chunks load over
the `od://` scheme, so PostHog cannot fetch a `.map` by URL either; the
chunk id is the ONLY correlation key, and we never attached it. Result:
the uploaded maps had nothing to match against.

Fix: error-tracking.ts now builds the same filename->chunkId map from
`globalThis._posthogChunkIds` that @posthog/core does, and stamps
`chunk_id` on each frame whose chunk registered one. Scrubbing preserves
it (spread + field override), and it is set before scrub, so PostHog can
correlate regardless of filename redaction.

* fix(web): key chunk id on the newest registration frame, not the outermost

PerishCode review: parseStack keeps Error.stack newest-first while
@posthog/core reverses to oldest-first, so copying its length-1..0 loop
selected the wrong end. For a multi-frame registration stack that keyed
the chunk id onto a webpack-runtime filename and left the real chunk
unmatched. Iterate from the newest frame (frames[0]) instead; regression
test now uses a multi-frame registration stack.
2026-07-15 08:10:16 +00:00
lefarcenandGitHub f1e8cb157a fix(packaged): label main-process startup telemetry production, not development (#5681)
Every `packaged_runtime_failed` event was tagged `env: development` in
production — 1,241 development vs 2 production over 3 days — so real
packaged startup crashes were filtered out of the env=production
dashboards and the daily stability report couldn't tell real users from
dev machines.

Cause: the daemon child is always spawned with NODE_ENV=production
(spawnSidecarChild), so daemon/web events correctly report `production`.
But the packaged MAIN process — which emits packaged_runtime_failed —
has no NODE_ENV of its own, and resolveTelemetryEnv defaulted an unset
NODE_ENV to `development`. apps/packaged only ever runs as a packaged
build, so that default was wrong.

Fix: resolveTelemetryEnv now treats anything that isn't an explicit
development marker as `production`, matching the daemon it spawns.
Explicit overrides (OD_TELEMETRY_ENV / OPEN_DESIGN_ENV / POSTHOG_ENV /
LANGFUSE_ENVIRONMENT) still win. Red-spec tests: env resolves to
production by default (unset NODE_ENV) and honors an explicit override.
2026-07-15 08:08:10 +00:00
Nicholas-XiongandGitHub 21f25cde52 fix: hide native caret while composer placeholder carousel runs (#5522)
An empty, focused composer runs a placeholder typewriter carousel that
draws its own decorative caret trailing the animated text. The native
browser caret also blinks at offset 0, so two carets show at once
(#5373). Suppress the native caret while the carousel is mounted; it
reverts automatically the moment the field has text.

Keyed off the carousel being a sibling of the editor (:has(~ ...)), so
it covers both the chat composer (.composer-input-wrap) and the home
hero (.home-hero__prompt-editor) without depending on the wrapper class.
2026-07-15 07:52:50 +00:00
lefarcenandGitHub b5d9a12f48 fix(web): break redirect-loop scripts that freeze the HTML preview (#5469)
* fix(web): break redirect-loop scripts that freeze the HTML preview

A generated or hand-edited artifact carrying a self-redirecting directive
— most reliably a `<meta http-equiv="refresh">` that reloads the same
document, or a cycle of meta refreshes (A -> B -> A -> ...) — reloads the
preview iframe forever and pegs the main thread until the whole design
workspace freezes. There was no loop detection or hop limit, so the
preview stayed unusable.

Add an in-iframe circuit breaker (`injectPreviewRedirectGuard`, always
injected by `buildSrcdoc`) that counts meta-refresh hops in `window.name`
— the one store that survives an iframe navigating itself — resets the
count once a full window elapses with no further refresh (timeout
safeguard so a slow legitimate auto-refresh never accumulates), and once
the hop budget is exceeded, or immediately for a near-instant
self-refresh, strips the offending `<meta>`, stops the frame, and posts
`od:redirect-loop-blocked` to the host. The host parks the srcDoc iframe
on a static "loop detected" placeholder, the reliable stop the browser's
unforgeable `window.location` otherwise denies.

Route any self-redirecting source onto the srcDoc path
(`htmlNeedsRedirectGuard`) so the guard is always present instead of the
URL-load path serving the document raw and unguarded.

Covered by a runtime spec that runs the real injected guard through a VM
modelling a sandboxed iframe reloading itself (hop budget, window reset,
immediate self-refresh kill, chain-break, host message) plus render-mode
detection specs.

Source: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/94709b20-f403-4ed7-a960-1e26572fa394

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

* fix(web): cover srcdoc redirect guard gaps

* fix(web): avoid redirect guard false positives
2026-07-15 07:07:34 +00:00
PerishFireandGitHub be98224b88 fix(web): clarify silent-update copy, footer layout, and preference seed (#5631)
* fix(web): clarify silent-update copy and seed preference on prompt show

Rewrite the silent-update strings so they mean "install later on startup,
not this session", fix the updater footer so long en labels wrap without
pushing actions to a second row, and on prompt open idempotently seed the
default preference when unset (undefined → true; existing booleans are
only read). Checkbox toggles now persist immediately.

* fix(web): address silent-update PR review (hydrate, persist fail, copy)

Gate default preference seeding on daemonConfigLoaded so a saved false is
not overwritten while app-config is still hydrating. Await checkbox
persistence and revert with a visible error when the daemon write fails.
Rewrite silent-update strings to say "future in-app updates" so the
installer prompt no longer promises silent behavior for installer builds.

* test(web): wait for community default category selection under load

The community-filter decoupling test only waited for the deck pill to
mount, then asserted aria-selected immediately. Under full-suite CI
load the pill can paint before resolveDefaultSelection applies, which
flaked Web workspace tests and failed the Validate workspace gate.

* fix(web): harden silent-update seed and non-optimistic daemon write

- Clear seed in-flight via refs so parent setConfig mid-flight no longer
  cancels finally and leaves the checkbox permanently disabled.
- Write allowSilentUpdates only after /api/app-config succeeds, then
  update app config — failed saves cannot leave global state dirty.
- Gate seed on a successful daemon GET (daemonAppConfigReady), not merely
  bootstrap completion, so a failed fetch cannot overwrite an unread opt-out.

* fix(web): non-optimistic silent-update save from Settings About

Route Settings → About through handleSilentUpdatePreferenceChange and
keep allowSilentUpdates out of optimistic handleConfigPersist so a failed
daemon write cannot leave Settings (or global config) on the rejected
value. Autosave is suppressed for the dedicated toggle path.

* fix(web): align Settings silent-update callback type for typecheck

SettingsDialog requires Promise<void>; the test helper allowed void | Promise
and broke Preflight typecheck. Narrow the helper to Promise-only.

* fix(web): serialize silent-update preference writes against races

Queue concurrent allowSilentUpdates writes so an earlier request cannot
commit after a later toggle (Settings About on→off). Disable the Settings
checkbox while a save is in flight, and unit-test the writer with
out-of-order completion scenarios.

* fix(web): only advance silent-update field in Settings autosave baseline

On silent-update save success, patch allowSilentUpdates onto the previous
autosaveLastSavedRef instead of spreading the whole latest draft. Concurrent
edits (e.g. accent color) that land while the request is in flight must still
reach onPersist via the generic autosave loop.
2026-07-15 06:54:19 +00:00
lefarcenandGitHub f3999261d6 fix(desktop): keep CJK typefaces intact in editable PPTX export (#5644)
* fix(desktop): keep CJK typefaces intact in editable PPTX export

Editable PPTX export (the default deck → .pptx mode) hands each slide's live
DOM to the vendored dom-to-pptx engine, which names ONE typeface per text run —
the first family in the CSS stack — and writes it to the PowerPoint <a:latin>,
<a:ea> and <a:cs> slots alike. The html-ppt templates lead every stack with a
Latin-only webfont (e.g. 'Inter','Noto Sans SC',…), so the browser renders CJK
glyphs via per-glyph fallback while the export mislabels those runs with the
Latin font. PowerPoint, WPS and Keynote each then substitute a different
fallback, so Chinese/Japanese/Korean decks render with wrong, inconsistent
fonts ("字体错乱").

Before dom-to-pptx reads the DOM, reorder each CJK text run's font-family so the
first CJK-capable family already in the authored stack leads. The full stack is
preserved (only reordered) so the browser keeps its own per-glyph fallback,
while the exported typeface now names a real CJK font all three suites resolve
the same way. Latin-only runs and stacks with no CJK family are left untouched.

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

* fix(desktop): promote CJK typeface on combined direct text of an element

Address review: promoteCjkTypefaces keyed `touched` off the FIRST non-empty
direct text node, so an element whose text is split across nodes — bilingual
markup like `Product Launch<br>产品发布` or `Welcome <strong>…</strong> 欢迎` —
marked the element done on its Latin chunk and skipped the later CJK chunk,
leaving the wrong typeface for exactly the runs this PR fixes.

Aggregate all direct TEXT_NODE children of each element and resolve once on the
combined text, so a CJK chunk after a Latin one still wins. Adds a combined-text
contract case plus a structural guard against the multi-text-node regression.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-15 06:23:32 +00:00
lefarcenandGitHub 1278816a8f fix(web): route cross-project chat file links in-app instead of a home-page window (#5611)
* fix(web): route cross-project chat file links in-app instead of a home-page window

Chat file links pointing at @-referenced projects arrive as absolute
managed-projects disk paths or app file routes for another project.
asInProjectFilePath refused both, so the click fell through to the
default target="_blank" behavior and Electron's window-open handler
produced a detached child window whose unroutable path rendered the
home screen instead of the file.

resolveChatFileLink now classifies those hrefs and navigates to the
owning project's file route in the same window; any remaining
schemeless path-like href is swallowed instead of falling back to the
useless home window. Thinking-block markdown gets the same handler.

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

* worker: 当有文件链接在chatpane中出现时,点击文件链接唤起的是home页的预览

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

* fix(web): classify owning-project chat links before the basename fallback and keep the handler on opener-less surfaces

Review round on #5611:

- resolveChatFileLink now classifies hrefs that name their owning project
  (app file routes, managed-projects disk paths) BEFORE delegating to
  asInProjectFilePath, so a cross-project /…/projects/other/index.html can
  no longer be captured by a same-named file in the current project and
  silently open the wrong workspace tab. Legacy name-keyed directories
  (projects/<name>/ from 0.10.x previews) consequently navigate to that
  name instead of basename-matching — an in-window bounce at worst.

- chatFileLinkClickHandler is always installed; only the workspace-file
  open action is gated on onRequestOpenFile. Surfaces that mount the chat
  without a workspace opener (design-system chat in DesignSystemFlow)
  now still navigate cross-project targets and swallow unresolvable
  path-like hrefs instead of falling through to the detached home window.

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

* chore: drop the looper task attachment accidentally committed to the branch

.looper-attachments/ is runner-local reference material and must stay out
of git; a stray worker commit (d17c3cc) had checked the bug screenshot in.

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

* fix(web): restore the current-project fallback for ambiguous disk paths and narrow href swallowing to file-like links

Review round 2 on #5611:

- resolveChatFileLink resolves the current project (including the
  known-file basename fallback for absolute paths) BEFORE managed-projects
  disk-route navigation. A /projects/<seg>/ boundary cannot positively
  prove another project's ownership client-side: legacy 0.10.x preview
  dirs are keyed by project NAME and imported folders may contain their
  own projects/ directory — both reference current-project files, per the
  maintained e2e contract (e2e/ui/project-file-link-routing.test.ts,
  verified locally: passes again). Only paths no current-project file
  matches navigate to the owning project (the actual 0.14.1 scenario).

- isPathLikeChatHref only swallows confirmed file-like hrefs (final
  segment carries an extension) and never protocol-relative URLs, so SPA
  routes like /automations or /projects/<id> and //host/… links keep
  their default behavior instead of becoming dead links.
  extractManagedProjectsDiskRoute also refuses //host/… hrefs.

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

* fix(web): prove disk-link ownership from the project's resolvedDir instead of inferring from /projects/ boundaries

Review round 3 on #5611: an imported-folder workspace whose baseDir
itself contains a projects/ segment (e.g. /Users/x/workspace/projects/acme)
made resolveChatFileLink misroute just-written current-project files to a
phantom 'acme' project, because ownership was inferred from any
/projects/<seg>/ boundary once the known-file match failed.

Ownership is now decided only from positive proof against the daemon-
computed resolvedDir (GET /api/projects/:id), plumbed from ProjectView
through ChatPane into AssistantMessage's link handler:

- a href under resolvedDir is a current-project file even when absent
  from the (stale) file list — imported folders included;
- for managed projects resolvedDir reveals the projects root, so sibling
  paths provably belong to that other managed project and navigate there
  (the 0.14.1 @-reference scenario; proof also outranks the basename
  fallback on collisions);
- a /projects/<seg>/ boundary anywhere else proves nothing and never
  yields project-file — the generic extractManagedProjectsDiskRoute
  inference is removed; unproven absolute paths only resolve through the
  current-project known-file fallback (e2e contract re-verified locally).

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

* fix(web): thread projectResolvedDir into the side-chat and design-system ChatPane hosts

Review round 4 on #5611: the resolvedDir proof anchor only reached the
primary ProjectView ChatPane. The side-chat tab (FileWorkspace ->
SideChatTab) mounted ChatPane without projectResolvedDir OR
projectFileNames, and DesignSystemFlow without projectResolvedDir, so
absolute managed disk links stayed unclassifiable (inert) on those
production chat surfaces.

- SideChatTab accepts and forwards projectResolvedDir; FileWorkspace
  passes its existing resolvedDir prop plus a known-file set derived
  from the same files list ProjectView feeds its primary ChatPane.
- DesignSystemFlow fetches the workspace project's resolvedDir via
  getProjectDetail and threads it into its ChatPane.
- Host-level regression added above AssistantMessage: FileWorkspace ->
  SideChatTab -> real ChatPane -> real AssistantMessage, asserting a
  managed cross-project disk link navigates in-window and a stale
  current-project disk link opens as a workspace tab. SideChatTab
  prop-forwarding spec added alongside.

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

* fix(web): swallow unroutable chat hrefs by asking the router instead of an extension heuristic

Review round 5 on #5611: narrowing isPathLikeChatHref to extensioned
last segments reopened the detached home window for extensionless file
links (Dockerfile, /tmp/README, ../Makefile, bare binaries) — their
clicks fell back to target=_blank again.

Routability is now decided by parseRoute itself: its catch-all home
fallback IS the detached-home-window symptom, so exactly the schemeless
hrefs that would trigger it are swallowed. Recognized SPA routes
(/automations, /projects/<id>, /design-systems/<id>, ...) keep their
default behavior — including gated routes, since the router's own flag
handling decides. Relative hrefs reaching the check are always
unresolvable (traversals, malformed encodings) and stay inert.

Regressions added for Dockerfile/README/Makefile shapes at the helper
and click-handler level; the round-4 spec that locked in false for
/usr/local/bin/node is flipped to the corrected expectation.

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

* fix(web): harden the routability check against throwing parseRoute and the root-path fallback shape

Review round 6 on #5611: parseRoute() decodes route segments with a bare
decodeURIComponent, so a malformed same-origin href like /projects/%E0
made isPathLikeChatHref throw out of the click handler; and the root
path (/, /?q=1) shares the {kind:'home', view:'home'} shape with the
unknown-route catch-all, so a legitimate link to the app root was
swallowed.

isPathLikeChatHref now special-cases the root path as routable before
consulting the router and wraps parseRoute in try/catch — an href the
router would crash on is not a route and stays inert. Regression specs
cover malformed app-route shapes (helper + click level, asserting no
uncaught error) and the root path.

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

* fix(web): classify Windows drive-letter hrefs as disk paths instead of URI schemes

Review round 7 on #5611: C:\... and C:/... hrefs match the RFC 3986
single-letter-scheme grammar, so isPathLikeChatHref treated them as
external URLs and Windows disk links skipped the inert-link swallow
entirely, reopening the detached home window on that platform; the
resolvedDir proof also only compared POSIX shapes.

- isWindowsDrivePath classifies drive-letter paths BEFORE the generic
  scheme check: an unresolved one is always inert (the SPA router can
  never serve it).
- resolveAgainstResolvedDir now compares over normalizeDiskPath output
  (backslashes unified to '/', drive letter case-folded), so the
  current-project prefix proof and managed-root sibling navigation work
  against a Windows daemon's resolvedDir, including mixed-separator
  hrefs; the traversal guard runs on the normalized form.

Regressions cover Dockerfile-style drive-letter hrefs at the helper and
click level plus resolver proof cases for backslash and mixed-separator
shapes.

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

* fix(web): case-fold Windows disk-path proofs while preserving returned casing

Review round 8 on #5611: normalizeDiskPath only case-folded the drive
letter, so a valid mixed-case pair like C:\users\ME\...\project-1 vs a
resolvedDir of C:\Users\me\...\project-1 — the same location on a
case-insensitive filesystem — failed both prefix proofs and the click
was swallowed instead of opening the file or navigating.

resolveAgainstResolvedDir now runs its prefix/suffix proofs over
case-folded comparison keys for Windows drive-letter paths (POSIX stays
exact — Linux filesystems distinguish case), while projectId/filePath
are still sliced from the original normalized strings so their casing
survives into the returned target. Folding falls back to exact
comparison when lower-casing would change string length, keeping slice
offsets aligned for rare locale characters.

Coverage: mixed-case current-project and sibling-project proofs (red on
the previous head), original-casing preservation for the navigated
projectId, and a POSIX case-sensitivity guard.

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

* fix(web): open design-system chat file links via the Files tab and prove UNC-share ownership

Review round 9 on #5611:

- The design-system chat threaded projectResolvedDir but neither
  projectFileNames nor an opener, so its own workspace file links were
  classified as workspace-file and then swallowed (opener undefined).
  DesignSystemDetailView now feeds ChatPane a memoized known-file set
  and an opener that switches to the Files tab before handing
  requestWorkspaceFileOpen the file — the workspace consuming the open
  request only mounts on that tab. Host-level regression added in
  DesignSystemFlow.test.tsx.

- normalizeDiskPath only recognized drive-letter absolutes, so UNC
  share workspaces (\\server\share\...) never matched a UNC
  resolvedDir and their links stayed inert. isUncPath classifies the
  backslash form (forward-slash //host/... remains excluded as
  protocol-relative), normalization keeps the //server/... shape, and
  isWindowsDiskPath folds case for both Windows shapes in the proof
  keys. isPathLikeChatHref treats unresolvable UNC hrefs as inert
  explicitly. Resolver tests cover current-project and sibling-project
  UNC cases.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-15 04:59:37 +00:00
4f1a87eed0 Add Seungki Kim (FABOR) user story to /stories (#5566)
Third Open Design story — /stories/seungki-kim/ + 10 locale variants + a
card on the /stories/ index. Self-contained v4 partial injected via set:html
(FABOR technical-sheet accents ride in its own <style>); chrome + prose
localized across all 11 active locales, with the customer's headline and
verdict quote kept in English everywhere. category = User stories.

Co-authored-by: Scarlett Huang <scarletthuang@Scarletts-MacBook-Air.local>
2026-07-15 04:33:59 +00:00
lefarcenandGitHub 7b991314c4 fix(desktop): reveal every deck-stage slide in multi-page PPT/PDF export (#5645)
* fix(desktop): reveal every deck-stage slide in multi-page PPT/PDF export

Exporting a multi-page deck to PDF/PPTX produced one populated page
followed by blank pages. Root cause is in the desktop slide-capture path
(apps/desktop/src/main/deck-capture.ts): `showSlide` selects the page to
screenshot by toggling active *classes* and setting *non-important* inline
styles.

That loses the CSS cascade for `<deck-stage>` decks. The custom element and
its injected fallback (packages/contracts/src/runtime/deck-stage-fallback.ts)
hide every slotted slide with `::slotted(*){visibility:hidden!important}` and
reveal only the one carrying the `data-od-deck-active` attribute. A plain
inline `visibility:visible` cannot override a shadow `!important` rule, and
the export never set that attribute — so only the initially-active slide 0
stayed visible and every other slide captured blank. `restackActiveSlide`
and `showAllSlides` already used `!important`; `showSlide` was the lone
non-important selector, which is why the screenshot PPTX/PDF path regressed.

Fix: `showSlide` now sets opacity/visibility/pointer-events/z-index with
`!important` priority (an inline `!important` beats a shadow `::slotted()
!important` rule) and toggles the `data-deck-active` / `data-od-deck-active`
attributes deck runtimes key their reveal rule on. Verified in a real
browser: showing slide index 1 flips its computed visibility from `hidden`
to `visible`.

Red spec: apps/desktop/tests/main/scroll-stitch-geometry.test.ts
"per-slide selection wins the deck-stage !important visibility cascade" —
red on the pre-fix body, green after.

Source: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/3cfcfe64-295c-4a1c-ac6a-156549f599e6

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

* test(desktop): tighten deck-stage regression oracle to the exact fallback contract

Address review feedback on #5645: the regression test's `slideRenders` oracle
was looser than the deck-stage fallback contract it cites. The fallback
(packages/contracts/src/runtime/deck-stage-fallback.ts) reveals ONLY
`data-od-deck-active`, but the oracle also accepted `data-deck-active` (which
belongs to the real deck-stage.js runtime), so a relapse that dropped
`data-od-deck-active` and the `!important` visibility could have kept the test
green while fallback-backed exports went blank again.

Tighten it: the oracle now honors only `data-od-deck-active`, and the test adds
explicit assertions that the selected slide is revealed via inline
`visibility:visible` at `!important` priority AND carries `data-od-deck-active`,
while non-selected slides are `hidden !important` and unmarked. Either half
regressing now fails the test.

Behavior unchanged; test-only. `pnpm --filter @open-design/desktop typecheck`
and the deck-capture suite (27/27) pass.

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

* fix(desktop): stop toggling data-deck-active on export; correct the cascade rationale

Address the CHANGES_REQUESTED review on #5645 (two valid points, both verified
in a real browser).

1) Blocking: showSlide toggled BOTH `data-od-deck-active` and the real
   deck-stage.js runtime's `data-deck-active`. Setting `data-deck-active` can
   replay authored `[data-deck-active]` entrance animations on a slide's
   descendants, which `prepareDeckStage`'s slide-level `animation:none` does not
   suppress (animation is not inherited) — risking a mid-transition capture for
   slide 2..N. It is also unnecessary: the real runtime hides slides with a
   NON-important `::slotted(*)`, so the inline `!important` visibility/opacity
   overrides already reveal those slides outright (importance beats non-important
   regardless of context — confirmed in-browser). Drop the `data-deck-active`
   toggle; keep only `data-od-deck-active` (the fallback's reveal attribute).

2) The prior comment/test claimed inline `visibility:visible !important` beats
   the fallback's shadow `::slotted(*){visibility:hidden !important}`. That is
   false: for `!important` declarations the inner (shadow) context wins before
   inline precedence is considered, so inline `!important` alone leaves a
   fallback slide hidden — the `data-od-deck-active` attribute is what reveals
   it. Verified in a real browser (inline-important-only → hidden;
   attribute-only → visible). Correct the code comment and rewrite the regression
   oracle to model the true cascade, focusing the unit test on the
   `data-od-deck-active` toggle (and asserting `data-deck-active` is NOT set).

Reveal still works for every runtime: fallback via `data-od-deck-active`; real
deck-stage.js and class/light-DOM decks via the inline `!important` overrides +
class toggle.

Validation: apps/desktop typecheck; deck-capture suite 27/27 (red-verified by
neutralizing the attribute toggle); full desktop suite 211/212 (the one failure
is the pre-existing updater.test.ts baseline flake); pnpm guard 86/86.

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

* test(desktop): relax over-constrained deck-active assertion to the fallback contract

Address non-blocking review feedback on #5645. The regression test asserted
`expect(slides[1].hasAttribute('data-deck-active')).toBe(false)`, which pins an
implementation detail rather than the fallback contract. The fallback
(packages/contracts/src/runtime/deck-stage-fallback.ts) only requires the
selected slide to carry `data-od-deck-active`; it does not care whether
`data-deck-active` is present. And the real export freezes descendant
animations in prepareDeckStage() (`*{animation-duration:0s!important;
transition-duration:0s!important}`) before slide selection, so an implementation
that also set `data-deck-active` would still be a correct fix — the assertion
would fail a valid future change for a reason the runtime does not care about.

Drop that assertion; keep the test focused on the fallback-specific contract
(selected slide has `data-od-deck-active`, others do not) plus the inline
`!important` override mechanism. Also soften the showSlide code comment: not
setting `data-deck-active` is primarily because it is unnecessary for reveal —
prepareDeckStage's freeze already neutralizes any `[data-deck-active]` motion —
not because setting it would be unsafe.

Validation: apps/desktop typecheck; deck-capture suite 27/27; pnpm guard 86/86.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-15 04:30:13 +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
098c5951ed fix(web): keep browser assist guidance in context (#5423)
* fix(web): keep browser assist guidance in context

Replace the global long-form toast with concise, light guidance beside the Browser download action so the workflow no longer obscures unrelated surfaces.

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

* fix(web): contain snapshot toast in workspace

Anchor page-snapshot feedback to the file workspace so it cannot overlap chat, and quiet the close affordance until interaction.

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

* fix(web): distinguish error and dismiss icons

Use an alert triangle for toast status so error feedback cannot be mistaken for the close control.

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

* fix(web): quiet toast dismiss control

Vertically center the dismiss affordance and remove its bright surface so error feedback remains the visual focus.

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

* fix(web): pin snapshot dismiss control to corner

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

* fix(web): remove snapshot success glyph

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

* chore: retrigger CI

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

* chore: retrigger CI

* test(web): align browser assist cleanup coverage

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

* test(web): align snapshot error toast expectation

* test(web): wait for default community facet selection

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Amy <1184569493@qq.com>
2026-07-15 03:14:48 +00:00