Commit Graph
2862 Commits
Author SHA1 Message Date
CaprikaandGitHub 86f18d0716 chore(pack): bump bundled Vela CLI to 0.0.22 (#5703)
* chore(pack): bump Vela CLI to 0.0.22

* chore(nix): refresh pnpm dependency hashes
2026-07-15 14:19:15 +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
8cc3a39907 fix(tools-dev): rebuild stale contracts before daemon (#5671)
Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
2026-07-15 12:19:29 +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
PerishFireandGitHub 925db8d2af ci: move nix and docker off the merge validation path (#5682)
* ci: move nix and docker off the merge validation path

Core ci.yml / Validate workspace / merge_group no longer schedule or require
Nix flake check or Docker image builds. Packaging lives on two standalone
workflows: nix.yml (flake check) and docker-image.yml (PR smoke build +
tag/release publish). Update scopes, topology tests, and agent docs.

* fix(ci): detect Docker publish from release inputs, not workflow_call event

Reusable workflow callers keep their own github.event_name (usually
workflow_dispatch), so matching event_name == workflow_call never fires and
stable GHCR publish would silently smoke-build only. Gate publish on tag push
or non-empty release_version / publish_latest inputs instead.

* fix(ci): keep smoke-only Docker sha tags off the publish path

Gate pr-/manual-sha metadata tags on publish != true so release-stable
workflow_dispatch callers do not push extra manual-sha-* tags to GHCR.
2026-07-15 08:47:03 +00:00
lefarcenandGitHub 4c73aba565 fix(pack): align prebundle better-sqlite3 pin to 12.10.0 so mac daemon starts (#5680)
The packaged macOS daemon fails to start for a large share of users: it
exits code=1 at boot with ERR_MODULE_NOT_FOUND, `missing_module` =
better-sqlite3 (~270 hits / 4 days, dominant across 0.13.0–0.15.0). With
the daemon dead the whole app is unusable and the shell keeps retrying —
the "app keeps crashing / restarting" reports.

Root cause: mac-prebundle.ts / win-prebundle.ts pin the daemon's runtime
externals at `better-sqlite3@12.9.0`, but apps/daemon/package.json and
the pnpm lockfile resolve `12.10.0`. The assembled app declares 12.9.0
as a dependency; electron-builder 26's pnpm node_modules collector keeps
a dependency only when the on-disk version semver-satisfies the pin.
12.10.0 does not satisfy exact `12.9.0`, so the collector DROPS
better-sqlite3 from Contents/Resources/app/node_modules with a non-fatal
warning, and the prebundled daemon's `import Database from 'better-sqlite3'`
can't resolve at runtime. blake3-wasm (pinned 2.1.5, matching disk)
survives, which is why it only occasionally shows the same error.

Fix: bump the pin to 12.10.0 in both prebundle manifests to match the
daemon / lockfile. Unit assertions updated (red on main's 12.9.0, green
here).
2026-07-15 08:34:33 +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
Abhijit SinghandGitHub 37515a4864 fix docker ghcr public pull verification (#5016) 2026-07-15 04:32:13 +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
CaprikaandGitHub 7533db3f43 docs: add v0.15.0 release notes (#5647) 2026-07-14 15:22:46 +00:00
a213e07d88 feat(landing): add PPT tools blog cluster with a Tools & Skills category (#5635)
* feat(landing): add PPT tools blog cluster with a Tools & Skills category

Ship a cluster of 9 posts on open-source PPT / slide tools — Slidev,
reveal.js, Marp, frontend-slides, ppt-master, dashiAI, guizang, and
codex-ppt-skill, plus a Claude PPT skills roundup. Each post gets a warm
editorial cover illustration and, where the upstream repo provides them,
real screenshots (template galleries, deck previews, style previews,
demo stills) with source attribution.

Add a new "Tools & Skills" blog category: enum value, a URL-safe filter
slug (tools-skills), the filter chip, and labels across all locales, then
file the 9 posts under it. categoryValue() now strips non-alphanumeric
runs so the ampersand cannot leak into the slug.

Localize every post fully for the 10 shipped non-English locales: title,
summary, category, and per-locale bodyHtml so article bodies render in
the reader's language instead of falling back to English.

* fix(landing): localize cross-post nav in claude-ppt-skills bodies

The English body's "Full guide to each" paragraph (8 internal /blog/
links) was missing from every locale's bodyHtml. Since [slug].astro
renders localizedBodyHtml as a full replacement, /zh, /ja and the other
localized routes lost the cross-post navigation. Add a localized version
of that paragraph — same 8 link targets, translated connective prose —
before the scorecard section in each of the 10 bodyHtml blocks.

---------

Co-authored-by: Joey <276262049+xne998808-ai@users.noreply.github.com>
Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
2026-07-14 14:58:34 +00:00
Ray XiandGitHub 574fd9a638 fix overlapping text in slide image and PDF exports (#5628)
* fix export text overlap in slide captures

* fix off-stage slides in multi-page exports

* fix live slide state in off-stage exports

Move the live slide through the capture layer with state-preserving DOM moves, then restore its position and capture-only styles before selecting the next page. The canvas regression now proves structural clones and disconnecting moves cannot satisfy the export path.

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

* test multi-slide off-stage capture restoration

Extend the live-content fixture across two translated slides so a restore-order regression recaptures the wrong page and fails before export.

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

* test consecutive off-stage slide captures

Exercise the production slide-selection and fallback capture path across two translated slides, proving the later exported page retains its live runtime content.

Generated-By: looper 0.10.4 (runner=fixer, agent=codex)
2026-07-14 12:32:05 +00:00
Ray XiandGitHub 88c238ec78 fix(web): reveal rendered deck thumbnails (#5626) 2026-07-14 12:31:42 +00:00
b87b2881d5 fix(landing-page): surface HTML to PPT in nav and footer, move Tools last (#5636)
The /solutions/html-to-ppt/ tool page has been live since #5387, but that PR
only wired it into the /solutions hub — the top-nav Solution dropdown and the
footer never linked it, so it was undiscoverable from navigation.

- header.tsx: add html-to-ppt to TOOL_ENTRIES, and move the Tools group to the
  bottom of the Solution dropdown (order is now use cases -> roles -> tools).
- site-footer.astro: mirror the full Tools group (incl. html-to-ppt) into the
  Solution column, under the use cases, with a small "Tools" subhead. Labels
  reuse each tool page's breadcrumb so nav, footer, and hub cards stay in
  lockstep.

Co-authored-by: Joey <276262049+xne998808-ai@users.noreply.github.com>
2026-07-14 12:25:49 +00:00
Hashim KarimandGitHub f6a891b1f5 [codex] fix linux AppImage launcher startup (#4494)
* fix linux AppImage packaged entry

* fix linux desktop launcher env

* fix linux appimage start env

* fix linux packaged entry keepalive

* fix linux appimage desktop metadata args

* fix linux appimage launcher env wrapper

* fix: preserve appimage lifecycle ownership

* fix: export direct apprun appimage marker
2026-07-14 12:24:30 +00:00
lefarcenandGitHub d7463072a8 fix(web): Plan 模式生成/重写 HTML 后自动打开生成的文件 (#5602)
* fix(web): re-focus HTML rewritten in place when a turn ends

Plan mode's core loop is plan -> generate -> edit the plan -> regenerate.
On the second generation the deliverable (index.html) already exists, so
the turn-end auto-open — a pure pre/post file-NAME diff — saw no new file
and left the viewer stuck on the markdown plan. Protocols that emit no
Write tool events (codex, gemini, opencode, ACP agents) have no per-write
fallback, so nothing opened at all.

Turn-end selection now also considers pre-existing project files whose
mtime lands inside the turn window (same attribution as AssistantMessage's
inferProducedFilesFromTurn), ranked by the existing HTML > markdown
preview priority. Turns without a start stamp keep the produced-only
behavior.

Red spec: e2e/ui/real-daemon-run.test.ts "Plan mode regeneration re-opens
the existing generated HTML file" (red on main, green here), plus a
first-generation companion spec and unit coverage for the new selector.

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

* worker: Plan模式下,不会自动打开生成的HTML文件

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

* fix(web): scope the turn-window auto-open to agent-touched files and bound it by turn end

Review follow-ups on the Plan-mode auto-open selector: the mtime
attribution window [turnStartedAt - 1s, +inf) could attribute a file the
USER was editing to the turn - in Plan mode plan.md autosaves from the
split editor during a run, so a text-only turn could yank focus back to
it - and the missing upper bound let post-turn edits fall in-window on
the reload/reattach recovery call sites.

selectAutoOpenTurnArtifact now accepts:
- agentTouchedFileNames: when the protocol emits Write/Edit tool events
  (claude stream-json), mtime-window candidates are restricted to files
  the agent actually wrote; protocols with no write events (codex,
  gemini, opencode, ACP) pass an empty set and keep the pure time
  window - the case the window exists for.
- turnEndedAt: bounds the window at endedAt + 60s, matching
  AssistantMessage's inferProducedFilesFromTurn on both sides; callers
  without an end stamp keep the open-ended window.

All three call sites resolve the agent's raw tool paths to project file
names through the existing findTouchedProjectFile matching (new
resolveAgentTouchedFileNames helper) and pass their terminal timestamp.

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

* chore: untrack the looper task-attachment directory

The runner's fallback lifecycle commit (a838f8a) accidentally captured
.looper-attachments/bug15_1.png (1.2 MB task-reference screenshot).
Those files are task-local reference material and must stay out of git;
remove them from the tree without rewriting pushed history.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-14 11:58:53 +00:00
TuTouPowerandGitHub 3e5725e543 fix(web): unwrap false-positive question-form open tag in prose (#5493)
* fix(web): unwrap false-positive question-form open tag in prose

When the model mentions `<question-form>` inside backtick-quoted prose
before emitting the real form, splitOnQuestionForms matched the first
open tag against the real close tag. The body between them failed JSON
parse, the real form was swallowed as plain text, and findFirstQuestionForm
returned null. Reopening the banner then fell through to the manual/historical
path, which locked all inputs (interactive=false).

Fix: when a body fails to parse, check for another open tag inside it.
If found, unwind to that position so the real form gets a clean parse
on the next iteration.

* fix(web): avoid prose duplication when unwinding false-positive question-form tag

Slicing from cursor re-emitted prose that was already pushed before the
false-positive tag was parsed. Slice from openStart so only the text
between the false-positive open tag and the real one is emitted.

Also assert segment reconstruction equals the raw input.

* fix(web): unwind false-positive question-form tags in both parser branches

Two changes:

1. closeIdx === -1 branch (tag-name mismatch): prose mentioning
   <ask-question> when the real form uses <question-form> now unwinds
   to an inner open tag instead of swallowing the entire remainder.

2. Malformed-body branch: rename inner→nestedOpen, drop always-true
   resumeAt guard, and replace line-number comment with durable prose.

Test: tag-name mismatch fixture + reconstruction assertion.
2026-07-14 11:13:39 +00:00
lefarcenandGitHub 4988021896 fix: use baked previews for slide presets (#5630) 2026-07-14 11:05:06 +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
Ray XiandGitHub e20ae0bdbe test(e2e): cover uploaded root-relative image preview (#5621) 2026-07-14 09:45:45 +00:00
lefarcenandGitHub 61ee3914d3 fix(web): paint the update-ready prompt above the raised home composer (#5608)
The updater prompt lives inside .entry-main__topbar, an isolated sticky
stacking context at z-index 10, so its own z-index 80 could not escape.
When the composer's agent picker (or another composer popover) is open,
.home-hero__input-card:has(...) raises the card to z-index 1700, painting
the composer and its popover over the update prompt on scrolled home
windows. Mirror the settings-menu precedent and lift the topbar to
z-index 1900 while .updater-popup is open.

Red spec: e2e/ui/updater-popup-stacking.test.ts reproduces the overlap
with elementFromPoint sampling (red on main, green here).

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-14 09:39:12 +00:00
lefarcenandGitHub 8f7c4bb80a fix(web): hold the next-step card while a question form awaits answers (#5616)
A clarification turn terminates its run while the emitted <question-form>
is still waiting for the user in the Questions tab, so the terminal-turn
next-step card (Continue working / Generate artifact / More) popped up
during the question phase. Gate showNextStepActions on the message's
question forms: hold the card until the immediate next user message
submits the form answers (skip-all submits through the same path), and
ignore suppressed direction forms since they render as a locked pill the
user cannot answer.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-14 09:29:39 +00:00
799c6395c0 fix(daemon): retry transient AMR runtime closes (#5564)
* fix(daemon): retry transient AMR runtime closes

* fix(daemon): classify runtime close before retry

* fix(daemon): retry generic stream failures

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-07-14 09:28:34 +00:00
a250ac662e fix(web): stop silent-updates settings toggle from crashing the page (#5609)
* fix(web): stop silent-updates settings toggle from crashing the page

Settings → About read event.currentTarget.checked inside a functional
setCfg updater. React clears currentTarget after the handler returns, so
when SettingsDialog already has pending lanes (about-updater status,
autosave), the deferred updater throws TypeError and lands on the
reload error page. Capture the checked value before setState.

* test(web): fix TS2352 in silent-updates race test null cast

The bad-pattern assertion cast `event.currentTarget` (typed
`HTMLInputElement | null`) directly to `HTMLInputElement`, which
`tsc --noEmit` rejects with TS2352 because the types don't overlap.
Route the cast through `unknown` (matching the fixed-capture assertion
below it) so Preflight typecheck passes.

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

---------

Co-authored-by: Looper <looper@noreply.github.com>
2026-07-14 09:08:24 +00:00
lefarcenandGitHub ad584ff901 fix(web): keep deck page counters in sync during keyboard paging (#5594)
* fix(web): keep deck page counters in sync during keyboard paging

Host-driven deck navigation (keyboard shortcuts and prev/next buttons)
used to mutate slide classes/transforms directly in the srcdoc bridge,
bypassing the artifact's own navigation script. Decks that render their
own page counter from an internal index never saw the move, so the
bottom page number stayed frozen on the first slide while the canvas
advanced. The bridge now prefers dispatching the navigation key to the
artifact's own keyboard handler and only falls back to direct DOM
mutation when no handler moves the deck.

The mirror gap: iframe-focused keyboard navigation on transform-track
decks mutates only the shared track element, which the bridge's
MutationObserver (slide attributes only) never saw, leaving the HOST
counter stale. The observer now watches the track's style too.

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): wait out deferred deck key handlers before the bridge's direct-DOM fallback

Review follow-up for #5594: the artifact-key probe treated only a
synchronous index change as proof the artifact handled the key. Decks
that defer their keydown state update (requestAnimationFrame or
setTimeout) fell through to direct DOM mutation immediately AND received
the key twice — the window dispatch and the document re-dispatch both
reach a deferred window listener — so one host "next" advanced two
slides and desynced the artifact's internal index.

The probe is now asynchronous: dispatch to one target, then wait one
bounded mutation window (MutationObserver on class/style/hidden plus a
48ms cap) for the active index or track transform to change before
trying the next dispatch target or the direct-DOM fallback. Synchronous
handlers still resolve on a zero-latency fast path, and a responsive
dispatch target is cached so later keypresses skip re-probing.

Decks with no keyboard handling must not pay the probe wait: the key
path is gated on the artifact actually shipping keyboard navigation,
seeded from a source scan of the artifact html before any od bridge is
injected plus registration-time tracking through the existing
addEventListener patch — the same two-pronged pattern the od:slide
message-listener flag already uses. This preserves the pinned invariant
that the bridge-owned fallback stays synchronous for handler-less decks.

Regression fixtures: a deferred window-listening deck (red on the
synchronous probe with a visible double advance to slide 3) and
seed-scan assertions proving od-injected bridge scripts do not
contaminate the keyboard-navigation flag.

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

* fix(web): catch external-script deck keyboard runtimes with a head-start registry hook

Review follow-up for #5594: detectArtifactKeyboardNavigation() only sees
inline HTML bytes, so decks that load their keyboard runtime via
<script src> (design-templates/html-ppt wires ../assets/runtime.js,
which registers an ArrowRight/ArrowLeft document keydown handler) were
classified handler-less. Their listeners also register before the deck
bridge executes at the end of <body>, so the bridge's own
addEventListener patches installed too late — the flag stayed false,
host-driven od:slide fell back to direct DOM mutation, and
artifact-owned counters / internal indices went stale again.

A tiny registry hook is now injected at the start of <head> — before any
artifact script — wrapping window/document addEventListener and flagging
keydown listeners that pass the shared navigation-key screen into
window.__odArtifactKeydownNavigation. The deck bridge reads that flag
lazily alongside its inline-source seed and its own late patches, so
registrations are observed no matter when they happen relative to the
bridge. The bridge marks its own top-of-file keydown listeners via
__odDeckBridgeOwnListenerInstall so the framework branch's key-token
source does not classify every framework deck as artifact-navigable.

The navigation-key screen is now a single shared snippet between the
hook and the bridge. Regression: a fixture that registers its keydown
handler before the bridge evaluates with no keydown token in the html
bytes (the script-src shape) — red on the previous commit (counter stays
1 while the canvas moves), green with the hook.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-14 08:59:13 +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