mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
3935aeb421ad75bbfe676c27ca52e4f60ce38bdc
232
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3935aeb421 |
Optimize packaged mac artifact size (#424)
* optimize mac package payload reporting * optimize(pack): package standalone web runtime * optimize(pack): default to standalone web runtime * chore(release): bump beta base version * fix(pack): compress mac artifacts and report packaged version * fix(pack): preserve Next server fallback * fix(pack): clarify standalone startup failures * fix(release): gate beta platform builds * fix(web): bind standalone backend to parent * fix(pack): harden standalone and beta publishing |
||
|
|
e5ed4bbc76 |
fix(packaged): preserve *_API_KEY env vars for CLI agents (#404)
* Forward API key env vars to packaged sidecars ## Summary Forward provider credential environment variables from the packaged desktop app process to packaged sidecar children. ## Why The packaged app currently forwards only a small fixed environment allowlist to sidecars: - HOME - LANG - LC_ALL - LOGNAME - TMPDIR - USER This prevents CLI agents launched by the packaged daemon from seeing provider credentials such as `CODEX_API_KEY`, even when the desktop app itself was launched with that variable set. ## Fix This keeps the existing allowlist and additionally forwards environment variables ending in: - `_API_KEY` - `_TOKEN` I intentionally did not change PATH or Bun-related behavior in this PR, since CLI discovery already works. The issue here is specifically credential forwarding. ## Verification I verified the root cause locally by patching the packaged output: `@open-design/packaged/dist/index.mjs` After forwarding `CODEX_API_KEY`, the packaged desktop app was able to launch Codex successfully. ## Testing I did not run the full project build locally. I verified the behavior by patching the installed macOS app output and confirming that forwarding `CODEX_API_KEY` fixes the Codex launch failure. * Add includeProviderSecrets parameter to env functions Updated. Provider-secret forwarding is now daemon-only: *_API_KEY / *_TOKEN are included only when spawning the daemon sidecar, while the web/Next sidecar stays on the original minimal allowlist. |
||
|
|
9979afa3ae |
fix: cap htmlPreviewSlideState Map to prevent unbounded memory growth (#488)
The module-level Map cached slide state per project+file key but never evicted entries. In long-lived sessions with many projects, this grows without bound. Add a 64-entry cap with FIFO eviction. Co-authored-by: Test User <test@example.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
827e955be7 |
docs: add 'Running the Project' section to README (#468)
* docs: add 'Running the Project' section to README - Add comprehensive instructions for running in web and desktop modes - Clarify that ports are ephemeral by default, not fixed - Include table of useful commands with descriptions - Link to QUICKSTART.md for detailed troubleshooting - Position section after Quickstart, avoid duplicating existing content * docs(readme): address review feedback on Running the Project section - Fix foreground mode comment (logs written to files, not streamed) - Add restart alternative for switching ports in existing session - Correct table descriptions for logs/check commands - Move section after First-run state to keep Quickstart intact Addresses review comments from @lefarcen and @mrcfps |
||
|
|
ec7dafc007 |
feat(daemon): add Kilo CLI (ACP) (#480)
* docs(readme): refresh contributors wall * docs(readme): refresh contributors wall * feat: kilo cli * fix: use default model option for kilo * chore: add agent_diff id unique test * chore: add deepseek to docs |
||
|
|
6380c48a48 | Update Codex sandbox invocation (#477) | ||
|
|
df2e8adba0 |
docs: add Brazilian Portuguese (pt-BR) translations (#460)
* docs: add Brazilian Portuguese (pt-BR) translations Translate README, CONTRIBUTING, QUICKSTART, and the e2e/cases, e2e/reports, skills/html-ppt, skills/guizang-ppt READMEs into pt-BR. Add the pt-BR entry to the language switcher in every existing locale variant (en, de, fr, ko, ja-JP, ru, uk, zh-CN, zh-TW for README; en, de, fr, ja-JP, zh-CN for CONTRIBUTING; en, de, fr, ja-JP for QUICKSTART) so readers in any language can jump to the Portuguese version. Code blocks, file paths, identifiers, license attribution links and brand names are kept verbatim with the source. * docs(pt-BR): use repo-relative links in e2e READMEs Replace author-local absolute paths (/Users/mac/...) with repo-relative links so the translated docs navigate correctly on GitHub and other checkouts. * docs(pt-BR): fix README badge anchor and skill reference link - Update Agents badge href to the Portuguese fragment (#agentes-de-código-suportados) so the badge jumps to the translated heading. - Restore the [`SKILL.md`][skill] reference-link syntax in the comparison table; the opening bracket was lost in translation, breaking the row. |
||
|
|
de62baa0e8 |
fix(web): aspect ratio cards no longer overflow into siblings (#476)
The 380px sidebar gives the 3-column aspect grid only ~92px per cell. The flex-row card layout reserved 34px for the glyph plus 7px gap, leaving ~31px for the label — but "Landscape"/"Portrait" at 13px / weight 650 are ~75px wide. Without `min-width: 0` on the flex/grid items, the strong text refused to shrink and overflowed, so each card's label visually covered the next card's glyph. Switch the aspect cards to a column layout (glyph above, label + ratio below) so the label has the full card width. Add the missing `min-width: 0` on `.newproj-option-card` and `.aspect-copy` as a defense against future long-text regressions, and rebalance glyph dimensions and font sizes to match the tighter cards. Closes #242 |
||
|
|
2473ab9567 |
fix(web): add copy buttons for FileViewer code blocks (#471)
* fix(web): add copy buttons for FileViewer code blocks * fix(web): harden FileViewer markdown copy controls * fix(web): restore focus after clipboard fallback * test(e2e): restore execCommand after markdown copy tests |
||
|
|
a4fb90a9f3 |
fix(design-files): clear selection on project switch (#465)
* fix(design-files): clear selection on project switch to prevent cross-project leak
When switching from project A to project B, filenames present in both
projects (e.g. index.html) would survive the intersection-prune and
remain selected, causing handleBatchDownload() to post against the
wrong projectId. Now selection is fully cleared on projectId change.
* fix(design-files): use key-based remount to prevent cross-project selection leak
Replace the useEffect projectId clearing with a key={projectId} on
DesignFilesPanel. useEffect runs after render, leaving a window where
stale selections from project A could be used against project B. The
key tells React to unmount and remount the component synchronously on
project switch — no async gap.
|
||
|
|
b4e69ac61b | fix(copilot): copilot prompt processing with correct command format (#466) | ||
|
|
661465527b |
feat(daemon): add DeepSeek TUI as a code agent adapter (#439)
* feat(daemon): add DeepSeek TUI as a code agent adapter
Register `deepseek` (with `deepseek-tui` cargo-only fallback) in
AGENT_DEFS via `deepseek exec --auto [--model X] <prompt>` and plain-text
streaming. Ships `deepseek-v4-pro` / `deepseek-v4-flash` as fallback
model hints; users can paste any other id (incl. NIM / Fireworks /
SGLang routes) via the custom-model input.
Web UI gets a DeepSeek-blue gradient icon, label/alias mapping, and
docs/agent-adapters.md §5.9 documents the auth state, prompt-as-argv
Windows size limit, and the upstream gap that prevents stdin delivery
today (clap declares `prompt: String` as a required positional).
Adds .deepseek/ to .gitignore alongside the other per-agent runtime
data dirs so first-launch trust files don't leak into git.
* fix(daemon): drop unsupported deepseek-tui fallback bin
The `deepseek` dispatcher owns `exec` / `--auto`; `deepseek-tui` is the
runtime companion it invokes. Listing `deepseek-tui` in fallbackBins
advertised availability for a host that only had the TUI binary, but
buildArgs still emitted `<resolved> exec --auto <prompt>` — which
deepseek-tui itself doesn't accept, so the first /api/chat run would
fail. Upstream documents both binaries as required (npm and cargo paths
install them together), so the fallback didn't correspond to a supported
install. Pin the absence in the agents test and update docs §5.9 + the
adapter table to match.
Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
* fix(daemon): pre-flight DeepSeek TUI prompts against argv byte budget
DeepSeek's exec mode requires the prompt as a positional argv arg (no
`-` stdin sentinel upstream), so a fully composed OD prompt — system
text + history + skills + design-system content + the user message —
can blow Windows' ~32 KB CreateProcess limit (or Linux MAX_ARG_STRLEN
on extreme edges) and surface as a generic spawn failure instead of
a DeepSeek-specific, user-actionable message. The adapter now declares
`maxPromptArgBytes = 30_000` (leaves ~2.7 KB argv headroom for `exec
--auto --model <id>` and Windows quoting), and the /api/chat spawn
path checks the composed prompt against that budget before calling
`spawn`. Oversized prompts fail fast with `AGENT_PROMPT_TOO_LARGE`
and guidance to reduce skills/design context or pick an adapter with
stdin support.
Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
* test(daemon): pin DeepSeek argv-budget guard with regression tests
The previous spawn-path guard inlined the byte-budget check in the
chat handler, so the only safety net for the DeepSeek argv-only
prompt-delivery shape was a static "the field exists" assertion —
nothing actually exercised the AGENT_PROMPT_TOO_LARGE path or the
short-prompt happy path. Extract the check into a pure
`checkPromptArgvBudget(def, composed)` helper in agents.ts, call it
from /api/chat before bin resolution (so the guard is order-
independent and fires regardless of whether the adapter binary is
on PATH in CI), and add a regression test that exercises both the
oversized-prompt branch (over the conservative under-Windows-
CreateProcess budget) and the short-prompt branch, plus a UTF-8
byte-vs-codepoint case and a stdin-adapter no-op case so the guard
can't silently regress or leak onto adapters that ship the prompt
over stdin.
Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
* fix(daemon): pre-flight DeepSeek prompts against Windows .cmd-shim quoting
The first-pass argv-byte guard only inspects the raw composed prompt, so
on Windows an npm-installed `deepseek` resolves to a `.cmd` shim and the
spawn path then wraps the call in `cmd.exe /d /s /c "<inner>"` with
every embedded `"` doubled by `quoteWindowsCommandArg`. A quote-heavy
prompt (code blocks, JSON-shaped skill seeds) under the 30,000-byte
budget can therefore still expand past CreateProcess's 32_767-char
`lpCommandLine` cap and surface as a generic spawn ENAMETOOLONG instead
of the DeepSeek-named, actionable `AGENT_PROMPT_TOO_LARGE` the budget
guard was meant to provide. Add a second pure helper
`checkWindowsCmdShimCommandLineBudget(def, resolvedBin, args)` that
mirrors the platform layer's per-arg quoting and recomputes the would-be
command line length whenever the resolved binary is a `.cmd` / `.bat`
shim, and call it from `/api/chat` after `buildArgs` / `resolveAgentBin`
so the same SSE error fires before `spawn`. Pin the new path with a
quote-heavy regression (prompt is under the byte budget but doubles
past the kernel cap) plus no-op tests for non-`.cmd` resolutions, null
bin, and stdin-only adapters so the guard can't drift back.
Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
* fix(daemon): extend DeepSeek argv guard to direct .exe Windows installs
The cmd-shim guard added in
|
||
|
|
bc04d61903 |
feat(design-files): add batch ZIP download with multi-select (#405)
* feat(design-files): add batch ZIP download with multi-select Add checkbox-based multi-select to the Design Files panel and a new POST /api/projects/:id/archive/batch endpoint that zips selected files using the project name as the archive filename. * fix(i18n): add missing batch-download keys to uk locale The upstream main branch has a uk.ts locale that didn't exist in the fork. Without these keys the web typecheck fails against the full locale set. * fix(design-files): harden batch archive and prune stale selections - Use lstat to reject symlinks, dotfiles, and .artifact.json sidecars in buildBatchArchive (mirror listFiles/collectFiles allowlist) - Reject invalid names explicitly instead of silently skipping - Prune stale filenames from selected set on files/projectId change * fix(daemon): tighten batch archive allowlist with segment-level checks - Check every path segment for hidden directories, not just basename - Walk intermediate directories with lstat to reject symlinks at any level - Fail-fast on any ineligible file instead of silently skipping |
||
|
|
da2b007a43 |
feat(daemon): add DeepSeek TUI as a code agent adapter (#439)
* feat(daemon): add DeepSeek TUI as a code agent adapter
Register `deepseek` (with `deepseek-tui` cargo-only fallback) in
AGENT_DEFS via `deepseek exec --auto [--model X] <prompt>` and plain-text
streaming. Ships `deepseek-v4-pro` / `deepseek-v4-flash` as fallback
model hints; users can paste any other id (incl. NIM / Fireworks /
SGLang routes) via the custom-model input.
Web UI gets a DeepSeek-blue gradient icon, label/alias mapping, and
docs/agent-adapters.md §5.9 documents the auth state, prompt-as-argv
Windows size limit, and the upstream gap that prevents stdin delivery
today (clap declares `prompt: String` as a required positional).
Adds .deepseek/ to .gitignore alongside the other per-agent runtime
data dirs so first-launch trust files don't leak into git.
* fix(daemon): drop unsupported deepseek-tui fallback bin
The `deepseek` dispatcher owns `exec` / `--auto`; `deepseek-tui` is the
runtime companion it invokes. Listing `deepseek-tui` in fallbackBins
advertised availability for a host that only had the TUI binary, but
buildArgs still emitted `<resolved> exec --auto <prompt>` — which
deepseek-tui itself doesn't accept, so the first /api/chat run would
fail. Upstream documents both binaries as required (npm and cargo paths
install them together), so the fallback didn't correspond to a supported
install. Pin the absence in the agents test and update docs §5.9 + the
adapter table to match.
Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
* fix(daemon): pre-flight DeepSeek TUI prompts against argv byte budget
DeepSeek's exec mode requires the prompt as a positional argv arg (no
`-` stdin sentinel upstream), so a fully composed OD prompt — system
text + history + skills + design-system content + the user message —
can blow Windows' ~32 KB CreateProcess limit (or Linux MAX_ARG_STRLEN
on extreme edges) and surface as a generic spawn failure instead of
a DeepSeek-specific, user-actionable message. The adapter now declares
`maxPromptArgBytes = 30_000` (leaves ~2.7 KB argv headroom for `exec
--auto --model <id>` and Windows quoting), and the /api/chat spawn
path checks the composed prompt against that budget before calling
`spawn`. Oversized prompts fail fast with `AGENT_PROMPT_TOO_LARGE`
and guidance to reduce skills/design context or pick an adapter with
stdin support.
Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
* test(daemon): pin DeepSeek argv-budget guard with regression tests
The previous spawn-path guard inlined the byte-budget check in the
chat handler, so the only safety net for the DeepSeek argv-only
prompt-delivery shape was a static "the field exists" assertion —
nothing actually exercised the AGENT_PROMPT_TOO_LARGE path or the
short-prompt happy path. Extract the check into a pure
`checkPromptArgvBudget(def, composed)` helper in agents.ts, call it
from /api/chat before bin resolution (so the guard is order-
independent and fires regardless of whether the adapter binary is
on PATH in CI), and add a regression test that exercises both the
oversized-prompt branch (over the conservative under-Windows-
CreateProcess budget) and the short-prompt branch, plus a UTF-8
byte-vs-codepoint case and a stdin-adapter no-op case so the guard
can't silently regress or leak onto adapters that ship the prompt
over stdin.
Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
* fix(daemon): pre-flight DeepSeek prompts against Windows .cmd-shim quoting
The first-pass argv-byte guard only inspects the raw composed prompt, so
on Windows an npm-installed `deepseek` resolves to a `.cmd` shim and the
spawn path then wraps the call in `cmd.exe /d /s /c "<inner>"` with
every embedded `"` doubled by `quoteWindowsCommandArg`. A quote-heavy
prompt (code blocks, JSON-shaped skill seeds) under the 30,000-byte
budget can therefore still expand past CreateProcess's 32_767-char
`lpCommandLine` cap and surface as a generic spawn ENAMETOOLONG instead
of the DeepSeek-named, actionable `AGENT_PROMPT_TOO_LARGE` the budget
guard was meant to provide. Add a second pure helper
`checkWindowsCmdShimCommandLineBudget(def, resolvedBin, args)` that
mirrors the platform layer's per-arg quoting and recomputes the would-be
command line length whenever the resolved binary is a `.cmd` / `.bat`
shim, and call it from `/api/chat` after `buildArgs` / `resolveAgentBin`
so the same SSE error fires before `spawn`. Pin the new path with a
quote-heavy regression (prompt is under the byte budget but doubles
past the kernel cap) plus no-op tests for non-`.cmd` resolutions, null
bin, and stdin-only adapters so the guard can't drift back.
Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
* fix(daemon): extend DeepSeek argv guard to direct .exe Windows installs
The cmd-shim guard added in
|
||
|
|
59e1e01233 |
fix(web): prevent vertical scrollbar on artifact preview frame (#453)
* fix(web): prevent vertical scrollbar on artifact preview frame The preview uses transform: scale() for zoom, which doesn't affect layout size. When previewScale < 1, the scale wrapper's layout size (100/scale%) overflows the parent, triggering overflow: auto on .viewer-body. Add overflow: hidden on .comment-preview-layer to clip the layout overflow before it propagates. * fix(web): use iframe-only clip wrapper instead of layer-level overflow Move overflow: hidden from .comment-preview-layer to a dedicated .comment-frame-clip wrapper that only wraps the iframe. This prevents the layout overflow from propagating to .viewer-body while keeping comment overlays and popovers unclipped. |
||
|
|
33c3b94b42 |
feat(daemon): add od mcp - expose Open Design as an MCP server (#399)
* feat(daemon): add `od mcp` subcommand for stdio MCP server
Lets a coding agent in a different repo (Claude Code, Cursor, Zed)
pull files from a locally-running OD project over the Model Context
Protocol — no export/import zip dance.
The MCP server is a thin stdio process that proxies read-only tool
calls to the daemon's existing HTTP API; no daemon-side changes
required. Exposes 8 tools:
list_projects, get_project,
list_files, get_file,
list_skills, get_skill,
list_design_systems, get_design_system
Wired exactly like `od media`: a hoisted flag set, a SUBCOMMAND_MAP
entry, a thin handler that resolves OD_DAEMON_URL and hands off to
src/mcp.ts. Tool dispatch is a switch over the tool name; each branch
fetches the matching daemon route and surfaces the response as MCP
text content. Binary mimes return a clear error pending phase-2
support.
Lifecycle gotcha worth flagging: Server.connect(transport) only
*starts* the stdio reader; the promise resolves immediately. Without
holding the function awaiting until transport/stdin close, cli.ts's
top-level process.exit(0) kills the server before the first request
arrives. The fix in src/mcp.ts holds until onclose / stdin EOF.
Wire-up example for a consuming repo:
{
"mcpServers": {
"open-design": {
"command": "od",
"args": ["mcp"],
"env": { "OD_DAEMON_URL": "http://127.0.0.1:7456" }
}
}
}
New dep: @modelcontextprotocol/sdk (MIT, official Anthropic SDK).
* feat(daemon): add MCP server instructions for zero-shot LLM context
Hand the consuming LLM a system-prompt-style overview of the OD
workflow so it picks the right tool without prompt-engineering on
the user's side. Mentions get_artifact and project-name resolution
ahead of their actual implementation; both ship in the same batch.
* feat(daemon): resolve MCP project args by UUID, name, or substring
Lets a consuming agent say `project: "recaptr"` instead of pasting a
UUID. Match order: exact id → exact name (case-insensitive) →
slug-normalized name (strips trailing " (N)", normalizes whitespace) →
substring (errors if multiple). UUID inputs short-circuit and never
hit the daemon.
* feat(daemon): surface entryFile and kind on MCP get_project response
Promote metadata.entryFile and metadata.kind to top-level fields so
consumers (including get_artifact in this branch) can find the entry
without digging through nested metadata blobs.
* feat(daemon): add MCP get_artifact tool for bundle retrieval
A design rarely lives in a single file. get_artifact pulls the entry
HTML/JSX plus every sibling it references (tokens CSS, JSX modules,
imported components) in one call, so a consuming agent doesn't need
to parse HTML and round-trip per file.
Three modes:
auto (default): BFS over relative <script src>, <link href>,
<img src>, <source/video src>, JSX import/from, CSS url(), with
depth cap 3 and a visited set. CDN, data:, mailto:, anchors, and
paths containing .. are skipped.
all: every textual file in the project (mirror of /archive
minus binaries).
shallow: just the entry file (same as get_file).
Output is a structured JSON blob with name/mime/size/content per
file and the project's manifest metadata at the top.
* feat(daemon): add /api/projects/:id/search route + MCP search_files
Server-side substring search across textual project files. Returns
file, 1-indexed line, and snippet, capped at 1000 matches. Exposed
through the MCP layer as search_files(project, query, pattern?, max?).
Treats the query as a literal substring (regex chars escaped) to
avoid catastrophic-backtracking attacks from LLM-supplied input.
Honors the project dir's existing path-safety guards via listFiles.
* feat(daemon): add since= filter to /files route + MCP list_files arg
Lets a consumer poll for "what's changed since I last looked" without
re-walking every file. Daemon-side: parse since= as ms, filter
listFiles output by mtime. MCP-side: forward as URL query.
* feat(daemon): expose skills and design systems as MCP resources
Catalog reads are stable reference material — they fit MCP's
resources surface (LLM-passive) better than tools (LLM-active).
Skills and design systems each become resources at
od://skills/<id>/SKILL.md and od://design-systems/<id>/DESIGN.md;
existing list_skills / get_skill / list_design_systems /
get_design_system tools remain as fallbacks for clients that don't
handle resources cleanly.
* fix(daemon): tighten MCP correctness in get_artifact and resources
Several silent-failure paths and minor footguns the first pass missed:
- get_artifact auto: the entry's own fetch now raises a clear
error instead of returning files: []. Previously a typo in
`entry:` looked like an empty project.
- get_artifact: invalid `include` value returns a clear error
listing the valid modes instead of silently behaving as auto.
- get_artifact all: includes binary files as metadata stubs to
match auto's behavior. Both modes are now strict supersets of
shallow.
- extractRelativeRefs: gate JS-only patterns (import/from/require/
dynamic-import) by file mime/extension so prose in markdown or
HTML doesn't generate spurious 404 round-trips on words like
"imported from 'X'".
- extractRelativeRefs: cover <iframe>, <audio>, srcset, and
CSS @import — common in real OD output.
- resources/list descriptions are collapsed to a single line
(newlines + repeated whitespace -> one space) so MCP UIs that
don't normalize whitespace render cleanly.
- fetchProjectFile: 0-byte binary files no longer report size: null
due to falsy short-circuit on Number(content-length).
* perf(daemon): cache MCP project list for 5s in resolveProjectId
A typical agent session calls list_files/get_file/get_artifact several
times in a row, each with a project name argument. Each previously
re-fetched /api/projects. Cache the list in module scope with a 5s
TTL so back-to-back lookups are local; renames in the OD UI still
propagate within a few seconds.
* feat(daemon): MCP UX polish — tool order, annotations, get_artifact maxBytes
Three changes well-behaved MCP clients pick up automatically:
- Tool ordering. list_projects + get_artifact are now first; LLMs
that weight earlier entries surface the bundle path before
per-file fetching. Catalog tools (list_skills, get_skill,
list_design_systems, get_design_system) sit at the bottom; they
are also exposed as MCP resources.
- readOnlyHint / idempotentHint / openWorldHint annotations on
every tool so clients can skip confirmation prompts on safe
tools and let the LLM know re-running is fine. Per-tool `title`
annotations give clients a friendlier display name than the
snake_case tool id.
- get_artifact gains a `maxBytes` arg (default 1.5MB). Once the
accumulated textual content crosses the cap, remaining files
are dropped and `truncated: true` is set on the bundle so the
consumer knows to use list_files / get_file for the rest.
* feat(daemon): expose user's active OD project/file via MCP
The "what file are you on?" round-trip the agent had to do every
session is now answered automatically. Three pieces:
- Daemon: in-memory active-context slot with 5-minute TTL.
POST /api/active sets {projectId, fileName}; GET /api/active
returns the current value enriched with projectName, or
{active:false} when the slot is empty/stale. Cleared on
daemon restart.
- Web: a small useEffect in App.tsx posts the active project +
file to the daemon on every route change. Best-effort fire-
and-forget; a missing daemon doesn't surface an error.
- MCP: get_active_context tool (no args) and a matching MCP
resource at od://focus/active. The tool is listed second,
right after list_projects, so an LLM picks it up before
asking for ids. Server instructions tell the model to call
it FIRST when the user says "this file" / "the design I have
open" / "what I'm looking at."
End to end: user opens a project in OD, agent in another repo
calls get_active_context() → gets {projectName: "recaptr",
fileName: "recaptr-onboarding-4.html"}, then immediately calls
get_artifact(project: "recaptr") with no further user input.
* feat(daemon): make MCP project arg optional, fall back to active OD context
get_artifact, get_project, get_file, search_files, and list_files now
accept project as optional. When omitted, the MCP resolves project
from /api/active so an agent in another repo can call
search_files({ query: "Polaroid" })
without first asking the user "which project?". get_file and
get_artifact also default their path/entry to the active file, so
get_file({}) returns whatever the user is currently looking at.
The implicit path stamps `usedActiveContext` on JSON responses (or a
separate `[od:active-context …]` content block on get_file) so the
agent can see exactly which project/file got chosen. Explicit
project args pass through with zero added overhead.
Cuts the common case from two MCP round trips
(get_active_context → search_files) to one. Server instructions and
get_active_context's own description are updated to point at the
new default.
* fix(daemon): require same-origin for /api/active POST and GET
The active-context endpoint was added without isLocalSameOrigin
guard. Since the daemon binds 0.0.0.0 by default, a LAN peer could
GET it to learn what file the user has open, or POST it to redirect
the MCP fallback to a project of their choice. Same-origin only is
the right scope: the web app proxies its requests through Next.js
on the daemon port, and the MCP runs over loopback in-process, so
both legitimate callers pass.
Pattern matches the existing /api/app-config etc. guards.
* feat(daemon): add /api/mcp/install-info for cross-platform install snippets
The Settings -> MCP server panel needs absolute paths to node and
the daemon's built cli.js so it can render snippets that work on a
fresh source clone (where `od` is not on PATH) and dodge the
/usr/bin/od octal-dump tool that ships on macOS/Linux and would
otherwise shadow ours.
Endpoint returns:
- command: process.execPath (the node binary running the daemon)
- args: [<absolute path to dist/cli.js>, "mcp"]
- daemonUrl: http://127.0.0.1:<port>
- platform: process.platform (so the panel can localize ~/.cursor
vs %USERPROFILE%\.cursor and Cmd vs Ctrl shortcuts)
- cliExists / nodeExists: existsSync checks on both binaries
- buildHint: human-readable build/reinstall instructions when
either path is missing
isLocalSameOrigin guard same as /api/active. Cached for 5s because
the panel may re-fetch on every open and the paths cannot change
without a daemon restart.
Test file covers the happy path, cross-origin rejection, two
allowed-Origin variants, and the cache by counting fresh resolves
across rapid calls. 5/5 pass.
* refactor(daemon): tighten MCP surface, trim descriptions, polish copy
Three intertwined cleanups that all live in mcp.ts + cli.ts:
1. Drop catalog tools from MCP. list_skills / get_skill /
list_design_systems / get_design_system are removed. The audience
is a coding agent in a separate repo consuming Open Design's
output; it cannot run skills (those are recipes Open Design uses
to generate) and design-system DESIGN.md is reference material
that already ships as an MCP resource. Keeping the catalog as
tools cost ~350 token-overhead per turn for capabilities the
agent could not act on. Tool count: 11 -> 7.
2. Trim tool descriptions. The active-context fallback explanation
was repeated in 5 separate tool descriptions; hoisted into
PROJECT_ARG and explained once in the server `instructions`
block instead. Saves ~150-200 tokens per tools/list response.
3. User-facing branding pass. Tool titles, tool descriptions,
resource names, error messages, comments, and `od mcp --help`
now consistently use "Open Design" rather than "OD". Internal
abbreviation `OD` is retained only inside the server
instructions block where it is introduced inline as "Open Design
(OD)" for compactness across multi-paragraph guidance.
Em dashes replaced with hyphens throughout, per project style.
* feat(web): add MCP server install panel in Settings
New "MCP server" section in the Settings dialog, surfacing
copy-paste install snippets for the major MCP-compatible coding
agents (Claude Code, Cursor, VS Code, Antigravity, Zed, Windsurf).
Highlights:
- In-brand custom dropdown (reuses the existing .ds-picker
pattern from the design-system / prompt-template pickers, click
outside / Escape to close, chevron animates) instead of a
native <select>.
- Per-client snippet that uses absolute paths to node + cli.js
fetched from /api/mcp/install-info on mount, so it works even
when `od` is not on PATH.
- Cursor gets a one-click "Install in Cursor" deeplink
(cursor://anysphere.cursor-deeplink/mcp/install) that pops an
approval dialog and writes the config for the user. UTF-8-safe
base64 so paths with accented characters do not throw.
- Per-OS path hints (~/.cursor on POSIX, %USERPROFILE%\.cursor
on Windows) and keyboard shortcuts (Cmd vs Ctrl).
- Build-required warning card when cli.js or the node binary
does not exist on disk; deeplink button disables in that state.
- Prominent "restart your client to pick up the new server"
callout below the snippet, with per-client guidance.
- Capability list ("what your agent can do") instead of a tool-
name dump, so non-developer designers can also tell what is
possible without reading MCP docs.
README adds a short "Use Open Design from your coding agent"
section that points at the panel and summarizes the per-client
flow (one-click for Cursor, JSON merge elsewhere). Read-only by
design; the daemon must be running locally.
* docs(readme): align MCP server section with the Settings panel
The "Use Open Design from your coding agent" section had drifted
from what the panel actually emits and lists.
- Add Antigravity to the supported-client list (previously missing).
- Drop the "(GitHub Copilot)" parenthetical from VS Code so the
label matches the panel.
- Fix the Claude Code line: we no longer emit a single
`claude mcp add ...` shell command. The snippet is JSON; the
panel additionally suggests `claude mcp add-json` as the safer
way to apply it instead of hand-editing ~/.claude.json.
- Swap the "find the Polaroid section" example for two more
universal phrases ("build this in my app", "match these
styles") that match what the panel surfaces.
- Add a one-line "restart or reload your client after install"
note - this was prominent in the panel and absent from the
README.
- Trim the /usr/bin/od octal-dump aside; it was technical detail
that did not earn its space at the README intro level.
* feat(web): add Codex CLI to the MCP server install panel
Codex is a first-class supported coding agent (listed alongside
Claude Code, Cursor, etc. in the README's PATH-detected agent
table) but the install panel was missing it.
Codex stores MCP server config at ~/.codex/config.toml (TOML, not
JSON) under an `[mcp_servers.<name>]` table, and the same file is
shared between the Codex CLI and the Codex IDE extension - so one
install covers both. Added a 7th client entry that emits the right
TOML snippet, expanded the snippet-lang union to include 'toml'
(behaves like 'json' for whitespace handling, just a different
syntax-highlight hint).
For our minimal payload (just command + args), JSON.stringify
happens to produce valid TOML literal values since TOML basic
strings use the same double-quote escape rules as JSON, and TOML
inline arrays match JSON array syntax. No new TOML serializer
needed.
README updated to list Codex among the supported clients.
Schema verified against https://developers.openai.com/codex/mcp.
* fix(daemon): accept any loopback origin in same-origin guard
The previous port-pinned check required the request's Origin to match
either the daemon's own port or OD_WEB_PORT. tools-dev does not pass
OD_WEB_PORT to the daemon process, so any browser POST to /api/active
proxied through the dev web (port 17573 etc.) was rejected with 403,
and get_active_context always returned {active: false}.
Relax to a loopback-prefix match: any http://127.0.0.1:*,
http://localhost:*, or http://[::1]:* origin passes regardless of
port. Cross-origin (https://evil.com) is still rejected. The
trade-off is that another local web app on a different loopback port
could now CSRF the daemon; same-origin checks are inherently a CSRF
defense, not a network ACL.
* fix(web): make Claude Code MCP snippet a real copyable one-liner
claude mcp add-json open-design '<json>' takes only the inner
server-config object, not the full {"mcpServers": ...} wrapper, and
rejected the wrapped shape with "Invalid configuration: : Invalid
input". Pass only the inner config, and inline the JSON into the
command itself so the snippet is a real one-liner the user can copy
and paste, no template substitution.
* test(daemon): drop loopback-prefix assertions superseded by upstream origin policy
The two proxy-flow allow tests were added in
|
||
|
|
02d24c4fde |
fix(web): language option button height truncation in settings (#447)
* fix(web): remove min-height constraint on language option buttons The hardcoded min-height: 40px on .settings-language-option caused content to overflow when locale labels wrap to two lines. Remove the constraint so buttons size naturally, and update the menu max-height budget from 360px (9×40px) to 406px (7×58px) to match. * fix(web): bump menu max-height to 428px to account for chrome The previous 406px budget didn't account for menu padding (4px), row gaps (2px × 6), and border (1px × 2). Bump to 428px so all 7 visible items fit without clipping. |
||
|
|
cfd359e05a |
[codex] Fix Gemini CLI trust handling (#352)
* Fix Gemini CLI trust handling * Preserve agent spawn env filtering |
||
|
|
e9cc1fbda8 |
fix(web): prevent vertical scrollbar on ws-tabs-bar (#448)
Setting overflow-x: auto implicitly sets overflow-y to auto per CSS spec. With a fixed height of 44px, this causes a vertical scrollbar when content overflows by even 1px. Explicitly set overflow-y: hidden. |
||
|
|
d637297313 |
feat(preview): live-reload iframes when project files change on disk (#409)
* feat(preview): live-reload iframes when project files change on disk Add a chokidar-backed file watcher per active project on the daemon, surface changes via an SSE endpoint at /api/projects/:id/events, and consume them in the web app to bump the file list. The new mtime then propagates to the FileViewer iframe through PR #384's ?v=${mtime} cache-bust, reloading the preview automatically — no manual refresh click. Daemon: - New apps/daemon/src/project-watchers.ts: refcounted per-project watcher registry. First subscribe lazy-creates a chokidar watcher; last unsubscribe closes it. Ignores .git, node_modules, .od, debug, .DS_Store. Returns a ready promise so callers can await initial scan. - New endpoint GET /api/projects/:id/events using the existing createSseResponse helper. Sends one ready event after chokidar binds, then one file-changed event per add/change/unlink. - Adds chokidar ^5.0.0 dependency. Web: - New apps/web/src/providers/project-events.ts exposing createProjectEventsConnection (pure, testable) and useProjectFileEvents hook. EventSource with exponential backoff (1s -> 30s cap), reset on a successful ready event. - ProjectView.tsx subscribes when daemonLive && project.id, and on each event bumps the existing filesRefresh signal — no FileViewer changes needed because PR #384 already URL-loads with mtime cache-bust. Tests: - 6 new daemon unit + integration tests (refcounting, real chokidar add/change/unlink, ignore patterns). - 8 new web hook unit tests (URL encoding, payload parsing, malformed payload tolerance, exponential backoff, backoff reset on ready, close cancels reconnects, no-op when EventSource missing). Closes #370 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(preview): test ignore patterns relative to watch root The ignore predicate matched against absolute paths, so segments in the watch root's ancestors (e.g. the daemon's own .od/ runtime dir, which contains every project) silenced every event. In production this meant zero file-changed events ever fired — every file inside a project sat under .od/projects/<id>/, and .od matched the ignore. Tests passed because mkdtemp puts test roots in /tmp/od-watchers-XXX/, which has no .od ancestor. Fix: compute the path relative to the watch root, then test segments. Add a regression test that reproduces the production layout (.od/projects/<id>/...) and asserts events still fire. Also folds in a small consolidation of the SSE route handler from the prior commit on this branch (single route, surfaces sub.ready before emitting `ready`, propagates err.message in the error path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(project-view): only auto-open files that exist in the project Agent Write/Edit tool results unconditionally called requestOpenFile on the basename of the edited path, which created permanent placeholder tabs ("Open a file from Design Files.") whenever the agent edited a file outside the project's working directory (e.g. an upstream repo source file). Add decideAutoOpenAfterWrite() — a pure helper that gates the auto-open on the file actually appearing in the refreshed project file list. Same nextFiles-from-then() pattern already used at ProjectView.tsx:968. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(preview): chokidar resilience + auto-open path-suffix matching Address PR #409 review feedback in one bundle: - chokidar error listener (codex P1): FSWatcher is an EventEmitter; without an error handler, transient FS faults (ENOSPC, EPERM, EMFILE) surfaced as unhandled exceptions and could crash the daemon. Watcher now logs in dev mode and continues; refcount cleanup unaffected. - followSymlinks: false (mrcfps): keep the watcher's resource boundary aligned with the project boundary so a symlink inside the project cannot traverse externally. Real-chokidar regression test included. - decideAutoOpenAfterWrite path-suffix matching (mrcfps): pass the agent's full file_path through (not just the basename); resolve via path-suffix match against project file paths, with single-unambiguous basename fallback only when filePath has no slash. Fixes the same-basename collision case where an external Write to App.jsx could open a project's prototype/App.jsx. - Dev-mode logs (lefarcen P3): warn when the broadcast subscriber loop or the SSE payload parser swallows an error, so subscriber bugs and payload-shape regressions don't go silent during testing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: DevForgeAI CI/CD Engineer <devforge-ai@development.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
47eeaf445d |
feat: Critique Theater foundation (contracts + parser, Phases 0-2) (#387)
* docs(specs): add Critique Theater design spec for panel-tempered artifacts * docs(specs): add Critique Theater implementation plan * docs(specs): rename UI to Design Jury, add lane-density modes, ship-rule explainer, label sizing * feat(contracts): add CritiqueConfig schema and defaults * fix(contracts): apply Task 1.1 review (CRITIQUE_PROTOCOL_VERSION rename, descriptions, RoleWeights export) * feat(contracts): add PanelEvent discriminated union and isPanelEvent guard * fix(contracts): apply Task 1.2 review (exhaustive event-type list, runId guard, import order) * feat(contracts): add CritiqueSseEvent variants and panelEventToSse mapper * test(daemon): add v1 wire-protocol golden fixtures for Critique Theater parser * feat(daemon): add v1 streaming parser for Critique Theater wire protocol * chore(contracts): add .js extensions to relative imports for NodeNext consumers * fix(daemon): satisfy noUncheckedIndexedAccess in v1 parser regex match access * test(daemon): cover parser failure modes; fix unclosed-PANELIST swallow bug * fix(daemon,contracts): address PR #387 review - parser now clamps panelist + DIM scores against the run-declared scale captured from <CRITIQUE_RUN scale=...>, not a hardcoded 100 - PANELIST appearing before any <ROUND n=...> opens now throws MalformedBlockError rather than emitting events with NaN round - DIM_RE and MUST_FIX_RE hoisted to module scope and lastIndex reset per call so the parser hot path stops recompiling regex per artifact - overflow check after drain simplified to a plain buf.length > cap test (the prior compound condition was always true on the right side and obscured intent) - scoreThreshold <= scoreScale refine gains a 1e-9 epsilon so floating slack does not reject semantically valid configs - round-1 designer ARTIFACT guard gains a comment naming the spec invariant and the v2 relaxation path - 3 new regression tests cover the panelist-without-round, scale=10 clamp, and scale=20 plumbing cases * docs(specs): rationale for non-goals, failure-mode rate targets, Phase 10 matrix, Phase 14 doc layout * Merge branch 'main' into feat/critique-theater Resolves the contracts/index.ts conflict by keeping the .js extensions added by chore(contracts) |
||
|
|
aefba56a3f |
feat(skills): open-design-landing rename, kami skills, landing OG (#428)
* feat(skills): open-design-landing rename, kami skills, landing OG - Rename editorial-collage skills to open-design-landing and -deck; refresh examples and compose script layout - Add kami-deck and kami-landing skills with HTML examples - Landing page: og.astro, index wiring, and style tweaks; package.json bump - Web i18n: German and Russian copy for renamed and new skills - Daemon test: update skill-asset-rewrite expectations for new paths - Design systems: README and atelier-zero doc touch-ups - Cross-skill SKILL.md reference updates Co-authored-by: Cursor <cursoragent@cursor.com> * docs(landing-page): document version-slot invariant and deprecation timeline Address P3 review notes on PR #428: - Note the `data-github-version` wrapper invariant (version string only) near the canonical URL block in `app/page.tsx`. - Expand the `formatVersion` helper comment in `app/pages/index.astro` with concrete `release.name` / `tag_name` example shapes for each branch of the regex fallback. - Tighten the `EditorialCollageDeckInputs` deprecation in `skills/open-design-landing-deck/schema.ts` to a specific removal version (v0.4.0) and add a "Migrating from editorial-collage-deck" section to the skill README. Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code) * docs(landing-page, skills): clarify version slot script and rename migrations - Describe GitHub version slots as driven by the inline enhancement script, not React hydration. - Add editorial-collage → open-design-landing migration notes; fix README link copy (Astro static landing app). - Extend deck README migration table with shared asset path renames. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(daemon): alias deprecated editorial-collage skill ids The PR renames the editorial-collage / editorial-collage-deck skills to open-design-landing / open-design-landing-deck, but the daemon persists exact skill_id strings on projects and resolves them via listSkills().find((s) => s.id === storedId). After the rename, any project saved against an old id silently composes without the intended skill prompt because the listing no longer exposes that id. Add a SKILL_ID_ALIASES map in skills.ts plus a findSkillById() helper that rewrites deprecated ids to their current canonical form, then route every server-side lookup (skill detail, example HTML, asset proxy, system-prompt composer) through it. Cover the alias map, the resolver, and end-to-end resolution against a temp skills directory with a regression test. Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code) * fix(kami-deck): route host od:slide messages through local go() The host bridge classifies kami-deck as class-driven because go() toggles .slide.active, but the visible slide is moved by deck.style.transform which the bridge cannot drive. Listen for od:slide messages and dispatch them through the local go() so toolbar next/prev and initialSlideIndex restore actually shift the deck. Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code) * fix(kami-deck): sync deck transform with host-driven .active changes The previous fix added a local od:slide listener but the host bridge in apps/web/src/runtime/srcdoc.ts also listens for the same message and calls setActive() (toggles .slide.active) without driving the deck transform. Both listeners fired, the bridge re-read the just-toggled active class, and overshot by one — and the bridge's restoreInitialSlide path could move .active without a message at all, leaving the deck on the original transform. Stop the bridge from double-handling by calling stopImmediatePropagation in the local listener (registered first because the bridge script is appended to </body>), and add a MutationObserver that pulls the deck transform onto whichever slide currently carries .active so the bridge's direct setActive calls (notably the initial-slide restore) move the deck too. Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code) * fix(i18n): align French content with renamed/new skills PR #434 (French localization) merged into main with French copy for the old editorial-collage / editorial-collage-deck skill ids; this branch renamed those to open-design-landing / open-design-landing-deck and added kami-deck and kami-landing. Update content.fr.ts to track the rename and add French copy for the new kami skills so the LOCALIZED_CONTENT_IDS coverage test passes once main is merged. Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code) * fix(open-design-landing-deck): sync deck transform with host-driven .active changes Apply the same fix that landed in skills/kami-deck/example.html (commits |
||
|
|
bf533c3d72 |
fix(web): split execution mode tabs and align active chip visuals (#418)
* Refine settings API configuration UX * Fix PR validation issues * Address settings review edge cases * Address BYOK settings review feedback * Fix BYOK provider labels and model display |
||
|
|
92087b515d |
fix(web): isolate preview blob export paths (#429)
* fix(web): isolate preview blob export paths * fix(web): complete preview sandbox rollout |
||
|
|
76b41ffef4 |
fix(daemon): expose skill resources via cwd-relative aliases (#435)
* fix(daemon): expose skill resources via cwd-relative aliases (#430) Skill bodies referenced repository-absolute paths in their preamble and relied on every agent CLI honouring `--add-dir` (or its equivalent) to open them. Most agents (codex, gemini, opencode, cursor-agent, qwen, pi, hermes, kimi) have no such flag, and Claude Code's directory-access policy can still block these reads under specific permission/trust configurations even when `--add-dir` is set, so SKILL side files fail to load mid-turn. Stage `.od-skills` and `.od-design-systems` directory links inside the project cwd before spawning the agent, and rewrite the skill preamble to address resources via the cwd-relative alias path. The alias resolves through a directory junction (Windows) or symbolic link (POSIX), so resources are reachable from inside every agent's working directory without needing an extra-allowed-directories flag. `extraAllowedDirs` is kept as a belt-and-suspenders fallback for Claude/Copilot — if the alias cannot be created (e.g. the user has a pre-existing real entry under one of the reserved names), `--add-dir` still covers the absolute paths. `ensureCwdAliases` is idempotent, repoints when the target moves, and refuses to overwrite a non-symlink entry under the reserved name to avoid clobbering user data. Closes #430. * fix(daemon): address review feedback — copy active skill, dual preamble paths Round 2 of PR #435 addressing the unresolved P1/P2/P3 review threads from @chatgpt-codex-connector, @lefarcen and @mrcfps. - P1 writable-symlink. Replace the directory link with a per-project `fs.cp` of the active skill into `<cwd>/.od-skills/<folder>/`. Writes through the staged copy no longer mutate the shipped repo resource. A regression test pins this down so a future "optimisation" that re-introduces a symlink would fail loud. - P1 absolute fallback. Rewrite the skill preamble to advertise both the cwd-relative alias path (primary) and the absolute path (fallback). When `stageActiveSkill` is skipped (no project cwd) or errors, Claude/Copilot still reach the resource via the absolute path covered by `--add-dir`; other agents that fall back to `PROJECT_ROOT` as cwd reach it natively because the absolute path is in-cwd there. - P2 symlinked source root. Use `stat()` instead of `lstat()` on the source so a symlinked `SKILLS_DIR` or skill folder is followed rather than skipped. - P2 stale ordering. `ensureCwdAliases` is gone; staging now happens independently of prompt composition and never silently diverges from what the preamble advertises. - P3 unsafe spec name. Validate `folderName` against an explicit segment policy (no path separators, no dot segments, no absolute, no null bytes) so a malformed caller cannot escape the alias root. - P3 strong types. Drop the `@ts-nocheck` pragma on `cwd-aliases.ts` and type the public API (`SkillStagingResult`, `SkillStagingLogger`). Also drops the `.od-design-systems` alias entirely. Design-system bodies are read by the daemon and folded into the system prompt directly, so an agent never has to open them through the filesystem; exposing that tree as cwd-visible was unnecessary surface area. Test additions: 17 cases in `cwd-aliases.test.ts` covering the copy semantics, the write-barrier regression, symlinked-source handling, legacy-symlink upgrade, alias-root collision refusal, cwd=null guard, and a parameterised set of unsafe folder names. * fix(daemon): unify effectiveCwd and use junctions on Windows test fixtures Round 3 of PR #435 addressing two non-blocking follow-ups from @mrcfps: - server.ts: collapse the chat-handler `cwd` resolution into a single `effectiveCwd = cwd ?? PROJECT_ROOT` and feed it consistently to `buildArgs`, `spawn`, and the ACP/Pi RPC session adapters. Previously `spawn(... { cwd: cwd || undefined })` let no-project runs inherit whatever working directory the daemon process was started from (packaged daemons / service launches do not start in the workspace root), which made the "absolute path is in-cwd" guarantee in the new skill preamble untrue for that exact case the comment claimed to cover. With `effectiveCwd`, agents in no-project mode land in `PROJECT_ROOT` and the absolute fallback path emitted by the preamble is genuinely a relative-to-cwd path. - cwd-aliases.test.ts: replace `symlinkSync(..., 'dir')` fixtures with a platform-aware `dirLinkType` (`'junction'` on Windows, `'dir'` elsewhere). Directory symlinks on Windows require SeCreateSymbolicLinkPrivilege / Developer Mode, which is not enabled on most CI images, so the previous form would fail the daemon test suite on Windows runners even though the production code (which uses `fs.cp`) does not need symlinks at all. |
||
|
|
c881c0ca34 | Add complete French localization (#434) | ||
|
|
653c506b10 |
fix(landing-page): deploy wrangler with npm (#421)
* fix(landing-page): deploy wrangler with npm * fix(landing-page): deploy pages with pnpm dlx * fix(landing-page): deploy wrangler from app workspace Generated-By: looper 0.5.1 (runner=fixer, agent=opencode) |
||
|
|
5a09e39f1f | fix(web): tighten entry-tab layout and design-system showcase color picker (#412) | ||
|
|
6c2a8ba09f |
feat(editorial-collage): introduce Atelier Zero style landing page as… (#366)
* feat(editorial-collage): introduce Atelier Zero style landing page assets and documentation - Added new design system for Atelier Zero, including a detailed `DESIGN.md` file. - Created an `editorial-collage` skill with associated assets for a magazine-grade landing page. - Included example HTML and image assets for various sections (hero, about, capabilities, etc.). - Updated README files to guide usage and customization of the new skill and design system. - Introduced a new image generation prompt pack for consistent visual style across the landing page. * fix(i18n): cover atelier-zero design system and editorial-collage skill in German content Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code) * fix(editorial-collage): align manifest with shipped assets and address PR review - Update image-manifest.json widths/heights/ratios to match the actual PNGs on disk: hero/about/cap/testimonial/cta = 1024x1024 (1:1), method-1..4 = 816x816 (1:1), lab-1..5 and work-1..2 = 768x1024 (3:4). Mirror the new dimensions in imagegen-prompts.md headings and in README.md. - Mark testimonial.png as rekey_on_brand_change so the manifest agrees with SKILL.md's "regenerate at minimum testimonial.png" guidance, and add work-1/work-2 to the rekey list in SKILL.md and README.md. - Add a Hero (I.) sec-rule and renumber every following section II..VIII in example.html so the eight sections walk sequentially I -> VIII and the page-of-008 counter starts at 001. - Delete editorial-artifact-system/ (16 duplicate PNGs + index.html + skills.md draft) — the canonical version is skills/editorial-collage/ and the duplicate had no consumer references. - DESIGN.md: spell out which dimensions of each magazine reference (Monocle/Apartamento/IDEA), document the rationale for single-accent vs multi-accent, and extend the anti-pattern list with AI-image-gen artifacts the system explicitly rejects. - SKILL.md: add italic_words validation guidance (trim, cap at 4, verb->noun rewrite, punctuation strip) and replace the broken-image fallback with an inline SVG placeholder sized to the slot's manifest aspect ratio. Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code) * fix(daemon): serve skill example assets via stable API route Skill example HTML such as `skills/editorial-collage/example.html` references shipped images via `./assets/*.png`. The web app loads the example into a sandboxed iframe via `srcdoc`, where relative URLs resolve against `about:srcdoc` and the PNGs render as broken images in the Examples preview. Add a `GET /api/skills/:id/assets/*` route that serves files under the skill's `assets/` directory with path-traversal guards, and rewrite `src='./assets/<file>'` / `href='./assets/<file>'` in the example response to point at that route. The disk preview keeps working because the on-disk files are unchanged. Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code) * feat(landing-page): add new static Next.js 16 site for Open Design marketing - Introduced a new landing page application using Next.js 16, featuring a static export setup. - Added essential files including `package.json`, `next.config.ts`, and TypeScript configuration. - Implemented global styles in `globals.css` to match the Atelier Zero design system. - Created a detailed `AGENTS.md` for module-level boundaries and purpose. - Included various image assets for the landing page, ensuring a visually cohesive experience. - Established a root layout and main page structure to support the marketing content. * style(landing-page): enhance topbar layout and improve responsiveness - Added nowrap styling to topbar elements to prevent text overflow. - Introduced media query to hide mid text in the topbar for screen widths between 1200px and 1280px. - Updated layout.tsx to suppress hydration warnings for better rendering consistency. - Removed redundant "Compiled by Open Design" text from the page component. * feat(landing-page): implement scroll-reveal animations for enhanced user experience - Added a new `RevealRoot` component to manage scroll-triggered reveal animations. - Updated `globals.css` with styles for elements using the `data-reveal` attribute, including opacity, translation, and scaling effects. - Modified `layout.tsx` to include the `RevealRoot` component for managing animations. - Enhanced `page.tsx` by adding `data-reveal` attributes to various elements for staggered reveal effects. - Implemented reduced motion support to ensure accessibility for users with motion sensitivity. * fix(landing-page): update import paths and enhance link styles - Changed the import path in `next-env.d.ts` to reference the correct routes type definition. - Enhanced `globals.css` with new styles for topbar links, work cards, and partner elements, improving hover effects and transitions. - Updated `page.tsx` to include canonical project URLs and made various links point to these URLs for better navigation and accessibility. * feat(landing-page): implement headroom-style sticky header with live GitHub star count - Introduced a new `Header` component to manage sticky navigation behavior on scroll, enhancing user experience. - Updated `globals.css` to style the sticky header, including transitions and visibility toggling based on scroll direction. - Modified `page.tsx` to replace the static header with the new `Header` component, which fetches and displays the live GitHub star count. - Ensured accessibility by providing a fallback for users who prefer reduced motion. * feat(landing-page): enhance editorial landing page with global ticker and new styles - Updated `next-env.d.ts` to reference the correct routes type definition for development. - Enhanced `globals.css` with new styles for the global ticker, including responsive design and improved overflow handling. - Introduced a new `WIRE_CITIES` and `WIRE_CONTRIBS` data structure in `page.tsx` to display a counter-scrolling marquee of cities and contributors. - Added a ghost button style for the navigation call-to-action in the header. - Updated various sections in `page.tsx` to integrate the new ticker and improve overall layout and accessibility. * refactor(landing-page): update paper texture overlay and remove multica-ai link - Enhanced comments in `globals.css` to clarify the purpose and behavior of the paper texture overlay. - Adjusted z-index of the overlay to ensure proper layering with other elements. - Removed the `multica-ai` partner link from `page.tsx` to streamline the partner section. * feat(landing-page): implement dynamic contributor marquee with GitHub integration - Added a new `Wire` component to display a counter-scrolling marquee of cities and contributors. - The contributor list is fetched live from the GitHub API, ensuring up-to-date information. - Updated `page.tsx` to integrate the `Wire` component, replacing the static contributor list with dynamic content. - Enhanced comments for clarity regarding the functionality and purpose of the global wire. * fix(i18n): add German display copy for editorial-collage-deck skill The Validate workspace test asserts that GERMAN_CONTENT_IDS.skills covers every curated skill on disk; the new editorial-collage-deck skill was missing from DE_SKILL_COPY, causing src/i18n/content.test.ts to fail. Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code) * feat(landing-page): migrate marketing site to Astro * perf(landing-page): remove React client runtime * perf(landing-page): serve images from Cloudflare resizing * fix(pr): address landing page review feedback --------- Co-authored-by: mrcfps <mrc@powerformer.com> |
||
|
|
483e00d24d | fix(pack): bundle prompt templates in desktop resources (#417) | ||
|
|
71bf5634e8 |
feat(daemon): allow OD_MEDIA_CONFIG_DIR to relocate media-config.json (#411)
* feat(daemon): allow OD_MEDIA_CONFIG_DIR to relocate media-config.json `media-config.ts:configFile()` always wrote to `<projectRoot>/.od/`, which fails when the daemon runs from a read-only install (Nix store, immutable image, etc.) — `PUT /api/media/config` then 500s with ENOENT/EROFS on the mkdir. Add an opt-in `OD_MEDIA_CONFIG_DIR` env var that, when set, redirects the file to `<OD_MEDIA_CONFIG_DIR>/media-config.json`. Existing workspace-local installs are unaffected — the default still resolves to `<projectRoot>/.od/media-config.json`. The override is intended to be pointed at the same writable directory as `OD_DATA_DIR` (`~/.od`, `$XDG_CONFIG_HOME/open-design`, etc.) by the supervisor that launches the daemon. Adds a test pinning the override path so the fallback semantics don't silently regress. * fix(daemon): align OD_MEDIA_CONFIG_DIR with OD_DATA_DIR semantics + cover write path Address PR review: * The previous diff trimmed the env value but skipped path expansion, so `~/.od` would land at literal `./~/.od/media-config.json` and relatives anchored against process.cwd instead of <projectRoot>. Mirror server.ts:resolveDataDir() — `~/` expands to homedir, relatives resolve against projectRoot. Pinned with three new test cases (absolute, `~/`, relative-against-projectRoot). * The packaged daemon (apps/packaged/src/sidecars.ts) doesn't pass OD_MEDIA_CONFIG_DIR through to the child and its allowlist won't forward it, so packaged installs would still fail. Add a fallback precedence: OD_MEDIA_CONFIG_DIR > OD_DATA_DIR > <projectRoot>/.od. Packaged installs (which already set OD_DATA_DIR) and the Home Manager / NixOS modules now route media-config there for free, no separate plumbing required. Workspace-local installs are untouched. * Add a write-path test that points OD_MEDIA_CONFIG_DIR at a not-yet- existing nested directory, calls writeConfig, and asserts both file creation and round-trip read. This reproduces the original PUT /api/media/config failure mode. * Document the precedence chain in AGENTS.md (single source of truth for module-level concerns) and the storage row of README.md, plus expand the file's header comment with a migration note for users who already had a custom OD_DATA_DIR alongside a workspace media-config.json. |
||
|
|
2fa1116111 |
docs(readme): refresh contributors wall (#407)
Co-authored-by: mrcfps <23410977+mrcfps@users.noreply.github.com> |
||
|
|
fcca9fd7c9 |
Update docs/assets/github-metrics.svg - [Skip GitHub Action] (#406)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
eb9693d56c |
fix(daemon): strip ANTHROPIC_API_KEY when spawning Claude Code (#400)
* fix(daemon): strip ANTHROPIC_API_KEY when spawning Claude Code Claude Code prefers ANTHROPIC_API_KEY over the user's logged-in subscription auth when both are present, silently billing API usage even though the daemon's "Local CLI" mode implies the user wants their `claude login` credentials (Pro/Max plan) to be used. The daemon inherited the launching shell's env, so anyone who ever exported ANTHROPIC_API_KEY for SDK work or scripts hit this. Strip ANTHROPIC_API_KEY from the env passed to spawn for the claude adapter only, so Claude Code's own auth resolution applies. Fixes #398 * fix(daemon): strip ANTHROPIC_API_KEY case-insensitively, extract helper, add tests Address review feedback on #400: - Extract spawnEnvForAgent helper in agents.ts so the strip is unit- testable and lives next to the other agent-spawn primitives. - Strip ANTHROPIC_API_KEY case-insensitively. Windows env-var names are case-insensitive at the kernel level; spreading process.env into a plain object loses Node's case-insensitive accessor, so a mixed-case Anthropic_Api_Key would survive `delete env.ANTHROPIC_API_KEY` and still reach Claude Code via the spawned env block on Windows. - Add four tests covering: uppercase strip for claude, case-insensitive strip for claude, preservation for non-claude adapters, and input immutability. --------- Co-authored-by: Sebastian Westberg <sebastianwestberg@Sebastians-MacBook-Pro.local> |
||
|
|
3b3275ea3d |
fix(daemon): support nested paths in project file serve route (#401)
The /api/projects/:id/files/:name route used a named Express param that does not match path separators, so requests for files stored in subdirectories (e.g. assets/logo-mark.svg, fonts/Aeroport.woff2) returned 404. Switch to a wildcard route and read req.params[0] so that any depth of nesting is handled correctly. This fixes missing media and fonts when opening an HTML project file as a standalone preview. |
||
|
|
0fe5ee66ed |
Add French Quickstart (#397)
* Add French Quickstart * Avoid hard-coded Design System count in French Quickstart * Refresh French Quickstart skills overview * Clarify French Quickstart live catalogs * Broaden French Quickstart skill result wording |
||
|
|
01fbe2681a |
Add Ukrainian localization (#395)
* feat(i18n): add Ukrainian localization * feat(i18n): register Ukrainian locale * docs(uk): fix section order, expand repo tree, align details with README.md * table and links fix in README.uk.md * translation polishing #1 * typo fix #1 * translation polishing #2 |
||
|
|
10e8e2d38d |
feat(i18n): add Russian gallery metadata (#396)
Co-authored-by: DrMaks22 <191687254+DrMaks22@users.noreply.github.com> |
||
|
|
179b23d7e7 |
Modernize multi-provider API proxy routing
Adds provider-specific API proxy routes for Anthropic, OpenAI-compatible, Azure OpenAI, and Google Gemini; normalizes provider SSE streams; updates web provider clients/settings/docs/tests; and forwards Gemini token limits. |
||
|
|
2618e5239d |
Add French locale and README (#326)
* Add French locale and README * Polish French UI copy * Place French after German in README switchers |
||
|
|
64609e6a2a |
Refactor RUNTIME_DATA_DIR resolution logic (#391)
* Refactor RUNTIME_DATA_DIR resolution logic Set OD_DATA_DIR=$HOME/.open-design #390 * Refactor resolveDataDir to accept projectRoot parameter Add validation if the resolved path is not writable. |
||
|
|
ce4eb92aa2 |
fix(desktop): show window on macOS dock activate (#270)
* fix(desktop): show window on macOS dock activate
On macOS, closing the Electron window via the red close button hides it
rather than destroying it. Without an app.on('activate') handler, clicking
the dock icon afterward does nothing — the window stays hidden indefinitely.
Add app.on('activate') in runDesktopMain that calls desktop.show(), and
add the corresponding show() method to DesktopRuntime / createDesktopRuntime
that calls window.show() + window.focus() when the window is not destroyed.
* fix(desktop): hide window on macOS close instead of destroying it
On macOS the red X should hide the window, not destroy it. Without this,
clicking close triggers window-all-closed → shutdown, so the activate
handler added in the previous commit can never show anything.
Intercept the close event on darwin and call event.preventDefault() +
window.hide() when the runtime has not been asked to stop. The stopped
flag is set to true before window.close() is called in the programmatic
shutdown path, so that path is unaffected.
---------
Co-authored-by: StotheC90 <StotheC90@users.noreply.github.com>
|
||
|
|
a719f02aa2 |
fix(web): normalize daemon proxy origins
Fix web sidecar proxy requests so same-origin browser requests reach the daemon without tripping origin validation, while unrelated origins remain rejected. Fixes #388. |
||
|
|
8381f4607c |
feat(i18n): refresh Russian locale and add README (#393)
Co-authored-by: DrMaks22 <191687254+DrMaks22@users.noreply.github.com> |
||
|
|
bc957aab6d |
fix(web): URL-load HTML preview iframes by default; ?forceInline=1 opt-out (#384)
Part 1 of 2 for #368. The FileViewer's HTML preview previously fetched the file plus every relative <link>/<script> and concatenated the bytes back into a srcDoc string. That model breaks down for multi-file artifacts: one bad fetch dark-frames the whole iframe, every script shows up as "Inline Babel script (n)" with no source map, and the asset tree gets re-fetched from scratch on every Open click. Switch the default HTML preview to <iframe src=...> against the daemon's raw-file endpoint — the same shape the existing Preview pane already uses. The browser handles its own asset resolution, source maps work, DevTools shows real filenames, and per-asset HTTP caching applies. Decks and comment-mode keep the existing srcDoc path so the deck and comment postMessage bridges (which must run before user scripts) stay intact. Non-deck HTML that depends on the in-iframe localStorage shim can opt back into the inline path with `?forceInline=1` on the host page. The decision logic is extracted to file-viewer-render-mode.ts so it's testable without a DOM harness; both helpers are covered by unit tests and the FileViewer integration test asserts the URL-load vs srcDoc markup for plain HTML, declared decks, and deck-shaped HTML. Refs #368 Co-authored-by: DevForgeAI CI/CD Engineer <devforge-ai@development.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
02638af353 |
Add linux x64 AppImage to tools-pack and release workflows (#369)
* feat(tools-pack): extend config types for linux platform
* feat(tools-pack): add linux resource files (icon, .desktop template)
* feat(tools-pack): export linuxResources paths
* feat(tools-pack): scaffold linux.ts module
* chore(tools-pack): add vitest devdep for linux lane unit tests
* feat(tools-pack): add buildDockerArgs helper for containerized linux builds
* chore: update pnpm lockfile after adding vitest dep
* feat(tools-pack): add renderDesktopTemplate helper
* fix(tools-pack): use @@ICON_PATH@@ token in linux .desktop template
Reviewer flagged the third .replace() in renderDesktopTemplate as dead code
because the template hardcoded Icon=open-design-@@NAMESPACE@@ instead of
using @@ICON_PATH@@. Switch the template to @@ICON_PATH@@ so install logic
controls the icon stem name independent of namespace, and move the
sanitizeNamespace assertion out of the renderDesktopTemplate describe block
into its own describe.
* feat(tools-pack): add matchesAppImageProcess helper
* test(tools-pack): cover matchesAppImageProcess missing APPIMAGE env case
Closes a coverage gap flagged by code review: a process whose executable
matches /tmp/.mount_*/AppRun but has no APPIMAGE env should be rejected.
The implementation already returned false for this case (undefined ===
installPath is false); this test pins the behavior explicitly.
* feat(tools-pack): implement packLinux native build path
* fix(tools-pack): packLinux extra resources, output pre-clear, publish never
Code review flagged three plan-level omissions in packLinux that mac.ts
handles correctly:
1. writeAssembledApp now writes packagedConfigPath (open-design-config.json)
with namespace, nodeCommandRelative, and namespaceBaseRoot. Without it
apps/packaged falls back to defaults at runtime and cannot find the
namespace runtime tree.
2. writeLinuxBuilderConfig now bundles the resource tree and packaged
config into the AppImage via extraResources. Without it the running
app cannot find skills/, design-systems/, craft/, frames/, or the
bundled bin/node.
3. runElectronBuilderLinux now pre-clears appBuilderOutputRoot and passes
--publish never to electron-builder, preventing stale-artifact bleed
between runs and accidental publish attempts in CI when env tokens
are present.
Also aligns appId with mac/win (io.open-design.desktop) and drops a
no-op productNameSafe template-literal.
* feat(tools-pack): implement containerized linux build via Docker
* feat(tools-pack): register linux CLI commands
* fix(tools-pack): align linux electron-builder config with mac.ts
Smoke testing the AppImage revealed the daemon sidecar was missing from
the bundled app.asar:
Cannot find module '@open-design/daemon/dist/sidecar/index.js'
Root cause: writeLinuxBuilderConfig was missing the 'files' field, so
electron-builder used defaults that excluded transitive workspace deps
from the asar. Plus several other mac.ts patterns that I dropped from
the plan: artifactName, executableName, extraMetadata.main/name/
productName/version, npmRebuild=false, nodeGypRebuild=false,
buildDependenciesFromSource=false, compression=maximum, top-level icon.
Switch asar:true → asar:false to match mac.ts (easier to debug missing
files; perf difference negligible for dev installs).
* feat(tools-pack): implement linux install
* feat(tools-pack): implement linux start with extract-and-run
The packaged sidecar's 35-second wait timeout is exceeded when the
AppImage runs from a FUSE-mounted SquashFS (Node module loads + daemon
init are slow through FUSE). Pass --appimage-extract-and-run as the
first arg so AppImage extracts to /tmp first; subsequent file reads
go through a real filesystem and daemon boot completes in time.
Wait for apps/packaged to write desktop-root.json (60s ceiling, generous
to cover AppImage extraction overhead), then fetch desktop status via
sidecar IPC, return the merged LinuxStartResult.
* fix(tools-pack): align linux start helper with mac.ts (log echo + write semantics)
Code review flagged two unjustified divergences from mac.ts in
startPackedLinuxApp:
1. Missing OD_DESKTOP_LOG_ECHO=0 in spawn extraEnv. Without it the
packaged logger echoes to the spawned process's stdout, which goes
nowhere (logFd: null). Added the suppression to match mac.ts.
2. The desktop log truncate writeFile() was wrapped in .catch(() =>
undefined), silently swallowing fs errors that would later surface as
confusing missing-log symptoms. Removed the .catch so errors
propagate per mac.ts.
Also added an inline comment explaining the 60s waitForMarker timeout
(vs mac's tighter ceiling) so the rationale is preserved at the
call site.
* feat(tools-pack): implement linux stop with marker validation
* fix(tools-pack): align linux stop with mac.ts (graceful shutdown + reason strings)
Code review flagged divergences from mac.ts in stopPackedLinuxApp:
1. No graceful IPC SHUTDOWN attempt before SIGTERM/SIGKILL. Mac's
pattern lets Electron renderers + sidecars flush state (SQLite WAL,
logs) first. `gracefulRequested: true` was hardcoded, lying to
callers about what actually happened. Now attempts SHUTDOWN with a
1500ms timeout and reports the actual outcome.
2. The dead-PID-but-marker-exists branch returned reason 'ok' (the
neutral placeholder from readDesktopRootIdentityMarker), which says
nothing useful. Override to 'marker-pid-not-running' to match mac.ts.
3. After a clean stop, remove the desktop-root.json marker so a
subsequent start has a fresh slate (mac.ts does this too).
* fix(tools-pack): clear stale desktop-root.json before linux start
Smoke-testing the install/start/stop loop revealed waitForMarker
returns instantly when a stale marker from a previous run still exists
on disk (e.g., the previous AppImage was killed without going through
'tools-pack linux stop'). The start function then reports success
without actually waiting for the new spawn to write its own marker.
Defensively remove the marker file before spawning. mac.ts removes it
in stop, so a clean stop->start sequence has nothing to remove here.
This only matters for crash-recovery.
* fix(tools-pack): linux stop validates extract-and-run AppImages
Smoke testing exposed a gap from Task 7: matchesAppImageProcess only
recognized FUSE-mode (/tmp/.mount_*/AppRun) but Task 13 launches with
--appimage-extract-and-run, which puts the live executable at
/tmp/appimage_extracted_<hex>/<binary>. Stop's cmdOk validation
returned false, marker validation failed, the running app was
classified as 'unmanaged' and stop refused to kill it.
Fix:
1. matchesAppImageProcess accepts both runner patterns. Extract-and-run
regex matches /^\/tmp\/appimage_extracted_[^/]+\/[^/]+$/.
2. stopPackedLinuxApp now passes paths.installAppImagePath (or the
built fallback) as the canonical install path, not marker.appPath
(which apps/packaged unhelpfully writes as '/' on Linux).
3. linux.test.ts gains 2 new tests covering the extract-and-run mode
(both positive and the wrong-APPIMAGE-env negative case).
* fix(tools-pack): resolve linux paths in stop (typecheck regression from previous commit)
* feat(tools-pack): implement linux logs
* feat(tools-pack): implement linux uninstall
* feat(tools-pack): implement linux cleanup
* docs(tools-pack): document linux lane in READMEs and AGENTS files
* ci(release): add linux x64 AppImage to release-beta and release-stable
Mirrors the existing build_mac/build_win pattern with a build_linux job
in both release workflows. Builds via `tools-pack linux build
--containerized --to appimage` so the AppImage is linked against the
electronuserland/builder glibc 2.27 baseline (portable across distros)
rather than the ubuntu-latest glibc 2.39.
The linux asset is uploaded to the immutable version release tag
alongside mac/win. The beta channel-feed release (latest-mac.yml,
latest.yml) is intentionally not extended with latest-linux.yml because
tools/pack/src/linux.ts has no electron-builder publish block wired,
so the auto-update feed would point users at a feed that never updates.
AppImage auto-update is a separate follow-on.
Linux is unsigned (no signing path in tools-pack yet), so the beta
asset uses the .unsigned suffix matching the windows convention; the
stable asset uses no suffix, matching the stable windows convention.
* fix(tools-pack): propagate --dir/--portable into containerized linux build
The inner `pnpm tools-pack linux build` invocation in `buildDockerArgs`
only forwarded `--to` and `--namespace`. Callers passing `--dir` (e.g.
the new release workflows using `--dir $RUNNER_TEMP/tools-pack`) had
their flag silently dropped: the container defaulted to writing under
/project/.tmp/tools-pack while the host's `findBuiltAppImage` looked at
the caller's chosen `--dir`, producing "expected AppImage not found"
on any non-default tool-pack root. Callers passing `--portable` had
the same drop, baking build-machine runtime roots into shipped artifacts.
Fix:
- Mount `${config.roots.toolPackRoot}:/tools-pack` (new third volume,
alongside the existing /project, /home/builder, and cache mounts).
- Forward `--dir /tools-pack` to the inner build so its output lands
inside the mounted host dir.
- Forward `--portable` when `config.portable` is true.
The mount overlaps harmlessly with /project when toolPackRoot lives
under workspaceRoot (default case): Docker exposes the same host inode
at both paths. The existing .docker-home and .docker-cache/* mounts
continue to shadow the parent at their specific /home/builder paths.
Document the shell-interpolation safety invariant on the inner command:
config.namespace is sanitized at config-time, config.to is enum-validated,
config.portable is boolean -- none can carry shell metacharacters.
Tests: add coverage for the new /tools-pack mount, --dir forwarding,
and --portable propagation (both true and false branches).
Resolves the P1 review feedback from the Codex bot on PR #369.
* docs(tools-pack): polish linux README based on PR review
Addresses non-blocking P2/P3 review feedback on PR #369:
- AppImage launch mode: name the test distros (Ubuntu 24.04, Arch Linux)
and frame the FUSE-vs-extract-and-run gap as an order-of-magnitude
improvement instead of an unspecified slowdown.
- Optional system tools: add a libfuse2 paragraph distinguishing FUSE
launch (needs libfuse2) from extract-and-run (does not), with the
Ubuntu-24-vs-pre-24 package name caveat.
- New section "Format choice: why AppImage first" anchoring the
AppImage-only decision against industry precedent (VS Code, Discord,
Slack, Cursor, Obsidian) so the rationale survives without a reviewer.
- Out of scope: convert the dense one-liner into a bulleted list, mark
AppImage signing as gated on GPG infra + verification flow design
(no ETA), explain the latest-linux.yml gap, and remove the now-stale
"release lane" entry since this PR adds it.
* fix(tools-pack): add --appimage-extract-and-run to installed .desktop launcher
The XDG .desktop file installed by `tools-pack linux install` invoked
the AppImage directly via `Exec=env OD_NAMESPACE=<ns> <exec> %U`. That
bypassed the extract-and-run flag that `tools-pack linux start` applies,
so menu launches and `od://` desktop activations could hit the FUSE
slow path that was already shown to make the daemon sidecar exceed
apps/packaged's 35-second startup timeout. CLI-spawned starts succeeded
while menu-launched starts could fail with the same artifact.
Add `--appimage-extract-and-run` to the template's `Exec=` line and
update the renderDesktopTemplate test expectation. New regression test
locks the flag into place so a future template edit can't silently
drop it.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): treat signal-terminated container builds as failures
`runBuildInContainer` resolved the build promise on `code === null`,
which in Node's child-process `exit` event means the child was
terminated by a signal (SIGTERM, SIGKILL, OOM-killer, parent process
death). A killed Docker build could therefore make `packLinux` report
a containerized build as complete even though the artifact was
partial or missing.
Accept the `signal` argument on the exit handler. Resolve only when
`code === 0 && signal == null`. Otherwise reject with a message
naming either the non-zero code or the terminating signal so the
failure mode is visible in CI logs and `tools-pack linux build --json`
output.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): tear down orphaned process tree on failed linux start
If `startPackedLinuxApp` spawned the AppImage but the post-spawn
readiness path then failed -- either because the 60s waitForMarker
ceiling elapsed without the daemon writing desktop-root.json, or
because fetchDesktopStatus threw -- the detached child was left
running. Because the marker is the only persistent identity source
used by `stopPackedLinuxApp`, future lifecycle commands could not
associate the orphan with the namespace, leaving stale Electron and
sidecar processes plus stale IPC sockets that would interfere with
subsequent starts.
Wrap the readiness wait + status fetch in try/catch. On failure,
collect the spawned child's process tree via listProcessSnapshots +
collectProcessTreePids and stopProcesses() it (the same path
stopPackedLinuxApp uses for its tree teardown), then rethrow the
original error. Cleanup errors are swallowed so the original failure
is preserved in the rejection.
Extract the tree-teardown helper as `teardownOrphanedStart` so the
intent is documented at the call site without inlining 4 imports of
implementation detail.
Resolves a P2 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): use `corepack pnpm` in containerized linux build
The inner command in `buildDockerArgs` started with `corepack enable`,
which writes pnpm/yarn/npm shims into the directory containing the
node binary. In `electronuserland/builder:base`, that directory is
owned by root, but the container runs as the host's non-root uid via
`--user` (so build artifacts come out owned by the caller, not root).
The `corepack enable` step therefore fails with EACCES before
`pnpm install` ever runs, blocking the new release `build_linux` job
from publishing the Linux AppImage.
Switch to `corepack pnpm install --frozen-lockfile && corepack pnpm
tools-pack linux build ...`, which resolves and runs the version of
pnpm pinned in package.json's `packageManager` field directly. No
shims, no global mutation, no root writes — corepack just dispatches
to the pinned binary as the unprivileged user.
Update the existing inner-command test to match the new corepack
invocation, and add a regression test that asserts the inner command
contains `corepack pnpm` and never `corepack enable` so a future edit
can't reintroduce the root-write requirement.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): accept menu-launched processes in linux stop/uninstall
stopPackedLinuxApp validated the live process via matchesStampedProcess
against the process command line, requiring a SIDECAR_SOURCES.TOOLS_PACK
stamp. That worked for `tools-pack linux start` (which spawns with
createProcessStampArgs), but rejected menu launches: the installed
.desktop entry only sets OD_NAMESPACE and does not pass stamp args, so
apps/packaged falls back to a SIDECAR_SOURCES.PACKAGED stamp written
into desktop-root.json -- a perfectly valid identity, just not the one
the validator accepted.
Symptoms with the old behavior:
- `tools-pack linux stop` reported `unmanaged` for menu-launched apps
and refused to stop them.
- `tools-pack linux uninstall` would happily remove the AppImage,
.desktop entry, and icon while the packaged app was still running,
breaking handles to the AppImage's mounted/extracted contents.
Switch the validator to read marker.stamp directly (the file content
written by apps/packaged itself, not the process command) and accept
either TOOLS_PACK or PACKAGED. The expected app/mode/namespace/ipc
fields are still required to match. Mirrors the dual-source acceptance
pattern in mac.ts:709-714.
The matchesAppImageProcess (cmdOk) and namespaceRoot checks are
preserved -- the marker still has to point at our AppImage at a path
in our namespace's runtime root.
Drop the now-unused matchesStampedProcess import.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): per-platform --to help text in CLI
addBuildOptions is shared across mac/win/linux but its --to help text
hard-coded the mac targets (all|app|dmg|zip), so:
- tools-pack linux --help advertised --to all|app|dmg|zip even
though resolveToolPackBuildOutput accepts only all|appimage|dir,
sending users at invalid targets and hiding the AppImage option.
- tools-pack win --help had the same problem (advertised mac
targets while accepting all|dir|nsis with default nsis).
Parameterize addBuildOptions(command, platform) and back it with a
TO_HELP_BY_PLATFORM table that mirrors the resolver's accepted targets
in config.ts. Update the three call sites.
Smoke verified by running --help for each platform:
linux: all|appimage|dir (default: all)
mac: all|app|dmg|zip (default: all)
win: all|dir|nsis (default: nsis)
The misleading "--signed: build a signed/notarized mac artifact" line
on win/linux is left alone -- out of scope for this fix and not part
of the review feedback.
Resolves a P3 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): use OD_PACKAGED_NAMESPACE in installed .desktop launcher
The installed .desktop entry's Exec= line set OD_NAMESPACE=<ns>, but
apps/packaged/src/config.ts:9 reads namespace overrides from
OD_PACKAGED_NAMESPACE, not OD_NAMESPACE. The env assignment was a
silent no-op for menu launches: the packaged app fell back to whatever
namespace was baked into open-design-config.json at install time,
ignoring the namespace advertised in the .desktop file.
Practical effect: a .desktop launcher created for namespace "foo"
could end up running as the namespace baked into the AppImage's
shipped config (typically "default"), so installs created across
multiple namespaces could collide silently from menu launches. CLI
launches via `tools-pack linux start` were unaffected because they
pass the namespace through createSidecarLaunchEnv which targets the
correct env var.
Switch the template to OD_PACKAGED_NAMESPACE. Update the existing
renderDesktopTemplate test fixture/expectation, and add a regression
test that asserts the Exec= line uses OD_PACKAGED_NAMESPACE and never
the wrong OD_NAMESPACE name.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): gate linux uninstall + cleanup on stop status
uninstallPackedLinuxApp called stopPackedLinuxApp first, then deleted
the AppImage / .desktop entry / icon unconditionally. cleanupPackedLinux
Namespace did the same with the output and runtime namespace roots.
Both ignored stop.status -- so when stop returned "partial" (some
processes survived SIGTERM->SIGKILL) or "unmanaged" (the running PID
failed marker validation), uninstall would yank the install files out
from under a still-running packaged app, breaking handles to the
mounted/extracted AppImage contents and leaving an orphan with stale
SQLite WAL files / log handles / IPC sockets.
Extract a small `isSafeToRemoveInstallFiles(stop)` helper that returns
true only for "stopped" or "not-running". Both uninstall and cleanup
short-circuit when it returns false:
- uninstall reports "skipped-process-running" for each removal slot
and "skipped" for the post-install hooks. Existing "ok" / "already-
removed" / "ok"|"missing"|"failed" paths are unchanged.
- cleanup leaves both removed* booleans false and adds a new
`skipped: boolean` field set to true. Old consumers that only read
the booleans see the same "nothing was removed" signal they would
have seen for an already-clean namespace; new consumers can
distinguish "nothing to remove" from "refused to remove."
LinuxUninstallResult.removed.{appImage,desktop,icon} now also accepts
"skipped-process-running"; LinuxUninstallResult.postUninstall.* now
also accepts "skipped". LinuxCleanupResult gains the `skipped` field.
Workspace typecheck clean -- the only consumer is the CLI's printJson,
which doesn't constrain the wire shape.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
|
||
|
|
6b012fc83c |
fix(web): lift coming-soon tip above sticky tabs and make it readable in dark theme (#382)
The "Coming soon" pseudo-tip on .viewer-action / .viewer-toggle had two bugs in the file viewer toolbar: 1. Hidden behind .ws-tabs-bar. The parent shell sets opacity: 0.55, which creates a stacking context, so the ::after's z-index was trapped locally and could not rise above the sticky tabs bar (z-index: 4) in the root context. Pin the parent to z-index: 5. 2. Invisible in dark theme. background: var(--text-strong) becomes off-white in dark mode against color: white. Switch to the same fixed dark slate already used by .comment-target-tooltip (rgba(17, 24, 39, 0.95)) so contrast holds in both themes. |
||
|
|
339a57c65c |
fix(security): bind daemon to localhost by default, add origin validation (#365)
* fix(security): bind daemon to localhost by default, add origin validation middleware The daemon's Express server previously defaulted to binding 0.0.0.0, exposing all 30+ API endpoints to the local network with zero authentication. This included endpoints that spawn CLI agents, write project files, and manage API keys. Changes: - Default bind address changed from 0.0.0.0 to 127.0.0.1 in both server.ts and cli.ts. Users who need network access can still set OD_BIND_HOST=0.0.0.0 explicitly. - Added origin validation middleware on /api/* routes: requests with an Origin header are only allowed from localhost/127.0.0.1/[::1] on the daemon's own port. Non-browser clients (no Origin header) are unaffected. - 7 unit tests for the origin validation logic. - Existing tests: 327/327 passing. * fix: address PR review — fail-closed, OD_WEB_PORT, HTTPS, Origin: null Address feedback from @lefarcen, @mrcfps, and Codex review: P1: Fail-closed when resolvedPort is not yet set (returns 403 instead of passing through). P2: Include OD_WEB_PORT in allowed origins for split-port proxy setups (web port ≠ daemon port). Add HTTPS variants. P3: Exempt Origin: null for sandboxed iframe preview fetches (/api/projects/:id/raw/*) so artifact previews keep working. P4: Update help text from 0.0.0.0 to 127.0.0.1. Test coverage expanded to 13 tests including: - OD_WEB_PORT split-port proxy scenario - Origin: null iframe preview - HTTPS origin variants - Fail-closed before port resolution - Cross-origin rejection with OD_WEB_PORT set All 333 tests passing, TypeScript clean. * fix: scope Origin: null bypass to raw-file previews only, support non-loopback bind host Two issues found in second review round: 1. Origin: null was a global bypass for ALL /api routes, allowing sandboxed iframes to reach state-changing endpoints (project create/delete, agent runs). Now only GET requests to /projects/:id/raw/* pass through with Origin: null — the original intent for iframe file previews. POST/DELETE with Origin: null are rejected. 2. Allowed origins only included loopback addresses. When daemon binds to a non-loopback address (--host for Tailscale, LAN, or 0.0.0.0), browser requests from that address would get 403. Now the bound host is included in allowed origins alongside loopback, keeping the documented network-access escape hatch working for browser clients. Added negative tests for Origin: null on POST/DELETE/non-raw GET, and positive tests for non-loopback bind host scenarios. All 339 tests passing. * fix: centralize origin policy, add spritesheet to Origin: null allowlist Addresses @mrcfps's third review round: 1. isLocalSameOrigin() now uses the same policy as the global origin middleware: HTTPS + HTTP, OD_WEB_PORT, and the explicit bind host (OD_BIND_HOST). Previously it only accepted HTTP on loopback hosts, so requests from https://127.0.0.1 or a Tailscale address could pass the global guard but 403 on per-route checks. 2. /api/codex-pets/:id/spritesheet is a read-only route that sets Access-Control-Allow-Origin: null for canvas drawing by sandboxed iframes. Added to the Origin: null allowlist so the middleware doesn't block it before the route handler runs. 3. buildAllowedOrigins() extracted as a closure so both the middleware and isLocalSameOrigin() share identical logic. 340/340 tests passing. * test: remove dead fail-closed test with unused 5th argument The 'fails closed when port is 0' test called request() with an extra argument the helper ignores, then never asserted on res. Real coverage lives in the dedicated 'fail-closed before port resolution' describe block. |
||
|
|
4de93fe35c |
Add OpenAI media OAuth fallback (#377)
Co-authored-by: Fu Yizheng <fyz3120@sina.cn> |
||
|
|
016c08183f | release: Open Design 0.3.0 open-design-v0.3.0 |