* fix: link od bin after fresh install
* test: lock root od bin shim path
* test: cover root workspace deps in postinstall scan
* chore(nix): refresh pnpm deps hash
* fix: dock comment board without clipping inspect
When the comment-side dock falls back to the stacked layout in narrow
panes, collapsing the side panel now shrinks the bottom strip to a
horizontal rail height instead of keeping the full panel-height row.
commentPreviewCanvasSize() also stops over-deducting the expanded
panel height in the stacked-collapsed path so the canvas sizing stays
in sync with what is rendered.
* fix(web): address docked comment panel review follow-ups
* Fix non-docked comment tool tablet scaling
* test(web): align comment panel tests with collapse API
The right-side @-button tools popover (ToolsPluginsPanel,
ToolsSkillsPanel, ToolsMcpPanel) inserts text into the composer
draft using the textarea's selectionStart at click time, but the
picker rows had `onClick` without `onMouseDown={(e) =>
e.preventDefault()}`. On a real mouse, mousedown fires first, the
textarea loses focus before the click handler runs, and
selectionStart resets — so the inserted token lands at offset 0
instead of at the user's cursor.
The @-mention popover already prevents this by calling
preventDefault on mousedown for every picker row (the comment at
ChatComposer.tsx:3039-3043 explains the reason). This change
mirrors that protection on the three tools-menu pickers.
The mention popover itself was unaffected, so design-file
mentions (which only flow through the @-popover via
`replaceMentionWithText`) are not impacted by this issue. The
reporter's mention of "design files" appears to refer to picking a
file via the @-popover, where the protection was already in place.
Closes#3195
Validation:
- pnpm exec vitest run tests/components/ChatComposer.tools-menu-caret.test.tsx
→ 3/3 passed (red on main, asserts each picker calls
preventDefault on mousedown)
- pnpm --filter @open-design/web test → 2501/2501 passed (260 files)
- pnpm --filter @open-design/web typecheck → green
- pnpm guard → green
The previous follow-up wrapped assertValidRuntimeDefInactivityTimeoutMs in a broad catch (any thrown value rewritten as AGENT_RUNTIME_DEF_INVALID). The helper is the strict fast-fail gate and currently only throws RangeError on invalid checked-in values, but a future bug inside it — or any unrelated exception introduced while touching this path — would be reported back to the caller as 'the runtime def is bad' instead of surfacing as the real failure.
Narrow the catch to RangeError so only genuine validation failures map to AGENT_RUNTIME_DEF_INVALID; anything else bubbles up to the outer chat-run starter and surfaces as AGENT_EXECUTION_FAILED. Add a pinning test that asserts the helper throws exactly RangeError for every invalid input, so the helper/wrapper contract cannot drift silently.
The previous follow-up emitted AGENT_RUNTIME_DEF_INVALID through design.runs.fail at run startup when the selected runtime def carries a non-finite / negative / fractional inactivityTimeoutMs, but the code string did not exist in packages/contracts/src/errors.ts#API_ERROR_CODES. design.runs.fail feeds the shared SSE/status error envelopes consumed by web and CLI, so the daemon was quietly widening the wire contract while the PR description claimed no contract change.
Land the code in the shared contracts union next to the other AGENT_* codes so the daemon/web/CLI surface stays aligned. Consumers reading ApiErrorCode pick the new member up for free — it is a union extension, not a break. Regression tests assert the literal is in API_ERROR_CODES and is assignable to ApiErrorCode at the type level.
Reviewer (#2579 non-blocking, round 3) noted that
`resolveChatRunInactivityTimeoutMs(def.inactivityTimeoutMs)` is called
deep in the chat-run path, *after* the runtime has already started
side-effectful setup: `.mcp.json` write/unlink under the project
managed cwd, auto-memory extract, prompt composition, prompt-file
write, etc. A bad checked-in `inactivityTimeoutMs` therefore aborted
the run only at watchdog-arm time, after that setup had already
mutated local state.
Extract the validation into a pure
`assertValidRuntimeDefInactivityTimeoutMs(agentDefault)` helper and
call it immediately after the runtime def is selected — same line as
the existing `def.bin` guard. An invalid value now fails the chat
run with a clear `AGENT_RUNTIME_DEF_INVALID` envelope before any
filesystem or prompt-building work happens, matching the fast-fail
goal of the earlier follow-up.
Six new regression tests pin the assert helper's contract (undefined
/ valid integer / NaN / Infinity / negative / fractional /
env-cannot-mask).
Reviewer (#2579 blocking, round 2) noted that the fast-fail check added
in the previous follow-up was placed *after* the env override
early-return. That ordering meant a finite
`OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS` would short-circuit the resolver
before the def was ever validated, so a checked-in typo like
`inactivityTimeoutMs: -1` would sit unnoticed in source for as long
as anyone had the env override set — exactly the opposite of the
fast-fail intent.
Validate `agentDefault` unconditionally at the top of the resolver
so the typo surfaces on every chat run regardless of which branch
wins the priority race. Add a regression test for the
`invalid def + finite env override` shape, and another sanity test
that env still wins when both values are valid.
Two reviewer points from #2579:
1. RuntimeAgentDef.inactivityTimeoutMs was leaking into the public
/api/agents response. DetectedAgent derived its shape via
`Omit<RuntimeAgentDef, ...>` and stripFns() spread the def through
unchanged, so the new field would have drifted the daemon
response away from packages/contracts/src/api/registry.ts#AgentInfo
(the shared web/CLI contract). Drop it from the omit list and
strip it explicitly in stripFns so the registry payload stays
unchanged — agents read the hint by importing the def directly.
2. resolveChatRunInactivityTimeoutMs(agentDefault) silently normalized
invalid values, so a runtime def like `inactivityTimeoutMs: -1`
or `Number.NaN` would have looked fine in code review while
quietly disabling the watchdog for that agent at runtime. The env
path stays lenient (operator-supplied, must not crash a chat run
on a typo), but the def path now throws RangeError on non-finite,
negative, or non-integer values — checked-in source has to surface
such typos loudly.
Copilot CLI goes silent (no stdout, no streamed events) for long
stretches during legitimate deck-generation and large-prompt turns —
the model is still working but the CLI does not emit keepalive frames.
The 600s chat-run inactivity watchdog used to kill those runs as
`stalled` even though the agent was healthy; the user's reported "stuck
after 10 mins and a few seconds" plus the need to type `Continue` to
get the files lines up exactly with the watchdog tripping mid-turn and
the next message reattaching to the still-live session.
Surface an optional `inactivityTimeoutMs` field on `RuntimeAgentDef`,
merge it under the existing `OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS` env
override, and set Copilot to 30 minutes. The env override still wins
so operators can shrink it when diagnosing a runaway session, and the
clamp / 24-hour ceiling apply to both the env path and the def path
so neither can schedule a negative or signed-32-bit-overflowing
setTimeout delay.
Closes#2467.
The 2026-05 plugins library rebuild introduced /plugins/skills/,
/plugins/systems/, /plugins/templates/ and a unified detail route
/plugins/<manifest-slug>/, but the old /skills/, /systems/, /templates/
catalogs were left live in parallel. Two equivalent page trees split SEO
equity, and the homepage, footer, quickstart, agents, official and blog
pages all still linked to the old routes.
Retire the legacy generators and 301 every old URL to its new plugins
equivalent so inbound links and search equity are preserved:
- Remove the /skills, /systems, /templates page generators (English +
[locale] wrappers) and the now-orphaned skill-row component, and prune
the skills/systems/templates branches from the [locale]/[...path]
catch-all (it now renders only craft + blog).
- Add the migration block to public/_redirects. Detail slugs differ from
the old folder names (new slugs are manifest-name based, e.g.
design-system-<x>, example-<x>), so systems/templates use a prefixed
splat plus a short degrade list, and skills map the 27 with a template
equivalent explicitly while the ~110 instruction-only skills and all
mode/scenario/category facet pages degrade to the section landing.
'replicate' is forced to the section to avoid colliding with the
design-system of the same name. Locale variants (zh, zh-tw, ja, ko)
strip to the section.
- Repoint in-site links to /plugins/* across page.tsx (footer, work,
labs pills), info-page-i18n.ts (en + zh + sourceNames), official,
quickstart, agents, blog and html-anything, and update the sitemap
serialize priority list. The system-card keeps linking through
/systems/<slug>/ so the 8 systems without a detail page ride the
redirect's degrade rather than pointing at a missing page.
Verified with a full astro build: old routes no longer emit any HTML,
the new section pages exist, _redirects is copied verbatim, and no
in-site link targets a removed route (the remaining /systems/<slug>/
hrefs are the system cards that 301 by design). astro check passes.
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
ChatComposer tracks the `@…` tokens this surface authored via the
@-mention popover plugin-pick path. When PluginsSection's chip strip
clears, we wire its `onCleared` and prune *only* those tracked
insertions from the draft so the textarea no longer holds orphaned
styled mentions whose chips just unmounted.
Architecture summary (rounds 1–9 collapsed; round 10 detailed below):
- `Array<{token, start, pluginId, insertionId?}>` tracking with
start offsets reconciled across each keystroke via an LCS+LCP
edit-range diff in
`apps/web/src/utils/pluginInsertionTracking.ts` (round 3-4).
`insertionId` is forwarded by `reconcileInsertions` so the
producer can locate its own entry across reconciles
(round 10).
- All draft mutations route through a single `updateDraft`
chokepoint that runs `reconcileInsertions` outside the
`setDraft` updater so React StrictMode's double-invoke is
harmless (round 4-5).
- Boundaries delegate to the shared
`inlineMentions.isMentionBoundary` /
`inlineMentions.isMentionRightBoundary` helpers so the
tracker can never diverge from the parser (round 5).
- `setActivePlugin` is a chokepoint for every applyById path,
filtering tracked entries to those matching the new active
plugin so a replace-plugin flow can never let stale entries
survive (round 6).
- Picker rollback double-snapshots draft + tracker so apply-
failure restores the tracker but only rewrites the draft
when no user keystrokes arrived during the await
(round 7-8).
- `stripPluginInsertedTokens` collapses whitespace seam-local
so user-authored multi-space spans elsewhere are preserved
(round 8).
- `setActivePlugin` is deferred past `await applyById` on
every path, and `onCleared` filters by
`pluginsSectionRef.current?.getActiveRecord()?.id` so a
pending-window clear scopes to the actually-mounted
plugin's tokens (round 9).
race in the picker rollback:
Round 9 made `onCleared` mutate the tracker and the draft when
it ran during a pending replace, and added the `getActiveRecord`
filter so the strip targets the still-mounted plugin's entries
only. The picker's failure-path rollback, however, still
restored `prevEntries` / `prevActiveId` wholesale — assuming
nothing else had touched the tracker during the await. If the
user clicked the still-mounted original chip's × during the
pending replace AND the deferred `applyById` then resolved
with a 500, the wholesale restore (a) resurrected entries that
`onCleared` had legitimately stripped (now stale offsets) and
(b) left the optimistic `@<target>` orphaned in the draft with
no chip ever having mounted — the original #2881 symptom
recurring inside the failure window.
Fix splits the failure rollback into two paths:
1. **Detect "intervening clear" via `activePluginIdRef.current
=== null && prevActiveId !== null`.** `onCleared` always
nulls the active id as its last action; our deferred
`setActivePlugin` never ran in the failure branch. So the
null-while-prev-not-null state is the smoking gun for an
intervening clear during the await.
2. **On detection, surgically remove only our optimistic
entry and only its `@<target>`.** Locate the entry by
`insertionId` (added to `TrackedInsertion` as an optional
field, forwarded by `reconcileInsertions` so the id
survives offset shifts) — this disambiguates the case
where the user picked the same plugin from the @-popover
more than once during the await window. Splice that entry
out and run `updateDraft((d) => stripPluginInsertedTokens(
d, [ourEntry]))` so the draft loses `@<target>` and any
remaining tracked entries (the in-flight target would have
no others, but a co-pending second pick could) get their
offsets reconciled. `activePluginIdRef` stays at `null` —
`onCleared`'s truth, since no chip is mounted.
The "no intervening clear" branch is the round 7/8 path:
restore `prevEntries`/`prevActiveId` wholesale and rewrite
the draft only if `draftRef.current === postInsertDraft`
(no user keystrokes during the await).
Regression coverage (additions):
- `apps/web/tests/components/ChatComposer.plugin-clear-prunes-draft.test.tsx`
— 18 integration specs total (17 prior + 1 new round-10):
* `@-popover pick A → @-popover pick B (apply pending) →
clear A's chip → resolve B with 500 → assert no orphan
@<target>, no orphan @A, no chip mounted, no stale
tracker entries`. Uses a deferred `Promise<Response>` so
the apply stays in flight while the chip-clear is fired,
then resolves with a 500 to drive the failure path. Pre-
fix this would resurrect Airbnb's stale entry AND leave
`@SecondPlugin` orphaned in the draft.
PluginsSection.tsx is unchanged. The host-local tracking +
draft-update chokepoint + parser-aligned boundaries + deferred
active-plugin scoping + transactional applyById + intervening-
clear-aware rollback + filtered `onCleared` keep the cross-
component contract identical to main — only ChatComposer touches
behavior, plus the utils module and two `inlineMentions` exports.
Validation:
- pnpm exec vitest run tests/utils/pluginInsertionTracking.test.ts → 36/36 passed
- pnpm exec vitest run tests/components/ChatComposer.plugin-clear-prunes-draft.test.tsx → 18/18 passed
- pnpm exec vitest run -c vitest.config.ts (full apps/web suite, 228 files) → 2202/2202 passed
- pnpm --filter @open-design/web typecheck → green
- pnpm guard → green
* feat(web): wire generation preview stage into workspace
Show a 3-step progress overlay (understand → generate → prepare) in the
preview area while artifacts are being generated, replacing the blank
empty state. Displays elapsed time, an estimated duration hint, and a
retry button on failure.
- Add GenerationPreviewStage component + CSS module + runtime helpers
- Integrate buildGenerationPreviewState into FileWorkspace
- Pass messages/artifact/error/retry from ProjectView to FileWorkspace
- Register i18n keys for en and zh-CN locales
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): keep generation preview alive and persistent across waiting states
Address UX feedback on the generation preview surface:
- Make the waiting card feel alive instead of frozen: breathing mark,
sweeping progress shimmer, pulsing running-step dot, and a live
activity snippet pulled from streamed events (respects
prefers-reduced-motion).
- Add an `awaiting-input` phase so the preview no longer reverts to the
empty "design will appear here" placeholder when the agent asks the
user a clarifying question (detects inline <question-form>).
- Add a `stopped` phase so a canceled/paused run keeps a contextual
paused card instead of blanking the surface.
- Fix workspaceHasPreviewSurface live-artifact tab match (was reading a
non-existent `tabId` field) and correct the unit assertion that
contradicted the helper's `thinking` handling.
- Populate generationPreview.* keys (incl. new awaiting/stopped strings)
across all locales.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): reveal generation steps progressively as the agent reaches them
- Only render steps the agent has actually reached (drop pending pills)
with a slide/fade entrance, so the card visibly evolves 1->2->3 instead
of always showing the same fully-populated row.
- Keep the "understand" step in progress during requesting/starting so a
fresh run opens with a single step rather than a pre-filled set.
- Stop surfacing status detail (e.g. the model slug from `requesting`) as
the live activity line; only genuine thinking/output text is shown.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): add dynamic sub-status to the generating step
Keep 3 high-level steps but give the long "generating" phase concrete,
moving feedback (option A) instead of splitting into more, less-reliable
steps:
- Derive a sub-status from the agent's TodoWrite plan: the in-progress
task label (activeForm) plus a done/total count, falling back to the
latest write/edit target file when no plan was emitted.
- The count counts the in-progress task toward `done` to match the
chat-side todo card (e.g. 3/7 on both sides).
- Suppress the higher-level narration line while the sub-status is shown
so only one dynamic line appears at a time (early phase = narration,
writing phase = concrete task + count).
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): drop elapsed timer and duplicate estimate from generation preview
The "usually 2–5 minutes" estimate showed twice (lead footnote + meta row)
and the elapsed counter added little signal, so remove both: delete the
meta row, stop falling back to the estimate footnote in the generating
lead (render the lead only when live narration exists), and drop the now
unused elapsed timer/util.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): make hand-off no-editors fallback perform a real reveal
The Finder/Explorer/File Manager fallback button was only calling an
optional onRequestRevealInFinder prop that the actual caller never
passes, so the surface advertised an action it never performed.
finder, explorer, and file-manager are real entries in the daemon's
open-in catalogue (open / explorer / xdg-open), so route the fallback
through openProjectInEditor(projectId, fallbackId) for a genuine
reveal. Keep the renderer reveal bridge as a secondary fallback if
the daemon spawn fails, and disable the button while busy so a double
click can't queue two reveals.
Adjacent: PreviewDrawOverlay's Send-while-streaming behavior is
intentional (sending is queued downstream, not blocked), and the
button already carries sendDisabledReason as its tooltip. Cover that
contract with a regression test so a future change can't silently
re-disable the control or drop the localized reason.
Scope note: the i18n hand-off key migration that previously rode on
this branch landed on main via a different key set, so this PR is
narrowed to just the fallback wire-up and the two regression tests.
* fix(web): surface daemon spawn failure inline in zero-editors fallback
The zero-editors HandoffButton fallback called setError() on a rejected
openProjectInEditor but returned only the <button>, so the error never
rendered. Production callers (ProjectView) mount the component without
onRequestRevealInFinder, so a daemon spawn failure became a silent no-op
— exactly the failure mode the PR was meant to cover.
Wrap the solo button in a handoff-wrap container and render the error
inline next to it. Adds a regression test for the rejected-spawn path.
* fix(web): align preview draw send-disabled test
* fix(web): show handoff fallback for zero editors
---------
Co-authored-by: nicejames <nicejames@gmail.com>
Co-authored-by: mrcfps <mrc@powerformer.com>
`opencode run`'s `-f, --file` is a yargs array option that greedily
consumes every trailing non-flag token, so the memory extractor's
`--file <prompt-file> "<message>"` invocation made OpenCode treat the
message text as a second attachment and exit 1 with "File not found".
Every LLM memory extraction failed for OpenCode Local CLI users.
Deliver the prompt on stdin like the chat-run path (def.promptViaStdin)
and drop the --file attachment. The connector-memory test now models
the real yargs --file array-greediness so it would catch a regression.
Long project names in the "Existing projects" section of the
automation project picker rendered verbatim with no truncate styling,
so a single name like "A very long project name that would otherwise
wrap onto several lines" blew up the row height and made the dropdown
messy to scan. The expected behavior is a single-line label with
ellipsis, with the full name still discoverable on hover.
Add the standard truncate triad (`white-space: nowrap`,
`overflow: hidden`, `text-overflow: ellipsis`) to
`.automation-popover__label`. The parent
`.automation-popover__body` already sets `min-width: 0`, so the
ellipsis renders cleanly. Thread an optional `title` prop through
`PopoverItem` and pass each project's full name from the picker
call site, so the native hover tooltip carries the unclipped name.
Other PopoverItems with fixed in-product copy (e.g. "New project
each run") deliberately omit the title — they never exceed the row
width and the redundant tooltip would be noise.
Regression test covers the DOM contract (every project row has
`title=<full name>`, fixed rows do not); the CSS half is verified by
code review since jsdom does not apply stylesheets.
- apps/web/src/components/ChatComposer.tsx
- apps/web/tests/components/ChatComposer.context-pickers.test.tsx
Clear stale absolute anchors when the pet composer menu is positioned fixed so the popover wraps its content instead of collapsing over the composer textarea.
* fix(daemon): detect and strip fabricated role markers in model output (#3247)
Three-layer defence against models emitting `## user` / `## assistant` /
`## system` lines mid-response, which the chat host interprets as real
turn boundaries and acts on as unauthorised instruction:
1. **System prompt**: anti-roleplay instruction elevated from a bullet
under "What you don't do" to a standalone `## CRITICAL` section in
`official-system.ts`, with a REMINDER pinned at the end of the
composed prompt for recency bias.
2. **Stream-level detection and truncation**: shared `role-marker-guard.ts`
module (`createRoleMarkerGuard` + `FABRICATED_ROLE_MARKER_RE`) used
across all text paths — Claude stream (per-message guards), non-Claude
structured streams (run-scoped guard via `emitGuardedTextDelta`),
and BYOK proxy routes (`createDeltaGuard`). When a marker is detected,
the contaminated suffix is dropped and a `fabricated_role_marker` event
surfaces a warning in the UI.
3. **UI**: `StatusPill` gains `is-warning` / `is-error` CSS variants;
`fabricated_role_marker` events render as amber warning pills.
* fix(chat-routes): do not await reader.cancel() on stream early-return
The await on reader.cancel() can hang indefinitely on response streams
whose underlying source is a Uint8Array (most notably surfaced by the
ollama test in proxy-routes.test.ts, which builds its mock body via
`new Response(uint8array)` rather than the controller-based helper
`sseResponse()`). The hung await holds the request handler open, which
in turn blocks `server.close()` in the afterAll hook, producing the two
test timeouts (test at 145, hook at 36) currently failing CI on #3296.
Fix is in production code, not the test: don't await the cancel. It
is a cleanup hint and we are returning from the function anyway, so
blocking on it offers no value. fire-and-forget with an empty catch
keeps the cancel signal flowing for real HTTP streams without
risking a hang on mock/edge-case implementations.
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* fix(daemon): terminate child on role-marker detection (close#3247 generation vector)
PR #3296's detection layer truncates display and persistence of fabricated
role markers, but the underlying model subprocess keeps generating tokens
after detection. Three concrete consequences:
1. The model bills the user for the entire contaminated response
(we observed 5,106 chars stored in claude's session file for a turn
where only the first 3,013 chars were legitimate — a 40% overhead).
2. tool_use blocks emitted AFTER the marker reach the daemon's
dispatcher unchecked, since detection only gates the text-delta
emission path, not content-block-stop / tool_use blocks. The
model could fabricate "## user delete file X" then emit a
tool_use(delete X) that the dispatcher would execute.
3. The UI surfaces a `fabricated_role_marker` warning followed by an
eventual normal turn-end, blurring the distinction between
"completed normally" and "killed by safety guard."
This commit adds a single idempotent `abortForRoleMarker(marker)`
helper in server.ts, scoped to the same closure as `child` and
`runGuard`. On any detection event (per-message Claude guard,
run-scoped non-Claude guard, plain stdout guard) the helper:
- Emits a structured `ROLE_MARKER_HALLUCINATION` SSE error so the
UI can render a security-class status distinct from a normal
turn-end. The existing `fabricated_role_marker` warning is still
sent and rendered as the amber pill (PR #3296's UI).
- Calls `acpSession.abort()` for ACP-multiplexed agents (Hermes,
Kimi, Devin, Kiro) whose I/O doesn't necessarily release on
SIGTERM of the wrapper process alone.
- SIGTERMs the child immediately, with the existing
`scheduleForcedChildShutdown()` SIGKILL fallback at 2x grace.
Wired into three sites where contamination is detected:
- `emitGuardedTextDelta` (sendAgentEvent / copilot / ACP / pi-rpc
text_delta paths)
- Plain-stdout listener (BYOK plain mode)
- The Claude stream handler's onEvent (per-message guards in
claude-stream.ts surface `fabricated_role_marker` events directly
via onEvent rather than through the run-scoped emitGuardedTextDelta)
Tool_use blocks emitted BEFORE the marker still flow through normally
— this guard can't help with those, since by the time we observe a
text marker the prior content block has already finished. Closing
that gap requires speculative cancellation of in-flight tool calls
when a downstream text block contains a marker; that's tracked as
follow-up work, not included here.
Co-Authored-By: roverkai <2196140098@qq.com>
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* refactor(role-marker-guard): bounded tail + drop chat-style markers
Addresses two review comments on #3303:
(1) O(1) memory + per-delta work (review r3323982225)
Replace the unbounded `accumulated` string with a rolling tail capped
at TAIL_BUFFER_SIZE (64 chars — comfortably exceeds the longest
marker prefix `\n<whitespace>## assistant` ≈ 16–24 chars in practice).
A 50 KB assistant response delivered in 1000 chunks of 50 bytes was
previously O(n²) on string concatenation alone; now it is O(1) per
delta regardless of message length. The `tail.length` value carries
the "already emitted" offset that the cut-point math needs, so the
offset semantics at L74–78 of the prior implementation are preserved
without re-introducing the full-text buffer.
(2) Drop chat-style markers entirely (review r3323982234, option (a))
`User:` / `Assistant:` / `Human:` / `AI:` are removed from the regex.
Rationale:
- The host parses ONLY `## user` / `## assistant` / `## system`
lines as turn boundaries (see `buildDaemonTranscript` in
apps/web/src/providers/daemon.ts). A model emitting chat-style
markers does NOT cause the original #3247 security failure.
- With kill-on-detection wired in this PR (`abortForRoleMarker`
in server.ts), a false positive aborts the whole run — far
more expensive than a stray unflagged `User:` line in chat
scrollback. Chat-style markers collide with legitimate output
(form labels, email contacts, JSDoc) often enough that pairing
them with kill-semantics is the wrong tradeoff.
The tradeoff is now documented in the regex docblock so the
kill-on-match behaviour is justified against the false-positive
surface.
Also aligns the prompt-side CRITICAL block in system.ts: drop the
"don't emit User: / Assistant: / Human: / AI:" bullet, since we no
longer enforce it. Less ambiguity for the model and the operators.
Test file updated:
- Chat-style positive tests flipped to negative ("does NOT match
User: — chat-style out of scope") so the intentional exclusion
has a permanent regression test.
- Two new tests cover the bounded-tail behaviour: a marker arriving
after 10 KB of clean text in small chunks, and a marker
straddling a chunk boundary after 100 prior chunks.
- Added test for legitimate `User: bob@example.com`-style content
not triggering contamination.
Test count is now 35 (up from 25); two of the new ones explicitly
exercise the new bounded-tail path.
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* fix(role-marker-guard): drop \`^\` anchor after first chunk (review r3324060995)
Blocking correctness bug introduced by commit 4 (bounded-tail refactor):
once \`tail\` is a rolling slice of mid-stream text, \`^\` in the
canonical regex \`(?:^|\\n)\\s*##\\s+(?:user|...)\` no longer represents
the genuine message start. As the rolling window slides forward chunk
by chunk, a sliced tail can begin with whitespace + \`##\` (or just
\`##\`), letting \`^\` anchor a match against text that the
full-buffer implementation correctly ignored. With kill-on-detection
wired in commit 3, that false positive now SIGTERMs the run and emits
a \`ROLE_MARKER_HALLUCINATION\` error — exactly the failure class
called out in the docblock at L22–29.
Reviewer's evidence (PerishCode, r3324060995): streaming
"…take a look at the ## user content section…" one character at a
time reports \`contaminated: true\` post-refactor; the same text in a
single feed stays clean.
Fix: keep the canonical \`FABRICATED_ROLE_MARKER_RE\` for the very
first non-empty feed (where \`^\` legitimately points at the message
start), and switch to an internal \`NEWLINE_ANCHORED_ROLE_MARKER_RE\`
(\`\\n\\s*##\\s+(?:user|...)\` — drops the \`^\` alternative) for all
subsequent feeds. A \`firstChunk\` boolean tracks the state. Real
newline-preceded markers straddling chunk boundaries are still caught
because the preceding \`\\n\` is retained inside the 64-char tail.
Regression tests added (\`apps/daemon/tests/role-marker-guard.test.ts\`):
- mid-line \`## user\` streamed char-by-char with no preceding \\n
(mirrors the reviewer's repro)
- space-preceded mid-line \`## user\` in a >130-char stream, which
long enough to force the rolling window past the marker — exercises
the exact slice condition that triggered the bug
- real \\n-preceded \`## user\` still caught after a long preamble
(positive case must not regress)
- \`## user\` as the very first chunk still caught (\`^\` legitimately
anchors on the first feed)
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* fix(role-marker-guard): case-sensitive + tighter prefix scope (reviews r3324151877 / r3324151882)
Two refinements addressing the third review on #3303:
== Blocking (r3324151877) ==
The regex over-matched legitimate Markdown headings, and with
kill-on-detection wired in commit 3 each false positive
deterministically aborts a real run. Three changes tighten the match
to the actual security surface — `## user` / `## assistant` /
`## system` lines the chat host parses as turn boundaries — without
losing any real attack pattern:
1. CASE-SENSITIVE. Dropped the `/i` flag. The host's turn-boundary
delimiter is lowercase (see `buildDaemonTranscript` in
apps/web/src/providers/daemon.ts), and the `## CRITICAL`
system-prompt block already forbids only the lowercase forms.
Title-Case headings like `## User Guide`, `## System Architecture`,
`## Assistant settings` are now ignored — these are legitimate
technical writing patterns LLMs emit constantly. `## USER NOTES`
(all-caps) likewise no longer flags.
2. POSITIVE LOOKAHEAD `(?=[^a-z])` after the role keyword. Without it,
`## userland`, `## userspace`, `## users guide`, `## systemd`,
`## assistance` all match via prefix in the alternation. The
lookahead requires the next character to exist and to not be a
lowercase letter, so:
- `## user\\n…` → match (newline is not lowercase)
- `## assistantR…` → match (R is uppercase; the glued-form
attack pattern still gets caught)
- `## assistant.` → match (. is not a letter)
- `## users guide` → no match (s is lowercase letter)
- `## userland` → no match (l is lowercase letter)
POSITIVE rather than NEGATIVE `(?![a-z])` because the negative
form is satisfied at end-of-string, which in a streaming context
means "we have `## user` but don't know what comes next yet" —
would fire prematurely if `land` arrives in a later chunk. The
positive form delays detection by one character in that edge
case, traded for correctness.
3. `[ \\t]` instead of `\\s` for inner whitespace. Markdown role
markers are single-line by convention; restricting to space/tab
prevents oddities like `##\\nuser` from matching across lines.
Test file: added Title-Case fixtures (`## User Guide`,
`## System Architecture`, `## Assistant settings`, `## USER NOTES`)
and prefix-of-longer-word fixtures (`## users guide`, `## userland`,
`## systemd`, `## assistance`) — each asserting NO contamination.
The existing `## usability` negative test gave false confidence as
the reviewer noted (only failed via alternation-miss, not via
word-boundary semantics); the new fixtures actually exercise the
lookahead. Also added a positive test for `## assistant.` (glued
punctuation) to balance the existing `## assistantReading`
(glued uppercase) coverage. Total tests: 35 → 50.
== Non-blocking (r3324151882) ==
Added `ROLE_MARKER_HALLUCINATION` to `API_ERROR_CODES` in
`packages/contracts/src/errors.ts` alongside the existing agent/AMR
codes, with a docblock comment explaining the emission contract:
emitted by `server.ts::abortForRoleMarker` alongside the existing
`fabricated_role_marker` warning event when the daemon detects a
fabricated Markdown role marker in agent output; retryable. The code
was already being emitted over the wire but unregistered — landing
the registration here keeps the contract and emitter in sync as
reviewer requested.
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* fix(role-marker-guard): defer complete-but-unconfirmed marker suffix
Addresses review r3324277xxx — the boundary case where a stream chunk
boundary lands between the role keyword and its lookahead character
violated the documented "everything from the marker onward is silently
dropped" contract. With (?=[^a-z]) as the lookahead, `feedText('## user')`
returned `## user` as safe (no char to satisfy the lookahead → no match
→ pass through), so the fabricated marker line leaked into UI and
app.sqlite before the next chunk confirmed contamination on the next
SIGTERM cycle.
Fix: introduce a `pending` state variable holding bytes that match the
COMPLETE-but-unconfirmed marker prefix at end of buffer
(/(?:^|\\n)[ \\t]*##[ \\t]+(?:user|assistant|assist|system)$/, no
lookahead, $ anchor instead). When the no-match branch detects this
suffix, withhold it from emission until the next feed either:
- Confirms it (next char non-lowercase) → main regex matches →
contaminated → withheld bytes dropped along with `## user`.
- Denies it (next char lowercase, e.g. `userl…`) → main regex no
longer matches the role keyword → withheld suffix is released
and emitted alongside the new continuation.
Also tied the firstChunk transition to actual byte emission rather
than feed count. Previously a message that starts with `## system`
followed by a separate `\\n` chunk would lose the `^` anchor on the
second feed (firstChunk had flipped after the first feed even though
nothing was emitted yet), silently breaking detection for that edge
case. Now `firstChunk` stays true until at least one byte has crossed
the emission boundary, matching the conceptual definition of "message
start".
Tests added (apps/daemon/tests/role-marker-guard.test.ts):
- `## user` deferred at chunk boundary, confirmed by `\\n` in next
- `## user` deferred at chunk boundary, denied by `land` continuation
- `## assistant` deferred, confirmed by punctuation
- `## User` Title-Case still passes through unconditionally
- `## system` as the very first chunk: deferred, confirmed by \\n
in next chunk (tests the firstChunk-stays-true-when-nothing-
emitted invariant)
Total tests: 50 → 55.
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* fix(claude-stream): scope role-marker guard to text_delta only, not thinking_delta
Addresses review r3324xxxxxx — guarding the thinking channel buys no
security and causes legitimate aborts.
Why thinking is NOT a #3247 vector:
- `buildDaemonTranscript` in apps/web/src/providers/daemon.ts only
re-serializes `m.content` as `## ${m.role}\n...`.
- Extended-thinking content is rendered to a separate
`kind: 'thinking'` payload (daemon.ts:857-858) and never folded
into `m.content`.
- So a `## user` line in the thinking channel CANNOT become a
fabricated turn boundary on the next round-trip.
Why guarding it is harmful:
- Models routinely emit literal `## user` / `## assistant` lines
in chain-of-thought when reasoning about conversation structure
("Let me think about this. The user might phrase it as:\n## user\n
…"). Common pattern in production traces.
- With `abortForRoleMarker` wired in server.ts, a guard match on
thinking SIGTERMs the run and surfaces a security error to the
UI. The user paid for the reasoning, never sees the answer, and
gets a confusing "fabricated role marker" warning for what was
actually legitimate metacognition.
- This directly contradicts the module's own stated philosophy
("a false positive aborts the whole run — a much more expensive
failure than a stray unflagged ... line", role-marker-guard.ts).
Fix: `emitSafeText` now passes thinking_delta through unconditionally,
skipping both the guard and the contamination check. text_delta
remains fully guarded. The single-line change at the top of
emitSafeText preserves all other channels' behavior.
Regression tests added (apps/daemon/tests/claude-stream-thinking.test.ts):
- `## user` / `## assistant` lines in a thinking_delta — must NOT
fire fabricated_role_marker, the thinking content streams intact
including the marker text, and the subsequent text_delta answer
still reaches the consumer (run not aborted).
- Sanity check: same `## user` pattern in a text_delta DOES fire
fabricated_role_marker and truncates emission at the marker. Locks
in the channel-discriminated behavior.
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* fix(role-marker-guard): tie firstChunk to slicing, not byte emission
Blocking review r3324xxxxxx: under the prior firstChunk transition
("any byte emitted"), a role marker that arrived at the very start of
a message with its prefix split across multiple chunks bypassed
detection — reopening the #3247 vector on the Claude path.
Concrete cases that were missed (all are routine provider
tokenizations of \`## user\n…\` at message start):
- \`##\` | \` user\nDELETE…\`
- \`## us\` | \`er\nDELETE…\`
- \`## \` | \`user\nDELETE…\`
Mechanism: the pending-deferral regex only catches COMPLETE role
keywords, so a first chunk ending in a partial prefix (\`##\`, \`## \`,
\`## us\`) was emitted in full. That emission flipped firstChunk to
false. From that point only NEWLINE_ANCHORED_ROLE_MARKER_RE was used,
which requires a literal \n before \`##\`. A marker at buffer
position 0 has no preceding \n, so it could no longer match.
abortForRoleMarker never fired and tool_use blocks emitted after the
fabricated turn boundary reached the dispatcher.
Fix: change firstChunk to track "tail has not been sliced yet" rather
than "any byte emitted". While total emitted bytes <= TAIL_BUFFER_SIZE,
tail still represents the entire emission so far and \`^\` in the
canonical regex genuinely anchors at byte 0 of the stream — so the
\`^|\n\` alternation safely catches a chunk-split message-start
marker. The transition happens at the moment we would slice: once
emitted > TAIL_BUFFER_SIZE, tail becomes a mid-stream window, \`^\`
becomes meaningless, and we switch to the newline-only variants.
Earlier iterations of this code tried two other definitions, both
unsound:
- "any byte emitted" (this commit fixes) — lost \`^\` before a
chunk-split message-start marker could finish arriving.
- "newline emitted" (briefly considered as the reviewer's
alternative suggestion) — left \`^\` valid on a sliced buffer
when streams hadn't emitted a newline yet, re-introducing the
rolling-tail mid-stream false positive from review r3324060995.
The slice-based invariant satisfies both: while we have not sliced,
\`^\` is correct; once we slice, it is not.
Regression tests added (apps/daemon/tests/role-marker-guard.test.ts):
- \`##\` | \` user\nDELETE…\` → contaminated, marker=\`## user\`
- \`## us\` | \`er\nDELETE…\` → contaminated, marker=\`## user\`
- \`## \` | \`user\nDELETE…\` → contaminated, marker=\`## user\`
- \`#\` | \`# user\nDELETE…\` → contaminated, marker=\`## user\`
The fourth case (single \`#\` first chunk) exercises an even more
adversarial tokenization than the reviewer's examples; it is also
caught.
Total tests: 55 → 59.
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* fix(tests): wrap events in stream_event envelope in thinking test
feedJsonl was feeding raw events without the `{ type: 'stream_event',
event: ... }` wrapper that createClaudeStreamHandler requires (line 141
of claude-stream.ts). Events silently fell through all branches, making
both tests pass vacuously. Also fix TS2532 on warnings[0].marker with
non-null assertion (safe after the toHaveLength(1) guard).
Co-Authored-By: RoverKai <roverkai@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: roverkai <2196140098@qq.com>
Co-authored-by: JasonBroderick <jason@buddyboss.com>
Co-authored-by: RoverKai <roverkai@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): preserve preview scroll across tools
Capture URL-loaded preview scroll state before tool handoff and restore it through an opt-in raw HTML bridge to avoid jumping back to the top.
Agent-Model: gpt-5
Agent-Family: openai
Agent-Session: 019e6ceb-c33d-7cd3-bff0-cbc20c642197
Agent-Step: 0.0.6
* test(daemon): cover scroll bridge injection paths
Agent-Model: gpt-5
Agent-Family: openai
Agent-Session: 019e6ceb-c33d-7cd3-bff0-cbc20c642197
Agent-Step: 0.0.6
---------
Co-authored-by: Codex <gpt-5@openai.com>
* fix(web): theme home hero select menu
Use theme tokens for HomeHero footer select dropdown panels so dark theme menus do not render light-only white backgrounds with low-contrast text.
Agent-Model: gpt-5
Agent-Family: openai
Agent-Session: 019e6ceb-c33d-7cd3-bff0-cbc20c642197
Agent-Step: 0.0.3
* test(web): cover dark model logo inversion
Agent-Model: gpt-5
Agent-Family: openai
Agent-Session: 019e6ceb-c33d-7cd3-bff0-cbc20c642197
Agent-Step: 0.0.7
---------
Co-authored-by: Codex <gpt-5@openai.com>
* fix: insert skill reference when selecting from tools panel
When selecting a skill from the tools panel (not via @ mention), the skill
reference was not being inserted into the input. The tools panel only called
applyProjectSkill() without inserting the text token.
This fix makes the tools panel skill picker behave consistently with other
reference types (MCP, connectors) by:
- Inserting the @skill token at the current cursor position
- Setting focus and cursor position after the inserted reference
- Closing the tools panel after successful insertion
Now users can:
1. Manually delete an @skill reference
2. Open the tools panel and select a skill
3. See the new @skill reference inserted at the cursor
Fixes#3188
* fix(web): preserve draft edits when inserting skill
---------
Co-authored-by: mrcfps <mrc@powerformer.com>
* fix(web): persist design-files view state across navigation
pageSize, sortKey, sortDir, and kindFilter reset on every navigation
because DesignFilesPanel remounts via key={projectId}. Persist them to
localStorage under od:design-files:view-state:v1:<projectId> so each
project's view prefs survive tab-switching.
- Read persisted state via lazy useState initializers (SSR-safe try/catch)
- Write back in a single useEffect keyed on all four values
- Scoped per-project so proj-a settings never bleed into proj-b
- Schema-guarded: invalid/missing fields fall through to defaults
- Red spec: apps/web/tests/components/DesignFilesPanel.view-state-persist.test.tsx
* fix(web): address review feedback on view-state persistence
- Add typeof window guard in readViewState for explicit SSR safety
- Consolidate 4 separate localStorage reads into a single useRef read at
mount time; each lazy useState initializer now reads from savedViewState.current
instead of re-parsing localStorage independently
* fix(web): harden design-files view-state persistence
- Validate restored kindFilter values against the current ProjectFileKind
union via isProjectFileKind() so stale stored values from a prior schema
are dropped silently instead of being cast unchecked.
- Introduce DEFAULT_SORT_KEY/SORT_DIR/PAGE_SIZE constants so the useState
initialisers and the new validation guard share a single source of truth.
- Add viewStateHasMounted ref to skip the first-render write in the persist
useEffect. Without this guard every project the user visits accumulates a
default-value entry in localStorage on mount, growing stale-key garbage
unboundedly and making future field additions silently inject defaults into
every existing entry.
- Harden kindFilter test: replace the silent early-return-on-missing-trigger
with expect(filterTrigger).not.toBeNull() so a render failure surfaces as
a real test failure rather than a passing no-op.
* test(e2e): design files view state persists across navigation and reload
Adds a Playwright UI smoke test in e2e/ui/ that exercises the three key
guarantees of the view-state persistence fix:
(a) Tab-away / tab-back: navigating to a file tab and returning remounts
DesignFilesPanel (conditionally rendered); all four prefs (sortKey,
sortDir, pageSize, kindFilter) are restored from localStorage.
(b) Hard reload: localStorage survives page.reload(); prefs are intact on
the next mount.
(c) Per-project key isolation: a second project starts with defaults and
does not inherit values from the first project's localStorage entry.
The test uses OD_PORT=18011 / OD_WEB_PORT=18012 to avoid port conflicts with
the default development ports.
Also fixes a race in DesignFilesPanel: the stale-kind cleanup useEffect was
running against an empty availableKinds set before the async file list arrived
on mount, which cleared a kindFilter correctly restored from localStorage.
Guard added: skip the cleanup when availableKinds is empty.
Red on origin/main (no persistence logic exists there); green on this branch.
* fix(e2e): address code-reviewer feedback on view-state-persist test
- Add data-testid='df-page-size-select' to per-page <select> in
DesignFilesPanel (W2: decouple test from i18n string 'Show')
- Add StrictMode comment to viewStateHasMounted guard explaining
the dev-mode double-write behaviour (W1: document the invariant)
- Switch nav-away from dblclick to single-click + Open button,
matching the pattern used in app-design-files.test.ts (W4)
- Raise timeout from 60s to 90s for cold CI runners (W3)
- Unify seedTextFile/seedPngFile into shared seedFile helper (N3)
- Add home-hero-input assertion in gotoEntryHome (N2)
- Switch waitForPageSizeSelect to use data-testid (W2)
* test(e2e): split design-files persist into nav, reload, and per-project scenarios
* fix(web): tighten isPageSize to discrete option set, add invalid-value regression test
* fix(web): isolate DesignFilesPanel.test.tsx from persisted view-state key
* fix(chat): surface OpenCode provider failures from its log on a silent stall
OpenCode's headless `run --format json` mode swallows provider failures: a
429 usage-limit is marked retryable and retried silently with nothing on
stdout/stderr, so the chat run only dies via the inactivity watchdog and the
daemon shows a bare "request timed out" with no reason. The real error
(statusCode + "Monthly usage limit reached…") is recorded only in OpenCode's
own session log.
On a failed OpenCode close where stdout/stderr carry no signal, read the
newest OpenCode session log, extract the latest `service=llm` provider error
(scoped to that one line so the embedded request body can't contaminate the
classification), and emit a structured, retryable SSE error (RATE_LIMITED /
AGENT_AUTH_REQUIRED / UPSTREAM_UNAVAILABLE) carrying the provider's message.
Refs #982.
* fix(chat): emit recovered OpenCode failure from the watchdog path, bound to the run
Addresses review on #3316.
Blocking: the recovery previously ran only in the child-close handler, but in
the inactivity-watchdog stall path (the exact case this targets)
failForInactivity sends its error and finish()es the run — which clears
run.clients — before the child closes. So the structured error reached zero
live SSE clients and only surfaced on reload. Recover and send the OpenCode
failure inside failForInactivity, before finish(), on the same pre-teardown
send path the generic stall message already uses. Keep the close-handler
branch for the case where OpenCode exits non-zero on its own (clients still
attached).
Non-blocking: bind the log lookup to the current run via an mtime gate
(since=run.createdAt) so a stale or concurrent session's error can't be
misattributed — skip log files last written before the run started.
* docs(opencode-log): note the concurrent-run limitation of the mtime gate
* fix(chat): skip close-handler failure emit when the watchdog already finished the run
Non-blocking review follow-up on #3316: on the silent-stall path both
failForInactivity and the child-close handler fired for the same run, so the
recovered RATE_LIMITED error was sent twice and the events-log stream was
reopened after finish() had closed it. Guard the close-handler failure emit
with !design.runs.isTerminal(run.status) — the watchdog already sent the
error and finalized the run; finalization below still runs (finish() no-ops
once terminal).
* fix(daemon): validate skillId on POST /api/projects against runtime source of truth
* fix(daemon): validate skillId on PATCH /api/projects/:id, sharing the POST validator
* test(daemon): cover skillId canonicalization, design-template ids, empty-string + null normalization, type rejection
* fix(platform): search mise shims dir so mise-installed CLIs are detected
- Add ~/.local/share/mise/shims (and MISE_DATA_DIR override + legacy ~/.mise/shims) to wellKnownUserToolchainBins.
- This makes Pi, Kimi, and other mise-managed coding agents visible to the daemon even when launched from GUI contexts with stripped PATH.
- Added tests for default and MISE_DATA_DIR cases.
- Also pinned pnpm@10.33.2 in root mise.toml for better mise ergonomics.
Before/after: more local CLIs now appear in the runtime picker (Kimi, Pi, Antigravity, Kilo, etc.).
Refs: discussion in session around improving detection for common mise users.
* fix(platform): address Copilot review on mise shims logic
- Generalize the shims comment (no hard-coded CLI examples).
- Make per-version Node toolchain scanning respect MISE_DATA_DIR
(use the same mise root for installs as for shims).
- Avoid duplicate shims entries when MISE_DATA_DIR makes legacy path
identical to the primary one.
Addresses the three inline comments from copilot-pull-request-reviewer
on PR #3319.
* test(platform): extend MISE_DATA_DIR test to cover installs scanning
Addresses non-blocking review feedback from @nettee on PR #3319.
The previous test only asserted shims behavior under a custom
MISE_DATA_DIR. This extends it to also create fixture trees under
customMise/installs/node/... and customMise/installs/npm-openai-codex/...
and assert that the install paths are discovered while default-root
paths are excluded.
This makes the test robust against regressions in the installs
scanning logic (existingMiseNpmPackageBinDirs + node version dirs).
* fix(platform): only fall back to ~/.mise/shims when no MISE_DATA_DIR is set
Addresses the remaining non-blocking review comment from @nettee on PR #3319.
When an explicit MISE_DATA_DIR is provided, we no longer inject the
legacy ~/.mise/shims path. This prevents stale shims from a previous
mise layout from being re-introduced into detection.
Also added a regression assertion in the MISE_DATA_DIR test.
* fix(daemon): make claude-stream dedup robust when final assistant wrapper lacks msgId
Prevents duplicated text and thinking output (especially visible during
design system generation with AMR/Vela).
Root cause: the textStreamed guard fell back to whenever the
final message arrived without a string uid=501(ramarivera) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),79(_appserverusr),80(admin),81(_appserveradm),399(com.apple.access_ssh),33(_appstore),98(_lpadmin),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),400(com.apple.access_remote_ae) (common in some
AMR flows and design system tasks), causing the full content to be
re-emitted even if it had already been delivered via streaming deltas.
Fix: track whether any text or thinking was streamed via deltas for the
current message and use that as a reliable fallback for the final wrapper
instead of only trusting presence.
* revert: remove dedup from claude-stream (PR #3319 should stay clean)
The Mark tool (#3081/#3277) captured the preview via the *active* iframe. For
URL-load previews — decks especially — the active frame is the bridgeless URL
iframe, while the snapshot bridge lives only in the (mounted but hidden) srcDoc
transport frame. So Send on a deck timed out and showed 'Could not capture the
preview. Try again to avoid sending only ink.'
Snapshot the srcDoc-render-mode frame instead (capture mode already keeps it on
full content, so it carries the bridge), with a short retry while it finishes
swapping to full content. Falls back to the active frame for the non-URL-load
case where they are the same.
Red spec: PreviewDrawOverlay.test 'snapshots the srcDoc bridge iframe, not the
visible URL-load frame' fails on main (targets the URL frame), passes here.
The homepage runs its own inline header enhancer instead of importing
the shared header-enhancer.astro component, and that inline copy only
ported the scroll-headroom and GitHub stars/version logic — it never
included the hamburger toggle handler. As a result the mobile menu
button rendered (and animated to an X via CSS) but clicking it did
nothing on / and /<locale>/, while sub-pages that do import the shared
enhancer worked fine.
Port the same toggle handler into the homepage inline enhancer: click
flips .is-open on header.nav (which CSS expands into the dropdown panel
below 1080px), and outside-click, Escape, and any in-menu link close it,
keeping aria-expanded in sync.
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
* fix(pack): add missing download and host packages to Linux INTERNAL_PACKAGES
The native (non-containerized) Linux AppImage build fails with npm 404
errors because @open-design/download and @open-design/host — runtime
dependencies of @open-design/desktop and @open-design/web — were not
included in the INTERNAL_PACKAGES list. Without tarballs for these two
packages, npm install in the assembled app directory tries to resolve
them from the public registry where they don't exist.
Add both packages to INTERNAL_PACKAGES and their build steps to
buildWorkspaceArtifacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pack): apply same download/host fix to mac and win lanes, add regression test
1. Extend the INTERNAL_PACKAGES fix to mac/constants.ts, mac/workspace.ts, win/constants.ts, and win/app.ts so all three pack lanes produce tarballs for @open-design/download and @open-design/host.
2. Add internal-packages-coverage.test.ts that derives required workspace runtime deps from apps/desktop and apps/web package.json files and asserts every pack lane's INTERNAL_PACKAGES includes them. This prevents the same drift from recurring when a new workspace dependency is added.
3. Update win-app.test.ts and workspace-build.test.ts mock directory lists to include the two new packages.
* fix(pack): include runtime packages in workspace build cache
* fix(pack): install platform with desktop prebundle packages
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Siri-Ray <2667192167@qq.com>
* fix(web/router): defer popstate dispatch to microtask
navigate() previously dispatched a synchronous popstate event after
mutating window.history, which caused React 18 to emit:
Cannot update a component (Router) while rendering a different
component (App). To locate the bad setState() call inside App,
follow the stack trace as described in
https://react.dev/link/setstate-in-render
This happens whenever a caller invokes navigate() from inside a
useState updater (e.g. App.tsx:479 routing first-run users through
the onboarding panel from inside the setConfig() update). The
synchronous popstate dispatch reaches useRoute() subscribers which
then call setRoute() while the parent component is still rendering.
Defer the popstate dispatch to a microtask. The window.history call
itself stays synchronous so the URL bar updates immediately; only
subscriber updates are pushed past the current render commit, which
removes the warning without changing observable behaviour for any
existing caller.
* fix(web/router): cover deferred navigation timing
---------
Co-authored-by: Visionboost <contact@visionboost.fr>
Co-authored-by: Siri-Ray <2667192167@qq.com>
Adds two new entry points and reworks the community page chrome to
match the wider landing-page direction (PRs #3222 and #3230).
Showcase / Plugin Everything (above Ambassadors)
Pitches Open Design as 'studio and gallery' in one address: anything
shipped through the studio (content, products, templates, Skills,
workflows) can return as a plugin, and the strongest pieces are
carried out to the registry, to X, to Discord's #showcase channel,
to the newsletter, and to the video reels. Right column holds a
zero-code Contribute card with a curl installer, a copy-to-clipboard
button, and a three-step flow for the od-contribute Skill.
Hero CTA row
Three buttons, in a single row: Stage your masterpieces (Showcase),
Become an ambassador (Ambassadors), Contributors hall of fame
(Maintainers).
Top nav
Pulls the breadcrumb out of the brand mark, surfaces Contributors /
Ambassadors / Showcase as anchors, and adds GitHub + X icon buttons
next to the Join Discord pill (mirrors PR #3230).
Footer
Restructured into columnar layout with brand summary plus Products,
Plugins, and Community columns; copyright moves to a bottom rule.
Ambassadors
Renaissance-voice three-column program (Vocation / Patronage /
Covenant) with an Apply on Discord CTA to the ambassador channel.
Discord
Card spans wider (max-width 1440px), copy reframed as 'the front
line of the agent-design era', two moderator profiles on the right
(Koki from the founding team, Victor as Discord steward), channel
list and CTAs on the left.
Recent signal
Kicker and headline framed as this week's leaderboard; backed by a
hand-curated RANKING_SNAPSHOT. A real refresh pipeline remains a
follow-up; data is hand-updated until then.
Other notes
Punctuation pass: replaced most em-dashes in prose with colons,
periods, commas, semicolons, parentheses; em-dashes only remain in
data placeholders, page title, and HTML comments. Logo size bumped
to 32px and now uses an alt of 'Open Design'.
Co-authored-by: koki yanlai xu <koki@kokideMacBook-Air.local>
* fix: pet hover card gets cut off at screen edges
* fix: address review feedback - viewport clamping + unadopted pet wake
- Add window.innerHeight check to prevent bottom-edge clipping
- Increase menuH estimate for safer positioning
- Open pet settings instead of no-op Wake for unadopted pets
* fix: address review feedback on pet menu positioning and wake action
- Add viewport height check (viewH) to prevent bottom-edge clipping
- Increase menuH estimate for safer positioning
- Open pet settings instead of no-op Wake for unadopted pets
* fix(daemon): grok-build runtime — pass prompt inline as -p value, drop stdin
Grok Build CLI 0.1.212 enforces `-p, --single <PROMPT>` as a value-requiring
flag — invoking with bare `-p` and piping the prompt to stdin now fails with:
error: a value is required for '--single <PROMPT>' but none was supplied
The previous runtime def used `promptViaStdin: true` + `buildArgs` returning
`['-p']`, which only worked against earlier grok builds that read the prompt
from stdin when `-p` had no inline value.
This change inlines the prompt as the `-p` argument value and flips
`promptViaStdin: false`. Linux `MAX_ARG_STRLEN` (128 KB) is enough headroom
for typical Open Design prompts; if we ever hit `E2BIG` on a very large
brief, a follow-up could shell out to `--prompt-file <tempfile>`.
Verified against grok 0.1.212 (b7b8204a4) — single-turn invocations now
return clean text replies instead of exit 2.
* fix(daemon): declare grok-build argv prompt budget + regression coverage
@mrcfps' review on #2259 flagged that moving the Grok Build adapter from
the (no-longer-working) stdin path to argv would regress oversized
composed prompts from the actionable AGENT_PROMPT_TOO_LARGE error we
already emit for DeepSeek to a raw spawn ENAMETOOLONG / E2BIG instead.
Fixed by mirroring the DeepSeek argv-budget shape:
- grok-build.ts: `maxPromptArgBytes: 30_000` (same headroom as DeepSeek,
~2.7 KB under the Windows CreateProcess 32_767-char cap) so
`checkPromptArgvBudget` pre-flights composed prompts (system + history
+ skills + design-system content + user message) before spawn.
- prompt-budget.ts: Grok-Build-specific message — names the `-p /
--single` flag, the xAI CLI 0.1.212+ behavior change, and points the
user at stdin-capable adapters (claude / codex / hermes) when they
need to ship large local context.
- Tests: 3 new vitest cases in prompt-budget.test.ts — pin the budget
field, exercise the strict-overrun + at-limit + CJK byte-count guards
exactly like the DeepSeek regression set, and assert the Grok-named
diagnostic copy. New `grokBuild` + `grokBuildMaxPromptArgBytes`
helpers exported alongside the existing `deepseek*` ones.
All 23 prompt-budget tests pass locally (`pnpm exec vitest run
tests/runtimes/prompt-budget.test.ts`).
---------
Co-authored-by: Sriram Sivakumar <sriram155@gmail.com>
Co-authored-by: Siri-Ray <2667192167@qq.com>