Commit Graph
243 Commits
Author SHA1 Message Date
Jie ZhuandGitHub 57d49ed798 fix(web): increase project meta line-height to prevent descender clipping (#834)
* fix(web): increase project meta line-height to prevent descender clipping

The project type label (e.g. 'blog-post · Neutral Modern') had its
descender characters (g, p, y) clipped by the header bottom border.
Increasing line-height from 12px to 15px gives 11.5px font enough
vertical space for lowercase descenders to render without being cut off.

* fix web: bump project meta line-height and parent max-height to fit descenders

.app-project-title .meta had line-height: 12px on font-size: 11.5px,
leaving only 0.5px of vertical breathing room. Descender glyphs (g, p,
q, y, j) extend below the baseline and were clipped by overflow: hidden.

Bump .meta line-height from 12px to 15px and .app-project-title
max-height from 31px to 32px so the full 16px + 1px gap + 15px = 32px
budget fits without clipping.

Refs: https://github.com/nexu-io/open-design/pull/834
2026-05-08 10:57:01 +08:00
56bf6ee1b6 feat: agent-callable research command and /search (#615)
* feat: pre-generation research (Tavily) for grounded generation

Adds an optional pre-generation research step so the agent can produce
slides / prototypes / decks grounded in real sources instead of guessing.

User flow:
  1. Settings -> Tavily Search -> paste API key (or set TAVILY_API_KEY).
  2. Click the new Research button in the chat composer.
  3. On send, the daemon runs a Tavily search, prepends the findings
     as a <research_context> block ahead of the system prompt, and
     spawns the agent. Research progress shows up as status pills in
     the chat stream; the agent cites sources inline as [1]/[2]/...

Phase 1 surface:
  - Single provider (Tavily), single depth ('shallow'), no LLM
    synthesis pass (Tavily's `answer` is the summary).
  - Composer toggle only; no popover / depth picker yet.
  - Reuses the existing `status` SSE agent payload + StatusPill UI
    so no new event variants or renderer code are needed.

Layers touched:
  - contracts: ResearchOptions / Source / Findings DTOs;
    ChatRequest.research; export from index.
  - daemon: apps/daemon/src/research/{index,tavily}.ts orchestrator
    + provider; tavily added to MEDIA_PROVIDERS and ENV_KEYS; hook
    in startChatRun before prompt assembly.
  - web: ChatComposer toggle + ChatSendMeta; threaded through
    ChatPane / ProjectView / streamViaDaemon into ChatRequest.

Side fix (required to land the feature, but useful on its own):
  contracts internal relative imports lacked the `.js` suffix that
  NodeNext module resolution requires. This was already breaking
  `pnpm --filter @open-design/daemon typecheck` on main; without the
  fix, none of the new research types were visible to the daemon.
  All internal contracts imports now carry `.js`.

Spec: specs/current/research-feature.md (phases 2-4 outlined for
follow-up: composer popover, multi-provider, deep recursion, example
skills with research_recommends).

Verified:
  - pnpm --filter @open-design/contracts typecheck/test
  - pnpm --filter @open-design/daemon typecheck (the chokidar
    project-watchers test is a pre-existing flake, unrelated)
  - pnpm --filter @open-design/web typecheck
  - node scripts/verify-media-models.mjs

* fix(daemon): clamp Tavily max_results to 20

Tavily's /search endpoint requires `max_results` in [0, 20]; sending a
larger value (e.g. when `research.depth: "deep"` resolves to 30) returns
400 and `runResearch` silently falls back to no-research. Clamp at the
provider boundary so Phase 2 depth tiers above 20 still produce results
instead of failing the request.

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

* Remove stale research merge leftovers

* Add agent-callable research search

* Fix Indonesian locale typecheck

* Fix research command invocation edge cases

* Harden slash search prompt expansion

* Honor research source caps in command contract

* Require search reports in design files

* Add research data provider settings

* Wire web research provider fallback order

* Update research provider fallback wording

* Revert "Update research provider fallback wording"

This reverts commit 86fb6001e3.

* Revert "Wire web research provider fallback order"

This reverts commit 4c9e16036b.

* Revert "Add research data provider settings"

This reverts commit 23630d1746.

* Add Dexter and Last30Days research skills

* Add DCF and Last30Days OD skills

* Add Last30Days and Dexter skills

* Resolve research review threads

---------

Co-authored-by: a1chzt <chizblank@gmail.com>
2026-05-08 10:33:44 +08:00
shangxinyu1andGitHub 7107623ee2 test: expand entry and settings automation coverage (#811)
* test: harden new project panel metadata coverage

* test: add settings and connector sync coverage

* test: expand entry e2e coverage

* test: satisfy exact optional property types in entry connector flow

* test: keep entry Playwright coverage under e2e/ui

* test: tighten coverage docs and settings test cleanup

* test: drop e2e docs from the guarded package

* docs: move automation coverage docs out of e2e

* test: restore clipboard cleanup without delete

* test: match composio save dialog behavior

* test: avoid placeholder assertion after composio save

* test: expect closeModal on settings saves

* test: align settings save assertions with closeModal flags

* test: fix settings save mocks

* test: align composio replacement hint
2026-05-08 09:30:16 +08:00
lefarcenandGitHub 2bb029cb58 release: Open Design 0.5.0 (#820)
0.5.0 已从 c21cbc6 发布(https://github.com/nexu-io/open-design/releases/tag/open-design-v0.5.0);本次 squash 把版本 bump 与 CHANGELOG [0.5.0] 条目带到 main 历史,便于后续 0.5.1 release 在 main 上走标准 dispatch 流程。
2026-05-08 00:41:01 +08:00
nmsnandGitHub 31c3ceac53 fix: prevent comment popover header overflow when label is too long (#833)
- Add min-width: 0 and overflow: hidden to comment-popover-head div
- Add text-overflow: ellipsis and white-space: nowrap to strong and span
- Add flex: 0 0 auto to close button to keep it fixed width
- Add title attribute to header div and close button for hover tooltip
2026-05-07 23:44:35 +08:00
9c64ef1b2b fix(web): wrap long note text inside picker/comment popover so it cannot push the layout sideways (#782) (#830)
The .board-note-item flex container holds a span (note text) and a
Remove button. The span had no width hints, so an unbroken long string
(URL, hash, base64, etc.) tried to fit on one line and pushed the row
wider than the 320px popover, distorting the overlay's right edge and
the surrounding picker UI.

Added flex: 1 + min-width: 0 + overflow-wrap: anywhere to the note
span. flex: 1 lets the span take remaining width next to the Remove
button; min-width: 0 lifts the default flex-item min-content floor so
shrinking actually works; overflow-wrap: anywhere allows the long
string to break at any character when natural word boundaries aren't
enough. No layout change for normal-length notes.

Co-authored-by: Nagendhra <nagendhra405@gmail.com>
2026-05-07 22:35:14 +08:00
c3c1b7c7b9 fix(web): wrap comment-popover action row so the Save/Sending button cannot exceed the popover edge (#779) (#829)
The .comment-popover-actions row laid out four buttons (Remove, Add note,
Save comment, Send to chat / Sending...) with display: flex,
justify-content: space-between, no flex-wrap, and no per-child shrink
ceiling. The popover itself is width: min(320px, calc(100% - 28px)) with
10px padding, leaving roughly 300px of inner room. Real button labels
(especially 'Save comment' + 'Send to chat' + 'Add note' together) exceed
that, so the rightmost button visibly spilled past the popover's right
edge. The 'Sending...' state in the screenshot is just where the user
happened to notice it; the underlying overflow is independent of the
button text.

Added flex-wrap: wrap so the row breaks onto a second line when the
labels do not fit, and a max-width: 100% on direct children so a single
oversized button collapses to the row width instead of pushing the row
out. No layout change at widths where the buttons already fit.

Co-authored-by: Nagendhra <nagendhra405@gmail.com>
2026-05-07 22:32:52 +08:00
5315a7dcae add social-media-matrix-tracker template skill (#810)
* add social-media-matrix-tracker template skill

Package a new template-mode live-artifacts skill for a cinematic social media matrix dashboard, including a default example and reusable template seed.

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

* fix(skill): unblock CI for social-media-matrix-tracker-template

- Add skill id to DE/FR/RU EN-fallback skill lists in content.{ts,fr.ts,ru.ts}
  to satisfy localized-content.test.ts coverage assertion (Validate workspace CI).
- Expand SKILL.md triggers with social analytics / content performance / TikTok /
  Instagram / YouTube + 中文 关键词 (新媒体 / 数据看板 / 抖音 / 小红书) per
  reviewer feedback (lefarcen P2#7).

Addresses merge-blocking CI failure flagged by @mrcfps and the discoverability
gap flagged by @lefarcen. Other code-level findings (chart precision, drag
overlay, tooltip clipping, touch input) will be addressed in follow-up commits.

* fix(i18n): add Indonesian translations for Cloudflare Pages deploy keys

Adds 18 fileViewer.* keys for Cloudflare Pages deploy support that were
introduced on main but never localized to id.ts, blocking typecheck for
any branch synced with main.

Translations cover: deploy provider selection (Vercel/Cloudflare Pages),
Cloudflare API token + Account ID inputs, generic provider deploy/error
messaging.

* fix(skill): address all P2 reviewer findings on social-media-matrix-tracker-template

Code fixes (template.html):
- ROI chart redraw now respects per-chart decimals from chartState (was hard-coded 0, lost roiChart precision after first interaction)
- Drag overlay state now cleared BEFORE redraw on mouseup, and on mouseleave during drag (was leaving stale overlay until next interaction)
- mixChart hover now updates insights focus panel with stack breakdown (was tooltip-only)
- sentimentChart hover now updates insights focus panel with sentiment label + share (was tooltip-only)
- Tooltip now measures itself + clamps inside viewport with edge-flip (was clipping at right/bottom edges)
- Added touchstart/move/end/cancel handlers + keyboard arrow-key navigation + Escape (was mouse-only, unusable on touch devices and keyboards)
- drawLineChart guards 0/1-element datasets (renders 'No data' or single labelled dot instead of NaN axis labels / Infinity min-max)

Docs (SKILL.md):
- Workflow now explicitly mentions tooltip clamping, panel update on every chart type, and touch/keyboard a11y
- Added 'Adapting the sample data safely' section with array shape, unit, decimals, and KPI lock-step contract
- Output contract now spells out artifact wrapper requirements, identifier convention, no external CDN/fonts, and single-document rule (no parallel index.html)

Addresses lefarcen P2 #1-5, P2 #7-10, and P3 #6.

* fix(skill): mrcfps round-2 review — fix touch dispatch + sync example.html

mrcfps Looper bot 0.6.2 caught two regressions in the round-1 fix push:

1. Touch dispatch threw TypeError: the previous adapter built a real
   Event then Object.assign-ed clientX/clientY/target onto it. Event.target
   is a read-only getter, so the assignment threw before the synthetic
   mousedown ever reached the line chart. Touch support was effectively
   broken on real devices.

   Replaced with plain pointer objects passed directly into dedicated
   handleTouchStart / handleTouchMove / handleTouchEnd handlers (which
   reuse nearestByMouse, redraw, updateInsights, etc). No more synthetic
   event dispatch, no read-only field assignment.

2. example.html was untouched in round 1, so the showcase that users
   open directly still had x+14/y+14 tooltip clipping, no touch support,
   no decimals state, no overlay-clearing fix, no insights update on
   mix/sentiment hover, no short-series guard — all the things the
   reusable template was just fixed for.

   example.html is identical to template.html except for the JS block
   (verified via diff of body + data calls), so we copy template.html
   over example.html so the two stay in lock-step. Future template fixes
   should mirror by 'cp assets/template.html example.html' until we
   adopt build-time generation.

---------

Co-authored-by: tuolaji <tuola@tuolajideMacBook-Air.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Tuola-waj <tuola@nexu.io>
2026-05-07 21:32:25 +08:00
d4b547caa7 fix(web): keep saved Composio API key indicator visible while typing a replacement (#741) (#751)
The saved-key badge was wired to `isSavedState = apiKeyConfigured && !hasPendingEdit`,
which made it disappear on the first keystroke as soon as the user
started typing a draft replacement. Users reading the settings panel
saw the saved key indicator vanish before they had clicked Save and
reasonably assumed the stored credential had already been overwritten
or removed. Credential editing is a high-trust workflow; a UI that
fakes a state change before the durable write is the wrong default.

Replaced the boolean derivation with a single helper
`deriveComposioCredentialState` returning one of `empty | pending-new |
saved | saved-pending`. The component now shows the saved-key badge
for both `saved` and `saved-pending`, so the indicator stays anchored
while the user types. The hint text differentiates all four states so
the unsaved-replacement case is still clearly called out.

Helper is exported and unit-tested in
`apps/web/tests/components/SettingsDialog.test.ts` against the
empty, pending-new, saved, and saved-pending states plus the
whitespace-only-draft edge case that should still resolve to
`saved`.

Co-authored-by: Nagendhra <nagendhra405@gmail.com>
2026-05-07 21:12:32 +08:00
SidandGitHub 1292fc5c41 fix(i18n): default id locale to English for untranslated keys (#822)
The Indonesian locale was added in fcc37c6 (#fcc37c6) right around
the same time the Cloudflare Pages artifact deployment feature
landed (09eb88f). The Cloudflare PR added 18 new keys to `Dict`
and translated them in every existing locale, but `id.ts` was
populated against the previous shape — so once both PRs sat on
main, every workspace `tsc -b --noEmit` failed with:

  src/i18n/locales/id.ts(3,14): error TS2740: Type ... is missing
  the following properties from type 'Dict':
  'fileViewer.deployProviderLabel', 'fileViewer.vercelProvider',
  'fileViewer.cloudflarePagesProvider',
  'fileViewer.deployToProvider', and 14 more.

That turns CI red on every open PR until someone adds the missing
keys. The same race will happen the next time a feature lands new
strings unless the locale defaults to English fallbacks the way
de / es-ES / fr / hu / ja / ko / pl / tr already do.

Apply the `import { en } from './en'; ...en, ...overrides` pattern
to id.ts. Existing Indonesian translations stay as overrides — no
regression on what the user already sees in Indonesian — and any
new key landed by a future feature inherits an English fallback
automatically. Translators can replace fallbacks one key at a
time without breaking the build in between.

Verified locally:
- web `tsc -b --noEmit` clean (was failing on `id.ts(3,14)`)
- `tsx scripts/i18n-check.ts` passes
2026-05-07 20:47:27 +08:00
988fd6db5e feat: import existing local folder as project (#597) (#624)
* feat(contracts): types for folder-import endpoint

Add ImportFolderRequest, ImportFolderResponse to the public contract
surface. Extend ProjectMetadata with a baseDir field — when set, the
project's files live at this absolute path instead of .od/projects/<id>/.
Stored as the realpath() result so symlinks cannot redirect later writes.

Refs nexu-io/open-design#597

* feat(daemon): support metadata.baseDir for folder-rooted projects

Add resolveProjectDir() and metadata-aware variants of listFiles,
readProjectFile, writeProjectFile, ensureProject so a project's files
can live under metadata.baseDir (the user's chosen folder) instead of
.od/projects/<id>/. metadata.baseDir is opt-in — projects without it
keep the existing .od/projects/<id>/ behavior unchanged.

When listFiles walks a baseDir-rooted project, it skips conventional
build / install dirs (node_modules, .git, dist, build, .next, .nuxt,
.turbo, .cache, .output, out, coverage, __pycache__, .venv, vendor,
target, .od, .tmp) so the file panel stays focused on design content
instead of being dominated by lockfiles and node_modules.

Add detectEntryFile() — best-effort lookup for index.html or any
.html at the folder root, used by the import endpoint to seed the
initial active tab.

Refs nexu-io/open-design#597

* feat(daemon): add POST /api/import/folder endpoint

Creates a project rooted at the submitted local folder. metadata.baseDir
points at that folder and OD reads / writes there directly — no copy,
no shadow tree, mirroring how Cursor / Claude Code / Aider behave. The
user owns the workspace and is responsible for their own version
control.

Safety:
- baseDir is canonicalized via fs.promises.realpath() at import time so
  user-controlled symlinks can't redirect later writes. resolveSafe
  enforces the bounds check against the literal stored path; without
  realpath, a symlink (e.g. ~/sneaky → /etc) would let writeProjectFile
  escape the project tree at every later call because the OS follows
  the symlink at open() time.
- Post-realpath lstat ensures the canonical target is itself a real
  directory (defense-in-depth).
- The data directory (RUNTIME_DATA_DIR) and its descendants are
  refused after symlink resolution so a redirect into the daemon's
  own state can't masquerade as a project import.

The web client wires this through state/projects.ts → App.tsx,
landing the user on the auto-detected entry file when present.

Refs nexu-io/open-design#597

* feat(desktop): expose native folder picker to renderer

Adds an Electron preload script that exposes window.electronAPI.pickFolder
via contextBridge. Wires dialog.showOpenDialog through ipcMain so the
web UI can open a native folder selector for project import. Browser-only
users fall back to a text input for the absolute path (handled in the
web layer); the picker stays an optional convenience on the desktop
binary.

ipcMain.handle() registers handlers in an internal map that is not
exposed via eventNames(), so the natural-looking guard
  if (!ipcMain.eventNames().includes('dialog:pick-folder')) ipcMain.handle(...)
is always true. On a second createDesktopRuntime() call (dev hot-reload,
packaged-vs-electron mode swap) the body re-runs and ipcMain.handle()
throws 'Attempted to register a second handler'. Use removeHandler()
+ handle() unconditionally — removeHandler() is a documented no-op
when nothing is registered, making the pair idempotent.

Includes *.cts in the apps/desktop tsconfig so the preload script is
typechecked.

Refs nexu-io/open-design#597

* feat(web): add 'From existing folder' option to New Project

UI surface for the import flow:
- A new 'Open folder' affordance in NewProjectPanel that uses the
  native picker on Electron (window.electronAPI.pickFolder) and falls
  back to an absolute-path text input in the browser.
- importFolderProject() in state/projects.ts: typed wrapper around
  POST /api/import/folder using @open-design/contracts types.
- App.tsx wires the response: prepend the new project to the list,
  navigate to it, and select the auto-detected entry file as the
  active tab.

Skill / design-system pickers from the existing prototype tab are
reused — folder import is a project-creation flow, not a separate
project type.

Refs nexu-io/open-design#597

* docs(architecture): document folder-import endpoint

Adds POST /api/import/folder to the daemon API table and a 'Folder
import' section explaining the single-mode design (direct read/write
in metadata.baseDir, mirroring Cursor / Claude Code / Aider), the
realpath() canonicalization, the RUNTIME_DATA_DIR refusal, and the
SKIP_DIRS list applied to listFiles for baseDir-rooted projects.

Refs nexu-io/open-design#597

* test(daemon): unit + integration tests for folder import

Two new files:

apps/daemon/tests/folder-import-projects.test.ts (13 unit tests):
- resolveProjectDir behavior under all metadata combinations,
  including the fallback when baseDir is relative and the
  isSafeId-bypass when baseDir is set
- detectEntryFile: index.html priority, .html fallback, null when
  no html, no descent into subdirs
- listFiles with metadata.baseDir: walk, SKIP_DIRS hides node_modules
  / .git / dist, back-compat for projects without baseDir

apps/daemon/tests/folder-import-route.test.ts (10 integration tests):
- Happy path: baseDir stored in metadata, importedFrom='folder',
  conversation created, entry file detected
- Error paths: missing baseDir, empty, relative, non-existent,
  pointing at a file
- Security: realpath canonicalization (the symlink test was the one
  that surfaced the original /var vs /private/var mismatch in
  RUNTIME_DATA_DIR comparison on macOS)
- Security: a symlink that resolves into RUNTIME_DATA_DIR is rejected
  after realpath, not before

Refs nexu-io/open-design#597

* fix(daemon): wire baseDir metadata into chat + deploy reads

Two bugs caught in Codex automated review of #624:

1. chat-route was passing the metadata object directly as the listFiles
   opts argument: `listFiles(PROJECTS_DIR, projectId, chatMeta)`. The
   listFiles contract reads opts.metadata, not opts itself, so this
   silently fell back to .od/projects/<id>/ instead of the imported
   folder. existingProjectFiles was empty for baseDir-rooted projects.
   Wrap as `{ metadata: chatMeta }`.

2. deploy.ts read project files via readProjectFile without the
   metadata third argument, so for baseDir-rooted projects the deploy
   and preflight endpoints would look in .od/projects/<id>/ and fail
   with file-not-found instead of reading the imported folder. Thread
   options.metadata through buildDeployFilePlan → readProjectFile and
   pass project?.metadata at the two server.ts callsites
   (`POST /api/projects/:id/deploy` and the preflight endpoint).

Add a regression test that locks the listFiles contract: passing a
bare metadata object as opts must NOT scan baseDir — it must fall back
to the standard project dir, otherwise callers can leak the wrong
folder by mistake.

Refs nexu-io/open-design#597, #624 (Codex review)

* fix(daemon): ensure correct metadata handling in folder import

Addressed issues with metadata handling in folder import functionality. Updated the listFiles and readProjectFile methods to correctly utilize the metadata.baseDir, ensuring that project files are read from the intended directory. Added regression tests to verify that passing a bare metadata object does not inadvertently scan the baseDir, maintaining the integrity of project file access.

Refs nexu-io/open-design#597

* fix(daemon): security hardening from Codex review of #624

P1 findings from automated review:

1. POST /api/projects + PATCH /api/projects/:id rejected
   client-supplied metadata.baseDir. baseDir is privileged: it lets a
   project root inside the user's filesystem, and the realpath() +
   RUNTIME_DATA_DIR reentry checks live only on /api/import/folder.
   Allowing it on the generic create/patch path lets an attacker
   smuggle e.g. /etc through and bypass every import-time guard.
   Both endpoints now refuse a baseDir field with 400.

2. resolveSafeReal() helper: realpath()s each candidate path (or its
   longest existing prefix for write paths) and re-validates against
   realpath(projectRoot). The original resolveSafe() only did a
   string-prefix check, which was fooled by symlinks *inside* a
   baseDir-rooted project. A repo containing 'assets -> /Users/me/.ssh'
   passed the literal prefix check but readFile() followed the link
   at open() time. resolveSafeReal() is now used by readProjectFile,
   writeProjectFile, and deleteProjectFile.

3. Multer chat-upload destination now resolves to metadata.baseDir for
   imported folder projects via a module-level lookup wired to db at
   startServer() boot. Previously attachments landed in
   .od/projects/<id>/ even for baseDir projects, so the agent (which
   runs with cwd=baseDir) couldn't open them.

P2 findings:

4. searchProjectFiles threads metadata through listFiles +
   resolveProjectDir so /api/projects/:id/search hits the right tree.
5. buildProjectArchive + buildBatchArchive now accept metadata so
   'Download .zip' works for imported folder projects.
6. Watcher subscribe() resolves to baseDir for imported projects so
   live-reload SSE actually fires when the user edits files in their
   own folder. Registry stays keyed by the canonical directory.
7. Template snapshotting reads source-project files with metadata
   so a template can be saved from a baseDir-rooted source.

Tests:

- Regression: POST /api/projects with metadata.baseDir → 400.
- Regression: descendant symlink (assets/leak.txt -> /etc/hosts) is
  refused on the raw read endpoint.

Refs nexu-io/open-design#597, #624 (Codex P1+P2 review)

* fix(daemon): close two regressions found in #624 review round 2

@mrcfps caught two more correctness gaps:

1. Archive root symlink escape — buildProjectArchive accepts an optional
   ?root=<subdir> param to scope the zip to a subdirectory. The path was
   resolved with the string-only resolveSafe(), so a directory symlink
   inside an imported folder (docs -> /Users/me/.ssh) passed the prefix
   check and collectArchiveEntries() then walked outside the project
   tree. Switch to the symlink-aware resolveSafeReal() — the same one
   that already protects raw read/write/delete paths. The walker itself
   already skips dirent symlinks via !isDirectory && !isFile, so
   canonicalizing the root is the only missing piece.

2. PATCH metadata wiped baseDir — updateProject() replaces metadata
   wholesale. The previous guard only blocked an explicit baseDir
   change, but a normal patch that *omits* baseDir (a UI editing
   linkedDirs only sends { metadata: { kind, linkedDirs } }) silently
   detached imported projects from their folder root. Subsequent
   reads/writes/watch/deploy fell back to .od/projects/<id>.

   Re-stamp the immutable folder-import fields (baseDir, importedFrom='folder')
   from the existing project record onto the incoming patch when the
   project is imported. A patch that supplies a *different* baseDir
   still gets rejected as before; a patch that supplies the *same*
   baseDir is accepted as a no-op. A patch on a non-imported project
   that tries to set baseDir is also still rejected (preserves the
   POST /api/projects guard from the previous round).

Tests:

- archive endpoint: ?root=<symlink-to-/etc> → 400.
- patch endpoint: PATCH that omits baseDir on an imported project keeps
  baseDir intact (project still resolves to the user's folder after).

Refs nexu-io/open-design#597, #624 (Codex P1 round 2)

* fix(web): add Indonesian deploy provider copy

---------

Co-authored-by: INFINITY <valentyn.sotov@trendarena.app>
Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-05-07 20:43:31 +08:00
bef8203ad9 fix: expand Codex picker coverage (#757)
* fix: add newer Codex model choices

* fix: expand Codex picker coverage

---------

Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com>
2026-05-07 20:17:15 +08:00
kamiandGitHub 09eb88f683 Add Cloudflare Pages artifact deployment
Adds Cloudflare Pages artifact deployment support.
2026-05-07 20:04:22 +08:00
TomandGitHub 8630fd380a feat(daemon): close pi adapter parity gaps
Closes pi adapter parity gaps for image paths, extra allowed dirs, error events, and sendAgentEvent routing.
2026-05-07 20:03:46 +08:00
yinjialuandGitHub 168cb8ab4d feat(web): add batch delete for selected design files (#771)
Adds batch deletion for selected design files.
2026-05-07 20:03:13 +08:00
7ce4eb4e82 feat(prompt-templates): add Notion-style team dashboard (Live Artifact) (#799)
* feat(prompt-templates): add Notion-style team dashboard (Live Artifact)

Adds a single image prompt template under the Live Artifact category — a
Notion-native team dashboard mockup with KPI grid, 7-day sparkline,
activity feed, and linked-database task table.

This is the first prompt template to use the curated Live Artifact
category, whose de/fr/ru localization slots were already reserved in
apps/web/src/i18n/content{,.fr,.ru}.ts. Only the new tag 'live-artifact'
is added to each locale's PROMPT_TEMPLATE_TAGS map (+1 line each) so the
arrayContaining check in e2e/tests/localized-content.test.ts continues
to pass.

Template-level only: no new surface, no loader changes, no schema or
TypeScript type changes.

* fix(prompt-templates,i18n): register 'Live Artifact' category and template ID fallback for de/fr/ru

CI's e2e/localized-content.test.ts enumerates LOCALIZED_CONTENT_IDS from
apps/web/src/i18n/content.ts and asserts:

  - ids.promptTemplates              ===   sorted(all template ids in prompt-templates/)
  - ids.promptTemplateCategories     ⊇     all categories actually used by templates
  - ids.promptTemplateTags           ⊇     all tags actually used by templates

The new notion-team-dashboard-live-artifact template introduced both
the first 'Live Artifact' category and the first prompt-template id
without a copy translation, so each locale needs:

- 'Live Artifact' added to *_PROMPT_TEMPLATE_CATEGORIES (currently
  consumed via arrayContaining; order doesn't matter)
- 'notion-team-dashboard-live-artifact' listed in
  *_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK so it joins ids.prommplates
  via the EN-fallback path (no per-locale title/summary copy needed)

The 'live-artifact' tag was already added to *_PROMPT_TEMPLATE_TAGS in
the previous commit on this branch.

3 files changed, +6 / -3.

* fix(i18n): register Live Artifact category + template id fallback (CI repair on #799)

* fix(i18n): register Live Artifact category + template id fallback (CI repair on #799)

* fix(i18n): register Live Artifact category + template id fallback (CI repair on #799)

* fix(prompt-templates): scrub live/connector affordances from notion-team-dashboard prompt (#799 review)

Reviewers (mrcfps, lefarcen) flagged that even with the amber "Sample
data — design preview" banner and the "(sample data)" footer, the inner
prompt blob still asked the model to render UI affordances claiming a
real Notion / Composio connector binding ("Live · synced" pill, "Last
refreshed just now", "Refresh from Notion" blue button, callout saying
numbers are "pulled from your {workspace} Notion workspace via the
Composio connector"). That contradicts the prompt-only contract and
reintroduces the #778 mock-honesty concern.

Rewrite the prompt blob so every UI element is consistently presented
as seeded sample data:

- topbar.preview_pill (was live_pill):ample · design preview' pill
  with explicit negative instruction no to render any live/sync pill
- page_header.meta_row: drop 'Last refreshed', 'Auto' toggle, and the
  'Refresh from Notion' blue button; explicit DO NOT instructions
- callout: 'prompt-only design preview ... seeded sample data ... not
  pulled from a real Notion workspace and not refreshed via the
  Composio connector. For real refreshable / connector-backed Live
  Artifacts, use the live-artifact skill.' Also removes the bare
  '{workspace}' placeholder that was not using {argument ...} syntax
  (P2 nit from lefarcen).
- activity_feed_card.subtitle: 'Notion-style seeded activity for
  design preview' (was 'From Notion')
- linked_database.title/subtitle: marked '(sample)' / 'seeded sample
  rows · no live connector binding'
- linked_database.row_styles: explicit negative instruction not to
  render an 'Updated ↻' refresh badge
- footer: 'Notion-style sample data · seeded design preview, not bound
  to any Notion workspace or Composio connector'
- honesty_rule: enumerates all live/sync/refresh affordanche
  generator must NOT render

Top-level metadata (id, title, summary, category, tags, model, aspect,
previewImageUrl, source) is unchanged. Preview PNG already shows the
amber banner and a layout without a Refresh button, so it matches the
new in-prompt language.

* fix(prompt-templates): scrub live/connector affordances from notion-team-dashboard prompt (#799 review)

Reviewers (mrcfps, lefarcen) flagged that even with the amber "Sample data — design preview" banner and the "(sample data)" footer, the inner prompt blob still asked the model to render UI affordances claiming a real Notion / Composio connector binding ("Live · synced" pill, "Last refreshed just now", "Refresh from Notion" blue button, callout saying numbers are "pulled from your {workspace} Notion workspace via the Composio connector"). That contradicts the prompt-only contract and reintroduces the #778 mock-honesty concern.

Rewrite the prompt blob so every UI element is consistently presented as seeded sample data:

- topbar.preview_pill (was live_pill): 'Sample · design preview' pill with explicit negative instruction not to render any live/sync pill.
- page_header.meta_row: drop 'Last refreshed', 'Auto' toggle, and the 'Refresh from Notion' blue button; explicit DO NOT instructions for all three.
- callout: 'prompt-only design preview ... seeded sample data ... not pulled from a real Notion workspace and not refreshed via the Composio connector. For real refreshable / connector-backed Live Artifacts, use the live-artifact skill.' Also removes the bare '{workspace}' placeholder that was not using {argument ...} syntax (P2 nit from lefarcen).
- activity_feed_card.subtitle: 'Notion-style seeded activity for design preview' (was 'From Notion').
- linked_database.title/subtitle: marked '(sample)' / 'seeded sample rows · no live connector binding'.
- linked_database.row_styles: explicit negative instruction not to render an 'Updated ↻' refresh badge.
- footer: 'Notion-style sample data · seeded design preview, not bound to any Notion workspace or Composio connector'.
- honesty_rule: enumerates all live/sync/refresh affordances the generator must NOT render.

Top-level metadata (id, title, summary, category, tags, model, aspect, previewImageUrl, source) is unchanged. Preview PNG already shows the amber banner and a layout without a Refresh button, so it matches the new in-prompt language.

* fix(prompt-templates): scrub live/connector affordances from notion-team-dashboard prompt (#799 review)

Reviewers (mrcfps, lefarcen) flagged that even with the amber "Sample data — design preview" banner and the "(sample data)" footer, the inner prompt blob still asked the model to render UI affordances claiming a real Notion / Composio connector binding ("Live · synced" pill, "Last refreshed just now", "Refresh from Notion" blue button, callout saying numbers are "pulled from your {workspace} Notion workspace via the Composio connector"). That contradicts the prompt-only contract and reintroduces the #778 mock-honesty concern.

Rewrite the prompt blob so every UI element is consistently presented as seeded sample data:

- topbar.preview_pill (was live_pill): 'Sample · design preview' pill with explicit negative instruction not to render any live/sync pill.
- page_header.meta_row: drop 'Last refreshed', 'Auto' toggle, and the 'Refresh from Notion' blue button; explicit DO NOT instructions for all three.
- callout: 'prompt-only design preview ... seeded sample data ... not pulled from a real Notion workspace and not refreshed via the Composio connector. For real refreshable / connector-backed Live Artifacts, use the live-artifact skill.' Also removes the bare '{workspace}' placeholder that was not using {argument ...} syntax (P2 nit from lefarcen).
- activity_feed_card.subtitle: 'Notion-style seeded activity for design preview' (was 'From Notion').
- linked_database.title/subtitle: marked '(sample)' / 'seeded sample rows · no live connector binding'.
- linked_database.row_styles: explicit negative instruction not to render an 'Updated ↻' refresh badge.
- footer: 'Notion-style sample data · seeded design preview, not bound to any Notion workspace or Composio connector'.
- honesty_rule: enumerates all live/sync/refresh affordances the generator must NOT render.

Top-level metadata (id, title, summary, category, tags, model, aspect, previewImageUrl, source) is unchanged. Preview PNG already shows the amber banner and a layout without a Refresh button, so it matches the new in-prompt language.

---------

Co-authored-by: joeylee12629-star <joeylee12629-star@users.noreply.github.com>
2026-05-07 19:42:09 +08:00
PerishFireandGitHub cb92c93ae0 Migrate beta release publishing to R2 (#805)
* Prebundle standalone web packaged runtime

* Harden mac standalone prebundle policy

* Prebundle mac daemon packaged runtime

* Prune mac Electron locales

* Maximize mac release artifact compression

* Publish beta mac artifacts to R2

* Use remote R2 uploads for beta releases

* Fail fast on beta R2 access issues

* Use S3-compatible uploads for beta R2 releases

* Decouple beta versioning from GitHub releases

* Remove legacy beta metadata source

* Address release beta review notes
2026-05-07 19:13:52 +08:00
5abca505b1 add FlowAI live dashboard template skill (#801)
* add flowai live dashboard template skill

Introduce a new template-mode skill under the live-artifacts scenario with a default interactive example and seed template so users can generate polished, refresh-ready team dashboards quickly.

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

* add preview screenshot for flowai live dashboard template

Attach the provided dashboard screenshot under docs/screenshots/skills so the template contribution includes a visual preview artifact.

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

* fix(flowai-template): reposition as static prototype dashboard skill

Address review feedback on PR #801:

- SKILL.md: drop `scenario: live-artifacts` and live-related triggers;
  align with peer single-page dashboard skills using
  `mode: prototype` + `scenario: operations` so the four-file
  live-artifact contract no longer applies.
- references/checklist.md: rewrite quality gates around the static
  prototype scope (export-from-DOM, responsive breakpoints, theme-aware
  charts).
- assets/template.html:
  - CSV export now reads every visible row from the table DOM,
    including the Workflow column, instead of a hardcoded fixture.
  - Add 1300px and 720px breakpoints; the main grid stacks to one
    column, stat cards fall back to two then one, tabs wrap, table
    scrolls horizontally on phones.
  - Move chart colors into CSS variables (--chart-stroke,
    --chart-fill, --chart-axis, --chart-bar-label, --chart-bar-value)
    so dark-mode toggling re-derives them; chart canvases are
    re-rendered after theme switch.
  - Hash-sync tabs (#members | #details | #activity), animate the
    role bar chart only on first reveal of the details tab,
    fall back when CanvasRenderingContext2D.roundRect is unavailable,
    add Esc to exit zoom and prevent tooltip clipping.
- example.html: title cleanup to match new skill identity.

Localized content:
- Add `flowai-live-dashboard-template` to DE/FR/RU
  SKILL_IDS_WITH_EN_FALLBACK lists in apps/web/src/i18n so the
  e2e localized-content test passes.

---------

Co-authored-by: tuolaji <tuola@tuolajideMacBook-Air.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Tuola Ge <gexingli@refly.ai>
2026-05-07 19:07:45 +08:00
Pratik RaiandGitHub 555dbebfe2 fix(web): add alert when pdf export popup is blocked (#664)
Fixes PDF export feedback when popup blockers prevent opening the export preview.
2026-05-07 18:39:42 +08:00
55aa24167b add live-dashboard skill (#778)
* add live-dashboard skill

Notion-style team dashboard rendered as a Live Artifact.

Wires the OD 0.4.0 connector catalog (#381) end-to-end:

refresh-on-open, manual Refresh tween, auto-refresh, stale state.

Falls back to seeded mock data when no connector is bound.

* address PR #778 review comments

P1 — security and correctness:
- skills/live-dashboard/assets/template.html · skills/live-dashboard/example.html: escape every connector-derived string before innerHTML interpolation. Adds a tiny e() helper and routes feed.who/action/target/suffix/icon, row.title/icon/due/prio, person.name/color/id, KPI label/delta through it. Closes lefarcen #3200122795 + #3200122820.
- skills/live-dashboard/SKILL.md (live behavior section): align connector poll URL with references/connectors.md — POST /api/od/connectors/poll with { project, read } body, not /api/od/connectors/<id>/poll. Closes codex bot #3200100897.
- apps/web/src/i18n/content{,.ru,.fr}.ts: register live-dashboard in DE_/RU_/FR_SKILL_IDS_WITH_EN_FALLBACK so the localized-content e2e check passes. Closes mrcfps #3200122059.
- skills/live-dashboard/references/connectors.md: prepend a Status callout that names skills/live-artifact/ as the canonical file/CLI live-artifact contract and frames the HTTP shape as a forward-looking proposal sitting alongside it (out-of-the-box the artifact runs on seeded data; only seedNextChange() needs swapping when POST /api/od/connectors/poll lands). Closes lefarcen #3200122811.

P2 — quality and honesty:
- skills/live-dashboard/references/connectors.md: rewrite the auth_ref resolution step to match apps/daemon/src/media-config.ts (OD_MEDIA_CONFIG_DIR → OD_DATA_DIR → <projectRoot>/.od/media-config.json, $HOME/~/relative paths handled via expandHomePrefix). Closes codex bot #3200100906.
- skills/live-dashboard/example.html: switch the live-pill to a sticky Sample data state with a grey static dot, rewrite the callout to admit the figures are seeded fixtures, retitle the toast and the refresh tooltip, and refuse to flip to Live · synced inside updateTimes(). Adds a .pill-live.sample CSS variant. Closes lefarcen #3200122823.
- skills/live-dashboard/assets/template.html: hoist <meta name=od:project> from <body> into <head>. Closes lefarcen #3200122832.
- skills/live-dashboard/assets/template.html · example.html: add role=button + tabindex=0 + aria-current to every clickable .ws / .side-search / .nav-item, and wire a single document-level keydown handler that maps Enter/Space to a synthetic click for any role=button div (skipping real buttons / anchors / form controls). Closes lefarcen #3200122837.
- skills/live-dashboard/assets/template.html: implement the KPI tween + flash + snapshotKpi() the SKILL.md prose already promised — first render builds escaped cards, subsequent renderKpi(prev) calls tween numeric values and flash() the cells that actually changed; refresh() now calls snapshotKpi() before mutating state and forwards prev. SKILL.md spells out the wire-up. Closes lefarcen #3200122839.

* gate KPI tween + flash + row/feed highlight on prefers-reduced-motion

Addresses mrcfps's non-blocking review item on PR #778 (comment #3200614137,
template.html:453). The CSS @media (prefers-reduced-motion: reduce) block
already neutralizes CSS animations and transitions, but the new JS-driven
helpers kept moving for opted-out users:

- tweenText() scheduled requestAnimationFrame updates for 600ms
- flash() toggled the .flash highlight class for 700ms
- renderFeed()/renderRows() applied .feed-row.new / .db-row.changed
  classes which carry transient backgrounds even when their CSS
  animations are off

Both runtimes (assets/template.html and example.html) now share a
reduceMotion() helper (window.matchMedia('(prefers-reduced-motion:
reduce)').matches). When it returns true:

- tweenText()/tween() set the final value immediately and return
- flash() returns without touching the class
- renderFeed()/renderRows() pass null as the highlight id so the .new /
  .changed classes are never applied

Normal-motion users see the existing tween + flash + highlight pulse
unchanged. Keeps the P0 prefers-reduced-motion row in
references/checklist.md honest for agents that copy this template
verbatim.

---------

Co-authored-by: joey <joey@joeydeMacBook-Air.local>
Co-authored-by: joeylee12629-star <joeylee12629-star@users.noreply.github.com>
2026-05-07 18:21:22 +08:00
24b234e3c7 fix chat pane overflow (#740)
Co-authored-by: KevinWu-Pm <wx19950829@163.com>
Co-authored-by: DoTheWorkNow <260719655+DoTheWorkNow@users.noreply.github.com>
2026-05-07 18:16:34 +08:00
netteeandGitHub 84ac93c945 fix(daemon): extend OpenAI image request timeouts (#788) 2026-05-07 18:02:31 +08:00
324b20ad81 fix(css): reduce ws-tabs-bar scrollbar width to avoid filename overlap (#781)
Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-05-07 17:49:53 +08:00
b95ba5e79e add waitlist-page skill (#555)
* add waitlist-page skill

* fix(waitlist-page): address PR review feedback

- Remove novalidate from example.html form
- Ensure checkValidity() guard present in both template and example
- Remove required from firstname input in template
- Add token escaping rules to SKILL.md workflow (step 9)
- Add token mapping/fallback rules for BORDER/SUCCESS/STRIPE/DECO (step 7)
- Fix mobile quality gate to be measurable (375x667, 390x844)
- Promote hardcoded #fff, rgba(0,0,0,0.9), rgba(255,255,255,0.9) to
  CSS variables (--btn-label, --ticker-bg, --ticker-fg) in template
- Create references/checklist.md with P0/P1/P2 tiers; countdown timer
  is now a hard P0 prohibition; a11y gate split into six specific checks"

* fix: resolve P0 color and accessibility issues

- Add role=status to success messages for screen reader announcement
- Replace all hardcoded hex/rgba colors with template tokens
- Update SKILL.md with comprehensive color token mapping rules
- SVG decorations now use CSS variables instead of hardcoded strokes

* fix: address PR review feedback on scope, scrolling, and font tokens

Fixes:
- Restore pricing-page files accidentally deleted in previous commit
  (skills/pricing-page/SKILL.md and example.html now back on branch)
- Remove temp-original.html scratch file from commit
- Fix mobile viewport scrolling: change 'height: 100vh; overflow: hidden'
  to 'min-height: 100svh; overflow-x: hidden; overflow-y: auto'
  so content doesn't clip on 375×667 and 390×844 screens
- Split font tokens into URL-safe and CSS-safe variants:
  * {{DISPLAY_FONT_URL}} and {{DISPLAY_FONT_CSS}} for display fonts
  * {{BODY_FONT_URL}} and {{BODY_FONT_CSS}} for body fonts
  This fixes encoding: spaces as '+' in Google Fonts URL, literal in CSS
- Update SKILL.md frontmatter with new font input fields
- Update token escaping rules to document the split

* fix: resolve token contract mismatch and remove hardcoded colors from example.html

P0 Fixes:
- Remove all hardcoded colors from example.html (except #2D6A4F for --success)
- Use CSS variables for all color values: --btn-label, --ticker-bg, --ticker-fg, --deco-stroke
- Fix gradient to use var(--deco) instead of hardcoded #D1632B
- Apply consistent color expressions across decorations and text

Token Contract Fixes:
- template.html now uses full CSS expressions for opacity-based colors:
  * {{BORDER_EXPRESSION}} instead of {{BORDER_HEX}} (no # prefix)
  * {{BTN_LABEL_EXPRESSION}} instead of {{BTN_LABEL_HEX}}
  * {{TICKER_BG_EXPRESSION}}, {{TICKER_FG_EXPRESSION}}, {{DECO_STROKE_EXPRESSION}}
- Remove extra quotes from font tokens in template:
  * --font-body: {{BODY_FONT_CSS}} instead of '{{BODY_FONT_CSS}}'
  * Font tokens are already quoted if needed, no wrapping
- Update SKILL.md frontmatter with all color expression inputs and descriptions
- Update token mapping rules to clarify the new contract:
  * Hex tokens: simple six-digit colors
  * Expression tokens: full CSS values (rgba/color-mix), no # prefix
  * Font tokens: CSS font-family values, no extra wrapping
- Update token escaping rules to reflect new contract

This ensures agents can follow SKILL.md instructions without producing invalid CSS.

* fix: remove final hardcoded colors from example.html - P0 complete

- Button text: #fff → var(--btn-label)
- Ticker background: rgba(0,0,0,0.9) → var(--ticker-bg)
- Ticker text: rgba(255,255,255,0.9) → var(--ticker-fg)
- Logo text: fill=white → fill=var(--btn-label)

All colors now derive from design system tokens. Only #2D6A4F (--success) allowed hardcoded exception.

* fix: correct --btn-label contrast for CTA readability

Change --btn-label from #1A1410 (same as button background) to #FDE8DF
(light background color) so button text has proper contrast against
the dark --accent button background.

This resolves the black-text-on-black issue that broke the main
email capture action and satisfies the checklist button contrast gate.

* fix: add visible focus indicator for input accessibility

P1 Accessibility Polish:
- Update .form-row input:focus to include outline and outline-offset
- Before: border-color only, removing default outline (no visible focus)
- After: border-color + 2px outline + 2px offset (clear focus indicator)

This satisfies the checklist P1 focus-style gate and ensures keyboard
users can see which form field has focus. Both example.html and
template.html updated so agents copy complete focus patterns.

* fix: remove hardcoded logo shadow color - P0 compliance

- Add --logo-shadow CSS variable derived from foreground
- example.html: box-shadow 0 2px 8px rgba(0,0,0,0.08) → var(--logo-shadow)
- template.html: add {{LOGO_SHADOW_EXPRESSION}} placeholder
- Update SKILL.md with logo_shadow_expression input and mapping rules

All colors in example.html now derive from design system tokens.
Ensures agents copy compliant reference without hardcoded shadow colors.

* fix: register waitlist-page skill in i18n localized content registry

Add waitlist-page to locale-specific skill fallback lists so the web
content coverage test passes when the new skill is discovered:

- apps/web/src/i18n/content.ts: Add to DE_SKILL_IDS_WITH_EN_FALLBACK
- apps/web/src/i18n/content.fr.ts: Add to FR_SKILL_IDS_WITH_EN_FALLBACK
- apps/web/src/i18n/content.ru.ts: Add to RU_SKILL_IDS_WITH_EN_FALLBACK

The skill falls back to English localization for now; localized
descriptions can be added to each locale file later.

Fixes: web content coverage test now passes (6/6 tests).

* fix: wire template and checklist into skill workflow as mandatory gates

Restructure waitlist-page SKILL.md workflow to enforce the hardened
template-based execution path:

- Add Preflight section: agents MUST read assets/template.html first
- Add explicit token mapping and escaping rules (steps 2-4)
- Add mandatory Validation section: run references/checklist.md P0/P1
  gates BEFORE emitting artifact; fail fast if any P0 gate fails
- Update Quality gates section to emphasize template-based execution
  and reinforce P0/P1 gate hierarchy
- Update Output section: only emit after P0 passes; re-validate on
  iterations

This prevents agents from writing HTML from scratch or skipping the
hardened seed (template) and validation (checklist) that this PR adds.

* refactor(waitlist-page): replace literal logo placeholder with token

- Replaced `[LOGO]` with `{{LOGO_MARK}}` in template.html
- Added `logo_mark` to inputs in SKILL.md
- Updated mapping rules in SKILL.md to handle raw SVG or text for logo
- Updated P0 validation gates in SKILL.md and checklist.md to ensure logo replacement

* fix(waitlist-page): enforce strict escaping and sanitization for logo token

- Mandate HTML-escaping for text initials.
- Enforce strict allowlist-based sanitization for inline SVG (stripping `<script>`, `on*`, `<foreignObject>`, `href`, `xlink:href`, `url()`).
- Add fallback to escaped text initials for invalid/unsafe SVG.

* docs(waitlist-page): sync logo_mark frontmatter description with rules

- Updated the `logo_mark` input description in the SKILL.md frontmatter to explicitly outline the new requirements for HTML-escaped text or strict allowlist-sanitized SVG.

* fix(waitlist-page): add logo_fg_expression to guarantee contrast in logo mark text fallback

- Added `--logo-fg` CSS variable mapped to `{{LOGO_FG_EXPRESSION}}`.
- Updated `.logo-container` in `template.html` to inherit typography styles and apply `--logo-fg` for safe fallback when rendering escaped initials.
- Enforced WCAG AA contrast for logo initials against container background in `checklist.md`.

* refactor(waitlist-page): migrate hex color tokens to full css expressions

* refactor(waitlist-page): strict validation for color expression tokens to prevent CSS injection

* docs(waitlist-page): update validation summary to reflect strict color grammar

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-05-07 17:39:17 +08:00
xunchaandGitHub a8418ac730 Fix Windows link code folder dialog (#698)
* Fix Windows link code folder dialog

* Add Windows folder dialog coverage

* Complete Indonesian locale copy
2026-05-07 17:27:01 +08:00
Aqil AzizandGitHub fcc37c6c2d feat(i18n): add Indonesian locale 2026-05-07 16:48:05 +08:00
PerishFireandGitHub 6efac8887e Improve Windows beta packaging and installer flow (#768)
* Optimize Windows packaged web output

* Fix packaged contracts runtime build

* Optimize Windows packaged size pruning

* Prune Windows root Next payload

* Remove Windows bundled Node runtime

* Prune Windows standalone duplicate Next

* Add tools-pack cache foundation

* Cache Windows packaged build layers

* Cache Windows workspace builds

* Cache Electron-ready Windows app

* Split Windows tools-pack module

* Cache Windows dir build outputs

* Split Windows pack build modules

* Document Windows NSIS smoke namespace limits

* Move Windows NSIS smoke note to agents guide

* Optimize Windows beta packaging

* Bump packaged beta base version

* Improve Windows installer namespace UX

* Improve Windows tools-pack cache keys

* Stabilize Windows beta cache version keys

* Cache Windows workspace build outputs

* Optimize windows release beta cache layers

* Cache windows release dependencies

* Trim windows release cache before save

* Refresh windows tools-pack cache key

* Improve windows installer preflight prompts

* Fallback NSIS installer strings to English

* Fix Windows installer cleanup and preflight

* Improve Windows NSIS state logging

* Fix system NSIS Persian language alias

* Use long-path removal for Windows uninstall

* Fix mac tools-pack tests on Windows

* Address Windows packaging review feedback

* Fix Windows installer cache namespace isolation

* Include web output mode in Windows tarball cache key

* Use unique Windows release cache save keys
2026-05-07 16:44:15 +08:00
Tom HuangandGitHub 38eb78a382 feat(web): add Inspect mode for live per-element style tuning 2026-05-07 16:40:30 +08:00
Nagendhra MadishettiandGitHub bb2015766a feat: Critique Theater Phase 5 (panel prompt template + system composer wiring) 2026-05-07 16:35:04 +08:00
JHRandGitHub c00f89dbe4 fix(daemon): allow portless Origin in CORS whitelist for Chrome compatibility 2026-05-07 16:29:06 +08:00
lefarcenandGitHub 984797b3cd fix: add DeepSeek v4 models to catalog (#722) 2026-05-07 15:55:41 +08:00
Nagendhra MadishettiandGitHub 25a3ffd298 fix(daemon): add legacy data dir migrator
Add a one-shot OD_LEGACY_DATA_DIR migrator so packaged Desktop users can recover 0.3.x repo .od data into the 0.4.x data root. The migrator stages payloads before promotion, refuses unsafe merges and symlinks, rolls back failed promotion or marker writes, and extends packaged daemon startup handling for long migrations while failing fast on daemon exits.

Closes #710
2026-05-07 15:19:04 +08:00
shangxinyu1andGitHub 9b501f12a5 Support overriding the Codex executable path (#755)
* Support overriding the Codex executable path

* Replace save-as-template prompts with an in-app dialog

* Seed local packaged app config from workspace

* Fix packaged config and connection test overrides

* Keep tools-pack mac config seeding self-contained

* Require absolute CODEX_BIN overrides
2026-05-07 15:00:52 +08:00
Nagendhra MadishettiandGitHub 4fe3a37209 fix(web): widen settings subtitle max width
Allow the English settings subtitle to fit on one line in the settings modal while preserving wrapping on narrow viewports.
2026-05-07 14:20:18 +08:00
SidandGitHub 01c22a87bd fix(web): replace SketchEditor text prompt with modal
Replace the SketchEditor text tool's window.prompt flow with an in-app modal so it works in Electron desktop.
2026-05-07 14:00:25 +08:00
SidandGitHub 0b2b456694 fix(daemon): deliver Copilot prompt via stdin
Avoid Windows ENAMETOOLONG by keeping Copilot prompts out of argv and sending them through stdin instead.\n\nFixes #705
2026-05-07 11:52:30 +08:00
f45055a8bf docs+web: surface @nexudotio X account in README + entry sidebar (#696)
* docs(readme): add @nexudotio X link + Stay in the loop section

- Add X/Twitter follow badge in the header next to Discord, so visitors
  can subscribe to release notes and milestone threads in one click.
- Add a short "Stay in the loop" section above "Star us" pointing to
  @nexudotio on X. Discord is for chat, X is for the public milestones
  (releases, new skills, new design systems).

No other changes.

* feat(web): add Follow @nexudotio pill to entry sidebar foot

- Surface the official X account next to the Settings/Pet/Language pills
  so users can subscribe to release notes from inside the app, not only
  the README.
- Uses the existing .foot-pill style (already supports <a>, has
  text-decoration: none) and the existing 'external-link' Icon — no new
  CSS, no new icons, no i18n key required (single short English label).
- Opens in a new tab with rel="noreferrer noopener".

Pairs with the README badge added in this same PR.

---------

Co-authored-by: Tuola-waj <ge@nexu.io>
2026-05-07 11:34:59 +08:00
monshunterandGitHub e6e5928be1 feat(web): add connection tests for execution settings (#507)
* feat(settings): add connection test for providers and CLI agents

Adds a "Test" action in the Settings dialog that verifies the configured
provider (Anthropic/OpenAI/Azure/Google) or CLI agent without sending a
real chat. Backed by a new daemon endpoint and shared contracts, with
categorized inline statuses and i18n strings across all supported locales.

* fix(settings): address connection test review feedback

* fix(daemon): pass empty MCP servers for connection probes

* fix(connection-test): address review blockers

* fix(daemon): fail json stream runs on structured errors

* fix(contracts): build connection test subpath export

* Use draft CLI env in agent connection tests

* fix(i18n): add fallback ids for new curated content
2026-05-07 11:25:37 +08:00
832ea7d864 fix: batch of small bug fixes (#283, #275, #390) (#530)
* fix(web): add hover tooltips to Design Files action buttons (#283)

The batch-download, select-all, and clear-selection buttons in
DesignFilesPanel had no title attribute, so users hovering them saw no
tooltip. The other action buttons (refresh, new sketch, paste, upload)
already had titles. Added titles to the three missing ones using the
existing translation keys, so hover behavior is consistent across the
panel.

Closes #283.

* docs: point pi-ai links to pi-mono packages (#275)

The pi project moved from a standalone repo to the pi-mono monorepo.
The old URL https://github.com/mariozechner/pi-ai now 404s. Replaced
both shapes of reference:

- The reference-style [piai]: definition now points at
  https://github.com/badlogic/pi-mono/tree/main/packages/ai
  (the multi-provider LLM API package).
- Inline links whose visible text is the CLI tool 'pi' or 'Pi' now
  point at
  https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent
  (the interactive coding-agent CLI), so a reader clicking 'pi' in
  the daemon-discovery section lands on the actual binary's docs.

Affected: README.md and 10 translated READMEs, plus docs/spec.md,
docs/architecture.md, docs/references.md, docs/roadmap.md.

Closes #275.

* fix(daemon): expand $HOME / ${HOME} in OD_DATA_DIR (#390)

Some launchers (systemd unit files, NixOS modules, certain Docker
entrypoints) pass OD_DATA_DIR with a literal '$HOME' or '${HOME}'
because no shell ever expands them. resolveDataDir previously only
handled '~/' shorthand, so '$HOME/.open-design' fell through to
path.resolve(PROJECT_ROOT, '$HOME/.open-design') and produced paths
like /opt/open-design/$HOME/.open-design.

resolveDataDir now expands '~', '~/...', '$HOME', '$HOME/...',
'${HOME}', and '${HOME}/...' to os.homedir() before the absolute /
relative branch runs. Rebuilds via path.join so the platform separator
is correct on Windows even when the input used forward slashes.

Tests: 7 unit tests cover empty/undefined, '~', '~/...', '$HOME',
'$HOME/...', '${HOME}/...', absolute paths, and relative paths.

Closes #390.

* fix(daemon): accept backslash separators + hermetic resolve-data-dir tests

Round 1 review feedback on PR #530.

The previous regex only matched forward-slash separators, so a Windows
launcher passing OD_DATA_DIR=$HOME\.open-design or ${HOME}\.open-design
fell through to path.resolve(projectRoot, ...) and produced a directory
named $HOME or ~ under projectRoot. The regex now accepts both forward
and back slashes for the home-prefix separator.

The previous tests called the real resolveDataDir against literal
~/od-test, $HOME/od-test, etc., which created and write-checked
directories under the developer's or CI runner's actual home. The tests
now stub os.homedir() with vi.spyOn to a per-test mkdtemp directory and
remove it in afterEach, so no test ever writes outside its own sandbox.
Added explicit fixtures for the Windows backslash forms ($HOME\od-test,
${HOME}\od-test, ~\od-test) so launcher coverage stays cross-platform.

12/12 resolve-data-dir tests pass, daemon typecheck clean.

* fix(docs,daemon): apply pi-mono links to README.es and await test cleanup

Round 2 review feedback on PR #530.

README.es.md was added in upstream #552 after my pi-mono link sweep
landed, so the daemon-discovery paragraph (line 222), the [piai]
reference (line 684), and the Pi table row (line 709) still pointed
at the broken https://github.com/mariozechner/pi-ai URL. Applied the
same replacements: the [piai] ref now points at packages/ai, and the
inline Pi link now points at packages/coding-agent. Spanish readers
get the same coverage as the other 11 locales.

The absolute-path test in tests/resolve-data-dir.test.ts dropped its
fixture via void rm(abs, ...), so a failed async removal could leak
rdd-abs-* directories from the suite. The test is now async and
awaits the rm in the finally block, matching the awaited cleanup in
afterEach. 12/12 resolve-data-dir tests still pass, daemon typecheck
clean.

* fix(daemon): share $HOME expander between OD_DATA_DIR and OD_MEDIA_CONFIG_DIR

Round 3 review feedback on PR #530.

resolveDataDir (server.ts) now expands $HOME / ${HOME} / ~, but
media-config.ts had its own resolveOverrideDir that only handled ~/.
Because configFile() falls back to OD_DATA_DIR when OD_MEDIA_CONFIG_DIR
is unset, setting OD_DATA_DIR=$HOME/.open-design split state: SQLite,
projects, and artifacts went to the expanded path while
media-config.json stayed under <projectRoot>/$HOME/.open-design.
Stored provider keys then appeared missing on the next read.

Extracted the home-prefix expansion into apps/daemon/src/home-expansion.ts
so resolveDataDir and resolveOverrideDir share one resolver. Both now
recognize ~ / $HOME / ${HOME} (bare tokens) and ~/, ~\, $HOME/, $HOME\,
${HOME}/, ${HOME}\ (prefix forms with either separator).

Three new media-config routing tests cover the OD_DATA_DIR fallback for
$HOME/..., ${HOME}/..., and the OD_MEDIA_CONFIG_DIR explicit-override
$HOME/... case so the co-location guarantee is locked down by tests.

Daemon typecheck clean. Tests pass on Linux CI; the existing pattern in
the file uses process.env.HOME which os.homedir() reads on POSIX.
Resolve-data-dir tests stay hermetic via vi.spyOn.

* docs(daemon): media-config comments reflect full $HOME / ${HOME} expansion

Round 3 review feedback on PR #530 (lefarcen, P3 non-blocking).

The file-header and resolveOverrideDir() function comment said only
~/ expands. Updated both to mention the shared expandHomePrefix()
helper and the full set of forms it handles (~, $HOME, ${HOME} with
either separator), so a future reader does not need to chase the
implementation to understand what env values are accepted.

* test(daemon): stub os.homedir() in media-config routing tests

Round 4 review feedback on PR #530.

The new $HOME / ${HOME} routing tests relied on process.env.HOME being
read by os.homedir(), which works on POSIX but is unreliable on Windows
(Node prefers USERPROFILE / profile APIs there). The tests would expand
to the real user home while fixtures were written under the per-test
homeDir, causing platform-specific failures in the same area this PR
is making cross-platform.

The inner describe block now stubs os.homedir() via vi.spyOn to return
the per-test homeDir, matching the pattern in resolve-data-dir.test.ts.
Restored in afterEach. The four $HOME-form routing tests now pass on
both POSIX and Windows.

Daemon typecheck clean. The two OAuth fallback test failures unrelated
to this change (real ChatGPT/Codex tokens in the local env) remain
out-of-scope.

* fix(i18n): drop duplicate uk.ts promptTemplates keys after rebase

Upstream #674 added the same Ukrainian translations my earlier
commit added. The rebase landed both copies; tsc rejects duplicate
property names. Drop my copies so #674 (which is now upstream) is
the single source for these keys.

---------

Co-authored-by: Nagendhra <nagendhra405@gmail.com>
2026-05-07 11:17:02 +08:00
Gabriel VazandGitHub 6abd7676c8 fix(daemon): unbreak Claude Design ZIP import on Node 24 and raise file ceiling (#591)
* fix(daemon): unbreak Claude Design ZIP import on Node 24 and raise file ceiling

- Skip inflateRawSync when an entry's central-directory uncompressedSize is 0;
  Node 24 rejects { maxOutputLength: 0 } with ERR_OUT_OF_RANGE, which silently
  killed the entire import for any zip containing an empty file or a streaming
  entry whose size is only present in the data descriptor.
- Raise MAX_FILES from 500 to 5000. Real-world design-system exports commonly
  exceed 500 files; MAX_TOTAL_BYTES (100 MB) and MAX_FILE_BYTES (25 MB) already
  cap pathological inputs.
- Add regression tests for both: zero-byte deflate entry, central directory
  advertising uncompressedSize=0, and a 600-file zip.

Refs #590

* fix(daemon): preserve real payload when central uncompressedSize is 0

Reviewers correctly flagged that the previous Buffer.alloc(0) fast-path
trusted the central directory's uncompressedSize, which is unreliable for
streaming/data-descriptor zips: an entry whose central record reports 0
can still carry real deflated bytes. The earlier fix wrote empty files to
disk in that case, and the post-condition body.length !== uncompressedSize
check still passed because both sides were 0.

- Inflate streaming entries with maxOutputLength = MAX_FILE_BYTES when the
  central directory advertises 0, so legitimate non-empty payloads decode
  fully instead of being silently truncated.
- Move size enforcement post-decode: per-file and total-byte budgets are now
  computed from the actual decoded length, and the strict equality check is
  skipped when central was 0 (i.e., genuinely unknown).
- Keep the empty-deflate degenerate case (compressed.length === 0) safe by
  short-circuiting before zlib instead of relying on uncompressedSize.

Tests:
- New: streaming-zip case with central uncompressedSize=0 + non-empty body
  asserts the on-disk file matches the original bytes (would have been
  silently truncated under the previous fix).
- New: oversized streaming entry (> MAX_FILE_BYTES) is still rejected even
  though the central directory under-reports.
- The original 0-byte and >500-file regressions remain covered.
2026-05-07 10:31:30 +08:00
Marc ChanandGitHub 5a29fed7d3 fix(web): align design system default test fixture (#708) 2026-05-07 09:07:28 +08:00
SidandGitHub 4c82e48e4f fix web design system selection persistence (#621) 2026-05-07 02:27:00 +08:00
SidandGitHub 1bd1f3a661 fix(daemon): surface OpenCode error frames + treat empty-output runs as failed (#700)
* fix(daemon): surface OpenCode error frames + treat empty-output runs as failed

Closes #691. OpenCode runs would silently complete in ~3 seconds without
producing any visible chat output and still be rendered as a successful
turn — three independent bugs along the structured-stream path conspired
to produce this silent-failure shape.

## Bug 1 — `apps/daemon/src/json-event-stream.ts:85-91`

OpenCode emits structured error frames on stdout (e.g. provider auth
failures, network errors, schema mismatches) and still exits 0. The
parser was downgrading these to `{type: 'raw', line: ...}`, which the
chat UI does not render as an assistant message. The error string was
discarded as "no-op output."

Fix: emit a proper `{type: 'error', message, raw}` event matching the
qoder-stream contract that the daemon's existing error-handling path
already recognises.

## Bug 2 — `apps/daemon/src/server.ts:4199-4205`

Even after Bug 1 was fixed, the json-event-stream branch wired the
parser to a bare `(ev) => send('agent', ev)` lambda — bypassing the
`sendAgentEvent` wrapper that interprets `type:'error'` events and
sets the `agentStreamError` flag the close handler reads to flip the
run to `failed`. So an emitted `error` event would just be forwarded
as a no-op `agent` SSE event with no lifecycle effect.

Fix: route json-event-stream through `sendAgentEvent`, mirroring the
qoder-stream-json wiring at line 4175.

## Bug 3 — `apps/daemon/src/server.ts:4220-4234`

Even after Bugs 1 and 2 are fixed, there's still a class of runs where
OpenCode never emits any error frame, never emits any substantive
event, and exits 0. Pre-fix this was marked `succeeded` and the user
saw a blank chat with no diagnostic.

Fix: track `agentProducedOutput` inside `sendAgentEvent` (set on
`text_delta`, `thinking_delta`, `tool_use`, `tool_result`, `artifact`
— deliberately NOT on `status` / `usage`, since a model can emit
token-usage numbers for an empty completion). When the close handler
sees `code === 0 && trackingSubstantiveOutput && !agentProducedOutput`
the run is marked `failed` with an explicit AGENT_EXECUTION_FAILED
SSE error so the chat shows a clear reason instead of a silent
empty turn.

The check is gated by `trackingSubstantiveOutput` so it only fires
on streams that actually contribute to the output flag (currently
qoder-stream-json and json-event-stream). ACP sessions and plain
stdout streams keep their existing success/failure determination.

## Tests

- 3 new unit tests in `apps/daemon/tests/json-event-stream.test.ts`
  pin the OpenCode error event shape: full repro
  (`error.data.message`), `error.name` fallback, and the
  generic-fallback shape when `error` is empty.
- All 60 daemon test files (851 tests) pass on `pnpm --filter
  @open-design/daemon test`. All 42 web test files (309 tests) pass
  on `pnpm --filter @open-design/web test`.
- Full repo `pnpm typecheck` clean.

## Live verification

Verified end-to-end via a stub `opencode` binary that mimics each of
the failure shapes against `pnpm tools-dev run web`:

1. Stub emits `{"type":"error",...}` then `exit 0` — run now ends as
   `failed` with the OpenCode error message surfaced as an SSE
   `error` event. Pre-fix this was `succeeded` with an empty chat.
2. Stub emits nothing then `exit 0` — run now ends as `failed` with
   "Agent completed without producing any output…" diagnostic.
   Pre-fix this was `succeeded` with an empty chat.
3. Stub emits a normal `step_start` / `text` / `step_finish` sequence
   then `exit 0` — run still succeeds. (Regression check.)

## Out of scope (mentioned for the next person)

- `claude-stream-json` and `copilot-stream-json` still wire to a bare
  `(ev) => send('agent', ev)` and don't currently parse `type:'error'`
  frames. If their CLIs ever start emitting structured error events
  the same pattern (route through `sendAgentEvent` + emit proper
  `type:'error'`) applies. Not in scope here because we have no
  evidence those CLIs do this today, and changing the wiring without
  a confirmed failure mode risks regressing currently-working flows.
- ACP sessions (`pi-rpc`, `acp-json-rpc`) own their own success /
  failure determination via `acpSession?.hasFatalError()` and the
  empty-output guard explicitly skips them via
  `trackingSubstantiveOutput`.
- Plain stdout streams have no event-level tracking, so the empty-
  output guard skips them too. Diagnosing a no-output plain-stream
  agent is a separate problem that needs different signals.

* chore: retrigger CI on top of green main (post #697 i18n backfill)
2026-05-07 02:00:19 +08:00
Jheison Martinez BolivarandGitHub 4368b8f163 feat(linux): add headless mode for install/start/stop operations (#686)
* feat(linux): add headless mode for install/start/stop operations

* docs(linux): document headless mode commands and usage

* refactor(linux-headless): write web-root.json instead of polling IPC for URL

* fix(linux-headless): fail start when web identity never appears instead of returning success

* docs(linux-headless): add use-case context and clarify launcher path dependency

* fix(linux-headless): ensure launcher, identity and shutdown align with tools-pack

- Bake OD_DATA_DIR into launcher so manual runs use the same paths as tools-pack
- Validate web-root.json fields before accepting to reject stale identity
- Remove web-root.json on successful stop
- Add IPC server for graceful STATUS/SHUTDOWN handling

* fix(linux-headless): create IPC server before writing web-root.json
2026-05-07 01:52:03 +08:00
a5aaeb7905 Fix: Remove element selector tooltip in Tweaks mode (#697)
* Fix: Remove element selector tooltip in Tweaks mode

* fix: cover localized content fallbacks

* fix: keep comment target labels testable

---------

Co-authored-by: bojiehuang <bojiehuang@bojiehuangdeMacBook-Pro.local>
Co-authored-by: mrcfps <mrc@powerformer.com>
2026-05-07 00:02:38 +08:00
576dfed9e1 feat: add accent color control and launcher for Open Design (#683)
* feat: add accent color control and launcher for Open Design

* fix: remove launcher binary from PR

* test: cover accent appearance edge cases

---------

Co-authored-by: ferasbusiness666 <ferasbusiness666@users.noreply.github.com>
2026-05-06 23:14:21 +08:00
42e4d080bd feat(skills): add social-media-dashboard skill + Totality Festival design system (#678)
* feat(skills): add social-media-dashboard skill + Totality Festival design system

- New skill 'social-media-dashboard': single-screen creator analytics
  dashboard with platform switcher (X / GitHub / LinkedIn / YouTube /
  Instagram), KPI row, growth chart with annotations, top-post / top-PR
  preview, trending topics, and top comments. Includes a self-contained
  example.html (Totality Festival styled, X + GitHub tabs, live KPI
  ticker, GitHub contributors grid, world-map audience geography).
- New design system 'totality-festival': cosmic-premium dark glassmorphic
  system with amber corona highlights and cyan atmospheric accents.
  Mirrors Google Labs' design.md spec example so skills can be
  validated against an upstream reference.
- Fix swatches parser in apps/daemon/src/design-systems.ts so it
  recognises the '- **Name:**' bold-with-inner-colon form used by
  several existing systems (ant, totality-festival, ...). Previously
  only the '**Name** (`#hex`)' form was matched, which left their
  picker thumbnails empty.

* feat(skills): polish social-media-dashboard example + add PR preview

- Top Post media block: replace empty gold frame with an inline SVG
  thumbnail (radial glow + ascending data curve + amber/cyan pulse dots
  + play icon + 'LIVE · 0:22' caption). Visually echoes the live-artifact
  story the post copy is selling.
- Hoist the brand-mark linearGradient into a global SVG defs block at
  the top of <body> so all three avatars (header, user, top-post) can
  reference url(#brandRing) and render the teal arrow consistently.
  Previously only the header SVG carried the gradient definition, so
  the user and post avatars rendered as empty rings under headless
  capture.
- Add hero.png preview to .preview/ for the PR description.

---------

Co-authored-by: Tuola Ge <gexingli@refly.ai>
2026-05-06 21:50:23 +08:00
98e40c1cfc feat(daemon): export project transcript to disk for downstream consumption (prereq for #450) (#493)
* feat(daemon): export project transcript to disk for downstream consumption

Adds exportProjectTranscript(db, projectsRoot, projectId, options?) — a
pure function that walks SQLite-backed conversation history and writes a
structured, lossless JSONL transcript to <projectDir>/.transcript.jsonl.

This is the input primitive that #450's "Finalize design package"
synthesis step needs. Landed ahead of the synthesis endpoint as a small,
reviewable, well-tested unit — no HTTP route, no LLM call, no web UI in
this diff. PR 2 will wire POST /api/projects/:id/finalize on top of it.

Format: JSONL with header line + per-conversation marker lines +
per-message lines. Compact encoding saves ~20–30% on synth-call tokens
vs pretty-printed JSON. schemaVersion field reserved on the header for
incompatible changes later.

Coalescing: events_json carries streaming text_delta / thinking_delta
chunks plus tool_use / tool_result / thinking_start markers and
telemetry. The export collapses runs of same-type deltas into terminal
text / thinking blocks via arrival-order with type-change flush,
preserving any interleaving with tool blocks. Telemetry (status, usage,
raw) is dropped. thinking_start is treated as an explicit flush trigger
so multiple thinking blocks in one message survive intact.

Content fallback: user-typed messages persist as plain text in
messages.content with events_json = NULL because user input does not
flow through the streaming pipeline. When event-derived blocks come
back empty, fall back to a single text block from content so user
prompts are not silently dropped.

Atomic write: tmp filename includes pid + crypto-random suffix so
concurrent exports for the same project cannot collide. fsync before
rename so a crash between rename and power loss cannot lose bytes.

Hidden file: leading dot keeps .transcript.jsonl out of listFiles
(projects.ts:54), the archive zip, and the gallery.

Tests: 14 unit cases — empty project, text/streaming coalescing, tool
ordering, telemetry filtering, type-change flush, text↔thinking↔text
interleaving, thinking_start flush, multi-conversation chronological
order, atomic-write hygiene, content fallback (both directions),
malformed events_json, and unsafe project id rejection.

Refs: nexu-io/open-design#450

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(daemon): address PR #493 review feedback for transcript export

Addresses every blocker, P2, and P3 raised on
https://github.com/nexu-io/open-design/pull/493:

- Blocker (event-shape mismatch): coalescer now switches on the
  PersistedAgentEvent kind discriminator (text/thinking/tool_use/
  tool_result), reading the shared `text` field for content kinds,
  matching what apps/web/src/providers/daemon.ts:347-394 actually
  persists into messages.events_json. Empirical confirmation: live
  SQLite contains zero `type` keys.
- Removed @ts-nocheck from the source file; added inline types for
  Db (Database.Database), ConversationRow, MessageRow, AttachmentRef,
  CommentAttachmentRef, and Block. Tests retain @ts-nocheck per
  codebase convention. Note: db.ts still uses @ts-nocheck, so the
  new types catch drift inside transcript-export.ts itself, not at
  the SQLite-helper boundary.
- parseEvents now distinguishes null / malformed / not_array / ok
  cases; non-null-but-unparseable rows emit a console.warn with
  project+message id before falling back to content.
- Switched temp-write from writeSync (which can return short) to
  writeFileSync({flag:'wx'}); explicit fsync via reopen before
  rename, per reviewer concern about partial-write durability.
- Added per-project lockfile (.transcript.lock) acquired with
  openSync(..., 'wx') and released in finally; concurrent exports
  throw the new TranscriptExportLockedError. Stale-lock recovery
  is documented as a known limitation in the file header.
- Header gains attachmentCount, commentAttachmentCount, and explicit
  attachmentsInlined: false. Per-message lines gain attachments /
  commentAttachments references (paths only, not bytes; synthesis
  reads files from disk by path). schemaVersion bumped 1 -> 2 so
  the change is explicit; v1 was never consumed.
- mkdirSync(dir, { recursive: true }) at entry covers projects
  with DB rows but no on-disk directory yet (codex bot finding).
- Refactored node:fs imports from named to default
  (import fs from 'node:fs') so vitest spies in tests #15-#17 can
  redefine properties on the underlying CJS exports object. ESM
  namespace imports of node:fs produce a frozen Module Namespace
  Object that vi.spyOn cannot mutate; default-import returns the
  CJS module.exports which is mutable.
- Inline PersistedAgentEvent union: the daemon tsconfig does not
  resolve the `@open-design/contracts/api/chat` subpath export, so
  the union is restated in the source. Schema-mismatch tests cover
  the case where the contract would diverge.
- Test count 14 -> 24: failure injection for writeFileSync /
  fsyncSync / renameSync, existing-file replacement, lockfile
  contention (lockfile-pre-create design — synchronous API can't
  race via Promise.allSettled), parse-warning cases (malformed +
  not-array), attachments header + per-message coverage, missing-
  project-dir case.

Refs nexu-io/open-design#450 (does not close).

* fix(daemon): preserve thinking-segment boundaries on status thinking-start

Codex flagged this as a P2 on PR #493 a39d430:
https://github.com/nexu-io/open-design/pull/493#discussion_r3188524878

The web translator emits `{ kind: 'status', label: 'thinking' }` at every
thinking_start (apps/web/src/providers/daemon.ts:367-369). The previous
default branch dropped all status events without flushing the active
accumulator, so two thinking segments separated only by that marker
merged into one block — losing the original boundary and making the
transcript non-lossless for downstream synthesis.

coalesceBlocks now matches `status` explicitly: when `label === 'thinking'`
the prior accumulator flushes; other status labels and usage / raw drop
without flushing as before. Behavior fix within schemaVersion 2; no
shape change.

Test #25 verifies the boundary preservation:
  [thinking 'a', thinking 'b', status 'thinking', thinking 'c', thinking 'd']
  → blocks: [thinking 'ab', thinking 'cd'] (two blocks, not one)

Existing test #5 still passes because it uses status with label 'streaming'
which remains pure telemetry and does not flush.

Suite: 506 -> 507 daemon tests, all green.

Refs nexu-io/open-design#493

---------

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-06 21:48:39 +08:00
shangxinyu1andGitHub 8301bcd46e test: add desktop settings and project flow e2e coverage (#306)
* test: add desktop settings regression coverage

* test: stabilize desktop smoke interactions on latest main

* fixer: address PR #306 follow-up items

Generated-By: looper 0.2.7 (runner=fixer, agent=codex)

* test: expand ui e2e automation suite

* fix: add missing Ukrainian prompt template labels

* chore: align desktop e2e helpers with layout guard

* chore: move settings protocol e2e into ui suite

* fix: preserve api provider settings across protocol switches

* fix: avoid leaking api keys across protocol drafts

* test: fold desktop smoke coverage into mac spec

* fix: dedupe Ukrainian prompt template labels
2026-05-06 21:48:12 +08:00
EliandGitHub 3298cb3756 feat(skills): add 5 Orbit briefing templates (#671)
* feat(skills): add 5 Orbit briefing templates

Introduces a new "orbit" scenario family in the Examples gallery for
morning-briefing surfaces. Each template lives at the top of "我的设计"
and aggregates yesterday's connector activity into a single page.

- orbit-general: adaptive bento dashboard that fans across 12-16
  connectors, where each module picks its own UI form by data type
  (list / avatar stack / status ring / heatmap / file grid / alert
  card / kanban / etc.)
- orbit-github: GitHub-flavored single-connector digest mirroring the
  Notifications + PR-diff visual language
- orbit-gmail: Gmail-flavored digest rendered as a Daily Digest email
  inside the three-pane inbox
- orbit-linear: Linear-flavored digest in the dark Inbox + cycle-
  progress layout
- orbit-notion: Notion-flavored digest authored as a native Notion
  page (callout / toggle / database table)

The new scenario value 'orbit' surfaces as a filter pill in
ExamplesTab automatically; no UI code change required.

* fix(skills): reframe Orbit skill descriptions as pipeline-triggered

The original descriptions framed each skill as a standalone "X-flavored
briefing template" the user picks. They are actually skills the Orbit
daily-digest pipeline selects automatically based on which connectors
the user has authenticated, then runs against live connector data.

Rewrites both `description` and `example_prompt` for all 5 templates:
- orbit-general: invoked when 2+ connectors are connected; aggregates
  the past 24h across every authenticated source
- orbit-github / orbit-gmail / orbit-linear / orbit-notion: invoked
  when the named connector is the user's only connection (or scope is
  explicitly limited to it); pulls the past 24h from that connection
  alone

All 5 now state explicitly that they are not user-triggered — the
Orbit scheduler invokes them.

* feat(examples): add Orbit pill to the mode filter row

Surfaces the Orbit briefing skills as a top-level "type" filter in the
Examples gallery, alongside Prototype · Desktop / Mobile / Slide deck /
Docs & templates. Filter matches skills with scenario === 'orbit'.

- ExamplesTab: extend ModeFilter and MODE_PILLS with 'orbit'; teach
  matchesMode and modeCounts about it
- i18n: add 'examples.modeOrbit' to Dict and to all 16 locale files
  ('Orbit' is left untranslated as a brand name)

* polish(orbit-general): real Figma preview image + revised comment

Replaces the empty gray placeholder in the Figma module with an
Unsplash UI-design photo, and rewrites the mock comment to read like
a substantive design-review note rather than a nit about button
placement.

* feat(examples): eager-load card previews via IntersectionObserver

Card previews previously only loaded on hover, leaving the example
gallery showing 'Hover to preview' placeholders for everything below
the fold. Now each card observes the viewport and prefetches its HTML
800px before scrolling into view, so the iframe is ready by the time
the user reaches it.

Hover remains as a fallback path (and for browsers without
IntersectionObserver, the card loads immediately on mount).

Also reverts the Unsplash photo on the orbit-general Figma module
back to the gray placeholder — the stock image semantically misread
as a Photoshop screenshot rather than a Figma artboard.

* feat(orbit-general): drop Figma connector module

Removes the Figma bento card and its scoped CSS, plus the orphaned
Top-3 entry that referenced a Figma comment. Reassigns Top-3 #2 to
a Notion document review so the priority list stays aligned with
the connectors actually rendered.

* i18n(skills): translate Orbit example prompts to English

The example_prompt is what gets injected into the chat input when a
user clicks 'Use this prompt', and is read by the agent verbatim. It
should match the SKILL.md description language (English), not the UI
locale. Replaces the Chinese drafts with English equivalents across
all 5 Orbit skills, and drops the Figma reference from orbit-general
since that connector module was removed earlier.

* fix(skills): rewrite Orbit SKILL.md bodies with reproducible specs

Earlier the bodies were too abstract (only a connector→UI mapping
table and a one-line style note), so agents running the skill could
not reproduce the shipped example.html and got stuck in long retries.

Each SKILL.md body now contains:
- exact color tokens lifted from the example.html
- type stack and font sizes
- a section-by-section page spec (top-to-bottom)
- chip / pill / icon rendering rules
- forbidden list

The example_prompt is collapsed back to a one-line user intent so the
skill body is the source of design truth.

Covers all 5 templates: orbit-general, orbit-github, orbit-gmail,
orbit-linear, orbit-notion.

* feat(orbit): make every connector item clickable

Each Orbit briefing template now links its rows / cards to the matching
source URL so users can jump straight from the morning digest to the
underlying connector.

- orbit-general: each bento card gains an 'Open in {connector} ↗' CTA
  built from a connector→URL map; each Top 3 card becomes an anchor
- orbit-github: every event row opens the corresponding github.com
  pull/issue URL parsed from the row identifier; the header logo links
  to the repo
- orbit-linear: each issue row gains a small ↗ button that opens
  linear.app/{team}/issue/{ID}
- orbit-gmail: action and reply buttons jump to a Gmail search URL
  scoped to the sender
- orbit-notion: page-link spans wrap as anchors and database rows are
  click-to-open against notion.so

All links use target="_blank" rel="noopener noreferrer".

* fix(skills): force agents to mirror example.html 1:1

Earlier skills told the agent the example was 'source of truth' but
left phrasing soft, so agents felt free to add extra UI elements
(snoozed-mail row, extra yellow stars on inbox rows, etc.) that
were not in example.html.

Each Orbit SKILL.md now opens with a 'Source-of-truth protocol' that
forces the agent to:
  1. read example.html before writing any output
  2. mirror its DOM structure / class names / module count / element
     order 1:1
  3. only refresh mock values; never invent additional UI elements,
     rail entries, sections, badges, or chrome ornaments

The reference sections that follow stay informative for tokens and
visual language but are explicitly demoted from spec to commentary.

* fix(orbit-gmail): remove three-pane / left-rail / inbox-list claims

The example.html is a single-column page: Gmail top header + the
opened Orbit Daily Digest email (toolbar / subject / sender / digest
body / reply bar). Earlier copy described a Gmail three-pane app with
Compose button, label list, Categories tabs, and an inbox listing —
none of which exist in the actual asset.

- example_prompt: drops 'three-pane inbox' phrasing
- description: same
- body: rewrites Page sections to mirror the real header → email-chrome
  layout, top to bottom; explicitly forbids left rail, inbox list, and
  Categories tab strip

* feat(orbit): forbid external design systems in all 5 skills

Each Orbit briefing template ships its own complete visual language
baked into example.html (Gmail / GitHub / Linear / Notion / Open
Orbit's editorial bento). Adds an explicit 'Design system policy'
block telling the agent to:

- ignore any DESIGN.md attached to the active project
- ignore brand tokens or Figma files supplied via chat
- use exclusively the colors / fonts / radii from example.html

This is a hard constraint: an Orbit briefing must look like the
connector it represents, not like the user's brand.

* feat(newproj): hide design-system picker for skills that opt out

Skills can declare 'od.design_system.requires: false' in SKILL.md to
opt out of DESIGN.md injection (the Orbit briefing skills do this —
their example.html ships with a complete connector-native visual
language). When the active default skill for a tab opts out, hide the
design-system picker so we don't ask the user to attach a brand we'll
then ignore.

Existing tabs that don't host a default skill (template, other) keep
the picker. The check only fires for prototype / live-artifact / deck.

* review: address P2 reviewer feedback

P2 — Connector family coverage gaps (orbit-general):
  Adds Finance, CRM/Sales, Support, Analytics, Infrastructure, Security
  rows to the connector→UI mapping table (now 16 families). Adds a
  'Fallback heuristics' subsection so unknown connectors are routed by
  data shape (numbers + time series → Alerts, rows + status field →
  Task mgmt, etc.).

P2 — 'Forbidden' rules too vague (all 5 skills):
  Rewrites every Forbidden section as a paired 'Don't / Do' constraints
  table so each negative is paired with a concrete positive. Replaces
  obvious bans (lorem ipsum) with substantive ones (real-shaped mock
  copy, plausible identifiers, dev-team label hues, etc.).

* ci: register orbit skills in de/ru/fr en-fallback lists

The localized-content coverage test asserts that every skill in
skills/ is either translated or explicitly declared as falling back
to English in the LOCALIZED_CONTENT_IDS bundle. The 5 new orbit
skills weren't in any bundle, so the workspace validation job failed
on the de/ru/fr coverage assertions.

Adds the 5 orbit-* ids to DE/FR/RU_SKILL_IDS_WITH_EN_FALLBACK so
those locales explicitly fall back to the SKILL.md English copy
(matching the minimal-change posture chosen earlier in this PR).
2026-05-06 21:39:52 +08:00