- Added Lexical as a dependency to improve text composition capabilities within the chat interface.
- Implemented mention functionality with the creation of `MentionNode` and related serialization/deserialization logic for inline mentions.
- Enhanced `ChatComposer` and `ChatPane` components to support queue-only message sending and improved state management for queued messages.
- Updated `DesignBrowserPanel` and `PreviewDrawOverlay` to incorporate new features for capturing and annotating browser snapshots.
- Refactored various components to streamline interactions and improve user experience during chat and design tasks.
These changes significantly enhance the text editing experience and provide better management of chat interactions, improving overall usability in the application.
- Enhanced the `loadTabs` function to utilize cached tab states, improving performance and user experience during data fetching.
- Implemented `normalizeTabsState` and caching functions to manage tab states effectively, including validation and error handling.
- Updated `writeCachedTabs` to ensure the latest state is stored in local storage, facilitating better persistence of tab information.
- Modified `listTabs` in the daemon to include `updatedAt` in the state retrieval, allowing for more accurate tracking of tab updates.
These changes streamline tab management and enhance the overall responsiveness of the application.
- Added support for queue-only chat sends, allowing for better handling of messages during busy conversations.
- Refactored chat send functions to improve state management and persistence of queued messages.
- Updated CSS styles in the DesignFilesPanel for better layout and responsiveness, including search control enhancements.
- Expanded tests for new chat send functionalities and search interactions, ensuring robust behavior.
These changes improve the user experience by streamlining chat interactions and enhancing the overall design file management interface.
- Updated project routes and server logic to include `browserTabs` in the request body, improving tab state management.
- Implemented validation for `browserTabs` to ensure it is an array, enhancing error handling.
- Refactored `setTabs` function to accommodate the new structure for managing tabs, including browser-specific tabs.
- Added tests for browser tab state persistence and management in the new `project-tabs-state.test.ts` file, ensuring robust functionality.
These changes improve the user experience by providing better management and persistence of project and browser tabs within the application.
- Updated database schema to include a new `state_json` column in the `tabs_state` table for improved project tab state management.
- Implemented functions to normalize and parse project tab states, including handling browser workspace tabs.
- Modified `listTabs` and `setTabs` functions to utilize the new state management features, allowing for better tracking of active tabs and saved states.
- Refactored related types in the contracts and web applications to support the new tab state structure.
These changes improve the functionality and user experience of managing project tabs within the application.
- Added optimistic folder path management in DesignFilesPanel to improve user experience during folder creation.
- Updated social share logic in FileViewer to handle protected deployments more effectively, ensuring clearer messaging and UI updates.
- Refactored CSS styles for better layout and responsiveness in both components, enhancing overall usability.
- Expanded tests for new folder management features and social share interactions, ensuring robust functionality.
These changes streamline folder management and enhance the social sharing experience within the application.
- Replaced favicon URL generation with a new `referenceIconUrl` function to provide reliable icons for curated design sites, improving visual consistency in the DesignBrowserPanel.
- Updated the FileViewer component to enhance social share functionality, including clearer messaging for protected deployments and improved UI for sharing links.
- Added CSS styles to support new visual states for social share components, ensuring better user experience.
- Expanded tests for the new `referenceIconUrl` function and social share interactions, ensuring robust functionality.
These changes enhance the design resource presentation and improve the sharing experience within the application.
- Added new reference categories in DesignBrowserPanel, including 'Inspiration', 'Real Interfaces', 'Color', 'Typography', and more, each with curated design resources.
- Improved folder creation logic in DesignFilesPanel to suggest names based on existing folders, enhancing user experience.
- Updated CSS styles for better layout and responsiveness across components, particularly in control rows and search functionalities.
- Added tests for new reference categories and folder creation features, ensuring robust functionality.
These changes enrich the design resource catalog and streamline folder management, improving overall usability within the application.
- Refactored `formatAddressDisplay` to utilize a new `formatAddressDisplayParts` function, separating URL and title handling for better clarity.
- Updated `DesignFilesPanel` to improve drag-and-drop interactions, including enhanced directory navigation and file management features.
- Adjusted CSS styles for better visual consistency and responsiveness across components.
- Added tests for new functionalities in `DesignBrowserPanel` and `DesignFilesPanel`, ensuring robust behavior.
These changes improve user experience by streamlining address formatting and enhancing file management capabilities within the application.
- Introduced new `share` command in CLI for building localized social-share targets for Open Design projects.
- Implemented `printShareUsage` and `runShare` functions to handle share requests and display usage instructions.
- Added API routes for social sharing, allowing users to create shareable links for projects.
- Enhanced project routes with new endpoints for listing and creating project folders, improving project organization.
- Updated relevant files and tests to support new functionalities, ensuring robust behavior.
These changes enhance user experience by facilitating social sharing and better project management within the application.
- Updated HandoffButton to include a tabbed interface for switching between editor and CLI options, enhancing user experience.
- Added support for opening the AMR website directly from the HandoffButton.
- Refactored ProjectView to implement a modal for custom instructions, allowing users to edit and review instructions more intuitively.
- Improved CSS styles for project instructions modal and handoff menu, ensuring better layout and responsiveness.
- Added keyboard accessibility for closing the instructions modal with the Escape key.
These changes streamline the handoff process and improve the usability of custom instructions within the application.
- Updated HandoffButton to support framework-specific CLI prompts and improved local project path handling.
- Enhanced DesignBrowserPanel to manage browser history with favicon support and improved address display.
- Introduced new utility functions for formatting addresses and extracting hostnames.
- Refactored CSS styles for better layout and responsiveness across components.
- Added tests for new functionalities in HandoffButton and DesignBrowserPanel, ensuring robust behavior.
These changes improve user experience by streamlining the handoff process and enhancing the design browsing capabilities within the application.
- Introduced a navigation stack in DesignBrowserPanel to manage back and forward navigation states.
- Updated the browser navigation logic to handle URL history and improve user experience.
- Added a settings menu in EntryShell for quick access to language and appearance options.
- Implemented CSS styles for the new settings menu, ensuring a consistent and user-friendly interface.
- Enhanced tests for navigation functionality and settings menu interactions.
These changes improve the overall usability of the application by streamlining navigation and providing easy access to settings.
- Updated the add tab button to toggle the visibility of the menu with improved accessibility attributes.
- Refactored the add menu to be portaled to the document body for better positioning and visibility.
- Adjusted CSS styles for the add menu to use fixed positioning and increased z-index for proper layering.
- Minor CSS adjustments in entry layout for consistent padding.
These changes improve the user experience when adding new modules in the FileWorkspace, ensuring the add menu is more accessible and visually consistent.
- Added a `+` icon to the Design Files tab for opening a new Browser module.
- The Browser module supports navigation features including back, forward, refresh, and address input.
- Integrated a curated list of design reference URLs for user convenience.
- Implemented browser data clearing functionality via IPC.
- Enhanced desktop runtime to support embedded browser with appropriate security measures.
- Added tests for browser functionality and URL handling.
This commit establishes a new workspace for browsing and referencing design resources directly within the application, improving user experience and accessibility to design tools.
* 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>
Resolve Comment picker hit testing against meaningful visible DOM leaves before falling back to annotated ancestors, while preserving Inspect mode's annotation-first selector behavior.
Filter generated React root annotations from Comment targets, keep real element bounds separate from hoverPoint, and avoid rendering the comments drawer inline when a configured dock portal is not mounted.
Inline @-mentioned filenames in the composer can be very lengthy,
causing line wrapping and visual crowding in the input area.
- Switch display from inline to inline-block for max-width
- Cap width at min(240px, 25vw)
- Apply overflow: hidden + text-overflow: ellipsis + white-space: nowrap
- Remove box-decoration-break (unused since content won't wrap)
- Tweak vertical-align for consistent inline-block alignment
Closes#3261
Co-authored-by: freshtemp-labs <freshtemp-labs@users.noreply.github.com>