Commit Graph
611 Commits
Author SHA1 Message Date
f898741f81 docs(readme): refresh contributors wall (#1117)
Co-authored-by: mrcfps <23410977+mrcfps@users.noreply.github.com>
2026-05-10 11:46:32 +08:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
fa5272ad03 Update docs/assets/github-metrics.svg - [Skip GitHub Action] (#1115)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-10 11:46:10 +08:00
Pratik RaiandGitHub 9f073f7b06 fix(web+desktop): handle popup-blocked PDF export with native Electron print dialog (#973)
* fix(web): add alert when pdf export popup is blocked (#664)

* fix(web): implement synchronous empty-tab strategy for pdf export

* Update popup-blocked alert with browser-specific instructions

* Add desktop preload script exposing native print-pdf IPC channel

* Add native Electron print dialog for PDF export via IPC handler

* fix(desktop): resolve electron preload, ipc security, and print callbacks

* fix(desktop): resolve PR review comments for IPC lifecycle and merge fallout

* fix(web): prevent double-print race by moving script injection to browser path

* fix: resolve timing issues for blob revocation and desktop print readiness

* fix(desktop): make waitForPrintableContent descend into sandboxed iframes

* fix(desktop): handle print dialog cancellation gracefully without throwing errors

* fix(desktop): send raw document to desktop bridge to fix readiness timing

* fix(desktop): restore iframe sandbox and implement postMessage readiness handshake

* fix(desktop): separate legacy print readiness from new handshake logic to fix regression

* fix(desktop): resolve print handshake race condition and add regression test

* fix(web): strip allow-modals from desktop sandbox to prevent hidden window stalls

* fix(desktop): ensure print readiness cache is injected for all bridge exports

* fix(web): make print readiness handshake explicitly wait for image completion

* chore(desktop): add 30s timeout to print readiness handshake

* fix(web): defer image readiness scan until after DOM load to catch lazy images

* fix(desktop): secure print readiness handshake with per-export nonce
2026-05-10 11:45:46 +08:00
587c783dc0 feat(web): add Finalize design package + Continue in CLI buttons (#451) (#974)
* feat(daemon): expose resolvedDir on GET /api/projects/:id (#451 prereq)

Native projects (no metadata.baseDir) live at <projects root>/<id>, where
projects root is daemon-side state. The web client cannot reconstruct an
absolute path on its own, and shell.openPath on a relative path is
undefined behavior. Without resolvedDir, the upcoming Continue in CLI
button (#451) would render permanently disabled for native projects.

Mirrors PR #832's pattern of exposing designMdPath in its response.
Computed via the existing resolveProjectDir(...) helper. No behavior
change to existing callers; they ignore the new field.

Adds ProjectDetailResponse contract type and a focused projects-routes
test covering imported-folder, native, and unknown-id paths.

* feat(web): add parseProvenance helper for DESIGN.md staleness checks

Pure helper that extracts Project ID, design system, current artifact,
transcript message count, and generated UTC timestamp from the
`## Provenance` section emitted by the daemon's finalize synthesis
prompt (apps/daemon/src/finalize-design.ts). Used by useDesignMdState
to derive the Continue in CLI button's stale/fresh state without an
additional daemon endpoint.

Handles missing section, "none" sentinels for design system /
artifact, and malformed timestamps without throwing. Tests cover all
four branches.

* feat(web): add buildClipboardPrompt template for Continue in CLI

Inline single-source-of-truth template per #451 spec §3.4. Names the
project, the working directory, and the DESIGN.md-first operating
contract for the receiving `claude` CLI session. Trailing TODO is
the blank task slot the issue body specifies — left empty so the user
fills it in before submitting.

Also lands the shared copyToClipboard helper (jsdom-safe canonical path
+ execCommand fallback) so the new button and any future caller share
one fallback path, mirroring the inline pattern in FileViewer.tsx.

Tests cover happy-path field rendering, "none"/"unknown" sentinels
when DESIGN.md fields are absent, and both clipboard branches.

* feat(web): add useProjectDetail + useDesignMdState hooks

useProjectDetail wraps GET /api/projects/:id, surfacing the resolvedDir
field and falling back to metadata.baseDir for older daemons that don't
include it. Continue in CLI needs an absolute working directory so the
desktop bridge can openPath it; the web client never reconstructs the
path itself.

useDesignMdState fetches the project's file list, downloads DESIGN.md
when present, parses the Provenance section, and computes a stale
verdict by comparing the recorded generatedAt against the max mtime of
non-DESIGN.md files and the max conversation updatedAt. Drives the
button's three-state UI (disabled / fresh / stale) without a
daemon-side endpoint.

Tests cover happy path, fallback, and both stale branches plus the
pure computeStale helper for the null-timestamp edge case.

* feat(web): add useFinalizeProject hook with cancel + error-code mapping

Wraps POST /api/projects/:id/finalize/anthropic for the Finalize design
package button. Three concerns:

  1. Lifecycle: idle → pending → success | error. Double-clicking the
     button aborts the prior in-flight request before starting a new
     one so the daemon never sees stacked finalize calls per project.

  2. Cancellation: AbortController plumbed through fetch + a 130 s
     timer (daemon timeout 120 s + 10 s buffer). Cancel returns to idle
     cleanly — it's a user gesture, not an error surface.

  3. Daemon error mapping: when the response is non-OK, body.error.code
     drives the canonical user-facing toast string (table covers all
     7 codes the daemon emits today plus a network-error catch-all).
     body.error.details, when a string, surfaces alongside the category
     message so account-usage-cap responses (Anthropic 400 →
     UPSTREAM_UNAVAILABLE) can show the upstream's own reason instead
     of just the daemon's category label — committed to lefarcen on
     #450 verification reply.

Tests cover request body shape, all 8 error codes via it.each, the
network-error path, the details-surfacing branch, the cancel ⇒ idle
flow, and the unknown-code → catch-all message branch.

* feat(web): add useTerminalLaunch with electron/web detection

Capability-detected wrapper around window.electronAPI.openPath. On
desktop the bridge forwards to shell.openPath, which opens the OS
file manager at the project working directory (per Electron's
contract for directory paths — it is NOT a terminal launcher;
spawning a terminal application is deferred per #451 Non-goals). On
browser builds the hook reports web-fallback so the caller renders
a manual-instruction toast naming the working directory.

Treats any non-empty string return from shell.openPath as ok: false
so platform-specific failures surface the manual fallback toast.
Behavior is exercised end-to-end by the upcoming
ContinueInCliButton tests.

* feat(desktop): expose shell.openPath via electronAPI bridge

Adds an openPath bridge method that the Continue in CLI button (#451)
uses to surface the project working directory in the OS file manager.
shell.openPath is part of Electron's contract and resolves to '' on
success / a non-empty error string on failure; the IPC handler
forwards the result so the renderer can decide between the success
toast and the manual fallback toast without a separate error channel.

Empty / non-string inputs short-circuit to a self-describing error
string so the renderer never needs to worry about undefined-input
crashes from the main process.

Web side: extracts Window.electronAPI into a single global declaration
at apps/web/src/types/electron.d.ts so future bridge methods land in
one place. Two pre-existing inline declare-global blocks
(NewProjectPanel.tsx, providers/registry.ts) are deleted in favor of
that single source of truth — the inline ones each carried a partial
shape of the bridge and were diverging from the desktop preload.

* feat(web): add FinalizeDesignButton, ContinueInCliButton, ProjectActionsToolbar

Project-level toolbar that hosts the two new actions from #451.
Mounted between AppChromeHeader and the chat/workspace split (wiring
lands in the next commit). Per-file actions (Export PDF/PPTX/ZIP,
Deploy) stay in the FileViewer share menu.

FinalizeDesignButton has three idle labels driven by DESIGN.md
existence + staleness, plus a pending state with a spinner and a
cancel link that maps to useFinalizeProject's AbortController. Error
toasts are owned by ProjectView so the button doesn't carry its own
toast surface.

ContinueInCliButton renders disabled with a Finalize-pointing
tooltip when DESIGN.md is missing (so the workflow is discoverable
rather than hidden), enabled when fresh, and enabled with a stale
chip otherwise. Chip text is the spec's canonical "Spec is stale —
regenerate?" — N-turns-ago is deferred per spec §4.6.

Toast.tsx is a tiny transient component that mirrors
PromptTemplatePreviewModal's state-based toast pattern; supports a
secondary details line so daemon error envelopes that carry an
upstream explanation (e.g. Anthropic account-usage cap) can surface
the real reason alongside the daemon's category label.

CSS appends one block to apps/web/src/index.css mirroring the
existing app-project-title token usage; no CSS modules in this
repo (verified by grep).

* test(web): cover ContinueInCliButton states + interaction wiring

Three rendered states (DESIGN.md missing → disabled with the
Finalize-pointing tooltip; DESIGN.md fresh → enabled, no chip;
DESIGN.md stale → enabled with the canonical "Spec is stale —
regenerate?" chip), plus three onClick branches (no-op when
disabled, fires once when fresh, fires once when stale).

Click-handler integration with clipboard / shell.openPath / toast
lives in ProjectView (the button is presentational and takes the
handler in via props), so those are covered by Phase K's wiring +
the manual smoke test rather than the per-component test.

* feat(web): wire Continue in CLI + Finalize buttons into ProjectView

Mounts the new project-actions toolbar between AppChromeHeader and
the chat/workspace split, hidden when workspaceFocused so the
focus-mode artifact view stays uncluttered.

Wires the four hooks (useProjectDetail, useDesignMdState,
useFinalizeProject, useTerminalLaunch) to a single shared toast
surface. handleFinalize reads the request body from the existing
config: AppConfig prop and uses effectiveMaxTokens(config) to match
the chat-flow's maxTokens defaulting; on success it refreshes
useDesignMdState so the toolbar re-renders with the new chip state.

handleContinueInCli builds the literal clipboard prompt, copies it,
opens the working directory via shell.openPath on desktop /
falls through to a manual-instruction toast on browser, and surfaces
shell.openPath failures with a fallback toast that names the path.

Errors lift into the same toast surface (a useEffect tied to
finalize.error) so the daemon's category message + body.error.details
reach the user as the spec's two-line render — covered by hook test
16a in the prior commit.

⌘+Shift+K (mac) / Ctrl+Shift+K (others) is the keyboard
accelerator for Continue in CLI; capture-phase, platform-gated,
no-op when DESIGN.md is missing. Mirrors the existing FileWorkspace
shortcut idiom and does not collide with ⌘+P (Quick Switcher).

* fix(web): distinguish timeout abort from user cancel in useFinalizeProject

Addresses codex P2 finding on PR #974: the catch block treated every
AbortError as a user-initiated cancel and reset to idle silently. If
the internal 130 s timeout fired, users saw no failure signal but the
daemon's synthesis call may still have been in flight.

Adds a timedOutRef set inside the setTimeout callback before
controller.abort(), and branches in the catch: timeout → status
'error' with new TIMEOUT code ("Finalize timed out after 130 s. The
daemon may still be running."), user cancel → existing idle reset.
Reset the ref at the start of every trigger() so a previous timeout
doesn't poison the next call.

Adds one test using vi.useFakeTimers() that advances past 130_001 ms
and asserts the TIMEOUT error surface.

* fix(web): surface clipboard failures by rendering the prompt in the toast

Addresses codex P2 finding on PR #974: handleContinueInCli ignored
copyToClipboard's return value, so when both clipboard paths failed
(restricted browser context / insecure origin) the toast still said
"paste the prompt" though nothing had been copied — leaving users
with no manual-copy recourse in exactly the environments where the
fallback should help.

handleContinueInCli now branches on copyToClipboard's boolean return.
On failure the toast renders the prepared prompt in a scrollable
<pre> block and pins itself open (no auto-dismiss) so the user has
time to select-and-copy manually. Includes a Dismiss button + the
working directory in the secondary details line so the user has the
information needed to proceed.

The folder-open call is skipped on copy failure because there's
nothing to paste yet; the user copies first, then re-clicks Continue
in CLI when they're ready.

Toast component grows an optional Updating VS Code Server to version 41dd792b5e652393e7787322889ed5fdc58bd75b
Removing previous installation...
Installing VS Code Server for Linux x64 (41dd792b5e652393e7787322889ed5fdc58bd75b)
Downloading:       0%  0%  0%  0%  0%  0%  0%  0%  0%  0%  0%  0%  0%  0%  0%  0%  0%  1%  1%  1%  1%  1%  1%  1%  1%  1%  1%  1%  1%  1%  1%  1%  1%  1%  1%  2%  2%  2%  2%  2%  2%  2%  2%  2%  2%  2%  2%  2%  2%  2%  2%  2%  2%  3%  3%  3%  3%  3%  3%  3%  3%  3%  3%  3%  3%  3%  3%  3%  3%  3%  3%  4%  4%  4%  4%  4%  4%  4%  4%  4%  4%  4%  4%  4%  4%  4%  4%  4%  4%  5%  5%  5%  5%  5%  5%  5%  5%  5%  5%  5%  5%  5%  5%  5%  5%  5%  5%  6%  6%  6%  6%  6%  6%  6%  6%  6%  6%  6%  6%  6%  6%  6%  6%  6%  6%  7%  7%  7%  7%  7%  7%  7%  7%  7%  7%  7%  7%  7%  7%  7%  7%  7%  7%  8%  8%  8%  8%  8%  8%  8%  8%  8%  8%  8%  8%  8%  8%  8%  8%  8%  8%  9%  9%  9%  9%  9%  9%  9%  9%  9%  9%  9%  9%  9%  9%  9%  9%  9% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99%100%100%
Unpacking:   0%  1%  2%  3%  4%  5%  6%  7%  8%  9% 10% 11% 12% 13% 14% 15% 16% 17% 18% 19% 20% 21% 22% 23% 24% 25% 26% 27% 28% 29% 30% 31% 32% 33% 34% 35% 36% 37% 38% 39% 40% 41% 42% 43% 44% 45% 46% 47% 48% 49% 50% 51% 52% 53% 54% 55% 56% 57% 58% 59% 60% 61% 62% 63% 64% 65% 66% 67% 68% 69% 70% 71% 72% 73% 74% 75% 76% 77% 78% 79% 80% 81% 82% 83% 84% 85% 86% 87% 88% 89% 90% 91% 92% 93% 94% 95% 96% 97% 98% 99%100%
Unpacked 4009 files and folders to /home/bryan/.vscode-server/bin/41dd792b5e652393e7787322889ed5fdc58bd75b.
Looking for compatibility check script at /home/bryan/.vscode-server/bin/41dd792b5e652393e7787322889ed5fdc58bd75b/bin/helpers/check-requirements.sh
Running compatibility check script
Compatibility check successful (0) prop and the auto-dismiss
TTL is suppressed whenever code is present. CSS adds .od-toast-code
(monospace, max-height 240 with overflow-auto) and .od-toast-dismiss
styling.

Six new Toast tests cover details rendering, code rendering,
no-auto-dismiss when code is present, auto-dismiss when code is
absent, and the Dismiss button affordance.

* fix(web): make ContinueInCliButton disabled-state guidance visible

Addresses mrcfps's PR #974 review: native <button disabled> does
not fire hover/focus events in browsers we ship against, so a
`title` tooltip on the disabled button never surfaces. The only
guidance for the missing-DESIGN.md state was effectively invisible —
defeating the spec's "discoverable, not hidden" intent.

Renders the help text as a visible sibling <span> next to the
disabled button instead. Adds aria-describedby pointing the button
at the hint's id so assistive tech announces the explanation when
the disabled button gets focus. The native `disabled` attribute
stays so the button still can't be clicked or submitted.

CSS adds .project-actions-disabled-hint (muted italic, 11.5px,
matches the existing meta/secondary text style on this surface).

Test asserts the role="note" hint is in the DOM with the canonical
text and that the button's aria-describedby links to its id.

* fix(web): keep ProjectActionsToolbar at natural height inside the .app grid

The .app container was `grid-template-rows: auto 1fr` — only two
rows. Adding ProjectActionsToolbar as a third child between
AppChromeHeader and the chat/workspace split made the toolbar the
2nd grid item, so it took the `1fr` row (filling roughly half the
viewport) while the split got pushed into an implicit auto row at
its content's natural height. Surfaced as a screenshot from Bryan
showing the toolbar's background bleeding across most of the screen.

Extend grid-template-rows to `auto auto 1fr` and pin the split to
`grid-row: 3` explicitly. Now:
- Toolbar visible: row 1 = header (auto), row 2 = toolbar (auto),
  row 3 = split (1fr, fills remaining viewport).
- Toolbar hidden via hidden=workspaceFocused → ProjectActionsToolbar
  returns null, row 2 collapses to 0px (auto with no content), split
  still fills row 3.

No JS changes; existing 609 tests still green.

* fix(web): guard useFinalizeProject state writes against superseded triggers

Addresses mrcfps's PR #974 P1 review on useFinalizeProject.ts:132
(also called out as P1.3 in lefarcen's deep-dive review).

Calling trigger() twice in quick succession aborted the first
controller and swapped abortRef to the new one, but the first
request's later AbortError catch still unconditionally called
setStatus('idle') / setError(null). That cleared the spinner and
re-enabled both toolbar buttons while the replacement finalize was
still pending — defeating the de-duplication this hook was meant to
enforce.

Adds an isCurrent() closure (`abortRef.current === controller`)
and gates every state-write site after the await: success path,
non-OK envelope path, AbortError-timeout, AbortError-cancel, and
network-error all bail early when the trigger has been superseded.
Per mrcfps: "make every state write request-scoped."

Regression test triggers twice in quick succession with a
never-resolving fetch, awaits the first promise (it rejects with
AbortError), and asserts status stays 'pending' rather than
collapsing to 'idle' under the replacement's lifetime.

* fix(desktop): allowlist-validate shell.openPath against registered project roots

Addresses mrcfps's PR #974 P1 review on runtime.ts:305 (also called
out as P1.2 in lefarcen's deep-dive review): the new
`shell:open-path` IPC handler accepted any renderer-supplied
string and forwarded it straight into Electron's `shell.openPath`,
widening the renderer→main trust boundary so XSS or a compromised
renderer dependency could open arbitrary local paths to the user.

Adds an explicit gate around the bridge:

  1. validateExistingDirectory(p) — floor check that rejects empty
     strings, relative paths, files, apps, and non-existent paths;
     realpath-resolves so symlink games can't be used to register
     one path and reach another.

  2. createProjectRootGate() — Set-backed allowlist of
     daemon-validated project working directories. The renderer
     calls registerProjectRoot(absDir) once per project mount via
     a new IPC method (preload bridge); the main process only
     opens paths that pass both the floor check and the allowlist.

ProjectView wires the registration via a useEffect tied to
projectDetail.resolvedDir, so the active project's daemon-supplied
working directory is always the one being approved (not a renderer-
synthesized string).

Threat-model caveat documented in the runtime.ts comment block: an
attacker that fully controls the renderer can also call register
with arbitrary paths. Closing that gap fully requires a daemon-side
round-trip to derive the canonical resolvedDir from the daemon's
project registry, which is deferred to keep this PR focused.
Today's allowlist still defends against accidental misuse, bugs,
and common XSS payloads that don't know to call register first.

Adds apps/packaged/tests/desktop-project-root-gate.test.ts with 13
cases: floor-validation rejection cases (empty / relative / missing
/ file), happy-path resolution, symlink realpath canonicalization,
and the allowlist's register/isApproved/reset semantics. Mirrors
the existing apps/packaged/tests/desktop-url-allowlist.test.ts
pattern from PR #911 — the packaged workspace hosts the test
because apps/desktop has no vitest setup yet.

* fix(daemon): wire request-lifecycle abort signal through finalize route

Addresses mrcfps's PR #974 P1 review on
apps/daemon/src/server.ts:3831-3837 (also called out as P1.1 in
lefarcen's deep-dive review): `POST /api/projects/:id/finalize/anthropic`
called `finalizeDesignPackage(...)` without threading any
request-lifecycle abort, so cancelling the browser fetch only
aborted the UI-side request — the daemon's 60–120 s Anthropic call
kept running and still wrote DESIGN.md after the UI returned to idle.

Adds an AbortController inside the route handler, fired from
`res.on('close')`, and threads its signal into the existing
`signal?: AbortSignal` parameter on `FinalizeOptions`
(finalize-design.ts:70). `callAnthropicWithRetry` already passes
the signal through to the underlying fetch, so a client disconnect
now propagates all the way to the Anthropic SDK call.

Listener-event choice: `res.on('close')` is the canonical event
for "client disconnected before response was sent" in Express. The
common alternative `req.on('close')` fires whenever the *request*
stream finishes — for POST routes that means as soon as the
body-parser middleware drains the body, well before the route does
any work. Using req.on('close') would have flipped the abort
controller in every successful run; the test caught this empirically.

Caveat documented in the route's comment block: an abort fired
*after* the upstream response has been received but *before* the
atomic write completes still allows the write to land. The SDK
contract bounds the network round-trip, not the post-network disk
handoff.

Adds tests/finalize-route-abort.test.ts: spins up the test server,
mocks global fetch to capture the daemon-side AbortSignal at the
Anthropic call, sends the request via raw http (so we can destroy
the underlying socket), waits until the server reaches the
Anthropic call, then destroys the socket and asserts that the
daemon-side signal received an abort event within 5 s.

Three pre-existing project-watchers chokidar tests show flaky
timeouts under full-suite concurrency but pass in isolation;
unrelated to this fix.

* fix(daemon): refactor finalize-route-abort test to satisfy strict TS narrowing

The CI typecheck (`pnpm --filter @open-design/daemon typecheck`,
which runs both tsconfig.json and tsconfig.tests.json) caught what
my pre-push validation missed: TS narrowed `capturedSignal` to
literal `null` because vitest's mockImplementation closure can't
prove its callback runs, leaving the bare `let capturedSignal:
AbortSignal | null = null` permanently typed at its initial value.
At line 184 (`expect(capturedSignal?.aborted).toBe(true)`) the
right-hand side of the optional-chain became unreachable, and TS
flagged it as `Property 'aborted' does not exist on type 'never'`.

Switches to the standard ref-object pattern
(`const capture: { signal: AbortSignal | null } = { signal: null }`).
TS narrows let bindings inside closures conservatively but treats
object-property writes as opaque, so `capture.signal` reads
correctly across the closure boundary. Logic is unchanged.

(Pre-push oversight: ran `pnpm --filter @open-design/web typecheck`
but not the full repo `pnpm typecheck` after the daemon test
landed; the daemon's own typecheck would have caught this. Adding
`pnpm typecheck` back into the standard pre-push checklist.)

* fix(desktop): make shell.openPath gate daemon-controlled and reject .app bundles

Addresses lefarcen + mrcfps PR #974 P1 reviews on the previous path
allowlist (commit 8bf56597):

  - mrcfps (runtime.ts:45): `validateExistingDirectory` accepted
    macOS `.app` bundles because they're directories, so the gate
    would forward `/Applications/Safari.app` (or any other app
    bundle) into shell.openPath and *launch* the application — a
    stronger capability than the bridge's intended "reveal the
    project folder" feature.

  - lefarcen (runtime.ts:396): the allowlist was renderer-controlled.
    A compromised renderer could call `shell:register-project-root`
    with any existing absolute directory and then `shell:open-path`
    that same path; the IPC injection issue I'd documented as
    "deferred" was the central reviewer concern, not an acceptable
    caveat. Both reviewers asked for the gate to be derived from
    a daemon-authoritative source.

The redesign drops the renderer-controlled register/openPath pair
and replaces it with a single `openPath(projectId)` bridge call.
The desktop main process resolves the project ID by calling the
daemon's `GET /api/projects/:id` endpoint over the web sidecar
proxy (which already forwards `/api/*` to the daemon — verified
in apps/web/sidecar/server.ts:209 and apps/web/next.config.ts:77),
parses `resolvedDir` from the response, validates it against the
floor (absolute, exists, is-directory, not .app), and only then
forwards to `shell.openPath`. The renderer never names the path
directly, so a compromised renderer cannot escalate to opening
arbitrary local paths — it can only name a project the daemon
already knows about, and the canonical path comes from the daemon's
own response.

Surface changes:

  - `runtime.ts`: `createProjectRootGate` removed.
    `fetchResolvedProjectDir(webUrl, projectId, fetchImpl?)` added.
    `validateExistingDirectory` rejects `.app` suffix after the
    realpath check (so symlinked launders are caught too).
    `shell:open-path` handler signature changes from `(path)` to
    `(projectId)`; `shell:register-project-root` handler removed.

  - `preload.cts`: `openPath(projectId)`; `registerProjectRoot`
    removed from the bridge surface.

  - `apps/web/src/types/electron.d.ts`: type updated to match.

  - `useTerminalLaunch.ts`: `open(projectId)` instead of
    `open(dir)`.

  - `ProjectView.tsx`: passes `project.id` to
    `terminalLauncher.open`; the registerProjectRoot useEffect is
    deleted. Toast text still reads `projectDir` (from
    `useProjectDetail.resolvedDir`) for fallback messages — the
    *display* path is independent of the *open* mechanism.

  - `apps/packaged/tests/desktop-project-root-gate.test.ts`:
    rewritten to cover `validateExistingDirectory` (8 cases
    including the new `.app` suffix and symlinked-bundle rejection)
    and `fetchResolvedProjectDir` (8 cases including empty/invalid
    project ids, daemon HTTP success/failure, missing resolvedDir,
    network error, and URL canonicalization).

Total: 16 passing tests, ~330 LOC churn including test rewrites.

Lesson learned (from the iteration loop, not the code): when a
reviewer asks for "ideally X, or at least Y," shipping Y with a
deferred-X note flags the gap rather than fixing it. Either ship X
or argue Y is sufficient; don't middle-ground.

* feat(contracts,sidecar-proto): add desktop-auth IPC + fromTrustedPicker

Schema-only prep for the PR #974 round-3 fix. Adds the two type
extensions the daemon HTTP gate and the desktop main process will
build on:

- packages/sidecar-proto: SIDECAR_MESSAGES.REGISTER_DESKTOP_AUTH, with a
  base64-validated `{ secret }` payload + RegisterDesktopAuthResult.
  Updates normalizeDaemonSidecarMessage to accept the new message and
  pins both branches (accept + reject) in tests/index.test.ts.

- packages/contracts: ProjectMetadata.fromTrustedPicker — a marker the
  daemon stamps on folder-imported projects whose POST /api/import/folder
  passed the desktop HMAC gate. The marker is privileged in the same
  way as `baseDir`: only the gated import handler sets it, and the
  desktop main process refuses to forward `shell.openPath` for
  folder-imported projects whose metadata lacks it.

* fix(daemon): gate /api/import/folder on desktop HMAC token

Closes the renderer→arbitrary-baseDir→shell.openPath bypass chain
flagged by lefarcen and mrcfps in round 3 of PR #974. Both reviewers
converged on the same gap: the previous round only moved path
resolution into the daemon, but renderer JS could still POST
/api/import/folder with any absolute path, get a project ID back, and
then call openPath(projectId) to reveal the attacker-chosen path.

Daemon-side closure:

- New module-scope desktop auth secret + setter exported from
  apps/daemon/src/server.ts. The secret is null at boot (web/standalone
  mode unaffected) and gets set when the desktop main process
  registers it over the daemon's sidecar IPC.

- New `verifyDesktopImportToken` pure helper. Verifies tokens shaped
  `${nonce}~${exp}~${signature}` against HMAC-SHA256(secret, baseDir +
  "\n" + nonce + "\n" + exp). Field separator is `~` (not `.`) because
  ISO 8601 expiries embed dots; `~` is in neither base64url nor ISO
  8601 character sets. Rejects expired tokens, replayed nonces, and
  expiries beyond 2× the 60s TTL.

- New middleware on POST /api/import/folder. When the secret is set,
  every request must carry a valid `X-OD-Desktop-Import-Token` header
  bound to the requested baseDir. Rejected requests return 403 with
  FORBIDDEN. When the secret is unset (no desktop registered), the
  route is unchanged so web-only deployments and standalone daemons
  keep working.

- Trusted imports get `metadata.fromTrustedPicker: true` stamped on
  the project. POST /api/projects and PATCH /api/projects/:id reject
  any client-supplied `fromTrustedPicker` (privileged the same way as
  `baseDir`), and the PATCH preservation block re-stamps the marker
  on partial-metadata patches so it cannot be silently stripped.

- Daemon sidecar IPC handler: REGISTER_DESKTOP_AUTH calls
  setDesktopAuthSecret with the base64-decoded secret. The HTTP and
  IPC servers share a process so the registration takes effect
  immediately for the next inbound /api/import/folder call.

Tests:

- apps/daemon/tests/desktop-import-token-gate.test.ts (15 cases): web
  mode acceptance, no-token rejection, malformed-token rejection,
  wrong-secret rejection, wrong-baseDir rejection, expired rejection,
  oversized-window rejection, valid mint + trusted-picker stamp +
  replay rejection, plus 6 pure-helper cases for verifyDesktopImportToken.
  afterAll() clears the secret to keep the shared HTTP server clean
  for sibling test files.

- apps/daemon/tests/projects-routes.test.ts (+2 cases): POST and PATCH
  reject `fromTrustedPicker` in client-supplied metadata.

Existing folder-import-route.test.ts continues to pass because none of
those tests register a desktop secret; the gate stays dormant.

* fix(desktop,web): atomic pickAndImport replacing pickFolder; openPath trusted-picker check

Closes the renderer→arbitrary-baseDir bypass at the bridge boundary.
The renderer no longer receives a raw filesystem path from the main
process; the picker dialog and the import call live in a single
main-process transaction.

Desktop main:

- runDesktopMain generates a per-process 32-byte secret and registers
  it with the daemon over the daemon's sidecar IPC *before* the
  BrowserWindow is created. registerDesktopAuthWithDaemon retries a
  few times because tools-dev / tools-pack spawn daemon, web, and
  desktop as siblings, so the daemon may not be listening yet on
  desktop boot. A failed registration logs a warning and the runtime
  refuses pickAndImport calls (no secret → no token can be minted).

- runtime.ts replaces the `dialog:pick-folder` IPC with
  `dialog:pick-and-import`. The handler shows the picker, mints an
  HMAC token bound to the chosen path, POSTs /api/import/folder via
  the discovered web URL with the token + body, and returns the
  daemon's ImportFolderResponse to the renderer (or a structured
  failure envelope). Renderer never sees the path or the token.

- shell:open-path now consults a new pure helper
  `isOpenPathAllowedForProject` that refuses folder-imported projects
  whose metadata lacks `fromTrustedPicker: true`. This is the literal
  interpretation of mrcfps's round-3 follow-up: openPath is gated to
  projects whose resolvedDir came from the trusted-picker flow, not
  just transitively via the import gate. Native projects (no
  baseDir → daemon-owned <projectsRoot>/<id>) are always safe to open.

- fetchResolvedProjectDir now returns a `ResolvedProjectDirContext`
  with hasBaseDir + fromTrustedPicker so the openPath handler can
  enforce the marker check.

- New `signDesktopImportToken` pure helper mirrors the daemon-side
  signer with the same `~`-separated wire shape, exported for the
  packaged workspace's test file.

Preload bridge:

- `pickFolder` is deleted. The new `pickAndImport(init?)` returns the
  daemon's import response or a structured failure. `openPath` keeps
  its existing signature; its trust gate now lives in the main
  process.

Web renderer:

- electron.d.ts drops `pickFolder` and adds `pickAndImport` with the
  shared DesktopPickAndImportResult union pulled from contracts.

- NewProjectPanel: when running on Electron (pickAndImport bridge
  present), the "Open folder" button calls pickAndImport atomically
  and forwards the response through a new `onImportFolderResponse`
  prop. On web (no bridge), the existing manual baseDir input keeps
  working — browser builds have no shell.openPath surface so a
  renderer-named path cannot escalate.

- EntryView and App.tsx pass through the new callback. App's
  `handleImportFolderResponse` updates state from the response without
  a second fetch (the import already happened in the main process).

Tests (apps/packaged/tests/desktop-project-root-gate.test.ts):

- 3 cases for `isOpenPathAllowedForProject`: native allowed,
  trusted-picker allowed, legacy folder-import refused.

- 6 cases for `signDesktopImportToken`: shape (~-separated), determinism,
  signature flips when secret/baseDir/nonce/exp changes.

- Existing fetchResolvedProjectDir cases extended for the new
  `context` shape and additional cases that prove the metadata
  inspection (hasBaseDir, fromTrustedPicker) reads the daemon
  response correctly.

* fix(daemon): make desktop import-folder gate fail-closed (PR #974 round 4)

lefarcen P1 on round 3 of PR #974: the gate's `secret == null → accept`
branch (originally intended to keep web-only deployments unaffected)
let a renderer bypass the import boundary in two real desktop edges:

- Startup race: desktop's REGISTER_DESKTOP_AUTH IPC hasn't reached the
  daemon yet, but the renderer is already alive in the BrowserWindow
  and races to fetch /api/import/folder directly with arbitrary baseDir.
- Daemon restart mid-session: the new daemon process boots tokenless
  while a desktop is still running. Same shape: renderer fetches the
  route, daemon falls through to "web mode", accepts the untrusted
  baseDir. shell.openPath rejects (no fromTrustedPicker marker) but
  the daemon's other file APIs (read/write project files, list
  directories) operate on the attacker-chosen path.

Two coordinated mechanisms close that:

(1) Sticky in-process flag. `desktopAuthEverRegistered` flips to true
    on first non-null `setDesktopAuthSecret(...)` and never goes back.
    setDesktopAuthSecret(null) (used by tests) does NOT relax the gate
    so production code can never silently fall back to fail-open. Add
    `resetDesktopAuthForTests()` for vitest cleanup.

(2) Orchestrator-pinned mode via OD_REQUIRE_DESKTOP_AUTH=1 read at
    module load. tools-dev / tools-pack / apps/packaged set this when
    the daemon is spawned in a desktop-bundled flow (separate commits).
    With the env set, the gate is active from request 0 — a renderer
    racing /api/import/folder before registration completes gets a
    503 DESKTOP_AUTH_PENDING (transient, retry).

Standalone-daemon (web-only) deployments where neither mechanism fires
keep the gate dormant and the route's behavior unchanged.

Also addresses lefarcen P3 (whitespace HMAC mismatch): the desktop
signs the exact picker output, so the daemon must verify the same
string. The previous version trimmed `baseDir` before HMAC, which
would reject legitimate paths whose final component carried edge
whitespace. Use the raw request-body baseDir for verification; the
existing trim()+realpath() logic still normalizes for fs operations.

New error code: `DESKTOP_AUTH_PENDING` (HTTP 503, retryable).

Tests:

- `stays fail-closed (503 DESKTOP_AUTH_PENDING) after a registered
  secret is cleared` — exercises the sticky flag.
- `verifies the exact request-body baseDir, not a trimmed version` —
  pins the round-4 P3 fix.
- All existing desktop-import-token-gate cases continue to pass; the
  beforeEach/afterEach/afterAll resetters now use
  resetDesktopAuthForTests() to honor the sticky flag.

* fix(tools-dev,packaged): pin desktop import-auth on daemon spawn

PR #974 round-4 P1 follow-through. The daemon-side fail-closed gate
needs OD_REQUIRE_DESKTOP_AUTH=1 in the daemon's spawn env whenever
the daemon is paired with a desktop, so the gate is active from
request 0 and the daemon-restart-mid-session bypass cannot reopen.

tools-dev:
- spawnDaemonRuntime accepts a `requireDesktopAuth` option that
  appends OD_REQUIRE_DESKTOP_AUTH=1 to the spawn env.
- startDaemon takes the same flag and additionally checks whether a
  desktop runtime is already alive in this namespace; either branch
  pins the env (revival case where the daemon died mid-session and
  the user runs `tools-dev start daemon` to bring it back up).
- startApp threads the bundled-target list down so the daemon spawn
  knows when desktop is queued in the same orchestration even though
  the daemon starts first.
- The `start` / `restart` / `run` command actions pass the resolved
  target list into startApp.

apps/packaged:
- Packaged builds always pair a desktop with the daemon, so
  startPackagedSidecars unconditionally sets OD_REQUIRE_DESKTOP_AUTH=1
  in the daemon child env. Headless builds also flow through this
  same path, so the same gate applies.

Standalone-daemon flows unaffected: `tools-dev start daemon` (alone,
no desktop running, no desktop in the bundled target list) does not
set the env, and the daemon's gate stays dormant — current web-only
behavior is preserved.

* fix(desktop,web): align project-id regex with daemon; surface pickAndImport failures

mrcfps round-4 nits on PR #974.

apps/desktop/src/main/runtime.ts (mrcfps #1): the previous client-side
regex `^[a-zA-Z0-9_-]+$` rejected `.` even though the daemon's
canonical isSafeId / POST /api/projects accept `[A-Za-z0-9._-]{1,128}`.
Result: dotted ids like `my-project.v2` were valid backend-side but
got "project id contains disallowed characters" before
fetchResolvedProjectDir even hit the network, regressing Continue in
CLI / Finalize for those projects. Align the regex with the daemon's
shape, comment-tag the rationale.

apps/packaged/tests/desktop-project-root-gate.test.ts: add a
regression case for a dotted id and one for the 128-char length cap
(the new regex exposes both, the old regex obscured the dotted one).

apps/web/src/components/NewProjectPanel.tsx (mrcfps #2): the
`if (!result || result.ok !== true) return` branch swallowed every
non-OK pickAndImport shape (`desktop auth secret not registered`,
`web sidecar URL not available`, daemon HTTP errors with details)
the same way as the explicit `{ canceled: true }` cancel — leaving
the user with a silent no-op when the trusted-picker flow couldn't
even get off the ground. Reserve silent-return for the cancel case
only; surface every other reason via a Toast (existing component,
already used by ProjectView for related Continue-in-CLI flows).
The new `formatPickAndImportErrorDetails` helper flattens daemon
ApiError envelopes into a single readable secondary line so the
operator sees both the category ("Open folder failed: daemon
returned HTTP 503") and the upstream reason
("desktop auth required but secret not yet registered").

* docs(architecture): document desktop folder-import auth boundary

lefarcen P3 on PR #974 round 4: the `Folder import` section in
docs/architecture.md still documented only realpath / sandbox /
RUNTIME_DATA_DIR checks and omitted the new desktop HMAC trust
boundary, replay/TTL behavior, fail-closed semantics, daemon-restart
edge, and legacy-import migration note. Without that subsection it's
hard to review whether the 60s TTL, the `~`-separated token shape,
or the legacy folder-imports needing re-pick are intentional product
decisions or overlooked gaps.

Add a "Desktop folder-import auth (PR #974)" subsection covering:
- The trust handshake (32-byte secret over sidecar IPC at desktop boot).
- Token shape (`${nonce}~${exp}~${signature}`), HMAC payload, and
  why `.` cannot be the field separator (ISO 8601 expiries embed dots).
- TTL and replay behavior (60s, single-use, 2× TTL upper bound).
- Fail-closed mechanisms — sticky in-process flag and
  OD_REQUIRE_DESKTOP_AUTH env var pinning.
- Web-only deployments are unaffected (browser builds have no
  shell.openPath surface).
- The `metadata.fromTrustedPicker` marker and the openPath-side
  defense-in-depth check.
- Legacy folder-imports need re-pick to use the Continue-in-CLI button.
- Daemon-restart edge: 503 DESKTOP_AUTH_PENDING until desktop
  re-registers; restart desktop to recover.

* fix(packaged): skip desktop-auth gate in headless mode (PR #974 round 5 P2)

Round 5 (lefarcen P2): packaged headless mode (daemon+web only, no
Electron) was inheriting OD_REQUIRE_DESKTOP_AUTH=1 from the round-4
unconditional pin in startPackagedSidecars. Headless never runs desktop
main, so no client could ever register an HMAC secret and folder import
returned 503 DESKTOP_AUTH_PENDING permanently — even though headless has
no shell.openPath surface to exploit.

Plumb a required `requireDesktopAuth: boolean` option through
startPackagedSidecars: apps/packaged/src/index.ts (Electron entry)
passes true; apps/packaged/src/headless.ts passes false. Extract
buildPackagedDaemonSpawnEnv as a pure helper so vitest can pin both
branches without spawning a child process.

Tests added in apps/packaged/tests/sidecars.test.ts cover both branches
plus OD_LEGACY_DATA_DIR / daemonCliEntry env forwarding edges.

Refs: nexu-io/open-design#974

* fix(desktop,daemon): lazy auth retry + canonical HMAC binding (PR #974 round 5 P1+P3)

Round 5 (lefarcen P1, mrcfps): a daemon restart under
OD_REQUIRE_DESKTOP_AUTH=1 left desktop holding a stale secret while the
new daemon process required a fresh registration — folder import
returned 503 DESKTOP_AUTH_PENDING permanently until the user restarted
desktop. Same dead-end if the startup handshake missed its retry window.

Round 5 (lefarcen P3): the daemon verified the HMAC against raw
request-body baseDir, then trimmed before realpath(). A picker selection
of "/tmp/foo " could authorize an import of "/tmp/foo" — token bound to
a different path than the one imported.

Three coordinated fixes:

1. P1 lazy retry: extract pickAndImportFolder as a pure helper that
   takes injected fetch / mintToken / registerDesktopAuth deps. On 503
   DESKTOP_AUTH_PENDING from /api/import/folder, re-invoke the
   registration callback once, mint a fresh token (new nonce + new exp
   keeps replay protection), and POST again. Single retry, no infinite
   loop. Other failure shapes return immediately to the renderer.

2. P1 wiring: runDesktopMain now ALWAYS passes desktopAuthSecret to the
   runtime regardless of whether the initial handshake succeeded, plus
   a registerDesktopAuthWithDaemon callback the runtime invokes lazily.
   Soften the startup warning text to match the new recovery semantics.

3. P3 binding: trim picker output ONCE on the desktop side before both
   signing the HMAC and POSTing. Daemon-side verification stays against
   raw request-body baseDir (round-4 behavior); the daemon's defensive
   trim before realpath() is now a no-op for desktop traffic and only
   load-bearing for web-mode callers (path.isAbsolute("  /foo  ") is
   false). End-to-end: desktop-signed string == request body == HMAC-
   verified string == realpath() input.

Tests:

- apps/packaged/tests/desktop-pick-and-import.test.ts (NEW, 7 cases):
  lazy-retry happy path; lazy-retry exhausted (re-register WAS called);
  single-attempt happy path (no unnecessary IPC); optional-callback
  no-op; non-503 failures bypass retry; network errors; non-PENDING 503
  bypasses retry.

- apps/daemon/tests/desktop-import-token-gate.test.ts: replace round-4
  whitespace test with two round-5 binding tests — the trimmed string
  flows end-to-end (HMAC verifies, project metadata.baseDir equals
  realpath of trimmed input), and a request whose body baseDir diverges
  from the HMAC-bound string is rejected 403.

docs/architecture.md §"Desktop folder-import auth" — update the daemon-
restart-edge bullet to describe the lazy-retry recovery (round 4 said
"restart desktop to recover", which is now wrong) and add a headless-
packaged-mode bullet describing the round-5 P2 gate exclusion.

Refs: nexu-io/open-design#974

* feat(sidecar-proto,daemon): surface desktopAuthGateActive over STATUS IPC (PR #974 round 6 prep)

Round 6 (mrcfps): the split-start dev flow `tools-dev start daemon` ->
`tools-dev start desktop` was leaving the daemon ungated because
`OD_REQUIRE_DESKTOP_AUTH=1` is only injected when daemon and desktop
spawn in the same orchestrator invocation. To fix that, tools-dev needs
to introspect the running daemon's gate state before launching desktop
main — but the existing STATUS IPC didn't carry the flag.

This commit extends `DaemonStatusSnapshot` with a required
`desktopAuthGateActive: boolean` and wires the daemon sidecar's STATUS
handler (and the public `status()` method on the handle) to recompute
the value from `isDesktopAuthGateActive()` per request, since the flag
flips after `REGISTER_DESKTOP_AUTH` and stays sticky.

Extracted `withCurrentDesktopAuthGate(snapshot)` as a tiny pure helper
so the wiring is testable without booting a real IPC server. The new
test pins four scenarios:
- no secret registered (web-only mode) -> false
- after `setDesktopAuthSecret(buf)` -> true
- after `setDesktopAuthSecret(null)` (sticky) -> still true
- input snapshot's stale value is overridden by the live flag

The orchestrator-side consumer lands in the next commit
(`tools/dev/src/desktop-auth-gate.ts`).

Refs: nexu-io/open-design#974

* fix(tools-dev): auto-restart ungated daemon before desktop start (PR #974 round 6 mrcfps)

Round 6 (mrcfps): the split-start dev sequence
`tools-dev start daemon` -> `tools-dev start desktop` was leaving the
daemon running without `OD_REQUIRE_DESKTOP_AUTH=1`. The env var is
only injected when (A) daemon and desktop spawn in the same
orchestrator invocation (`startApp` line ~682) or (B) a desktop
runtime is already alive at daemon spawn time (`startDaemon` lines
~595-596). Neither fires for the split flow, so a renderer (or any
local HTTP client) could `POST /api/import/folder` directly with an
arbitrary `baseDir` before the desktop's first registration POST.
Round-5's lazy retry didn't help: it triggers on `503 DESKTOP_AUTH_PENDING`,
and the ungated daemon returns 200.

Close the gap by introspecting the running daemon's
`desktopAuthGateActive` (added to the STATUS IPC in the prior
commit) at the start of `startApp(DESKTOP, ...)`. When the daemon
reports the gate inactive, stop the daemon (and web, if running),
respawn the daemon with `requireDesktopAuth: true`, restart web,
then proceed with the desktop start. Restart order is critical and
pinned by tests: web stops FIRST (so the web->daemon proxy doesn't
serve a transient 502 against the down-then-up daemon), then daemon
stops, then daemon respawns gated, then web restarts.

The bundled-targets path (`pnpm tools-dev`) is unaffected because
trigger (A) already armed the gate at first daemon spawn — the
helper costs one ~800ms STATUS IPC roundtrip and returns no-op.

Helper lives in its own module (`tools/dev/src/desktop-auth-gate.ts`)
so the regression test can import it without triggering the
`cli.parse()` side effect at the bottom of `tools/dev/src/index.ts`.
Five `node:test` cases pin the call sequence — no daemon, gate
active, gate inactive + no web, gate inactive + web running, log
shape — so a future refactor can't silently regress the gate.

Two synthetic `DaemonStatusSnapshot` literals in `inspectAppStatus`
and `inspect` (used when the IPC is unreachable) get
`desktopAuthGateActive: false` to satisfy the now-required type
field — semantically correct since "no daemon answering" trivially
means "no gate active."

`docs/architecture.md` adds a new bullet under the Desktop folder-
import auth section describing this auto-restart behavior.

Refs: nexu-io/open-design#974

* fix(daemon): combine finalize request-abort + timeout signals (PR #974 round 7 lefarcen P1)

Round 6 wired the route handler to pass `finalizeAbort.signal` into
`finalizeDesignPackage`, but the helper only created its own
DEFAULT_TIMEOUT_MS controller when no caller signal was supplied. The
result: a client that stayed connected could hold the finalize lock and
upstream call indefinitely. Always create the timeout controller; when
the caller passes a signal, combine both via `AbortSignal.any` so
neither cancel path replaces the other.

Adds two regression tests in finalize-design.test.ts:
- timeout fires when caller signal never aborts
- pre-aborted caller signal still cancels

Adds an internal `timeoutMs` option to FinalizeOptions so tests can
exercise the abort path without a 120 s wait or fake-timer chains.
Production callers omit it; default remains DEFAULT_TIMEOUT_MS.

* fix(daemon): allow PATCH preserving existing fromTrustedPicker marker (PR #974 round 7 lefarcen P2)

The PATCH /api/projects/:id handler was rejecting any metadata that
contained `fromTrustedPicker`, including the unchanged `true` marker
that the linked-folder UI re-spreads when editing `linkedDirs`. Trusted
folder-imported projects could not update other metadata fields without
400-ing on their own marker.

Switch the rejection condition from `'in'` to a value comparison: only
reject when the incoming value differs from the persisted one
(`patch.metadata.fromTrustedPicker !== existingMeta?.fromTrustedPicker`).
That keeps acquisition (existing=undefined, patch true) and flip
(existing=true, patch false) attempts blocked while letting the UI
re-spread the existing marker.

POST /api/projects stays strict; that path has no existingMeta.

Adds two regression tests in desktop-import-token-gate.test.ts:
- allows PATCH preserving the existing fromTrustedPicker:true marker
- rejects PATCH that flips fromTrustedPicker on a trusted project

* fix(desktop,packaged): main-process api uses daemon URL not webUrl (PR #974 round 7 lefarcen P2)

Packaged builds load the renderer from `od://app/` and report that URL
through `discoverWebUrl`. But Node-side `globalThis.fetch` (undici) does
not route through Electron's registered `od://` protocol handler — that
handler runs in the renderer's protocol scope, not in main-process Node.
So `pickAndImportFolder` and `fetchResolvedProjectDir` calls from main
silently failed in packaged builds against the protocol scheme.

Add `discoverDaemonUrl` to `DesktopRuntimeOptions` and `DesktopMainOptions`.
The packaged shell already has the sidecar's real `http://127.0.0.1:<port>`
URL (`sidecars.daemon.url` from STATUS IPC) — thread it through to the
runtime. Main-process API calls now prefer the daemon URL and fall back
to the renderer URL for tools-dev (where it is itself http://127.0.0.1).

`PickAndImportFolderDeps.webUrl` renamed to `apiBaseUrl` so the boundary
is explicit at the type level; `fetchResolvedProjectDir`'s first
parameter renamed similarly. tools-dev callers see no behavior change —
their web URL is already an http://127.0.0.1 URL Node fetch can hit.

Test (`apps/packaged/tests/desktop-pick-and-import.test.ts`):
- existing 7 cases updated to the new prop name (no behavior change)
- new case pins URL composition: builds `${apiBaseUrl}/api/import/folder`
  and never produces a custom-protocol URL.

Note for review: this test pins URL composition; full Electron protocol
handler integration (renderer fetch through `od://`) is not exercised in
unit tests here.

* fix(tools-dev): preserve daemon/web ports across desktop-auth gate restart (PR #974 round 7 lefarcen P2)

Round 6 added the split-start auto-restart in ensureDaemonGateForDesktop
to close the dev-flow gap where `start daemon` then `start desktop`
left the daemon ungated. The restart was passing the current
`start desktop` CLI options to startDaemonGated/startWeb, which meant a
stack started with `--daemon-port 17456 --web-port 17573` could be
silently moved to random ports during the hardening restart, breaking
browsers and scripts pinned to those ports.

Extract the running ports from the STATUS snapshots (daemon.url and
web.url) and forward them as explicit `{ port }` callback args. The
closure in `tools/dev/src/index.ts` overrides the corresponding option
when a port was extracted; null falls back to the original CLI flags.

Adds three regression tests in tools/dev/tests/desktop-auth-gate.test.ts:
- preserves the running daemon port across the hardening restart
- preserves the running web port across the hardening restart
- falls back to caller options (port:null) when the URL has no port

* fix(web): refresh useDesignMdState on file/chat events (PR #974 round 7 mrcfps)

useDesignMdState() previously only recomputed on mount and on explicit
refresh() (called once after finalize). Once the user kept working —
editing files or sending more chat turns — the stale/fresh badge could
drift out of sync because file mtimes and conversation updatedAt moved
past the recorded generatedAt without the hook re-checking.

Hook accepts an optional `refreshKey: number` arg; ProjectView keeps a
counter and bumps it on three events:
- file-changed SSE (covers tool-emitted file mutations)
- live_artifact* SSE (covers chat turns that emit artifacts)
- streaming `true → false` edge (covers pure-text chat turns)

The hook treats refreshKey as a compute() dep; React's Object.is
comparison short-circuits the no-op renders, so each bump is a single
recompute pass.

Adds a regression test in useDesignMdState.test.tsx:
- flips stale state after a refreshKey bump without remounting

* fix(web): degraded-state useDesignMdState on malformed provenance (PR #974 round 7 mrcfps)

useDesignMdState used to report `{ isStale: false, staleReason: null }`
when the parser could not extract a comparison timestamp from the
DESIGN.md `## Provenance` section. The pinned test made that the
documented behavior. As mrcfps pointed out, that fails open exactly
when the freshness signal is most untrustworthy: any provenance-
formatting drift silently disables the staleness warning.

Extend `DesignMdStaleReason` with a third variant `'unknown-provenance'`.
On `generatedMs === null`, return `{ isStale: true, staleReason: 'unknown-provenance' }`.
ContinueInCliButton renders a distinct chip text "Spec freshness
unknown — regenerate to refresh signal" for that variant; the button
stays enabled because not-comparable is not the same as broken state.

Tests:
- modify the existing pinned test to assert the new degraded state
- add an end-to-end useDesignMdState test feeding a malformed Provenance
  section through compute() so a regression that re-pins fresh-on-null
  at the hook level (not just computeStale) fails fast
- add ContinueInCliButton render + click tests for the new chip

---------

Co-authored-by: DevForgeAI CI/CD Engineer <devforge-ai@development.ai>
Co-authored-by: lefarcen <935902669@qq.com>
2026-05-10 11:44:32 +08:00
nmsnandGitHub 726a9ab5be fix: hide surface filter tabs with zero count in Design Systems view (#922) (#965)
* fix: prevent comment popover header overflow when label is too long

- Add min-width: 0 and overflow: hidden to comment-popover-head div
- Add text-overflow: ellipsis and white-space: nowrap to strong and span
- Add flex: 0 0 auto to close button to keep it fixed width
- Add title attribute to header div and close button for hover tooltip

* fix: hide surface filter tabs with zero count in Design Systems view (#922)

* fix: reset surfaceFilter and category when counts become zero

Handle dynamic systems changes where the currently selected surface
filter has zero items. When surfaceFilter is no longer valid, fall back
to 'all'. Also reset category to 'All' when it is no longer present in
the filtered list.

Fixes review feedback on PR #965.

* fix: move categories memo above useEffect to avoid TDZ

categories was referenced in the useEffect dependency array before
its declaration, causing a Temporal Dead Zone error. Move the
categories useMemo above the effect.
2026-05-10 11:28:56 +08:00
0dfa922208 fix(landing-page): correct SEO canonical, add robots.txt + favicons (#1061)
The Astro `site` default was `https://open-design.dev`, but the live
Cloudflare Pages deployment is bound to `open-design.ai`. As a result
every `<link rel="canonical">`, `og:url`, and sitemap entry pointed at
the wrong origin, and search engines saw no robots.txt or favicon at
the apex.

- astro.config.ts: switch the default `site` to `https://open-design.ai`
  and document that `OD_LANDING_SITE` stays as the preview-deploy hatch.
- astro.config.ts: filter `/og/` out of the sitemap; that route is the
  1200x630 OG screenshot surface and already carries `noindex`.
- public/robots.txt: allow-all + Disallow `/og/` + canonical sitemap URL.
- public/favicon.svg: 32x32 SVG mark mirroring the on-page `Ø` brand
  glyph (ink ground, paper-stroked italic ellipse, coral slash).
- public/apple-touch-icon.png: 180x180 PNG rendered from the same
  geometry without the rounded corners (iOS applies its own mask).
- index.astro: link `/favicon.svg` (`type="image/svg+xml"`) and
  `/apple-touch-icon.png` from the document head.

Verified locally with `pnpm --filter @open-design/landing-page build`:
the rendered head emits `https://open-design.ai/` for canonical and
`og:url`, the sitemap contains exactly the canonical `/` URL, and the
two `landing-page-deploy.yml` verification scripts (no client JS, >=16
Cloudflare resized image URLs, no local /assets/*.png) still pass.

Co-authored-by: joey <joey@joeydeMacBook-Air.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 11:28:04 +08:00
PerishFireandGitHub cc343f8828 ci: optimize beta release packaging cache (#1095)
* ci: optimize beta release packaging cache

* fix: version windows builder cache

* fix: forward linux app version in container
2026-05-10 10:11:05 +08:00
c61ba320fd feat(nix): Add official flake with home-manager and NixOS support (#402)
* nix: add official flake with home-manager and nixos modules

* Pin pnpm version

* Format README.md

* Populate PATH files to discover installed CLIs

* Revert "Populate PATH files to discover installed CLIs"

This reverts commit 18d88781a88b8781913cf5a8b680dfb38eabf7e4.

* Fix missing sqlite issue

* Fix system issue

* Reapply "Populate PATH files to discover installed CLIs"

This reverts commit d02ea994e6.

* Handle different ports for web frontend

* Provide documentation for getting pnpm hash

* Enable nix flake checks for code changes

* Set `OD_WEB_PORT` on daemon when declared

* fix: Fix environmentFile for macOS targets

* chore: Ignore nix and direnv related files

* fix: Read version directly from `package.json`

* feat: Make nix shell entry prettier

* chore: Update pnpm hashes

* chore: Bump `pnpm` hashes

* docs: Add blurb about dev shell in `README.md`

* Address review comments

* Add support for `OD_WEB_ORIGINS`

* Fix `isLocalSameOrigin`

* Update pnpm checksums

* docs: Update documentation on host origins

* Move allowedOrigins mapping out of the webFrontend.enable guard

* fix: Bump pnpm hashes

* Remove changes to `daemon` with `main` changes

`main` merged a feature that addressed our need for allowed origins.
Since this feature branch no longer needs it, remove any remaining
changes in `daemon` code so that this is a pure Nix change.

* Update documentation around `OD_DAEMON_URL`

* Rewrite option docs to match same-origin proxy contract

The port, webFrontend, and webFrontend.port option descriptions still
described OD_DAEMON_URL as the runtime contract for the SPA, but the
SPA issues relative /api/*, /artifacts/*, /frames/* requests and there
is no runtime daemon-URL injection. Rewrite the three blocks to
describe what the caddy / custom proxy must actually do.

* Document daemon-side requirements for custom-server proxy paths

The bring-your-own-server path in section (3) and the same-origin
contract in section (4) understated what the daemon needs: any proxy
whose origin differs from the daemon's bind (including loopback
split-port like 127.0.0.1:8080 while the daemon stays on :7457) is
403'd by the daemon's same-origin gate until told about that origin.

Add a callout under section (3)'s table, expand section (4) with a
decision table covering same-port, loopback split-port (OD_WEB_PORT or
webFrontend.allowedOrigins), and non-loopback (webFrontend.allowedOrigins)
cases, and rewrite the webFrontend.allowedOrigins option description to
enumerate the cases where it's required and surface OD_WEB_PORT as an
alternative for the loopback split-port case.

---------

Co-authored-by: lefarcen <935902669@qq.com>
2026-05-09 23:50:16 +08:00
020ecb1d6a feat(provider-models): sort fetched models alphabetically (#1097)
Co-authored-by: haolin122 <hl6593@nyu.edu>
2026-05-09 23:48:31 +08:00
617fb043fe feat(settings): add fetch models button for BYOK providers (#1034)
* feat(settings): add fetch models button for BYOK providers

* fix(settings): exclude Ollama from fetch models, add manual-entry hint

* fix(provider-models): classify non-JSON upstream errors by HTTP status

* fix(i18n): drop redundant English overrides from non-English locales

* fix(provider-models): allow ollama through allowlist, return unsupported_protocol

---------

Co-authored-by: haolin122 <hl6593@nyu.edu>
2026-05-09 22:28:03 +08:00
e2713cb0f4 refactor(web): flush sidebar tabs to edge with internal scrolling (#1038)
Remove borders, padding, and overflow from the sidebar container so the
NewProjectPanel tab component fills the full sidebar width. Move scroll
responsibility into the tab body so only the content panel scrolls, not
the entire sidebar. Tab buttons sit flush at the top edge.

Co-authored-by: 克隆(宗可龙) <zongkelong@soyoung.com>
Co-authored-by: mrcfps <mrc@powerformer.com>
2026-05-09 22:23:33 +08:00
Justin GaoandGitHub 49b55761d3 feat(daemon+web): add install/uninstall for skills & design systems (#1003)
* feat(daemon+web): add install/uninstall for skills & design systems (#497 Phase 2)

Phase 2 of the Library Settings feature. Adds the ability to install
skills and design systems from GitHub repos or local paths, and
uninstall user-installed items. Built-in items remain read-only.

Daemon:
- Multi-directory scanning: built-in + ~/.open-design/{skills,design-systems}
- Install from GitHub (git clone --depth 1) or local path (symlink)
- Uninstall removes cloned dirs or symlinks under user dir only
- 4 new API routes: POST install + DELETE for skills and design systems
- Reuse isBlocked() from linked-dirs.ts for path safety

Web:
- Install form with GitHub/Local tab switch in Library settings
- Source badge ("Built-in" / "Installed") on cards
- Uninstall button (trash icon) on installed items only
- Native-language i18n for all 17 locales

Contracts:
- Add source field to SkillSummary and DesignSystemSummary
- Add InstallInput, InstallSkillResponse, InstallDesignSystemResponse, UninstallResponse types

* fix(daemon): follow symlinks in scanners and respect OD_DATA_DIR for user libraries

Two fixes from PR review feedback:

1. Scanners (skills.ts, design-systems.ts) used Dirent.isDirectory() which
   skips symlinks on macOS/Linux, so locally-installed items never appeared
   in API responses. Now also accepts isSymbolicLink() entries.

2. User library directories were hardcoded under os.homedir(), ignoring
   OD_DATA_DIR. This broke test isolation and packaged runs. Now derived
   from RUNTIME_DATA_DIR so they respect the data dir override.
2026-05-09 22:19:48 +08:00
SidandGitHub 63a9a08b83 fix(daemon): prefer opencode-cli over opencode binary so OpenCode Desktop installs resolve to the CLI (#814) (#818)
OpenCode Desktop ships two binaries on PATH after install:

  - `opencode`     — GUI launcher; double-clicking it opens the
                     desktop app, never reads stdin.
  - `opencode-cli` — headless CLI; speaks the
                     `run --format json --dangerously-skip-permissions -`
                     protocol the daemon expects.

The OpenCode AGENT_DEF set `bin: 'opencode'`, so
`resolveAgentExecutable` resolved the GUI launcher for every user
with the desktop install. The daemon happily spawned that binary,
but the GUI launcher discards stdin and never emits the JSON event
stream — so the agent silently produced no output for the entire
conversation. Issue #814 tracks the user-visible symptom.

Fix: declare `bin: 'opencode-cli'` and `fallbackBins: ['opencode']`
on the OpenCode adapter. `resolveAgentExecutable` walks `bin`
first, then each fallback in order, so this gives us:

  - Desktop install (both binaries on PATH) → resolves
    `opencode-cli` (the CLI), GUI is ignored. The #814 fix path.
  - Legacy CLI-only install (only `opencode` on PATH, no
    `-cli` suffix) → falls through to `opencode`, which is the
    actual CLI in that install shape, so the daemon keeps working
    unchanged.
  - Neither installed → falls through to null, same as before.

Same mechanism Claude Code uses to fall back to `openclaude`
(issue #235), so this is well-trodden ground in the codebase.

Tests:
- One declarative test asserts `opencode.bin === 'opencode-cli'`
  and `opencode.fallbackBins.includes('opencode')` — runs on every
  platform including Windows, catches anyone who accidentally
  flips `bin` back to the GUI name.
- Two filesystem-backed tests pin the resolution priority: when
  both binaries are on PATH (the desktop case) `opencode-cli`
  wins; when only the legacy `opencode` is on PATH the fallback
  takes over.
- Existing comment guarding "most agents have a single binary"
  updated to remove `opencode` from the example list.

Verified locally:
- daemon vitest: 993/993
- daemon `tsc -p tsconfig.json` and `tsc -p tsconfig.tests.json`: both clean
- i18n-check passes

Reported by @giuliastro, diagnosed by @lefarcen who also asked for
the `opencode-cli --version` output in the issue thread — happy to
narrow this further if their reply turns up subcommand differences
between the two binaries.

Fixes #814
2026-05-09 22:11:46 +08:00
FinnandGitHub d30cf18941 feat(skills): add release notes one pager + supporting docs (#873)
* add release notes one pager skills and supporting templates

* Update SKILL.md and template.html for release notes: refined upgrade note and enhanced responsive design elements in the template.

* Update SKILL.md to clarify topnav CTA label and href destination requirements, and enhance instructions for handling missing details in release notes.

* Refactor buttons to links in release notes templates and checklist: updated button elements to anchor tags for improved navigation and added explicit checklist items for handling missing sections in release notes.

* Update topnav link labels in SKILL.md and template.html for release notes: changed placeholder links to specific destinations (#added, #fixed, #upgrade-note) for improved navigation clarity.

* Enhance SKILL.md by adding section ID requirements for release notes structure: ensure each section (Added, Fixed, Breaking changes, Known issues, Upgrade note) includes a corresponding `id` attribute for improved navigation and consistency.

* Update example.html for release notes: refine title, update logo text, and modify section summaries to indicate no entries or actions required.

* Add consistent spacing for lists in template.html and update checklist.md to require matching id anchors for release-note sections

* Update SKILL.md, example.html, checklist.md, and layouts.md for improved clarity and structure in release notes

* fix: p2 anchor + p2 dead links

* fix: add id to section block

* fix: replace placeholder URLs with real destinations in SKILL.md, checklist.md, and layouts.md

* fix: update release notes guidelines to omit CTAs without real destinations and ensure no placeholder URLs remain

* fix: clarify CTA handling in release notes and remove placeholder links from template

* fix: add guidelines for using Layout 7 in release notes to remove header CTA row

* fix: add 'release-notes-one-pager' to localized content IDs for French, Russian, and German

* fix: remove 'release-notes-one-pager' from localized content IDs for French, Russian, and German
2026-05-09 22:10:36 +08:00
Nicholas-XiongandGitHub 74c9158410 fix: increase top padding in modal footer for better visual balance (#957)
* fix: increase top padding in modal footer for better visual balance

Fixes #913

The primary action button in modal footers (e.g., Deploy to Cloudflare Pages)
was sitting too close to the top border, making the footer area feel cramped.

Changes:
- Increased top padding from 12px to 16px in .modal-foot
- Kept bottom padding at 12px to maintain existing bottom spacing
- Left and right padding unchanged (uses --modal-padding variable)

This provides better visual balance and makes the primary action button
feel less cramped against the divider line above it.

* fix: add fallback value for --modal-padding variable

The --modal-padding variable is only defined under .modal-settings,
but .modal-foot is also used by generic modals (FileViewer, SketchEditor).
Without a fallback, the padding declaration is invalid for those modals.

This adds a fallback value of 22px to ensure the footer padding works
for all modal types, not just settings modals.
2026-05-09 22:07:54 +08:00
5390541f34 fix daemon browser opener on Windows (#953)
Co-authored-by: Sapientropic <194952804+Sapientropic@users.noreply.github.com>
2026-05-09 22:04:07 +08:00
Marc ChanandGitHub ad2c03d106 fix(packaged): build desktop before typecheck (#1093) 2026-05-09 22:00:16 +08:00
Jie ZhuandGitHub 84f4b03cb9 fix: prevent project type selector arrows from overlapping tabs (#1091)
* fix: prevent project type selector arrows from overlapping tabs

* fix: apply padding to parent container for arrow safe zone
2026-05-09 21:59:24 +08:00
Jie ZhuandGitHub 68dc78030a fix(web): prevent creating new conversation when current is empty (#1086)
* fix(web): prevent creating new conversation when current is empty

* fix(web): use ref to track loaded conversation for race-free empty check

* fix(web): eagerly update ref on new conversation to prevent rapid-click duplicates
2026-05-09 21:55:51 +08:00
039cc3d674 fix(settings): add install onboarding links for unavailable local CLIs (#985)
* fix(settings): add install onboarding links for unavailable local CLIs

* fix(settings): rename Claude config dir label to config directory

---------

Co-authored-by: mrcfps <mrc@powerformer.com>
2026-05-09 21:49:44 +08:00
Marc ChanandGitHub 3bcb3547d1 fix(actions): map pull_request_target to contributor bot event (#1092) 2026-05-09 21:49:13 +08:00
SidandGitHub 5db578123e fix(web): dispatch Examples preview on od.preview.type (#897) (#1001)
* fix(web): dispatch Examples preview on od.preview.type (#897)

The Examples gallery unconditionally fetched `/api/skills/:id/example`,
and the daemon endpoint only resolves HTML files (`example.html`,
`assets/template.html`, `assets/index.html`, `examples/*.html`). Skills
that declare `od.preview.type: image` (`hatch-pet`) or
`od.preview.type: markdown` (`dcf-valuation`, `last30days`,
`x-research`) ship no such HTML — the fetch returns 404 and the modal
landed on the misleading "Couldn't load this example. The example HTML
failed to fetch." copy.

Dispatch on `previewType` at the data layer (`fetchSkillExample`) and
at the render layer (`PreviewModal`):

- `fetchSkillExample(id, previewType)` short-circuits any non-`html`
  value to `{ unavailable: true, kind }` without firing a network call.
- `PreviewView` grows an optional `unavailable: { kind }` shape; the
  modal renders a calm "no shipped preview" placeholder distinct from
  loading and error states. The Share menu disables (no HTML to export).
- `ExamplesTab` tracks `previewUnavailable` per skill alongside the
  existing `previews` / `previewErrors` maps; the card placeholder
  swaps to "open to learn more" copy so users don't hover waiting for
  a render that won't come.
- New `preview.unavailableTitle` / `preview.unavailableBody` and
  `examples.unavailablePlaceholder` / `examples.shareUnavailable` keys
  shipped across all 17 locales. Body copy uses the raw preview kind
  (`{kind}` placeholder) so future kinds slot in without a copy change.

Tests: registry-level coverage that the dispatch never hits the network
for non-html types; PreviewModal-level coverage that the unavailable
affordance is mutually exclusive with loading/error and disables the
Share menu; ExamplesTab-level coverage that the gallery renders the
unavailable state for image/markdown skills and routes html skills
through the existing fetch path. Updated the existing `#860` retry
regression test for the new two-arg signature.

* fix(web): use neutral noun for preview.unavailableBody copy (#1001 review)

P3 from lefarcen: `'a {kind} document'` reads awkwardly when `{kind}`
is `image` ("a image document") and the article disagreement
undermined the PR body's claim that future kinds slot in without
copy changes. Drop the article and replace `document` with a more
neutral noun (`output` / `resultat` / `产物` / `出力` / etc.) so
every kind reads naturally:

- `produces {kind} output` (English)
- `produit un résultat {kind}` (French)
- `生成 {kind} 产物` (Simplified Chinese)
- … and 14 more

`{kind}` placeholder stays literal in every locale; surrounding
vocabulary for skill / prompt / chat preserved per existing
file conventions.
2026-05-09 21:32:45 +08:00
CIoudherdandGitHub 724d071c01 feat: add design file rename support (#894)
* feat(contracts): add project file rename contract

* feat(daemon): add safe project file rename API

* feat(web): support renaming design files

* fix(daemon): handle case-only file renames

* fix(web): prevent rename collisions with pending sketches

* fix(daemon): preserve source names during rename

* test(daemon): cover rename symlink escapes

* fix(daemon): avoid clobbering rename targets

* test(web): align rename tests after rebase

* test(web): align rename tests with latest main
2026-05-09 21:24:36 +08:00
meshackmandGitHub 461a312002 fix: fix link handling in example preview iframe sandbox (#701)
* fix: fix link handling in example preview iframe sandbox

* fix: added missing single quote

* fix(srcdoc): security/correctness issues
- Use location.hash = href for hash-link navigation so hashchange
  events, CSS :target, history/back and named anchors work correctly
- Always pass 'noopener,noreferrer' to window.open() for _blank links
- Guard e.target with instanceof Element before calling .closest() to
  avoid TypeError on text node click targets

* fix(srcdoc): security/correctness issues
- Added a protocol allowlist
- Re-navigation to same hash
- Consistency with rest of shim IIFE using var instead of let and const

* fix(srcdoc): scroll to top on bare hash links (#)

Intercept href="#" clicks and scroll to top of page in the iframe sandbox to mimic native browser behaviour.

* test(web): update PreviewModal sandbox test assertions to include popup flags
2026-05-09 21:24:25 +08:00
0264ad6150 fix(web): scroll Settings content back to top on section change (#997)
Switching to a different settings section (e.g. clicking About after
scrolling Library) kept the previous scrollTop on the right-hand
content panel, so the new section's heading often landed below the
fold and the panel read as half-loaded. The complaint in #634 was
specifically about reaching About from a sidebar position near the
bottom and finding the About content not fully visible.

Add a ref on .settings-content and a useEffect that resets scrollTop
to 0 whenever activeSection changes. The sidebar's own scroll position
is left alone, so users who scrolled to reach a tab still see that tab
in view after clicking it.

Co-authored-by: Nagendhra <nagendhra405@gmail.com>
2026-05-09 21:22:05 +08:00
Yuhao ChenandGitHub 5dbe7795aa docs: add Traditional Chinese QUICKSTART and fix Chinese doc links (#753)
- Add QUICKSTART.zh-TW.md (Traditional Chinese) based on the latest
  QUICKSTART.zh-CN.md, with regional terminology for zh-TW locale.
- Update the language nav in all 6 existing QUICKSTART translations
  to include the 繁體中文 entry.
- Fix CONTRIBUTING.zh-CN.md: point QUICKSTART references to the
  Chinese translation (QUICKSTART.zh-CN.md) instead of the English
  original, closing the follow-up promised in PR #578.
2026-05-09 21:19:08 +08:00
Marc ChanandGitHub b03a504da6 release: Open Design 0.6.0 (#1080) 2026-05-09 19:58:11 +08:00
7518cfc107 feat: add macOS Intel (x64) build support to release workflows (#759)
* feat: add macOS Intel (x64) build support to release workflows

Add build_mac_intel job to both release-beta.yml and release-stable.yml
using macos-13 runners (last Intel-based GitHub Actions runner).

Key changes:
- release-beta.yml: add enable_mac_intel input (default false), build
  job, and wire into publish/verify/summary
- release-stable.yml: add always-on build_mac_intel job, wire into
  publish (downloads + copies to GitHub Release), verify, and summary
- publish.sh: add ENABLE_MAC_INTEL uploads, outputs, and metadata entry
- verify.sh: add mac-intel URL verification when enabled
- summary.sh: add macOS x64 (Intel) row to platform/report tables
- mac-intel.sh: new asset script for unsigned DMG+ZIP production

Intel builds are unsigned (like Windows). No auto-update feed.
Artifact naming: open-design-<ver>.unsigned-mac-x64.{dmg,zip}

Closes #746

* fix: resolve beta macIntel asset name mismatch (P1)

Add MAC_INTEL_ASSET_SUFFIX to publish.sh (mirroring existing
WIN_ASSET_SUFFIX / LINUX_ASSET_SUFFIX pattern) so that the beta
publish job can correctly locate unsigned Intel artifacts.

- publish.sh: add mac_intel_asset_suffix variable with fallback
- release-beta.yml: pass MAC_INTEL_ASSET_SUFFIX: .unsigned to publish

---------

Co-authored-by: ZengGanghui <zghui0@gmail.com>
2026-05-09 19:50:50 +08:00
Yuhao ChenandGitHub 2e0cf52764 fix(web): localize MCP server settings panel for Chinese UI (#760)
- Add 31 typed i18n keys in types.ts for MCP settings strings
- Add English values in en.ts (source of truth)
- Add zh-CN and zh-TW translations
- Move MCP_CLIENTS from global const into IntegrationsSection
  so buildMethod/buildInstruction/deeplinkLabel can use t()
- Wire all JSX strings in IntegrationsSection through useI18n
- Change deeplinkLabel from string to () => string to support
  dynamic translation

Command/config snippets and product names remain untranslated
per maintainer guidance.

Fixes #745
2026-05-09 19:43:40 +08:00
84c788e7bb feat: add contributor card bot workflow (#932)
* feat: add contributor card bot workflow

Adds a production workflow that triggers on merged pull requests and opened issues, checks out the standalone contributor bot, and runs it with the Open Design bot GitHub App credentials. The main repository only owns the workflow trigger; card rendering and Vaunt contribution lookup remain isolated in open-design-bot-sandbox.

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

* fix: run contributor bot on pull_request_target

Use pull_request_target for closed merged PRs so the workflow receives repository secrets when external fork PRs are merged. The job only checks out the trusted contributor-bot repository, not contributor PR code, preserving the intended security model while allowing cards to post for fork contributors.

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

---------

Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 19:40:07 +08:00
zztdanandGitHub fe879036fb fix(web): restore media config from daemon on startup (#687)
* fix(web): restore media config from daemon on startup

* fix(media): preserve stored keys on settings save

* fix(web): harden daemon media restore flow

* fix(web): unify media provider empty-state rules

* fix(desktop): retry loading discovered web url

* fix(web): preserve local media providers on partial daemon reload

* fix(web): preserve media providers on daemon reload

* fix(web): skip media migration for masked-only local state

* fix(web): preserve daemon media state across reloads
2026-05-09 19:31:08 +08:00
Tom HuangandGitHub 643d0cf637 feat: add scheduled routines for unattended agent runs (#1033)
* feat: add scheduled routines for unattended agent runs

Generalizes Orbit's single hard-coded daily-digest scheduler into
user-defined routines: each one fires on a schedule (hourly / daily /
weekdays / weekly with IANA timezone) and starts a fresh agent
conversation, either inside an existing project or in a new project
minted on the spot.

Backend:
- New RoutineService with timezone-aware nextRunAt computed via
  Intl.DateTimeFormat (no new dependency); two-pass tzWallToUtc so
  DST transitions stay correct. Each fire chains rescheduleOne in
  finally() to keep the cadence alive.
- routines + routine_runs SQLite tables; schedule_json is the
  authoritative form, with legacy schedule_kind/value kept populated.
- /api/routines CRUD + /api/routines/:id/run + /api/routines/:id/runs.
- Run handler resolves agent (routine override -> app config -> first
  available), creates project (or reuses configured one) and a fresh
  conversation per fire, then dispatches into startChatRun.

UI (Settings -> Routines):
- Pill-chip schedule kind picker, time + timezone fields, weekday
  picker for Weekly. Live preview line ("Runs daily at 9:00 AM GMT+8").
- Routine list with inline status pill, next/last meta, expandable run
  history; each history row links into the project the run wrote to
  via the existing router primitive.

* fix(daemon): swallow trailing finally rejection for inflight cleanup

Without a terminal `.catch`, the promise returned by `promise.finally(...)`
mirrors the original rejection and produces an unhandled rejection — fatal
in modern Node — when the run handler rejects before producing a start
handle. Callers still see the rejection on the returned `promise`.

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

* fix(daemon): handle DST spring-forward gap in tzWallToUtc

The two-pass conversion picked the pre-gap candidate when the requested
wall time fell inside a spring-forward gap (e.g. 02:30 in
America/New_York on 2026-03-08), so the resulting instant rendered back
as 01:30 local and a 02:30 routine fired an hour early on the
transition day. Routines are local wall-clock schedules, so firing
before the requested time breaks the contract.

Now we round-trip both candidates through partsInTimezone, return the
one whose wall-clock matches the request, and on a gap day where
neither matches return the later candidate so the routine fires at the
first valid post-gap instant on the same day.

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

* fix(daemon): preserve both wall-time candidates on DST fall-back day

On a fall-back day, the requested wall time inside the repeated hour
(e.g. 01:30 America/New_York) maps to two distinct UTC instants. The
previous tzWallToUtc collapsed them to the first (pre-transition) one,
so a daemon that woke between the two instants would skip the second
01:30 entirely and fire a day late once per fall-back. Replace it with
tzWallToUtcCandidates (returns all valid instants, ascending) plus a
gap-only fallback for spring-forward, and have nextWallTimeMatching
walk both ambiguous candidates before advancing to the next day. Adds
fixtures for the repeated-hour case so the intended behavior stays
locked in.

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

* fix(web): make routine timezone picker IANA-complete and DST-truthful

The timezone dropdown was a hardcoded subset, but the backend validator
accepts any IANA zone — so users could not pick zones like
`America/Phoenix` or `Africa/Johannesburg` unless they happened to be
local. And `gmtLabel()` always derived the offset from `new Date()`,
which drifted seasonally for DST-observing zones (a New York routine
created in winter rendered `GMT-5` while it would actually fire on
`GMT-4` after DST started).

Source the picker from `Intl.supportedValuesOf('timeZone')` (with a
curated fallback for older runtimes) and anchor the GMT label to the
routine's next fire time. When the next fire time is unknown (e.g.
the live preview while the form is open) and in the dropdown itself,
fall back to the IANA city, which is stable year-round.

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

* fix(web): always include UTC in routine timezone picker

`Intl.supportedValuesOf('timeZone')` returns only canonical region
names on current runtimes (Node 24, recent browsers) and omits `UTC`,
so the previous picker dropped the most common non-local zone unless
the runtime itself was already UTC. The backend validator and the
contract examples still accept `UTC`, so a user on a non-UTC machine
could not create a documented UTC routine from Settings.

Prepend `UTC` inside `listSupportedTimezones()` when the runtime list
omits it, so the picker stays aligned with the supported schedule
surface.

Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)
2026-05-09 19:30:22 +08:00
Paul SteanandGitHub f3535cdd9f feat(web): scroll question forms to top of viewport instead of pinning to bottom (#1044)
* feat(web): scroll question forms to top of viewport instead of pinning to bottom

* fix: reset scroll state on composer send after form has claimed control

* fix: use getBoundingClientRect for form scroll position; guard early-return on streaming

* fix: smooth scroll for all chat-scroll operations; polyfill scrollTo for jsdom tests

* fix: revert streaming bottom-pin to instant to avoid scroll event thrash

* fix: revert initial-load bottom-pin to instant to avoid scroll event thrash
2026-05-09 19:29:54 +08:00
343c1080e9 feat(design-systems): add hud, loom, trading-terminal with locale coverage (#1069)
* feat(design-systems): add hud, loom, trading-terminal with locale coverage

- Add hud, loom, trading-terminal to DE_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK
- Add loom and trading-terminal to FR_DESIGN_SYSTEM_SUMMARIES (FR locale)
- Add loom and trading-terminal to RU_DESIGN_SYSTEM_SUMMARIES (RU locale)
- Add DESIGN.md for hud, loom, trading-terminal design systems

* fix: restructure to canonical 9-section schema, fix WCAG claims, fix locale ordering

* fix: match canonical 9-section schema, correct WCAG guidance

* fix(trading-terminal): add missing --space-* tokens to :root block

* fix: align Loom spec — font labels (Inter not monospace), light-mode FR/RU summaries

---------

Co-authored-by: Ayush Mahadik <ayushmahadik@gmail.com>
2026-05-09 19:28:19 +08:00
79957890d2 feat(skills): consolidate hyperframes video template updates (#1079)
Bundle four pending template skills and retag eight related skills to video/hyperframes so the categorization and i18n fallback coverage can be reviewed and merged in one pass.

Co-authored-by: Tuola Ge <gexingli@refly.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 18:09:30 +08:00
lefarcenandGitHub ba44de4193 fix(web): keep entry footer pills compact (#1045) 2026-05-09 18:08:47 +08:00
CaprikaandGitHub 21dcd74abb Increase agent inactivity timeout (#1071) 2026-05-09 17:07:47 +08:00
c0062e856a add swiss-creative-mode-template skill (#1068)
Introduce a new Swiss editorial template skill with interactive multi-scene HTML seed files and add DE/FR/RU fallback coverage so localized-content validation stays green.

Co-authored-by: tuolaji <tuola@tuolajideMacBook-Air.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 17:01:48 +08:00
8518292605 add editorial-burgundy-principles-template template skill (#1065)
Add a burgundy editorial live-artifact template with a headline metric slide, studio keyword cloud, and eight-principles card grid. Include DE/FR/RU fallback ids so localized-content coverage stays green.

Co-authored-by: Tuola Ge <gexingli@refly.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 17:01:03 +08:00
dd439c29bf add swiss-user-research-video-template template skill (#1054)
Introduce a Swiss editorial user-research live-artifact template with self-contained HTML seed/example and checklist, and register i18n fallback ids for DE/FR/RU so localized coverage stays green.

Co-authored-by: Tuola Ge <gexingli@refly.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 17:00:00 +08:00
ecddcd4fbd add after-hours-editorial-template template skill (#1053)
Add a new dark editorial HyperFrames template skill with seed/example/checklist assets and register the skill id in DE/FR/RU i18n fallback lists so localized-content validation stays green.

Co-authored-by: Tuola Ge <gexingli@refly.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 16:59:27 +08:00
Marc ChanandGitHub 223d35f073 fix: improve Orbit and packaged data-dir startup errors (#1067) 2026-05-09 16:47:01 +08:00
Siri-RayandGitHub 671b9006f6 Fix desktop prompt template close hitbox (#1056)
* Fix desktop prompt template close hitbox

* Fix prompt template modal viewport spacing

Generated-By: looper 0.6.0 (runner=fixer, agent=codex)
2026-05-09 16:33:37 +08:00
NotLeaped84andGitHub 4c15ea45fa docs: add design system authoring guide (#961)
* fix(i18n): add loom and trading-terminal to FR/RU summaries only

* docs: add design system authoring guide

* fix: remove loom/trading-terminal from locale (missing dirs), fix docs contradictions
2026-05-09 15:34:11 +08:00
EliandGitHub 1bf7836471 feat(web): redesign top bar — lift Share/Present, zoom dropdown, focus toggle (#1048)
* feat(web): redesign top bar — lift Share/Present, add zoom dropdown, move focus toggle

- AppChromeHeader: add #app-chrome-file-actions portal anchor so file viewers
  can render their primary actions (Present/Share) up in the project chrome
  instead of cramming a second toolbar row.
- HtmlFileViewer / LiveArtifactViewer: portal Present + Share into the top
  bar via createPortal; Share gets a real chrome-action-primary button.
- HtmlFileViewer: replace the 100% reset button with a zoom dropdown
  (50/75/100/125/150/200) with click-outside + Esc handling.
- HtmlFileViewer: move Preview/Source tabs next to Reload (left side, view
  modes); move Tweaks to the right cluster next to Inspect/Edit.
- HtmlFileViewer: showPresent no longer requires deck — any HTML artifact
  with loaded source can be presented (prototype/slide/regular HTML).
- LiveArtifactViewer: add Present (in-tab/fullscreen/new-tab) with iframe
  ref + previewBodyRef wrapper; in-tab present hides chrome and overlays an
  exit button (Esc also exits).
- ChatPane: add chevron-left collapse icon in chat header (onCollapse prop)
  so users can hide the chat from where it lives.
- FileWorkspace: focus toggle is now icon-only and only renders when chat is
  collapsed, sitting on the LEFT of the workspace tabs row as a chevron-right
  expand button — direction matches where chat re-emerges from.
- index.css: add chrome-action-primary/secondary, zoom-menu, present-exit-btn,
  app-chrome-file-actions styling, plus a narrow-width media query that
  collapses secondary action labels.

* fix(web): tests — fall back to inline render when chrome portal anchor missing

The Share/Present primary actions render via createPortal into
#app-chrome-file-actions, which only exists when AppChromeHeader has
mounted. Vitest renders FileViewer / LiveArtifactViewer in isolation,
so the portal anchor was absent and the buttons disappeared from the
test DOM, breaking 7 share-menu tests.

- HtmlFileViewer / LiveArtifactViewer: when chromeActionsHost is null,
  render the present/share JSX inline instead of returning null. UX is
  identical in production (host is always present); tests now find the
  buttons without needing a portal-aware harness.
- FileWorkspace tests: rewrite the "focus toggle in tab bar" assertions
  to reflect the new design — the toggle lives in ChatPane while the
  chat is open, and FileWorkspace only renders an icon-only expand
  button on the LEFT of the tab bar once chat is collapsed.
2026-05-09 15:26:22 +08:00
c16e1179fe docs: fix - update prompts path from web to daemon in README files (#756)
* docs: fix - update prompts path from web to daemon in README files

Update all references from apps/web/src/prompts/ to apps/daemon/src/prompts/
across all README language files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: fix - update prompts path in specs and fix French README parity

P2 — Current specs:
- specs/current/critique-theater.md
- specs/current/critique-theater-plan.md
  Update prompts path from apps/web/src/prompts/ to apps/daemon/src/prompts/
  to reflect daemon ownership of prompt composition.

P3 — Historical spec:
- specs/2026-04-29-live-artifacts/spec.md
  Update to current path (historical snapshots kept accurate to latest codebase).

P3 — Language parity:
- README.fr.md
  Fix missing prompt references in Anti-AI-slop and References sections
  to match other language variants (6 refs total).

Migration context: Prompts moved from web to daemon as an ownership/boundary
change (daemon composes and injects prompts at agent spawn time), not a
mechanical rename. Web app no longer owns prompt composition logic.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: fix - update git command path in critique-theater-plan.md

Fix missed git add command example at line 1585.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: fix - update prompt test paths from web/tests to daemon/tests

Update test file paths to match daemon ownership of prompts.
Tests for apps/daemon/src/prompts/* should live in apps/daemon/tests/prompts/.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-09 15:23:23 +08:00
f5564c93a7 i18n: add full Thai translation (th) (#1018)
* i18n: add full Thai translation (th-TH)

* i18n: fix placeholders, update tests and complete documentation for Thai (th)

* i18n: fix placeholders, update tests and complete documentation for Thai locale

* chore: revert unrelated docker deployment changes (fix scope drift)

---------

Co-authored-by: ryu <ryu@example.com>
2026-05-09 15:19:47 +08:00
lefarcenandGitHub 13ab430b45 docs: add skills contributing guide (#1035)
* docs: add skills contributing guide

External skill PRs are coming in faster than we can write per-PR
acceptance feedback, and the existing skill section in CONTRIBUTING.md
gave contributors the merge bar without showing the dev loop or the
patterns we routinely close on. This adds a dedicated guide that
contributors land on before opening a PR.

- New docs/skills-contributing.md (the how-to): quick start, anatomy,
  local dev loop, merge bar checklist, PR description template, and
  the eight rejection patterns we've actually used recently.
- CONTRIBUTING.md "Adding a new Skill" shrinks from 73 lines to ~20
  and points at the new guide. Skill section was the longest in the
  file; trimming it keeps the four i18n variants easier to maintain.
- skills/README.md is new — first thing a contributor sees when they
  open the skills/ folder. Routes them to the contributor guide and
  the protocol spec.
- docs/skills-protocol.md gets a cross-link at the top so readers who
  land on the protocol can find the contributor flow.

Discovery is the point: any path a contributor takes (CONTRIBUTING.md,
skills/ folder, protocol spec) now routes to the same single guide.

* docs(skills-contributing): expand modes to 7, fix broken checklist link

Both flagged by review on #1035:

- The mode enumeration listed 4 modes (prototype | deck | template |
  design-system) but apps/daemon/src/skills.ts:24 actually defines 7
  (adds image | video | audio), with shipped media skills under
  skills/{image-poster,video-shortform,audio-jingle}. Updates every
  enumeration in skills-contributing.md (frontmatter cheat sheet, PR
  template, running-locally instructions, IS-list) and skills/README.md.

- The merge-bar checklist pointed at skills/dating-web/references/
  checklist.md as an example, but that path doesn't exist on this
  branch — dating-web ships only SKILL.md + example.html. Repointed
  at skills/web-prototype/references/checklist.md, which is the
  closest prototype-mode skill that actually ships a checklist.

Adds a "Media skills (image / video / audio)" line to the references
section pointing at the three shipped media skills as imitate-able
starting points.

* docs(skills-contributing): address review — i18n bar, external skills, daemon refresh

Three findings from review on #1035:

P1 — i18n merge-bar mismatch (line 172 of original)
  e2e/tests/localized-content.test.ts:144 enforces skills.toEqual(skillIds)
  for every locale, so a non-featured skill PR following the previous
  guidance ("no edits to apps/web/src/i18n/") would fail CI on the
  `skills display copy` assertion.

  Fix:
    - "Single self-contained folder" item now explicitly carves out the
      *_SKILL_IDS_WITH_EN_FALLBACK line as a required outside edit.
    - New "i18n coverage (every skill, not just featured)" subsection
      directs contributors to add their id to all three fallback arrays
      (DE / FR / RU) — bare id, no TODO comment per existing convention.
    - "Featured skills" subsection now describes replacing the fallback
      with full localized copy, instead of being the only path that
      touches i18n.
    - PR template Validation list adds the fallback-arrays step as a
      required checkbox for every skill PR.

P2 — daemon does not auto-watch skills/ (line 139 of original)
  apps/daemon/src/skills.ts:2 explicitly states "No watching in this MVP
  — re-scans on every [/api/skills call]". Previous wording about chokidar
  was aspirational, not current behavior.

  Fix: replaced with "refresh the picker — the daemon re-scans skills/ on
  every /api/skills request" + restart escape hatch for parse failures.

P2 — missing alternative for vendor workflows (line 60 of original)
  Previous "No" list pointed contributors at heavier daemon/feature paths
  for vendor-specific workflows, ignoring that skills-protocol.md §3
  supports user-global skills via ~/.claude/skills/. Concrete cases like
  payment-provider and regional-marketplace skills (which we've been
  closing as out-of-scope) actually fit the external-bundle path.

  Fix: added a "Third option: ship as an external skill bundle" paragraph
  before the discussions CTA, linking to the protocol's discovery section.
2026-05-09 15:17:45 +08:00
Zoltán SzőgyényiandGitHub 07ac46249f update: add awesome-design-md reference back to QUICKSTART (#940) 2026-05-09 15:11:25 +08:00
CaprikaandGitHub b020f1e39a fix opencode todowrite footer state (#1046) 2026-05-09 15:08:19 +08:00