mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
cb5fd3baabaef2afd9e567d2a7be3a60ca4fd549
1314
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cb5fd3baab |
fix(web): auto-fit wide html previews (#5751)
* fix(web): auto-fit wide html previews Generated-By: looper 0.0.0-dev (runner=worker, agent=codex) * test(e2e): allow auto-fit preview zoom Generated-By: looper 0.0.0-dev (runner=worker, agent=codex) * fix(web): gate preview auto-fit on content width Generated-By: looper 0.0.0-dev (runner=worker, agent=codex) * fix: measure preview width through iframe bridge * fix: bridge powered preview sizing |
||
|
|
1b2eace97f |
fix(updater): harden update handoff flow (#5766)
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com> |
||
|
|
ba92a075b9 |
fix(web): portal deploy success toast above modal backdrop (#5326) (#5455)
The deploySavedToast was rendered inline inside the component tree, so it ended up underneath the deploy modal backdrop (z-index 1700). Portal it to document.body like the adjacent deployActionToast, and raise the placement-top toast z-index from 1200 to 1800 so toasts always render above active modals. |
||
|
|
8a1eb3deb8 |
Fix large HTML preview fallback (#5710)
* Fix large HTML preview fallback * fix: refresh promoted large HTML previews |
||
|
|
265aa51630 |
feat(web): make Studio discovery visual and inline (#5496)
* feat(web): make Studio discovery visual and inline Move clarification forms into the chat flow, add artifact-aware visual style choices, and keep follow-up actions hidden until discovery is complete. * feat(web): keep clarification entirely in chat Remove the legacy Questions workspace tab, preserve discovery analytics in the inline form, and keep Skip, Submit, and answered summaries in one conversation flow. * docs: split Studio Chat input and response work Frame visual discovery as the input-side initiative and record response aggregation as a separately reviewed follow-up so the current PR stays bounded. * docs: reorganize Studio Chat experience plans Separate the overall journey, visual discovery, and response architecture so each scope can be reviewed and delivered independently. * docs: define first-artifact satisfaction prompt Limit the feedback prompt to the first successful previewable artifact so it gathers a useful signal without interrupting every iteration. * docs: keep Studio Chat plans independent Preserve the existing discovery proposal as one document and keep response architecture as a separate review surface without an extra master layer. * fix(web): preserve inline discovery behavior Keep visual direction single-select while restoring custom input, optional auto-continue, analytics, and current end-to-end coverage for the inline flow. * fix(web): preserve files when skipping discovery step * fix(web): exclude skipped file answers * fix(web): align inline discovery with slim prompt Preserve the latest discovery contract while keeping inline forms, uploads, drafts, and schema-driven visual selections consistent after the rebase. * feat(web): add style catalog preview images * fix(web): clarify discovery form completion * fix(web): preserve question form context * fix(web): time out unanswered question forms * test(web): cover inline question form next steps * fix(web): roll back partial inline uploads * ci: trigger validation after rebase * fix(web): unlock blocked inline question submits * fix(web): retain previewless visual answers * fix(web): load visual style previews from R2 * fix(web): clean blocked inline uploads * test(web): expect R2 style preview assets * feat(web): show full visual style catalog * fix(web): keep inline question answers consistent --------- Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com> |
||
|
|
e70f0eeefc |
fix(web): contain absolute tool paths to the project root when resolving touched files (#5352) (#5577)
attribution, restricted to agent-touched files resolved through
findTouchedProjectFile. Two residual gaps in that matching:
- A ROOT-level project file (candidate contains no '/') never
suffix-matches an absolute event path like /workspace/index.html, so a
mixed turn that rewrites index.html plus any resolvable file drops the
rewritten HTML from the touched set and nothing auto-opens.
- An out-of-root absolute path sharing a tail with a project file
(/tmp/site/index.html, or a '..' escape resolving outside the root)
CAN suffix-match and mis-attribute an external write.
findTouchedProjectFile now takes the project's resolvedDir: absolute
paths are lexically normalized ('.'/'..') and must sit under the root,
in which case they match by their project-relative form; out-of-root
paths (including '..' escapes) are rejected before any suffix matching.
Paths whose '..' climbs above their own anchor are always rejected.
Callers without a usable root keep the legacy behavior.
Red on main / green here:
apps/web/tests/components/ProjectView.touched-path-containment.test.ts
(6 of 10 cases fail on main: both root-file misses and out-of-root
false positives).
Claude-Session: https://claude.ai/code/session_017peXvs7Q3sA5dhZ6vsYYwN
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
c8bad6bcb4 |
fix(web): keep report-only Design runs off the ARTIFACT_NOT_FOUND path (#5724)
A successful Design turn with zero produced files is only a missing deliverable when the run actually attempted to mutate project files. Image-analysis and report-only audit turns answer in prose without ever calling a write tool; classify them as report_only and let them finish as normal text results instead of appending ARTIFACT_NOT_FOUND. |
||
|
|
6e38058dcf |
feat(web): add agent-assisted home headline (#5699)
* feat(web): add time-aware home greeting * feat(web): add contextual greeting emoji * feat(web): use Agent-focused home headline * fix(web): refine agent-focused home copy * fix(web): keep German home copy informal --------- Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com> |
||
|
|
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. |
||
|
|
05cb03c8ab | fix(web): sandbox the speaker-notes presenter deck iframes (#5571) | ||
|
|
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> |
||
|
|
de4ca325d3 | fix(web): keep model lock affordances visible (#5533) | ||
|
|
a795322b63 | feat: add SiliconFlow Global BYOK preset (#5108) | ||
|
|
24c7876b37 | fix(web): preserve delivery for in-place HTML edits (#5689) | ||
|
|
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
|
||
|
|
27f8421851 | fix(web): reset entry scroll on route changes (#5607) | ||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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 (
|
||
|
|
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> |
||
|
|
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> |
||
|
|
88c238ec78 | fix(web): reveal rendered deck thumbnails (#5626) | ||
|
|
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 (
|
||
|
|
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. |
||
|
|
4988021896 | fix: use baked previews for slide presets (#5630) | ||
|
|
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 (
|
||
|
|
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) |
||
|
|
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) |
||
|
|
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> |
||
|
|
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) |
||
|
|
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> |
||
|
|
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
|
||
|
|
9d055a0fa6 |
feat(amr): add artifact-based upgrade guidance for Free users (#5591)
* feat(amr): add artifact-based upgrade guidance * fix(amr): fail open when billing plan is unavailable --------- Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com> |
||
|
|
db4613797e |
fix(web): replace retired Home/nav-rail logo with the current brand glyph (#5599)
The web app still shipped the pre-rebrand mark (444x444 dark tile with a white cursor ring) in /logo.svg, /logo.png, and /brand-icon.svg, so the Home hero, the entry nav rail, and the cloud onboarding page all showed the old logo. The landing page moved to the ink superellipse glyph in PR #3444; copy that glyph in as the web app's brand assets. Because the new glyph is a solid ink mark (not self-contrasting like the old dark tile), render it through a shared .od-brand-glyph primitive that masks /brand-icon.svg with --text-strong — the same pattern the cloud onboarding logo already used — so it reads as ink in light mode and cream in dark mode. Hero/nav-rail img tags become masked spans, and the hero's panel disc behind the mark is dropped since the glyph is self-shaped. home-logo-assets.test.ts now pins the asset contents (current glyph path present, retired markers absent) instead of only the img src strings. Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code) |
||
|
|
bd67d8cc7b | fix(web): align fullscreen settings controls (#5550) | ||
|
|
e2502d0e5d |
feat(web,daemon): keep annotation position anchors when the screenshot is missing (#4105)
Re-applied onto current main (the original branch predated the server.ts -> runtimes/chat-prompt-inputs.ts extraction and 400+ upstream commits; a mechanical rebase was not meaningful). - normalizeCommentAttachments keeps screenshot-less visual marks that carry a usable anchor (positive-size pagePosition or selector), and drops anchor-less ones so the agent is never hard-scoped onto fields that identify no region - renderCommentAttachmentHint / web context render the structured location instead of a screenshot line when none exists, with a screenshot-less intent copy on both sides - ChatComposer builds the visual attachment whenever the mark has a location, with or without the screenshot upload (#4080 capture failures no longer reduce the send to bare prose) Fixes #4084 |
||
|
|
125c7141ad |
fix(web): anchor browser-assist guide and workspace snapshot toasts to their panes (#5590)
The Download Page guide toast emitted when Continue extraction cannot read any snapshot (and the other brand continue-extraction flow toasts) rendered through the viewport-fixed fallback, floating bottom-center across both panes and covering the composer send area. Scope them to the chat pane so they anchor between the transcript and the composer, matching the browser-assist confirm guide fixed in #5403. The workspace-owned Download Page snapshot progress/result toast (and tab launcher errors) had the same defect: a bare fixed .od-toast centers on the whole window. Wrap them in a .workspace-toast-anchor pinned to the workspace pane's bottom-center, with .workspace as the containing block. Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code) |
||
|
|
906d6ff1f9 |
fix(web): keep the sticky home topbar above the popover-elevated composer card (#5596)
Opening any popover inside the home composer card (agent/model switcher, session-mode toggle, plugin picker, template menu) lifted the whole card to z-index 1700, which out-stacked the sticky entry topbar (z-index 10) and the fixed modal family (automation 80, plus-menu 90, plugins-import 100, use-everywhere 920). With the home view scrolled, the card body painted over the topbar chips (GitHub star, Teams, Discord, settings). The lift only needs to beat the static home content below the hero, whose stacking contexts top out at 8, so 9 restores the intended layering: content < elevated card < sticky topbar < fixed modals. Popover z-indexes inside the card are local to its stacking context and keep working unchanged. Source: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/928fae75-d660-426f-be8e-f98ed4eb88e1 Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code) |
||
|
|
53eff935e1 |
feat(amr): enable Parallel Search MCP (#5552)
Co-authored-by: Siri-Ray <2667192167@qq.com> |
||
|
|
f23c96edff |
fix(web): classify deploy failures instead of the generic "Error" code (#5554)
* fix(web): classify deploy failures instead of the generic "Error" code `artifact_deploy_result` reported `err.name` on the deploy catch path, which collapses to the generic "Error" for a plain thrown Error — ~half of failed deploys (~40/day) land in that opaque bucket with no way to tell an auth failure from a network drop from a provider 5xx, so the deploy failure rate can't be root-caused from telemetry. Add `deployErrorCode(err)` (mirrors export-error-code.ts / #5220): structured `.code` wins, then map the common transport / provider-API signatures (NETWORK, TIMEOUT, RATE_LIMITED, FORBIDDEN, NOT_FOUND, HTTP_<status>, desktop sidecar skew) to fixed codes, else fall back to err.name. Only fixed codes are emitted — never free-form message text — so no deploy token, account id, zone, or URL can leak into analytics. * fix(web): preserve deploy HTTP status/code so classification isn't lost upstream Review follow-up (#5554): deployErrorCode can only emit HTTP_<status> or a structured code if that survives into the deploy catch, but deployProjectFile threw a bare `new Error(payload.error.message || ... )`, dropping the status whenever the provider/daemon returned a human message. So the daemon-backed deploy failures the classifier was meant to bucket still collapsed to "Error". deployProjectFile now attaches a `.code` to the thrown error: a structured provider/daemon `error.code` when present, else `HTTP_<status>`. deployErrorCode reads `.code` first, so those failures now bucket by status instead of "Error". Transport failures (fetch throws before a response) still classify via message. * fix(web): ignore the daemon's generic deploy envelope code so status bucketing runs Follow-up to @nettee's review: the daemon deploy route (apps/daemon/src/routes/deploy.ts) collapses every non-404 provider failure's code to `BAD_REQUEST` (and 404 to `FILE_NOT_FOUND`) while keeping the REAL provider HTTP status on the response and the real message in the body. Since the prior change preferred `payload.error.code`, a live 403/429/5xx was rethrown as `{ code: 'BAD_REQUEST' }` and deployErrorCode returned that generic code, never reaching HTTP_403 / RATE_LIMITED / etc. Ignore the generic envelope codes (shared `GENERIC_DEPLOY_ENVELOPE_CODES`) at both the throw site (fall back to the real `HTTP_${resp.status}`) and inside deployErrorCode as a safety net, so status/message-based bucketing runs. Added registry tests that exercise the actual daemon envelope (BAD_REQUEST+403 → HTTP_403, FILE_NOT_FOUND+404 → HTTP_404) and a deployErrorCode generic-code test. |
||
|
|
ae27ad2541 |
Add Atlas Cloud BYOK provider preset (#5406)
* Add Atlas Cloud BYOK provider preset * Add UTM to Atlas Cloud provider link --------- Co-authored-by: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com> |
||
|
|
6426847fba |
fix(web): separate run success from result delivery (#5525)
* fix(web): distinguish run success from result delivery Persist delivery outcomes separately so successful design runs cannot report completion before a usable artifact is available. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): recover missing design delivery after reload * chore(ci): rerun pull request checks * fix(web): avoid reattaching local delivery finalization * fix(web): retry delivery failures * chore(ci): rerun pull request checks * chore(ci): retrigger managed checks --------- Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
35e8ded24d | fix(web): restore silent update checkbox geometry (#5547) | ||
|
|
d55c3ff216 |
fix(web): align skills filter counts (#5450)
* fix(web): align skills filter counts Generated-By: looper 0.10.1 (runner=worker, agent=codex) * fix(web): type skills search locale helper Generated-By: looper 0.10.1 (runner=fixer, agent=codex) --------- Co-authored-by: Looper <looper@noreply.github.com> |
||
|
|
e6bef56e19 |
fix(web): keep the split resize handle hitbox off the chat scrollbar gutter (#5526)
The chat/workspace split handle extends its pointer hitbox with a ::before overhang of left: -2px / right: -10px. The 2px chat-side overhang sits exactly on the outer edge of the chat-log's 8px scrollbar gutter, so clicks aimed at the (~4px visible) scrollbar thumb start a panel resize instead of scrolling, and hovering the gutter flips the cursor to col-resize with the divider highlight. In RTL the physical right: -10px lands on the chat side and covers the entire gutter. Switch the overhang to logical properties with a hard invariant: the hitbox never crosses the handle's inline-start edge (where the scrollbar lives in both directions) and extends 8px on the workspace side only (inset-inline-start: 0; inset-inline-end: -8px). Effective grab width stays 16px; resize JS is untouched. Red spec: e2e/ui/split-resize-scrollbar-hitbox.test.ts probes elementFromPoint on the gutter edge (LTR + RTL), asserts hover does not enter the resize posture, and keeps a drag-the-handle regression guard. Source: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/5c505d35-42e9-40e7-ab07-ba6e3bbfb2ee Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code) |
||
|
|
7e302031d3 |
fix(web): use current logo on home surfaces (#5396)
Fixes #5365 Verified with focused RED/GREEN regression, web typecheck, pnpm guard, root typecheck, visual DOM readback, and independent Codex diff review. |
||
|
|
4b4c7f4028 |
Improve empty agent output guidance (#4241)
* Improve empty agent output guidance * Preserve updated empty-output guidance |
||
|
|
5127ad188b |
fix: add Kiro to agentLabels.ts (AGENT_LABELS + AGENT_ALIASES) (#5085)
* fix: add Kiro to agentLabels.ts (AGENT_LABELS + AGENT_ALIASES)
- Add kiro to AGENT_LABELS so exactAgentDisplayName('kiro') returns 'Kiro'
- Add 'kiro cli' and 'kiro-cli' aliases to AGENT_ALIASES
- Kiro icon (kiro.svg) already exists, this was purely a missing label mapping
Closes #5081
* test: pin Kiro label and alias resolution in agentLabels utility
PerishCode's review on PR #5085: mapping change is merge-safe but the
new behavior should be pinned in the existing utility test matrix.
Added 4 tests covering:
- agentDisplayName resolves 'kiro cli' → 'Kiro' (alias)
- agentDisplayName resolves 'kiro-cli' → 'Kiro' (alias)
- exactAgentDisplayName returns 'Kiro' for exact id 'kiro' (canonical label)
- exactAgentDisplayName returns null for 'kiro cli'/'kiro-cli' (aliases are
not themselves agents — substring aliases stay routed via agentDisplayName
and do not pollute the exact-id table)
Locks the fix from db025410 and prevents regression.
* test: update kiro alias assertions to match exactAgentDisplayName behavior
|