Commit Graph
142 Commits
Author SHA1 Message Date
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>
2026-05-04 20:47:22 +08:00
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 96b255b, 8cbca30) to the open-design-landing-deck composer
runtime: the host bridge classifies this deck as class-driven because
go() toggles .slide.active, but the visible slide is moved by
deck.style.transform which the bridge can't drive. Add an od:slide
message listener that calls stopImmediatePropagation() and routes nav
through the local go(), plus a MutationObserver that pulls the deck
transform onto whichever slide carries .active so the bridge's direct
setActive calls (notably restoreInitialSlide) move the deck too.

Regenerates example.html via scripts/compose.ts; the regeneration also
picks up upstream nav-cta and brand-meta CSS additions in the sister
open-design-landing styles.css that the example had drifted from.

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

* docs(open-design-landing): align deploy story with Astro landing app

- Update SKILL contract: apps/landing-page is Astro static; clarify
  nextjs-app output_format as a historical enum label and <out>/nextjs
  as a legacy folder name.
- Replace optional-deploy section with fork + pnpm --filter landing-page build.
- Fix styles.css header and regenerate landing + deck example.html so
  inlined comments match.

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

* fix(deck-runtime): bypass interaction lock for host/observer slide sync

The slide deck runtimes for kami-deck and open-design-landing-deck
gate go() behind a 700ms `lock` so wheel/key/touch input bursts can't
overshoot the transform transition. But applying the same gate to the
host bridge's od:slide messages and the MutationObserver watching
`.slide.active` creates a startup race: go(0) at the end of init sets
lock=true, and any host-driven `.active` change inside that window
(notably restoreInitialSlide) fires the observer, which calls go(i),
which exits at the lock guard — leaving the visible deck on slide 1
while the host counter advances to N.

Split the actual state update into an unthrottled `applySlide(n)`
helper that updates transform, `.active`, dot nav, and the progress
bar. Keep `lock` only on the user-input path through `go()`. Route
the message listener, the MutationObserver, and the initial render
through `applySlide` directly so host-driven sync always reaches the
deck transform regardless of the throttle state.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 19:22:46 +08:00
monshunterandGitHub 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
2026-05-04 18:07:30 +08:00
AresdgiandGitHub 92087b515d fix(web): isolate preview blob export paths (#429)
* fix(web): isolate preview blob export paths

* fix(web): complete preview sandbox rollout
2026-05-04 17:57:52 +08:00
David ZandGitHub c881c0ca34 Add complete French localization (#434) 2026-05-04 16:38:58 +08:00
Tom HuangandGitHub 5a09e39f1f fix(web): tighten entry-tab layout and design-system showcase color picker (#412) 2026-05-04 13:49:41 +08:00
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>
2026-05-04 13:39:58 +08:00
Dev codexandGitHub 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
2026-05-04 08:48:56 +08:00
10e8e2d38d feat(i18n): add Russian gallery metadata (#396)
Co-authored-by: DrMaks22 <191687254+DrMaks22@users.noreply.github.com>
2026-05-04 04:29:30 +08:00
lefarcenandGitHub 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.
2026-05-04 04:23:32 +08:00
David ZandGitHub 2618e5239d Add French locale and README (#326)
* Add French locale and README

* Polish French UI copy

* Place French after German in README switchers
2026-05-04 04:10:59 +08:00
lefarcenandGitHub 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.
2026-05-04 03:39:19 +08:00
8381f4607c feat(i18n): refresh Russian locale and add README (#393)
Co-authored-by: DrMaks22 <191687254+DrMaks22@users.noreply.github.com>
2026-05-04 03:38:05 +08:00
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>
2026-05-04 00:54:09 +08:00
emilneanderandGitHub 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.
2026-05-04 00:10:03 +08:00
lefarcenandGitHub 016c08183f release: Open Design 0.3.0 2026-05-03 23:07:28 +08:00
3c954ad2b3 feat(i18n): add French locale (#376)
Co-authored-by: Rudy Palfray <rpalfray87@gmail.comy>
2026-05-03 22:21:20 +08:00
Tom HuangandGitHub e90e2f9e1f feat(skills): add taste-skill-derived web prototype and HTML PPT examples (#358)
* feat(skills): add taste-skill-derived web prototype and HTML PPT examples

Adds five skills distilling Leonxlnx/taste-skill into the project's
SKILL.md + example.html convention so authors can pick a deliverable
shape (web prototype / deck) and a design register (editorial / soft /
brutalist) without the LLM defaulting to Inter-on-white slop.

- web-prototype-taste-editorial: minimalist-skill applied (warm
  monochrome, Instrument Serif x Inter Tight, hairline bento)
- web-prototype-taste-soft: soft-skill applied (silver canvas, ambient
  mesh, double-bezel cards, button-in-button CTAs, spring motion)
- web-prototype-taste-brutalist: brutalist-skill Swiss-print mode
  (newsprint, Archivo Black, viewport-bleeding numerals, hazard-red,
  ASCII syntax)
- html-ppt-taste-editorial: 10-slide investor memo deck in the editorial
  register, compatible with the existing html-ppt static-preview pattern
- html-ppt-taste-brutalist: 11-slide tactical-telemetry / CRT-terminal
  incident debrief deck (scanline overlay, hazard-red alerts, audit log)

Each SKILL.md cites the upstream taste-skill source file and lists hard
rules, banned patterns, required components, and a pre-flight checklist.

* fix(i18n): register new taste skills in German content registry

Adds the five new taste-derived skills to DE_SKILL_IDS_WITH_EN_FALLBACK
so the German display content coverage test sees them.

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

* fix(skills): close stray </small> in html-ppt-taste-brutalist telemetry cell

The paged-engineers tcell's delta span on slide 04 closed with
`</small></span>`, leaving the example with invalid HTML that downstream
previews/validators can flag.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
2026-05-03 20:31:05 +08:00
emilneanderandGitHub 2e28cb0bc9 fix(web): scroll workspace tabs in place instead of the window (#363)
Merged per maintainer approval.
2026-05-03 17:29:03 +08:00
monshunterandGitHub 68493b7b72 feat(web): task completion sound + browser notification (#359)
Merged per maintainer approval.
2026-05-03 16:00:39 +08:00
9d700ec74f feat(daemon): persist code agent startup (#255)
* feat(daemon): persist code agent startup

* fix: complete all suggestions

* fix: types for app config

* chore: revert local origin

* chore: format to single quotes

* fix: duplicate headers

* fix: isLocalSameOrigin rewriting issue

---------

Co-authored-by: mrcfps <mrc@powerformer.com>
2026-05-03 12:14:04 +08:00
SidandGitHub 9c71fc20eb feat(web): add "Export as Markdown" pass-through download to share menu (#345)
Closes #279.

Adds an `Export as Markdown` entry to the HTML / deck artifact share menu
in `FileViewer`. The entry downloads the artifact source verbatim with a
`.md` extension and `text/markdown; charset=utf-8` Content-Type — no
HTML→markdown conversion runs. The file body is byte-for-byte identical
to what the Source view shows.

The use case (per the issue thread): users want to pipe the artifact's
raw source into markdown-aware tooling — LLM context windows, vault
apps, etc. — that key off the `.md` extension. A lossy text-extraction
conversion (turndown / similar) was the first thing I tried, but it
loses the embedded styles + scripts that make the deck reproducible
and was rejected by the issue author in favor of pass-through. Keeping
the implementation byte-exact also leaves room for a future
"Convert to Markdown" entry to live alongside this one without
overlap.

Changes:

- `apps/web/src/runtime/exports.ts` — new `exportAsMd(source, title)`
  helper; module-level comment updated to include MD in the export
  family description.
- `apps/web/src/components/FileViewer.tsx` — new share menu entry,
  positioned right after `Export as standalone HTML` and before the
  `Save as template` divider so the file-format exports stay grouped
  together.
- `apps/web/src/i18n/types.ts` + 14 locale files (`en`, `de`, `ja`,
  `ko`, `zh-CN`, `zh-TW`, `es-ES`, `fa`, `pt-BR`, `ru`, `ar`, `pl`,
  `tr`, `hu`) — `fileViewer.exportMd` key, mirroring the structure of
  the existing `exportHtml` / `exportZip` entries.
- `apps/web/src/runtime/exports.test.ts` — three new cases under
  `exportAsMd`: bytes-verbatim with the markdown content type, title
  fallback when sanitization yields nothing, and UTF-8 round-trip.
  The tests stub `URL.createObjectURL` + a minimal `document` so the
  download path works under `environment: 'node'`.

No new dependencies. No daemon-side changes — all client-side.

Test plan:
- [x] `pnpm --filter @open-design/web test` — 23 files / 143 tests green
- [x] `pnpm --filter @open-design/web typecheck` — clean
- [x] `pnpm typecheck` (full repo) — clean
- [x] Manual browser test via `pnpm tools-dev run web`: clicked the
      new menu entry on a 10-slide deck artifact; download blob was
      `text/markdown; charset=utf-8`, 36,990 bytes, started with
      `<!doctype html>...` matching the Source view byte-for-byte.
2026-05-03 11:56:47 +08:00
ccfuncyandGitHub f12471f29d fix(i18n): update locale coverage after main merge (#251) 2026-05-03 11:36:57 +08:00
6e11eae675 fix(web): treat inlined script content as literal in FileViewer (#343)
`inlineRelativeAssets` reduced the replacements with `next.replace(from, to)`
where `to` was the fetched script/stylesheet body. `String.prototype.replace`
with a string replacement interprets `$&`, `$'`, `$\``, `$1`–`$9`, and `$<n>`
as back-reference patterns, so any inlined file containing one of those
sequences (e.g. JSX with `'-$'` currency formatting) had its content
mangled — typically with everything-after-the-match spliced into the middle
of a string literal, producing "Unterminated string constant" parse errors
and preventing the inlined React app from booting.

Switching the second arg to a function replacer makes the body literal and
fixes the Open path for any project with `$`-bearing inlined assets.

Co-authored-by: DevForgeAI CI/CD Engineer <devforge-ai@development.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:33:51 +08:00
monshunterandGitHub 30f8036c9a fix(web): make share-menu "Download as .zip" return the actual project tree (#341)
* fix(web): download project tree as zip from share menu

The "Download as .zip" share action previously produced a single-file
ZIP of the rendered HTML srcdoc. Add a daemon archive endpoint that
bundles the on-disk project tree (scoped to the active top-level
directory when applicable) and have FileViewer call it, falling back to
the in-memory single-file ZIP on failure. UTF-8 filenames are preserved
via RFC 5987 Content-Disposition.

* fix(daemon,web): address PR #341 review — diagnostics, comments, more tests

- Stat the archive root up-front so a missing/non-directory target
  surfaces a clear ENOENT/ENOTDIR ("does not exist" vs "is empty"),
  instead of being swallowed by the recursive walk and reported as
  empty. Distinguishes a deleted project from one with no archivable
  files for on-call diagnostics.
- Document the DEFLATE level-6 choice in the archive builder so future
  maintainers don't have to guess at the speed/ratio trade-off.
- Add a daemon test that the baseName preserves a multi-byte UTF-8
  directory name (café-design), covering the path that feeds RFC 5987
  filename* on the server.
- Add a daemon test that a missing archive root surfaces ENOENT with a
  "does not exist" message, distinct from the empty-directory case.
- Export archiveRootFromFilePath and archiveFilenameFrom for testing,
  and add web unit tests covering the Content-Disposition fallback
  chain (UTF-8 filename* → legacy quoted filename= → root slug → title
  slug, plus malformed-encoding fall-through).
- Replace the CJK example string in code comments and tests with a
  Latin-extended example (café-design) so the codebase stays English
  while still exercising multi-byte UTF-8 handling.
2026-05-03 10:34:33 +08:00
laurinandGitHub 0c65a0508e fix(web): keep Design Files view active after deleting a file (#329)
* fix(web): keep Design Files view active after deleting a file (#115)

Deleting a file from the Design Files panel was navigating the user
into another file's tab (typically the previously-opened file such as
`index.html`). Two unrelated symptoms contributed:

- `FileWorkspace.handleDelete` always called
  `setActiveTab(nextActive ?? DESIGN_FILES_TAB)`, which yanked the user
  out of the Design Files panel whenever `tabsState.active` still
  pointed at a real file (clicking the Design Files tab only updates
  local `activeTab`; it does not touch persisted `tabsState.active`).
- The row popover had `onMouseDown` propagation guard but no
  corresponding `onClick` guard on the destructive action path.

Fix in `FileWorkspace.handleDelete`:
- If the deleted file is the one being viewed (`activeTab === name`),
  fall back to another open tab (or Design Files if none remain) — the
  prior behavior, which is correct in this case.
- Otherwise (deletion came from the Design Files panel, the typical
  path), leave `activeTab` alone so the user stays in the list view.
  Only null out `tabsState.active` when it referenced the deleted file
  to avoid leaving a dangling pointer for the hydration `useEffect` to
  resync against.

Defensive cleanup in `DesignFilesPanel`:
- Add `onClick` propagation guard on the popover wrapper (mirrors the
  existing `onMouseDown` guard).
- Stop propagation on each popover button (Open / Download / Delete)
  and on the download `<a>` so destructive/menu actions can't bubble
  into row click handlers under any DOM arrangement.

Closes #115

* fix(web): address review feedback + add multi-tab delete regression E2E

Review feedback (#329):
- Drop the redundant `onClick={(e) => e.stopPropagation()}` on the
  download `<a>` wrapper; the inner `<button>` is the actual click
  target and already stops propagation.
- Expand the comment in `handleDelete`'s else branch to explicitly
  state that `activeTab` is preserved because the user is in a
  different context (Design Files panel or another tab) and shouldn't
  be navigated away.

Regression coverage:
- Extend `runDesignFilesDeleteFlow` to upload a sibling file
  (`keep-me.png`) before `trash-me.png`, so deletion has a fallback
  tab that the buggy code would have navigated to. After deletion,
  assert the Design Files tab still has `aria-selected="true"` and
  the sibling tab is still present (just not auto-activated).
2026-05-03 09:34:17 +08:00
lefarcenandGitHub b4e00cfb03 fix(i18n): add missing designFiles.showMore key to ar/hu/ko/pl/tr (#335)
PR #239 added `designFiles.showMore` to en + 8 other locales but
missed ar, hu, ko, pl, tr — `pnpm --filter @open-design/web typecheck`
fails with TS2741 on each of those five and the failure has been
blocking every PR's CI on `main` since #239 landed.

Translations follow the verb/counter conventions already used inside
each locale file (ja/zh use "show {n} more"-style; de/es/pt-BR/ru
prefer "show +{n} more"-style; ko uses the standard `개` counter).
2026-05-03 09:21:23 +08:00
223ac39241 fix(web): use response-order matching for bulk upload aggregation (#323)
uploadProjectFiles built a Map keyed by `f.originalName ?? f.name` from
the server response and matched browser File names against it. The
server's `originalName` is the multer-decoded UTF-8 filename, which can
diverge from `File.name` under encoding edge cases (NFC vs NFD, double-
encoded UTF-8 round-trips, certain CJK combining characters), so the
client marked uploads as failed even when the server stored them
successfully.

The server iterates `req.files` in order and pushes successes to the
response array (apps/daemon/src/server.ts), so position is reliable.
Replace the name-equality matching with positional matching: every
entry in `responseFiles` corresponds to `batch[i]`. The only legitimate
partial-failure path is the daemon's `try/catch` around `fs.stat` for
files that vanished mid-flight, which produces a shorter response - the
unmatched tail of `batch` is then marked failed.

The on-disk timestamp prefix in the multer filename callback is
intentional (collision avoidance) and unchanged. The user-visible name
in the chat composer continues to use `f.originalName ?? f.name`.

Fixes #116

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-05-03 09:06:39 +08:00
Dev codexandGitHub b06021cd6f [codex] Improve project input performance (#239)
* Improve project input performance

* update: improvements to input performance

* Fix buffered stream cancellation lifecycle
2026-05-03 08:56:21 +08:00
ALI HAIDERandGitHub 38946687f8 feat(i18n): add full Arabic language support and RTL layout (#316)
* feat(i18n): add full Arabic language support and RTL layout

* refactor(i18n): address code review feedback on RTL typography and dev-tool timeouts

* fix(i18n): address copilot review (tests, css scoping, readmes)
2026-05-03 08:54:19 +08:00
ccfuncyandGitHub 816cc1d135 feat(settings): split API provider protocols (#250)
* feat(settings): split API provider protocols

* fix(settings): preserve API protocol config

* fix(settings): harden API protocol migration

* fix(settings): set apiProviderBaseUrl during v1 migration for known providers
2026-05-03 01:00:37 +08:00
6fa2077651 feat(web): add pet companion with Codex hatch-pet integration (#296)
* feat(web): add pet companion with Codex hatch-pet integration

Introduces a customizable floating pet companion (overlay + entry-view rail
+ composer menu + dedicated Settings → Pets section) that supports built-in
pets, user customization (glyph/image/spritesheet), and one-click adoption
of pets packaged by the upstream Codex `hatch-pet` skill via a new
`/api/codex-pets` daemon endpoint. Vendors the unmodified `hatch-pet`
skill under `skills/hatch-pet/` and adds i18n strings across all locales.

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

* feat(scripts): sync community Codex pets from public catalogs

Adds `pnpm sync:community-pets` which fetches all pets from
codex-pet-share.pages.dev (paginated Supabase Functions API) and
j20.nz/hatchery (single-shot JSON), then writes each one as
`<id>/pet.json` + `<id>/spritesheet.webp` under
`\${CODEX_HOME:-\$HOME/.codex}/pets/`. The existing daemon
`codex-pets` registry already scans that folder, so synced pets
appear under Settings → Pets → Recently hatched and adopt with one
click — no manual upload. Supports --source/--out/--force/--limit
flags and validates magic bytes so HTML error pages never end up
masquerading as `.webp` files.

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

* fix(daemon): tighten codex-pets validation and document vendoring

- sanitizeId now rejects ids that still contain `..` after collapsing,
  closing a defensive gap on the path-traversal guard for the
  `/api/codex-pets/:id/spritesheet` route.
- listCodexPets emits the sanitised folder name as the public id so the
  download route resolves directly against the on-disk folder, even when
  `manifest.id` differs (manual drops, sanitiser-touched manifests).
- Drop `@ts-nocheck` from `codex-pets.ts`; module is now strict-typed
  with explicit interfaces, an unknown-narrowed JSON.parse path, and a
  `pickString` helper guarding manifest fields one by one.
- Restrict the spritesheet response CORS header to sandboxed-iframe
  callers (Origin: null) instead of unconditional `*`, matching the
  existing raw-file route pattern. Same-origin web traffic does not
  need the header (web proxies `/api/*` through the daemon).
- Add `skills/hatch-pet/README.md` explaining the vendoring trade-off,
  provenance, and re-sync procedure.
- Add `docs/codex-pets.md` covering where pets live, how to populate the
  registry without Codex installed, and the manifest contract.

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

* fix(i18n): add pet.* keys to Hungarian locale

Hungarian locale was added on main after this branch diverged, so the new
pet.* dictionary keys never landed there and tsc -b reports hu's Dict as
incomplete once main is merged in.

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

* feat(web): atlas-driven pet animations + bundled community pets

Builds on the existing pet companion (#296) with a richer animation
loop, a curated set of community pets that ship with the repo, and a
one-click sync into ~/.codex/pets/.

- Atlas-mode rendering: PetSpriteFace can now play the full Codex 8x9
  sprite atlas and swap rows from a JS-driven frame index. PetOverlay
  classifies pointer interactions (idle / hover / drag-direction /
  long-idle waiting) and maps them to the matching atlas row, so the
  pet waves on hover, runs on drag, and falls into a waiting pose
  after 6s of stillness. Single-strip pets keep their existing CSS
  steps() animation, with the steps timing fixed to jump-none so frame
  cells line up on cell boundaries.
- Atlas adoption: PetSettings exposes both "Use full atlas (animated)"
  and "Freeze to this row" — full mode keeps every row for the
  interaction state machine, single-row mode crops one strip via the
  existing canvas helper. New prepareCodexAtlas downscales the atlas
  to a localStorage-friendly PNG while preserving the grid layout.
- Settings tabs: pet sources are now split into Built-in / Custom /
  Community tabs so each origin gets its own dedicated surface.
- Bundled pets: scripts/bake-community-pets.ts seeds a curated set
  (clippit, dario, nyako-shigure, slavik, trump, tux, yelling-dario,
  yorha-sit-2b) into assets/community-pets/. The daemon scans this
  alongside the user's ~/.codex/pets/ root, with user pets winning
  when ids collide. CodexPetSummary gains a `bundled` flag so the UI
  can tag those cards with a "Bundled" pill.
- One-click community sync: daemon-side port of sync-community-pets
  exposed via POST /api/codex-pets/sync. Returns the same
  wrote/skipped/failed/total summary the CLI prints. Web Pet settings
  surface this as a "Download community pets" button under the
  Community tab.
- Avatar dropdown + hide rail: EntryView's avatar button is now a
  small menu (mirrors the project-view AvatarMenu) with toggles for
  hiding/showing the pet rail and opening Settings. PetRail gets a
  matching × button for the same hide flow.
- Locales: 7 new pet.* keys for tabs, sync, hide/show, atlas full
  mode, and the Bundled pill — translated into all 13 supported
  locales.

Typechecks pass across all workspace packages; daemon + web vitest
suites stay green.

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

* feat(web): bundled-pets built-in tab, ambient atlas animations, and community sync button

The Built-in tab now sources its catalog from the bundled spritesheets
at `assets/community-pets/` instead of the eight emoji placeholders that
felt boring next to the Codex hatch-pet atlases.

- Daemon: `listCodexPets` flags `bundled: true` by curated-set membership
  in `assets/community-pets/`, not by which folder the sprite happened to
  be read from. Previously a fully-synced user inbox preempted every
  bundled id and left the tab empty.
- Settings → Pets → Built-in renders the same sprite-card grid as
  Community, filtered by `bundled: true`, and reuses the existing
  `adoptCodexPet` flow. Community tab filters to non-bundled so the
  curated set never appears twice.
- Community tab gains the long-promised "Download community pets"
  trigger that calls `/api/codex-pets/sync` and shows an inline status
  line for the run summary. Strings already existed in every locale; we
  just plumbed the button.
- `PetOverlay` gets ambient atlas-row choreography — while idle, the
  overlay occasionally swaps `idle` for a random non-idle row (wave /
  hop / look) so the pet doesn't feel frozen. User gestures cancel the
  beat and take over instantly. `pickAmbientRow` lives next to
  `pickAtlasRow` so both row pickers share the fallback discipline.
- One-shot `migrateCustomPetAtlas` heals configs adopted before the
  overlay learned row switching by re-downloading the full spritesheet
  so hover / drag / ambient variety light up on next launch.
- `BUILT_IN_PETS` is now an empty array (the type stays for backwards
  compat); legacy configs whose `petId` still points at an emoji id
  (`mochi`, `pixel`, …) fall back to the user's custom slot in
  `resolveActivePet` so the overlay never renders blank.
- i18n: refresh `pet.tabBuiltInHint` (drop "emoji companions") and add
  `pet.builtInEmpty` across all locales.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-02 23:45:39 +08:00
laihenyiandGitHub 254a9ff479 feat(skills): add pptx-html-fidelity-audit + wire into export prompt (#307)
* feat(skills): add pptx-html-fidelity-audit + wire into export prompt

Adds a python-pptx audit skill that catches the silent drift modes
between an HTML deck and its PPTX export — footer overflow, off-canvas
shapes, missing italic/em, hero slides not centered, oversized
bounding boxes, lost CJK type slots.

The skill ships:

- SKILL.md — five-step workflow (extract dump → walk HTML → audit
  table → re-export with footer-rail + cursor-flow → verify)
- references/layout-discipline.md — Cursor primitive, hero budget
  centering, per-slide-type code snippets
- references/font-discipline.md — five-layer font audit:
  mapping → presence → variable-vs-static → XML latin/ea/cs slots
  → CJK + Latin italic interaction
- references/audit-table-template.md — fixed table format
- scripts/extract_pptx.py — dump every shape (text, geometry,
  per-run typography) as JSON
- scripts/verify_layout.py — assert footer-rail + canvas
  invariants; nonzero exit on any violation

Wires the skill into the FileViewer "Export as PPTX" flow:

- handleExportAsPptx in ProjectView.tsx now instructs the agent to
  apply the footer-rail discipline up front (CONTENT_MAX_Y = 6.70",
  Cursor + budget centering, explicit a:latin / a:ea slots), then
  run verify_layout.py as a mandatory gate before declaring done.
- If 🟡 typography issues surface, the prompt points the agent at
  font-discipline.md.

Verifier exit code is the gate: agent cannot claim "fidelity correct"
without 0 rail violations. If the environment lacks python-pptx, the
agent must say so explicitly rather than eyeball the deck.

* fix(pptx-fidelity): address review feedback from #307

P2 fixes (codex):
- ProjectView prompt now quotes the verifier path so filenames with
  spaces (e.g. "Q2 review.pptx") parse as a single argument instead
  of two.
- extract_pptx color extraction no longer gates on font.name/size —
  a run can inherit name/size from the theme yet set its own color,
  and that drift is exactly what the audit needs to catch.

P3 polish (lefarcen):
- SKILL.md "When this skill applies" now shows a 7-line HTML snippet
  so readers can self-assess scope at a glance.
- Cursor.take(gap=0.12) gets an inline comment explaining the default
  is ~1 line of whitespace at 14pt, adjustable per design system.
- New "Customizing the rails" callout under the constants block, with
  pointers to the verifier flags (--content-max-y, --canvas-h/w) and
  the export-prompt now documents the same path.
- New "Why geometry-based verification, not visual diff" section
  captures the design tradeoff (platform lock-in, imprecision, setup
  cost) that previously only lived in the PR body.
- font-discipline.md Layer 2 now includes a PowerShell snippet for
  Windows font discovery alongside fc-list / mdfind.
- layout-discipline.md gains a "How to use this file" header so
  readers know to skim §1-3 then jump to slide-type snippets.

Also: scripts/.gitignore for __pycache__.

* fix(skills): register pptx-html-fidelity-audit in German display metadata

CONTRIBUTING.md requires every skill in skills/ to have a corresponding
German display metadata entry in apps/web/src/i18n/content.ts; the
content.test.ts assertion catches drift. The CI failure on #307 was
this check tripping on the new skill.

Adds the German example prompt entry. Test suite passes locally.
2026-05-02 23:32:56 +08:00
9ee2c1994c feat(web): add brand design systems, card thumbnails, and DESIGN.md side-by-side preview (#289)
* feat(web): add brand design systems, card thumbnails, and DESIGN.md side-by-side preview

- Add 7 new brand design systems (arc, canva, discord, duolingo, github, huggingface, openai)
- Show live showcase HTML thumbnails on Design Systems cards
- Add toggleable DESIGN.md side panel in preview modal with syntax-highlighted spec view
- Make preview iframe responsive: render at fixed design viewport and scale to fit so opening the side panel never reflows showcases into broken breakpoints
- Add floating collapse/expand handles on the sidebar boundary for direct hide/show

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

* fix(web): guard ResizeObserver and re-fire sidebar lazy-load on content swap

- Guard `new ResizeObserver(...)` in PreviewModal so the modal mounts in
  jsdom (the existing preview-modal-fullscreen test was failing with
  `ReferenceError: ResizeObserver is not defined`) and in older embedded
  WebViews. Fall back to a window resize listener when the constructor
  is unavailable.
- Add a `contentKey` hint to PreviewSidebar so the lazy-load `onToggle`
  callback re-fires when the underlying side-panel source swaps while
  the sidebar stays open. Wire `system.id` through from
  DesignSystemPreviewModal so swapping design systems with the spec
  panel open primes a fresh DESIGN.md fetch instead of leaving it stuck.

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

* fix(web/i18n): add missing ds/preview keys to hu locale

The Validate workspace check failed after main's hu.ts landed
without the four i18n keys introduced by this PR (ds.specToggle,
ds.specLoading, preview.showSidebar, preview.hideSidebar).

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-02 23:19:00 +08:00
Tom HuangandGitHub bf44394f91 feat(web): add per-tool renderer registry for generative UI (#282)
* feat(web): add per-tool renderer registry for generative UI

Introduce a registry on the web runtime so external surfaces (skill-emitted
tools, MCP tools, future plugins) can render their own tool cards instead
of falling through to the generic command/output fallback. Registered
renderers are consulted before the hardcoded family ladder, mirroring
CopilotKit's `useCopilotAction({ render })` extension point and AG-UI's
four-state lifecycle (`inProgress` / `executing` / `complete` / `error`).
Returning null from a renderer falls back to the built-in card.

Forward `runStreaming` from AssistantMessage so the registry can
distinguish a live tool call from one that was interrupted before the
result arrived.

* fix(web): isolate failing tool renderers from the message stream

Address PR #282 review:

- Wrap the registry-renderer call in ToolCard with try/catch so a
  third-party renderer that throws synchronously falls back to the
  built-in family card instead of taking down the surrounding
  AssistantMessage. Log via console.error for visibility.
- Drop the unused `use` parameter from `deriveToolStatus` (its inputs
  are only `result` + `runStreaming`); removes the awkward `void use;`.
- Document the dispose-before-reregister contract on
  `registerToolRenderer` for dynamic skill load/unload callers.
- Add a test asserting the throwing-renderer fallback path.

* docs(web): pin the hook-free contract for tool renderers

Address PR #282 review:

- Document on `ToolRenderer` that the callback runs inside ToolCard's
  render and must therefore be hook-free. Hookful cards should return
  a component element so React mounts the inner component and owns its
  hooks across re-registers, fallbacks, and replacements.
- Add a test that exercises the documented pattern: register a renderer
  whose body uses useState wrapped in a component element, render,
  dispose, then register a different hookful renderer. Verifies that
  swapping renderers with different hook shapes doesn't violate the
  Rules of Hooks on ToolCard.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
2026-05-02 22:41:31 +08:00
Nader DabitandGitHub 8c61e43c44 Added Devin for Terminal (#301)
* added Devin for Terminal

* updates based on PR feedback
2026-05-02 22:40:08 +08:00
lefarcenandGitHub 62b01a6dbf release: Open Design 0.2.0 (#297) 2026-05-02 22:28:59 +08:00
5fa01efcd6 fix(i18n): remove duplicate keys in content.ts introduced by #295 squash-merge (#303)
PR #295 was squash-merged and the squash commit dropped the branch's
cleanup commit that deleted upstream's stub entries. As a result main's
apps/web/src/i18n/content.ts now carries three TS1117 duplicate-key
errors, breaking the Typecheck workspaces CI job:

- DE_PROMPT_TEMPLATE_CATEGORIES has both 'Game UI' -> 'Spiel-UI' (line 345)
  and 'Game UI' -> 'Game UI' (line 358). Keep the German 'Spiel-UI'.
- DE_PROMPT_TEMPLATE_COPY has both an empty {} stub and a filled-in
  German entry for 'game-ui-ancient-china-open-world-mmo-hud'. Keep the
  filled-in entry.
- DE_PROMPT_TEMPLATE_COPY has both an empty {} stub and a filled-in
  German entry for 'social-media-post-sensational-girl-dance-storyboard-8-shots'.
  Keep the filled-in entry.

Local verification:
- apps/web tsc -b --noEmit: passes
- apps/web vitest: 19 files / 102 tests pass

Co-authored-by: Joey <joey@open-design.local>
2026-05-02 22:18:37 +08:00
569f280a0d feat(prompt-templates): add crayon kid-drawing style-transfer template (#295)
* feat(prompt-templates): add crayon kid-drawing style-transfer template

- illustration-crayon-kid-drawing-rework: an image-to-image prompt that
  reworks any reference image into a hand-drawn crayon illustration with
  childlike whimsy (castles, candy, stars, clouds, rainbows) on clean white
  paper. Tuned for gpt-image-2 edits and works across product shots,
  portraits, and screenshots.
- Ships with a cover preview rendered from the Open Design X profile
  screenshot via the template's own prompt, downscaled to 1536x1024 JPEG.
- Category reuses existing 'Illustration' enum value.

* fix(prompt-templates): address crayon template review feedback (#295)

- aspect: switch from unsupported 3:2 to 4:3 (per @mrcfps). The app contract
  only recognizes 1:1 / 16:9 / 9:16 / 4:3 / 3:4 (MediaAspect in
  packages/contracts and MEDIA_ASPECTS in apps/daemon/src/media-models.ts);
  gpt-image-* silently falls back to 1024x1024 for unknown aspects, which
  would have made this template generate square images instead of landscape.
- Cover preview re-cropped from 1536x1024 to 1365x1024 (4:3) so the
  thumbnail matches the declared aspect. Content preserved (center crop).
- prompt: add 'Preserve the general layout and spatial relationships of the
  original image — transform the style first, embellish second' so small UI
  elements, faces, and logos don't get overwhelmed by castles/candy/stars
  decoration (per @lefarcen).
- tags: add 'rework' and 'transform' to improve discoverability for users
  searching 'rework my photo' / 'transform screenshot' (per @lefarcen).
- summary: clarify that this is an image-to-image edit requiring a reference
  image upload, not a pure text-to-image generation prompt (per @lefarcen).

* fix(i18n): register German content for new templates and their tags

The 'Validate workspace' CI job (apps/web i18n/content.test.ts) enforces
that every template slug, category, and tag that appears in a shipped
JSON template also has a German translation in apps/web/src/i18n/content.ts.

This PR's new template plus two previously merged first-party templates
(#187: game-ui-ancient-china-open-world-mmo-hud and
social-media-post-sensational-girl-dance-storyboard-8-shots) were missing
from the German dictionaries, so the suite was red.

Changes:
- DE_PROMPT_TEMPLATE_CATEGORIES: add 'Game UI' -> 'Spiel-UI'.
- DE_PROMPT_TEMPLATE_TAGS: add 11 tags used by the three templates
  (ancient-china, childlike, crayon, dance, fashion, game-ui, hand-drawn,
  hud, illustration, image-to-image, mmo, open-world, rework, sequence,
  storyboard, style-transfer, transform, wuxia) with German equivalents.
- DE_PROMPT_TEMPLATE_COPY: add German title + summary for
  game-ui-ancient-china-open-world-mmo-hud,
  illustration-crayon-kid-drawing-rework, and
  social-media-post-sensational-girl-dance-storyboard-8-shots.

Local verification: apps/web vitest suite (14 files, 68 tests) passes.

---------

Co-authored-by: Joey <joey@open-design.local>
2026-05-02 22:05:19 +08:00
5edd852cfa fix(daemon): restore startServer Promise contract — return url / { url, server } (#268)
* fix(daemon): restore startServer Promise contract — return url / { url, server }

PR #258 ("Standardize agent communication via stdin and remove
Windows-specific shims logic") removed the Promise wrap around
`startServer`'s `app.listen()` call. Two regressions shipped to main as
a result:

1. **`returnServer: true` returns just `server`, not `{ url, server }`.**
   Both `apps/daemon/sidecar/server.ts:65` and
   `apps/daemon/tests/version-route.test.ts:10` cast the return value to
   `{ url, server }` and read `started.url`. Post-#258 those reads land
   on `undefined`, which becomes `"undefined/api/version"` URLs at
   runtime — the sidecar can no longer report its bound port to the
   parent process, and `version-route.test.ts` fails with
   `ERR_INVALID_URL`.

2. **`returnServer: false` returns nothing.** `apps/daemon/src/cli.ts:77`
   calls `startServer({ port }).then(url => console.log(\`[od] listening on \${url}\`))`
   — post-#258 that prints `[od] listening on undefined`, and the
   subsequent `spawn(opener, [url], …)` to open the browser also gets
   `undefined`. The daemon still binds, but the two paths that *consume*
   the URL are silently broken.

Both stem from the same change: the new code synchronously kicks off
`app.listen` and returns either the server object or `undefined` before
`listen`'s callback fires, so neither the resolved port nor the URL
string ever reach the caller.

This restores the pre-#258 Promise wrap, which already had the right
shape for all three call sites:

```ts
return await new Promise((resolve) => {
  const server = app.listen(port, () => {
    const url = `http://127.0.0.1:${server.address().port}`;
    resolve(returnServer ? { url, server } : url);
  });
});
```

Behavior contract restored:

| `returnServer` | resolves with |
|---|---|
| `false` (default) | `string` — `http://127.0.0.1:<port>` |
| `true` | `{ url: string, server: http.Server }` |

The Promise resolution waits for `listen`'s callback so `port=0`
(ephemeral) callers always see the actual bound port, never the
placeholder.

Test coverage: `tests/version-route.test.ts` already exercises the
`{ url, server }` path via `await fetch(\`\${baseUrl}/api/version\`)`.
After this fix the suite is back to 15 files / 225 tests green
(2 of those — both in `version-route` — failed against current main).

Verification:
- `pnpm --filter @open-design/daemon test` — 15 files / 225 tests pass
- `tsc -p apps/daemon/tsconfig.json --noEmit` — clean
- `tsc -p apps/daemon/tsconfig.tests.json --noEmit` — clean (was failing
  on the `as { url, server }` cast before)

* fix(daemon): reject startServer Promise on bind failure / null address

Folds in the two non-blocking improvements @lefarcen flagged on the
review of the parent commit:

1. **Explicit `error` event listener.** `app.listen` throws synchronously
   for some failure modes (e.g. invalid port) and emits an `error` event
   for others (EADDRINUSE on certain Node versions, EACCES, EADDRNOTAVAIL).
   Without an `error` listener the returned Promise would hang forever
   on the event-emitting paths instead of surfacing the failure to the
   caller.

2. **Null-`address()` guard.** `server.address()` typing allows
   `string | AddressInfo | null`. For TCP listeners it's always
   AddressInfo, but if a future code path ever returns null (or a Unix
   socket path string), the previous fallback `port` would still be
   `0` for ephemeral-port callers and quietly produce a
   `http://127.0.0.1:0` URL that fetches would fail on. Reject the
   Promise instead with an explicit diagnostic.

Both keep the success path identical — `version-route.test.ts` still
exercises the same `{ url, server }` contract on `port: 0` and passes
unchanged. 15/15 daemon test files / 225/225 tests pass.

Refs review feedback on #268 (P2 suggestions, non-blocking).

* fix(daemon): drop a93246d workaround for restored startServer contract

After rebasing onto current `main`, this PR's restored
`{ url, server }` return shape from `startServer` collides with the
workaround applied in `a93246d` (PR #271), which had patched the two
consumers — `apps/daemon/sidecar/server.ts` and
`apps/daemon/tests/version-route.test.ts` — to call `started.address()`
on a bare `Server` handle.

Revert both consumers to the pre-`a93246d` `{ url, server }` shape so
they line up with the contract restored in `5c2f520`. With this:

- `pnpm --filter @open-design/daemon test` — 246/246 pass
- `pnpm --filter @open-design/daemon build` — tsc clean for both
  tsconfig.json and tsconfig.sidecar.json
- `apps/daemon/src/cli.ts` (which expects a URL string and was
  printing "listening on undefined" on `main`) is now correct without
  any further change.

* fix(i18n): add missing chat.comments.* keys to hu.ts

PR #288 added Hungarian without the 12 `chat.comments.*` keys that
PR #284 introduced — the two landed in parallel and only en + the
older locales got both. Without these, `pnpm --filter @open-design/web
typecheck` errors on `src/i18n/locales/hu.ts(3,14): TS2740 ... missing
the following properties from type 'Dict': 'chat.comments.attached',
'chat.comments.emptyAttached', 'chat.comments.saved', and 9 more`,
which blocks every PR's CI.

Drive-by unblock with English fallback strings — proper Hungarian
translations should follow in a separate i18n pass.

---------

Co-authored-by: lefarcen <935902669@qq.com>
2026-05-02 20:56:06 +08:00
6a6aba9042 feat Hungarian (hu) locale (#288)
Co-authored-by: Komlósi Zsolt <zsolt.komlosi@gvsx.hu>
2026-05-02 20:15:58 +08:00
Sina MashiniandGitHub 6805628458 fix: language dropdown overflow in settings modal (#281) (#287)
* fix: language dropdown overflow in settings modal (#281)

Smart-place the Settings language menu: open downward when there is enough space below the trigger, otherwise fall back to opening upward. Expose the available height to CSS via --menu-available-h so the menu is bounded in either direction, scrolls internally, and prevents long labels from overflowing item rows.

* fix(SettingsDialog): address PR review feedback for #281

- Document the 200px threshold used to prefer downward placement.
- Close the language menu on window resize so the placement (computed once at open) cannot become stale.
- Cap the menu max-height at 60vh so it stays comfortable on short viewports (mobile landscape ~400-500px tall).
2026-05-02 19:51:26 +08:00
CaprikaandGitHub 0c00f241e7 Add preview comment attachments (#284) 2026-05-02 19:23:46 +08:00
sysCat64andGitHub 1592beb961 feat(web): add dark mode support with system/light/dark toggle (#259)
* feat(web): add dark mode support with system/light/dark toggle

Closes #93.

Adds a three-way theme toggle (System / Light / Dark) to Settings >
Appearance. The chosen preference is persisted in localStorage alongside
the existing config and applied immediately via a data-theme attribute on
<html>.

Implementation
- AppTheme type ('system' | 'light' | 'dark') added to AppConfig
- CSS custom properties for the dark palette defined under
  [data-theme="dark"] and @media (prefers-color-scheme: dark) so the
  System option follows the OS without JavaScript
- Appearance section added to SettingsDialog with a sun-moon icon
- Several hardcoded light-only colour values replaced with CSS variable
  equivalents so they adapt correctly in dark mode
- i18n: appearance/theme strings added to all 11 supported locales

Note: theme variables are currently inlined in index.css. A follow-up
could extract them into per-theme files to support community themes
(e.g. Catppuccin) -- happy to explore that direction if it fits the
project roadmap.

Also fixes (pre-existing)
- Syntax error in locales/tr.ts that was masking missing-key errors
- Missing translation keys in de, es-ES, ja, tr locales
- locales.test.ts EXPECTED_LOCALES not updated after Korean was added

* fix(web): apply theme before first paint to prevent FOUC

Add a synchronous inline script in layout.tsx that reads the saved theme
from localStorage and sets data-theme on <html> before React hydrates,
eliminating the flash of unstyled content that occurred when the theme
was only applied inside a useEffect.

* fix(web): use useLayoutEffect for theme sync to avoid 1-frame flash

useEffect fires after the browser paints, so switching themes in Settings
could show a single-frame flicker of the old theme. useLayoutEffect fires
synchronously before the paint, making theme transitions atomic. Safe
because the component tree is rendered with ssr:false.

* fix(web): fix 3-column theme selector layout and add seg-title overflow guard

.seg-control used a hardcoded 2-column grid; the theme toggle has 3 buttons
so Dark was falling onto a second row. Switch to a CSS custom property
(--seg-cols, default 2) so each usage can declare its own column count.
Also add white-space/overflow/text-overflow to .seg-title so long
translations never overflow their cell.

* fix(i18n): add missing maxTokens keys to Dict and Korean locale

settings.maxTokens and settings.maxTokensHint were used in SettingsDialog
and present in all locale files but absent from the Dict interface, causing
typecheck to fail. Also adds the two keys to the Korean locale to restore
full locale parity.

Fixes the blocking typecheck failure flagged in PR review.

* fix(i18n): remove duplicate maxTokens keys in ko.ts and Dict

Rebase introduced duplicate settings.maxTokens and settings.maxTokensHint
in both ko.ts and types.ts (Dict interface), causing TS1117/TS2300 errors.
Remove the second occurrence of each to restore clean typecheck.

* test(i18n): add pl to EXPECTED_LOCALES in locales.test.ts

Polish locale was added upstream in #273; update the hard-coded list so
the locale parity test passes on CI.

* fix(web): live-preview theme in Settings before Save

AppearanceSection now applies data-theme to <html> immediately on toggle
via useLayoutEffect, so the page re-themes in real time while the dialog
is open. SettingsDialogs useLayoutEffect cleanup reverts to the original
saved theme when the dialog is dismissed without saving.
2026-05-02 17:46:09 +08:00
lefarcenandGitHub 9e8177d80a feat(media): integrate xAI Grok Imagine (image + video + native audio) (#276)
* feat(media): integrate xAI Grok Imagine (image + video + native audio)

Adds a real provider integration for xAI's Imagine API alongside OpenAI,
Volcengine and HyperFrames. The route surfaces as two model entries:

  * grok-imagine-image — POST /v1/images/generations, synchronous, asks
    for b64_json so the bytes arrive in one round-trip; sniffs the
    returned magic bytes so JPEG payloads land with the right extension
  * grok-imagine-video — POST /v1/videos/generations + GET /v1/videos/{id}
    polling, with a 4s tick + onProgress heartbeat (mirrors the
    Volcengine handler so the agent's bash watchdog doesn't kill long
    polls). Native audio (AAC) ships in the same file as the H.264
    video — that's the differentiator vs Seedance and Sora.

Picker visibility is gated by NewProjectPanel's hardcoded surface→
provider allowlist; grok is added to image + video. Settings UI picks up
the provider automatically off MEDIA_PROVIDERS.

Credentials: XAI_API_KEY (canonical, matches the official SDK) or
OD_GROK_API_KEY override; both are honoured ahead of any value pasted
into Settings, so users who already export XAI_API_KEY don't have to
re-enter it in the UI.

Verified end-to-end with a real key:
  * image: 1024×1024 JPEG, single round-trip
  * video: 5s 16:9 H.264 + AAC, ~46s wall clock, pending→done

* fix(i18n): include pl in EXPECTED_LOCALES so locales.test passes

Drive-by unblock for CI on this branch — `pl.ts` and `LOCALES`/`Locale`
in types.ts already include Polish, but the test's hardcoded
EXPECTED_LOCALES didn't, so every PR has been red on
locales.test.ts since the locale was added.

* fix(media): grok review feedback — credential precedence comment + poll timeout diagnostics

Two small fixes from PR #276 review:

* media-config.ts — comment claimed XAI_API_KEY won, but readEnvKey
  iterates the array in order so OD_GROK_API_KEY actually wins
  (matches every other provider's OD_* override convention). Rewrite
  the comment to describe what the code does instead of flipping the
  array — the precedence is correct, the comment was wrong.

* media.ts renderGrokVideo — single throw at the bottom couldn't tell
  "timed out after N seconds with status still pending" from "submit
  returned neither inline video nor a request_id to poll." Split into
  two branches so operators know whether to bump
  OD_GROK_VIDEO_MAX_POLL_MS or file an upstream contract bug.
2026-05-02 17:06:17 +08:00
karolgajda-techsquareandGitHub c2e395eea6 feat Polish (pl) locale (#273) 2026-05-02 16:28:52 +08:00
Marc ChanandGitHub a93246d892 chore(ci): add GitHub CI workflow (#271)
* Add GitHub CI workflow

* Address CI workflow review feedback

Generated-By: looper 0.3.0 (runner=fixer, agent=openai/gpt-5.5)
2026-05-02 16:14:33 +08:00
SidandGitHub bb11c68b9f fix(i18n): replace hardcoded 'Claude' with '助手' in zh-TW assistant role (#262)
Spotted by @lefarcen in #59 follow-up review: while every other locale
file has been switched to a neutral, localized assistant label
(`Assistant` / `助手` / `アシスタント` / `Assistent` / `Assistente` / etc.),
`zh-TW.ts` still hardcodes `'Claude'` for `'assistant.role'`. That string
is the per-message fallback used by `assistantRoleLabel()` in
`apps/web/src/components/AssistantMessage.tsx` when no agent metadata is
attached to a turn — so on Traditional Chinese installs running Codex /
Cursor / Gemini / OpenCode, the older message rows can still render as
"Claude" instead of a neutral label.

Fix: align zh-TW with zh-CN by setting `'assistant.role': '助手'`. The
character `助手` is identical in Simplified and Traditional Chinese, so
this matches the rest of the locale file's register without introducing
any character-form drift.

Refs #59
2026-05-02 14:59:33 +08:00
Marc ChanandGitHub 0e166bb799 Optimize Electron release artifact size (#249) 2026-05-02 14:22:16 +08:00
lefarcenandGitHub c2b3d737f2 fix: make max_tokens configurable (closes #29) (#78)
* fix(web,daemon): make max_tokens configurable (closes #29)

BYOK users on custom Anthropic-compatible providers (e.g. Xiaomi MiMo)
hit the hardcoded 8192 cap and saw artifacts truncated mid-stream.

- AppConfig.maxTokens with Settings input (EN/CN + 8 other locales)
- ProxyStreamRequest.maxTokens contract field
- anthropic, anthropic-compatible, and openai-compatible providers all
  forward cfg.maxTokens
- /api/proxy/anthropic/stream and /api/proxy/stream payloads honor it,
  defaulting to 8192 when unset so prior clients are unaffected

Original sketch by @mashu in #78 (50a9d14); rebased to the apps/web
layout and extended to the proxy paths actually used when baseUrl is
set, which is where #29's user actually traffics.

* feat(web): per-model max_tokens defaults

Adds a hand-maintained MODEL_MAX_TOKENS table (Claude 4.5 line → 64k,
mimo-v2.5-pro → 32k) and an effectiveMaxTokens helper layered over the
override field added in 6a3ae5f, so #29's user — and others on supported
models — don't have to discover Settings to avoid mid-stream truncation.

- apps/web/src/state/maxTokens.ts: lookup + helpers
- providers/{anthropic,anthropic-compatible,openai-compatible}.ts:
  forward effectiveMaxTokens(cfg) instead of cfg.maxTokens ?? 8192
- SettingsDialog: input becomes an optional override (blank = default,
  shown as placeholder)
- 10 locale hint strings updated to the new semantics

* feat(web): vendor LiteLLM model metadata for max_tokens defaults

Replaces the 4-entry hand-rolled MODEL_MAX_TOKENS map from 544e67e with
a vendored slice of BerriAI/litellm's model_prices_and_context_window
JSON (1970 chat models, ~97KB raw / ~25KB gzip). Future model launches
land in maxTokens.ts via `pnpm sync-litellm-models` instead of manual
edits.

- scripts/sync-litellm-models.ts: fetches the upstream JSON, filters to
  chat-mode entries, projects each entry to its max_output_tokens (or
  max_tokens fallback), and writes a sorted, license-attributed JSON
- apps/web/src/state/litellm-models.json: generated artifact, committed
- apps/web/src/state/maxTokens.ts: lookup is now
  OVERRIDES → LITELLM_MODELS → FALLBACK_MAX_TOKENS. The OVERRIDES table
  shrinks to just `mimo-v2.5-pro` (LiteLLM only ships MiMo via
  OpenRouter/Novita aliases, not the canonical id Xiaomi's API uses).

LiteLLM is MIT-licensed (BerriAI/litellm/blob/main/LICENSE); attribution
is preserved in both the script header and the generated JSON's
_license field.

* test(web,docs): cover maxTokens lookup + document sync workflow

- apps/web/src/state/maxTokens.test.ts: six vitest cases pinning the
  three-tier lookup (override → LiteLLM → fallback) and the
  effectiveMaxTokens user-override path. Guards against a future sync
  silently dropping the Anthropic 4.5 entries we rely on.
- CONTRIBUTING.md / CONTRIBUTING.zh-CN.md: new "Updating model
  max_tokens metadata" section pointing future maintainers at
  scripts/sync-litellm-models.ts and explaining when OVERRIDES is
  appropriate (it's the rare exception, not the default).

* fix(web): mark Max tokens label as optional in 10 locales

The Settings field is optional (blank means "use the per-model default")
but the label gave no visual cue, breaking the implicit pattern that
every other API-mode field (key/model/baseUrl) is required. Append
"(optional)" — using the locale's natural parenthetical convention
(Chinese full-width brackets, Japanese 任意, Russian опционально, etc.)
— so the field reads as discretionary at a glance.

* fix(web): validate maxTokens override against advertised UI bounds

Addresses Siri-Ray's review on commit 0d98185. The Settings input
declares min={1024}/max={200000}/step={1024}, but until now
effectiveMaxTokens trusted any defined cfg.maxTokens, so a stale or
hand-edited localStorage value (negative, zero, fractional, billions)
would pass straight to the Anthropic SDK on the direct path while the
daemon proxy quietly clamped it back to 8192 on the proxied path —
same config, divergent behavior depending on route.

- maxTokens.ts: add MIN_MAX_TOKENS / MAX_MAX_TOKENS exports and
  isValidOverride helper. effectiveMaxTokens only honors the override
  when it is a finite integer in [1024, 200000]; otherwise falls back
  to modelMaxTokensDefault.
- SettingsDialog.tsx: input bounds now reference the same constants so
  the UI promise can't drift from the runtime check.
- maxTokens.test.ts: six new cases pinning the rejection of negative,
  zero, sub-MIN, super-MAX, non-integer (fractional / NaN / Infinity)
  overrides plus the inclusive MIN/MAX boundaries.

The daemon proxy's existing `> 0` fallback stays as defense-in-depth.
2026-05-02 13:52:54 +08:00