mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
603 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
35900834be
|
feat(contracts): move shared CritiqueRoundSummary / CritiqueRunStatus into the contracts package (#1016)
The daemon's persistence layer was the source of truth for three pieces of public critique state: the CritiqueRoundSummary record, the terminal CritiqueRunStatus union, and the CRITIQUE_RUN_STATUSES enumeration that defines the canonical display order. None of those are daemon-private: the web layer (Phase 7 reducer + Theater UI) needs the same shapes to render run history, status filters, and the rerun affordance, and the root AGENTS.md requires shared web/daemon DTOs to live in packages/contracts so the two sides can't drift. Move all three to packages/contracts/src/critique.ts and have apps/daemon/src/critique/persistence.ts re-export them so existing daemon imports (orchestrator, server, run-registry, tests) keep working unchanged. The CHECK-constraint-aware ALL_VALID_STATUSES set that adds the in-flight 'running' value stays daemon-side because it is strictly a DB invariant, not a public surface. This unblocks the web layer's eventual rerun / history wiring without forcing it to either duplicate the shapes or import daemon source (which would violate the apps/web → apps/daemon boundary). Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
2f7fbd68d7
|
Update docs/assets/github-metrics.svg - [Skip GitHub Action] (#998)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
ed470bb6bd
|
docs(readme): refresh contributors wall (#1004)
Co-authored-by: mrcfps <23410977+mrcfps@users.noreply.github.com> |
||
|
|
8a8e33d31b
|
fix(web): hide stale upload error banner when previewing other files (#994)
The upload-failure banner in FileWorkspace renders unconditionally inside ws-body, so a failed upload's error message stayed pinned above any file the user opened next. The banner is information about the Design Files upload surface, not the active file viewer; once the user clicks an existing file in the list, the message becomes unrelated stale state. Scope the banner to activeTab === DESIGN_FILES_TAB so previewing any file hides it. The error stays in state, so returning to the Design Files tab brings it back: the failed upload is still unaddressed and we do not want to silently drop that signal entirely. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
afb331a288
|
feat: add opt-in Langfuse telemetry (#800)
* docs(specs): add langfuse telemetry change spec
Captures the design for forwarding completed agent runs to Langfuse,
including data-model mapping, field-budget caps, privacy gates,
build-secret injection, GDPR right-to-deletion approach, and the
resolved decisions on default consent, identifier shape, region, and
ownership.
* feat(daemon): add langfuse-trace module and telemetry prefs
Adds the dependency-free building blocks for forwarding completed
agent runs to Langfuse. Two layers:
- AppConfigPrefs gains installationId and a TelemetryPrefs object with
metrics / content / artifactManifest gates. The daemon validator
treats telemetry like agentModels — replace-on-write, drop-when-empty,
reject non-boolean inner values.
- New langfuse-trace.ts builds a {trace-create, generation-create}
pair from a ReportContext, capping prompt at 8 KB, output at 16 KB,
artifacts at 50 entries, and dropping any batch larger than 1 MB
before send. reportRunCompleted is no-op when LANGFUSE_PUBLIC_KEY /
LANGFUSE_SECRET_KEY are unset (so dev runs and forks never emit) and
short-circuits on prefs.metrics === false.
Server-side wiring into the run-close path lands in a follow-up.
* fix(langfuse): default to US Langfuse region
End-to-end smoke against the project's actual dev key on 2026-05-07
returned 401 from cloud.langfuse.com (EU) and 207 from
us.cloud.langfuse.com (US), confirming the org lives in US. Update the
default base URL, the matching test, and the spec's Q3 decision row to
match. Self-hosted or EU-region operators can still override via the
LANGFUSE_BASE_URL env var.
* feat(daemon): wire langfuse trace forwarding into run-close
Adds the daemon-side glue to forward completed agent runs:
- runs.ts gains an optional onTerminate hook fired once per run after it
reaches a terminal state. Errors thrown from the hook are caught and
logged, never propagated, so telemetry can never break the run path.
- New langfuse-bridge.ts assembles a ReportContext from the in-memory
run record, the conversation's persisted assistant message, and the
user's app-config preferences. It tolerates a missing message (e.g.
when web has not yet PUT the final delta) and a missing app-config.
- server.ts stashes the original user prompt on the run object inside
startChatRun so the bridge can include it without crossing the
createChatRunService boundary, and registers the hook callback when
building the run service.
Behavior remains a no-op unless LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY
are set in the daemon env AND telemetry.metrics is true in app-config.
A live smoke against us.cloud.langfuse.com on 2026-05-07 confirmed the
matching trace + generation schema is accepted (HTTP 207, both events
201 created).
* fix(langfuse): address PR #800 review feedback
P1 — Move trace forwarding off the daemon-internal run-close hook and
onto the message-persistence path. The original onTerminate hook ran
inside finish() the moment the SSE 'end' event was emitted, which is
*before* the web client's onDone handler refreshes project files and
PUTs producedFiles + final assistant content back to SQLite. Reading
SQLite at that moment routinely missed both. The fix: drop the runs.ts
hook entirely and trigger from PUT /api/projects/:id/conversations/:cid/
messages/:mid when the saved row carries a terminal runStatus. A
reportedRuns Set guards against the multiple PUT calls web makes per
turn (each retry / state update). Set entries auto-evict after the same
30 min TTL the runs map uses. Web persists a terminal-status message in
all three completion paths — onDone (succeeded), onError (failed), and
cancel (canceled) — so this catches every run shape.
P2 — postLangfuseBatch now parses the 207 Multi-Status response body.
Langfuse legacy ingestion always returns 207, and response.ok is true
for 207, so per-event validation errors used to slip through silently.
We now warn when body.errors is non-empty. Two new unit tests.
P2 — truncate() and the HARD_BATCH cap now compare UTF-8 byte length,
not String.length (which counts UTF-16 code units). A 4096-character
CJK prompt occupies 12 KB, well over the 8 KB input cap. truncate also
walks backwards to a UTF-8 leading byte so the cut never lands inside a
multi-byte codepoint. New unit test covers '设'.repeat(4096).
P2 — Spec R7 now lists the actual Langfuse trace deletion endpoint
(DELETE /api/public/traces/{traceId} for single, DELETE /api/public/traces
with body for batch). Verified by curl on us.cloud.langfuse.com:
DELETE /api/public/traces/X → 200; the path the original spec named
(POST /api/public/trace/X) returns 404. Reference link points at
langfuse.com/docs/administration/data-deletion.
P3 — Q4 (legacy ingestion vs OTel) moved from Open Questions to
Resolved Decisions. The implementation already commits to legacy and
the trade-off was discussed during design; the open-question status was
stale.
* feat(web): privacy consent surface + Settings → Privacy tab
Adds the user-facing half of the telemetry feature so the daemon-side
hook from PR #800 has something to talk to.
- AppConfig gains optional `installationId` (anonymous v4 uuid generated
on first opt-in; null after explicit decline; undefined when the user
has never seen the consent surface) and `telemetry: TelemetryConfig`
({metrics, content, artifactManifest}). syncConfigToDaemon round-trips
both fields so the bridge module sees the same prefs.
- SettingsDialog grows a Privacy section with two states. When the user
has never made a consent decision (typical first-run path), the
section renders the GDPR-aligned consent card: a kicker, the disclosure
body listing both metrics and conversation content as separate bullets,
and two equally-prominent buttons ("Share usage data" / "Don't share").
The Don't-share path keeps the app fully usable (core app must work
with all tracking declined). After a decision the same panel switches
to three independent toggles + the anonymous ID + a "Delete my data"
button that rotates the ID and turns everything off.
- App.tsx points the welcome modal at the new Privacy section so the
consent decision is the first thing a fresh installation sees.
- 17 i18n keys land in en + zh-CN + zh-TW with hand-translated copy,
and as English placeholders in the remaining 14 locales — enough for
the parity check to pass while leaving room for proper localisation
in a follow-up. Dict type updated.
- Minimal index.css for the consent card + toggle rows so the panel is
legible without depending on follow-up design polish.
Telemetry remains a no-op end-to-end until the user clicks Share usage
data: the daemon gate (prefs.metrics === true) keeps every code path
short-circuited otherwise.
* refactor(web): rebuild Privacy panel using project-native settings primitives
The first cut used custom .settings-privacy-* classes + raw HTML
checkboxes that didn't match any other Settings tab. Replace with the
shell other sections already use:
- settings-subsection containers with section-head + h4 + .hint
- seg-control / seg-btn pill toggles ("active" / "offline") for each of
the three telemetry preferences, mirroring NotificationsSection
- a 2-cell seg-control for the consent card so Share usage data and
Don't share carry identical visual weight (the GDPR equal-prominence
requirement that the previous accent / outline split missed)
- ghost button + readonly text input for the installation id row,
mirroring the API-key field pattern elsewhere
Drop the bespoke CSS block in favor of inheriting the existing
settings-section / seg-control / ghost styling. The only privacy-
specific style left is a tight definition list inside the consent card
for the metrics + content disclosure rows.
* refactor(web): use .toggle-row iOS switch for Privacy preferences
Active/offline pills (the seg-control single-cell pattern that
NotificationsSection uses) read awkwardly for a flat preference list.
Switch the three telemetry toggles to .toggle-row — the same control
NewProjectPanel uses for "speaker notes" / "animations": label + hint
on the left, iOS-style sliding switch on the right, full-row click
target. The consent card's two-button seg-control stays as-is — there
the equal-weight pill pair is exactly what GDPR equal-prominence wants.
* feat(web): standalone first-run privacy consent banner
Replaces the Settings-dialog-as-onboarding hack with a dedicated
bottom-right banner card that mounts whenever the user has never made
a privacy decision (cfg.installationId === undefined). The banner is
prominent (anchored to the corner with a soft shadow) but
non-blocking, mirrors cookie-consent UX, and shares the project's
panel styling — same .modal-elevated background, --radius-lg corners,
--shadow-lg lift.
Wiring:
- App.tsx imports PrivacyConsentModal and renders it at the root,
gated on installationId === undefined && !settingsOpen so it doesn't
double up with the Privacy tab's own consent card when Settings is
already showing.
- Share / Don't share both go through handleConfigPersist, so the
resulting installationId + telemetry prefs land in localStorage and
the daemon at the same time, reusing the existing autosave plumbing.
- The previous attempt that pinned the welcome SettingsDialog to the
Privacy section is reverted; onboarding now stays focused on agent
configuration, and the consent decision lives in its own surface.
* fix(web): keep privacy banner visible while Settings welcome modal is open
The banner gated itself on `!settingsOpen` to avoid double-rendering
with the Privacy tab's consent card. But the first-run path opens the
Settings welcome modal automatically when `onboardingCompleted=false`,
which fired immediately after bootstrap — so the banner flashed for a
moment and then vanished behind the modal backdrop.
Drop the `!settingsOpen` clause so the banner stays mounted whenever
the user has not yet made a privacy decision, and bump its z-index
above the modal backdrop (200 vs 100) so first-run users can actually
reach the consent buttons. The minor visual overlap with the Privacy
tab's own card is fine: clicking either copy resolves both surfaces.
* copy(privacy): soften consent button labels
Banner action buttons now read "Help improve Open Design" / "Not now"
(en, with hand translations in zh-CN / zh-TW and English placeholders
in the other 13 locales) instead of "Share usage data" / "Don't share".
The new wording aligns the affirmative action with the kicker copy
("Help us improve Open Design") and reads less alarming, while the
disclosure list above still names both data categories explicitly so
the consent stays informed under GDPR. The decline button stays as a
soft "Not now" rather than an aggressive "Don't share" so the reject
path doesn't read as hostile to the user.
No structural change — the two-cell seg-control still gives the buttons
identical visual weight, and the underlying side-effects are unchanged
(installationId is generated on Help / nulled on Not now, and the
telemetry prefs flip the same way).
* feat(telemetry): expand trace fields for evals & dataset construction
Each Langfuse trace now ships the full per-turn + per-install fact
sheet that the eval/dataset workflow needs, instead of only the bare
turn id + token count from before. Everything below is gated by
`prefs.metrics === true`; nothing here is content (those gates remain
separate).
Per-turn:
- model — first-class generation.model field, drives Langfuse cost
lookup and model-grouping in the UI; also mirrored in trace.metadata
and trace.tags so list-view filters work.
- reasoning — generation.modelParameters.{ reasoning } so the Model
Parameters card lights up; mirrored in metadata.
- skillId / designSystemId — metadata + tags, so dataset slices can
group by which skill/DS produced which output.
Per-process / build (constant within one daemon run, cached at start):
- appVersion / appChannel / packaged from app-version.ts
- nodeVersion (process.version), os (platform()), osRelease,
arch (os.arch())
- clientType — desktop vs web, derived from a new X-OD-Client header
the web layer sets in providers/daemon.ts (with a User-Agent sniff
fallback for third-party callers).
Plumbing:
- startChatRun stashes model / reasoning / skillId / designSystemId
on the run object alongside the existing userPrompt stash.
- POST /api/runs reads X-OD-Client and stores run.clientType.
- langfuse-bridge collects RuntimeInfo once per process and merges
per-run client carrier; ReportContext gains optional `turn` +
`runtime` blocks; existing fields stay backward compatible.
Spec gains a "Telemetry Fields Catalog" section enumerating every
field, its source, and the gate it lives under, so the eval team has a
single place to look up what's available without reading the trace
schema by example.
Tests:
- new langfuse-trace tests cover turn tags, runtime tags, generation
model/modelParameters promotion, modelParameters omission when
reasoning is unset, and metadata mirroring.
- langfuse-bridge gains an end-to-end "turn-level config" test that
threads model/reasoning/skill/DS/clientType + appVersion through
the bridge and asserts the Langfuse payload shape.
- existing tests adjusted to tolerate host-dependent os tag.
* copy(privacy): trim Share button to verb phrase only
"Help improve Open Design" overflowed the equal-width 2-cell
seg-control on the consent banner — the product name is already in
the kicker + headline above the buttons, so the button itself only
needs the verb phrase. Drop the product name from all locales:
- en: Help improve Open Design → Help improve
- zh-CN: 帮助改进 Open Design → 帮助改进
- zh-TW: 協助改進 Open Design → 協助改進
The decline button ("Not now" / "暂不" / "暫不") was already short, so
the two buttons now have comparable length and the equal-prominence
seg-control fits cleanly. Standalone Settings → Privacy panel uses
the same labels for consistency.
* fix(web): defer Settings welcome modal until privacy decision is made
Previously bootstrap raced two surfaces against each other on first
launch: the privacy consent banner (gated on installationId ===
undefined) and the Settings welcome modal (gated on
onboardingCompleted === false). The banner's higher z-index kept it
above the backdrop visually, but having two foreground surfaces at
once is still confusing UX.
Sequence them instead: bootstrap only opens the welcome modal when
the user has *already* resolved consent (installationId !== undefined).
Until then the banner owns the foreground alone. Once the user clicks
Help improve / Not now, the corresponding handler hands off to the
welcome modal if onboarding is still pending. End state matches what
it was before — just without the simultaneous-render flash.
* debug(privacy): log banner gate state to track sudden disappearance
Two console.log points to find which setCfg call (or stale bundle) is
flipping cfg.installationId from undefined to a value while the banner
is visible. To remove once the regression is reproduced.
* fix(privacy): keep installationId + telemetry out of localStorage
Daemon is now the single source of truth for the privacy decision.
Why this matters: the consent banner gates on
\`config.installationId === undefined\`, but loadConfig() merges
localStorage on top of the daemon's reply, so a stale uuid in
\`open-design:config\` (left over from a previous opt-in) was
re-hydrating the React state and immediately syncing back to the
daemon — defeating "Delete my data" and re-suppressing the banner
within milliseconds of every page load.
The deeper reason to fix it here, not just patch the gate: a privacy
identifier persisted in browser storage that the user can't see or
clear without DevTools is a compliance liability. Anything users can
revoke needs one canonical place to store it. Daemon \`app-config.json\`
already serves that role for everything else gated through
syncConfigToDaemon, so installationId + telemetry now ride that path
exclusively:
- saveConfig() strips both keys before writing localStorage.
- loadConfig() strips both keys when reading older stale payloads,
so existing installs migrate transparently on next launch.
- syncConfigToDaemon() / mergeDaemonConfig still round-trip them, so
the React state stays in sync with the daemon as before.
Net effect: clearing app-config.json (or hitting "Delete my data") now
fully resets the install identity, with no residual cohort key in
browser storage.
* feat(privacy): scrub secrets + PII from prompt/output before send
When prefs.content is on, daemon now runs the prompt and assistant
text through a regex scrubber (apps/daemon/src/redact.ts) before
posting to Langfuse. The scrubber is the simplest thing that gives
the user-facing copy a truthful claim — pure regex, zero new
dependencies, fully auditable in this Apache-2.0 repo (vs. pulling a
single-maintainer 5-month-old npm package into a core process).
Categories covered (each replaced with [REDACTED:<kind>]):
- Anthropic / OpenAI sk- keys (incl. proj/live/test/ant variants)
- Langfuse pk-lf- / sk-lf- (specific rule wins over generic sk-)
- GitHub gh[opsur]_ tokens
- AWS access key ids (AKIA + 16 uppercase)
- Google API keys (AIza + 35)
- Slack xox[abprs]- tokens
- Stripe live/test keys
- JWT header.payload.signature triples
- Bearer-header values (scheme word stays readable)
- Emails, IPv4, US-style phone numbers
- Credit cards — 13–19 digit runs that pass a Luhn check, so order ids
and unix-nanos timestamps that fail Luhn pass through unchanged
Not covered, stated openly in spec + i18n: names, postal addresses,
business-secret semantics, raw 40-hex tokens (too high a false-positive
cost for artifact slugs). Those would require an ML layer.
Wired in:
- apps/daemon/src/redact.ts — exports redactSecrets() +
redactSecretsWithCounts() helper for future audit-summary metadata.
- apps/daemon/src/langfuse-bridge.ts — runs both prompt and output
through redactSecrets() before they reach the trace builder.
- 18 unit tests cover every pattern plus negative cases (Luhn-failing
digit runs, out-of-range IPv4 octets, idempotence on re-redacted
text, ordinary prose passthrough).
- i18n privacyContentHint on en + zh-CN + zh-TW (plus 14 locale
placeholders) enumerates the categories so the consent disclosure
matches the implementation — the GDPR informed-consent requirement.
- spec gains a Pre-send Redaction subsection with the regex shape
table + intentional non-coverage list.
Drive-by: dropped the [privacy] debug logs that traced the now-fixed
bootstrap regression.
* fix(telemetry): make Langfuse reporting resilient
* feat(telemetry): nest Langfuse turn observations
* feat(telemetry): emit Langfuse tool spans
* fix(telemetry): report after finalized message writes
* fix(telemetry): honor persisted terminal status
* fix(web): let consent banner yield page clicks
* fix(telemetry): report current turn prompt only
|
||
|
|
f4eb1f1779
|
fix(web): hide the unsupported Save comment button on Pods selections (#993)
The board comment popover already short-circuits Save comment for selectionKind === 'pod' by setting disabled=true, so the button shows up greyed-out alongside Add note and Send to chat. That looks like a disabled-because-of-input-state action even though the operation is not supported on Pods at all, which is the same misleading "shown but not real" pattern issue #792 asked us to clean up. Drop the rendering entirely when selectionKind === 'pod' so the footer collapses to Add note + Send to chat (which are the actual Pods affordances) and the disabled prop only carries the draft-empty constraint for non-Pods selections. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
5c67651bfd
|
fix: make Azure api version optional (#941)
Co-authored-by: haocn-ops <259245673+haocn-ops@users.noreply.github.com> |
||
|
|
6c0de72826
|
fix(daemon): write SSE events atomically in createSseResponse.send (#972)
Three separate res.write() calls per event (id, event, data) split into three TCP chunks on localhost. Consumers that read chunk-by-chunk and look for the `event: <type>` marker can return before the matching `data:` payload arrives in the next chunk, producing partial events. Combine the three writes into one so each SSE event lands as a single TCP frame for sub-MTU events. Fixes the four chat-route SSE tests (`surfaces Qoder assistant error records...`, `fails Qoder runs when the result reports is_error...`, `fails stalled json-stream runs after the inactivity timeout elapses`, `marks stalled runs failed even when the child ignores SIGTERM`) which were racing against the chunk split in the readSseUntil test helper. Daemon test suite: 86/86 pass after the change. |
||
|
|
92d558d570
|
fix: reset inactivity watchdog on raw stdout bytes, not just parsed events (#976)
Structured stream handlers (Codex item.completed, pi-rpc, ACP, and
json-event-stream) only call noteAgentActivity() on parsed JSON-line
events. During long model reasoning or buffered artifact generation
(generating a full landing page HTML), the stdout pipe can stay silent
for >120s even though the child process is working. The result is a
false 'Agent stalled without emitting any new output' error.
Add a universal child.stdout.on('data', noteAgentActivity) listener
before any format-specific stream dispatch so every raw byte resets
the inactivity watchdog. Covers all seven stream formats: plain,
claude-stream-json, qoder-stream-json, copilot-stream-json,
json-event-stream, pi-rpc, and acp-json-rpc.
|
||
|
|
01851aa32a
|
feat(design-systems): add Slack design system (#899)
* feat(design-systems): add Slack design system * fix(i18n): cover slack design system in de/ru/fr fallback lists The localized-content coverage test scans design-systems/*/DESIGN.md and asserts every id appears in each locale's designSystems list. Adding the slack design system without a locale fallback entry breaks de/ru/fr CI. --------- Co-authored-by: lefarcen <935902669@qq.com> |
||
|
|
0d66643f33
|
Add Cisco and Webex design systems (#991)
* Add Cisco and Webex design systems * Address review feedback for Cisco and Webex systems --------- Co-authored-by: Rahul Jain <rajain5@cisco.com> |
||
|
|
0b039777b9
|
fix/Bug#772-DesignFilesSortButton-DesignFilesTableNowSortableColumns (#804)
* Disclaimer: Changes made using OpenCode with Big Pickle, an AI code assistant.
1. Removed the ↑ button from DesignFilesPanel.tsx (was a "back" button that
closed the preview pane — confusingly placed in the header as if it were for
sorting).
2. Converted the file list to a single <table> with sortable columns:
- Replaced the section-based grouping (Pages, Sketches, Scripts, Images,
Other) with a flat, sortable table
- Added column headers: Name, Kind, Modified — all clickable to toggle
sort direction
- Default sort is by modified time descending (same as previous behavior)
- Sort indicator arrows (↑/↓) show the active sort column and direction
- Live artifacts remain as a separate section above the table
3. Added i18n keys designFiles.colName, designFiles.colKind, designFiles.
colModified to all 17 locale files and the type definitions.
4. Updated CSS with table layout styles (.df-table, .df-file-row, column
width classes, sortable header styles).
Files modified:
- apps/web/src/components/DesignFilesPanel.tsx
- apps/web/src/index.css
- apps/web/src/i18n/types.ts
- apps/web/src/i18n/locales/en.ts
- apps/web/src/i18n/locales/*.ts (all 16 other locale files)
* Updated to preserve keyboard access to sorting
* Fixed keyboard to focus/activate/launch file from Design Files list. Single space bar will show preview, double spare bar will open the file as a tab
* Top pagination bar (above the table):
- "Show" dropdown with options 15, 30 (default), 45, 60, All
- Page range indicator (1–20 of 45)
- Previous / Next buttons
Bottom pagination bar (below the table):
- Previous / Next buttons
- "Go to page" dropdown listing all page numbers
- Same page range indicator
Implementation details:
- All controls use native <select> and <button> elements — fully keyboard
accessible (Tab, arrow keys, Enter/Space)
- Page resets to 0 when page size changes
- safePage clamps to valid bounds when file count changes (e.g. after delete)
- "All" sets page size to total file count (effectively one page)
- Prev/Next buttons show disabled state at boundaries with reduced opacity
* All 46 test files, all 385 tests pass. Here's what the regression test covers:
┌────────────────────┬──────────────────────────────────────────────────────┐
│Test │What it verifies │
├────────────────────┼──────────────────────────────────────────────────────┤
│default page size │500 files → only 30 .df-file-row elements in DOM │
├────────────────────┼──────────────────────────────────────────────────────┤
│page size All │changing per-page to "All" shows all 500 rows │
├────────────────────┼──────────────────────────────────────────────────────┤
│page size 60 │changing to 60 shows 60 rows │
├────────────────────┼──────────────────────────────────────────────────────┤
│Next navigation │clicking Next advances page and shows file-31 (sorted │
│ │by mtime desc) │
├────────────────────┼──────────────────────────────────────────────────────┤
│Prev/Next disabled │Prev disabled on page 0, Next disabled on last page │
│states │ │
├────────────────────┼──────────────────────────────────────────────────────┤
│jump to page │bottom dropdown jumps to page 3 (shows file-91) │
├────────────────────┼──────────────────────────────────────────────────────┤
│page info text │1–30 of 500 → after Next → 31–60 of 500 │
├────────────────────┼──────────────────────────────────────────────────────┤
│render time │renders 500 files in under 2s │
└────────────────────┴──────────────────────────────────────────────────────┘
* Fixed i18n for DesignFiles, and Fixed DesignFilesPanel Test
* Fixed - P3 — .df-thead rule defined but never applied
* Fixed keyboard use for file navigation, focus and button usage
* Fix i18n for x of y in design files pagination
* Fixed SafePage clamping
* Fixed dupe file total count
* Fixed x of y i18n
* Fixed DeleteSelected i18n and missing from Test
* fix effective pagesize issue, and change duplicate file kind to a filesize
* Readded page/everything selection and i18n
* Fixed i18n issues
* Resolved indonesian i18n issue with cloudflare keys
* Fixed unrelated cloudflare i18n issues as requested in Pull Request by reviewer
* Fix e2e test: click filename button instead of row for preview
The DesignFilesPanel was refactored from <button> rows to a <tr> with
a nested <button> for the filename. The e2e test was still clicking
the <tr> which has no onClick handler, so the preview never appeared.
* Remove duplicate formatSize helper, reuse humanBytes instead
|
||
|
|
644a7daf2d
|
fix(web): bump deploy modal footer top padding so primary action breathes (#992)
Issue #913: in the deploy modal, the primary action button sits flush against the divider that separates the dialog body from the footer, which reads as visually cramped because the form above already pushes content right up to that border. The shared .modal-foot rule uses 12px vertical padding which works for shorter dialogs (sketch close, template save) but feels tight in the deploy flow where the body ends in dense token / domain config rows. Add a deploy-modal-scoped override that nudges the footer top padding to 18px so the primary button has the same breathing room as the content's column gap. Bottom padding stays at the shared 12px so the overall modal height does not jump. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
a7ac2a81b5
|
fix(web): scroll the active workspace tab into view when the strip overflows (#990)
Issue #775: when many files are open the tab strip scrolls horizontally and the active tab can sit fully off-screen, so users lose track of what they have selected. Design Files is already sticky-pinned at the left edge from PR #842, but every other tab still vanished once the overflow grew past the viewport. Add a small effect keyed on activeTab that finds the .ws-tab.active element inside the existing tabsBarRef and calls scrollIntoView with { block: 'nearest', inline: 'nearest' }. That makes opening a tab via Cmd+P, clicking a chat file chip, or activating from the design panel guarantee the tab is visible without yanking the user around when it already was. The Design Files tab is excluded because the sticky CSS already keeps it visible. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
5300585845
|
Fix Image template creations not automatically executing the selected prompt (#752) | ||
|
|
84e1bb3b1a
|
fix(web): confirm before closing a dirty sketch so unsaved strokes are not lost (#988)
Issue #139: clicking Close in the sketch editor immediately discarded unsaved strokes with no confirmation, even though the toolbar already tracked dirty state and surfaced the bullet indicator. Wrap the existing onCancel in a window.confirm() guard, gated on the dirty flag so a clean sketch still closes in one click. Same pattern the codebase uses for conversation/design/file delete and the media-provider Clear button. i18n: new key 'sketch.closeConfirm' translated across all 17 locales. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
3c94fa575c
|
feat(daemon): expose gemini-3 preview models and 2.5-flash-lite in the Gemini picker (#986)
* feat(daemon): expose gemini-3 preview models and 2.5-flash-lite in the Gemini fallback list Issue #981: the Gemini agent picker only offered 2.5 Pro / 2.5 Flash even though the Gemini CLI ships gemini-3-pro-preview, gemini-3-flash-preview, and gemini-2.5-flash-lite. Users had to fall back to "Default" or type the id by hand to reach the newer generation. Add the three model ids to fallbackModels with the same shape as the existing entries so the picker surfaces them inline. * test(daemon): pin Gemini picker fallbackModels order mrcfps flagged on PR #986 that the new fallback ids only had implicit coverage via the static array, which means a future reorder/removal in AGENT_DEFS could silently reshape the Settings UI without tripping CI. Mirror the codex/qoder regression-test pattern so the Gemini picker contents stay locked to the documented priority order: default, gemini-3-pro-preview, gemini-3-flash-preview, gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite. This is especially worth covering for Gemini because the CLI accepts arbitrary custom ids, so a regression in the picker would not surface during manual QA. --------- Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
72fd9a73a2
|
fix(web): keep chat auto-scroll glued to bottom across streaming chunks (#989)
Issue #983: when an assistant turn streamed in, the chat log stopped auto-scrolling even though the user was sitting at the bottom of the conversation. Root cause: the auto-scroll effect re-measured `scrollHeight - scrollTop - clientHeight` AFTER the new chunk had already grown the element. A single tool-use card or markdown render adds 100+ px in one tick, so the post-content distance check (`< 80`) skipped the scroll exactly when the user expected it most. Switch the gate to the existing `scrolledFromBottom` state. That flag is maintained by the user-driven scroll listener (only flipped by a real scroll event, not by content growth), so it carries the user's pre-content intent through to the effect. New content auto-scrolls when the user was glued to the bottom; scrollback sessions still preserve their position. Existing chat-scroll-preservation tests still pass (6/6); the prior- state behavior we test there is bottom-pinned vs absolute-restore on tab switch, which this change does not affect. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
34d781b364
|
fix(web): differentiate selected, hover, and focus states in the language switcher (#987)
Issue #628: hover and selected rows in Settings → Language used the same --bg-subtle background, so the currently-selected language was hard to distinguish from a row being hovered, and keyboard focus had no separate treatment at all. - Selected row gets --accent-tint background + accent text + medium weight so it reads as the active choice even when nothing is hovered. - Selected + hover lifts to --accent-soft so the cursor still gives feedback over the active row. - :focus-visible adds a 2px accent outline (inset) so keyboard users see exactly where they are while scrolling the list. CSS-only change; the existing .active class wiring in the React component already drives this. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
ee3ca5f4f0
|
refine typography-hierarchy craft docs — clarify edge cases and make lint measurable (#979)
* add typography-hierarchy and typography-hierarchy-editorial craft rules
Adds two layered craft files extending typography.md:
- typography-hierarchy.md: core hierarchy contract, vectors, failure modes,
controlled violations, and lint checklist
- typography-hierarchy-editorial.md: editorial pacing, dramatic scale jumps,
whitespace hierarchy, display tracking overrides, and editorial-specific lint
Both files are registered in craft/README.md with guidance on when to require them.
Includes a new editorial stack example showing the layered opt-in pattern.
Validation:
- pnpm guard: PASSED
- Universal craft knowledge (not brand-specific)
- Stable slugs: typography-hierarchy, typography-hierarchy-editorial
- No new dependencies or breaking changes
Passes craft additions lane per code-review-guidelines.md.
* wire typography base into editorial skills craft stack
All three editorial skills now require the complete layered stack:
[typography, typography-hierarchy, typography-hierarchy-editorial, rtl-and-bidi]
The new hierarchy files (typography-hierarchy.md, typography-hierarchy-editorial.md)
explicitly extend typography.md and depend on its base contract (scale ranges,
tracking values, line-height guidance, weight system). Without typography in
requires[], the hierarchy rules arrive at runtime without their foundational
contracts, making them incomplete.
Skills updated:
- skills/blog-post/SKILL.md
- skills/docs-page/SKILL.md
- skills/digital-eguide/SKILL.md
This completes the craft injection for the editorial stack as documented in
craft/README.md and ensures both base typography and hierarchy extensions load
together at runtime.
* refine craft docs for clarity and edge cases
Address P2/P3 reviewer feedback:
- typography-hierarchy-editorial.md §4: Add script-aware exception for Arabic/Hebrew/Persian
tracking (cursive joining breaks with negative letter-spacing; use scale/space instead)
- typography-hierarchy.md Controlled violations: Add concrete safeguards for 'information
flow remains intact' (reading order, proximity grouping, single primary, quick scanability)
- typography-hierarchy-editorial.md §2 Whitespace: Qualify 'no divider' rule to allow
separators for publication identity, not just space alone
- typography-hierarchy-editorial.md Anti-patterns: Add docs-page carve-out for 'UI chrome'
(functional controls in code/API blocks are OK; decorative badges belong outside measure)
- typography-hierarchy-editorial.md Lint: Make checks measurable — display/deck ratio >=1.5x,
section spacing ratio rules (one gap >=1.5x baseline, another <=1.2x), separator identity
check, and explicit guidance markers for auditability
* address P1/P2/P3 reviewer feedback: RTL conflicts, script accuracy, tracking scope, measurability
P1 — RTL physical-direction conflicts:
- typography-hierarchy-editorial.md: Change 'ragged right' to 'text-align: start with
ragged inline-end' for RTL compatibility
- Separators in RTL: Add note on logical directions (inline-start/inline-end)
- skills/digital-eguide/SKILL.md: Replace 'cover left, spread right' with
'inline-start/inline-end' and 'pinned right-side' with 'pinned to inline-end'
P2 — Script-group accuracy:
- Split script groups correctly: Arabic/Persian/Urdu cursive-joining (letter-spacing 0);
Hebrew is RTL but not cursive-joining
P2 — Tracking exception scope:
- Extend script exception to pull-quote tracking row (was only on display)
- Mark both as Latin-only with joining-script carve-out
P2 — Primary-count inconsistency:
- Clarify 'one at a time per visual region' with explicit long-form pacing resets note
P3 — Measurable lint:
- Replace vague 'meaningfully larger' with >=1.5x baseline rhythm OR one token scale step
P3 — Orphaned reference:
- Fix cross-ref to 'typography.md §letter-spacing' (was §display tracking)
P3 — Markdown typo:
- Fix backtick nesting in '1.6–1.7' line-height range
* fix: RTL logical 'ragged' wording; make digital-eguide pull-quote script-aware; align section-separators lint with prose
* fix(P2): include Persian/Urdu in pull-quote script exception; remove 'space' from section-separators lint
* fix: align pull-quote bullet indentation in digital-eguide SKILL.md
* fix: align digital-eguide pull-quote bullet nesting
* fix: use logical pull-quote alignment wording in editorial craft
|
||
|
|
83ddf7609c
|
fix(web): correct srcdoc injection and deck bridge for JS strings con… (#938)
* fix(web): correct srcdoc injection and deck bridge for JS strings containing HTML tags
- Use indexOf for </head> (real tag precedes JS string occurrences)
- Use lastIndexOf for </body> (real tag follows JS string occurrences)
- Scope deck bridge slide selector to direct children of deck containers
to avoid counting cloned overview thumbnails
- Update .slide-number data attributes and .progress-bar width from
deck bridge's updateDeckChrome so page counter and progress track
with deck navigation
* fix(web): move deck chrome sync into report() for all slide modes
.slide-number and .progress-bar updates were only in updateDeckChrome()
which is called exclusively from setActive(). Scroll decks go through
scrollGo() → report() without ever calling setActive(), so their page
counter and progress bar stayed frozen.
Move the sync logic into report() which is the convergence point for
all navigation paths (class-toggle, scroll, keyboard-dispatch).
* fix(web): use DOMParser for srcdoc injection instead of string matching
Replace all regex/indexOf/lastIndexOf based </head> and </body> matching
with DOM-based injection via domMutate helper. DOMParser correctly
separates raw-text element content from structural markup, so </head>/
</body> inside JavaScript strings no longer hijack injection points.
Also: scope deck bridge slide selector to direct children of deck
containers, and move .slide-number/.progress-bar sync into report()
so all navigation paths update page chrome.
* fix(web): add robust string fallback for srcdoc injection in non-browser environments
When DOMParser is unavailable (Node tests), fall back to structural
boundary matching: lastIndexOf('</head>', before <body) and
lastIndexOf('</body>', before </html>) to skip literal occurrences
inside script/style blocks. Annotate catch blocks.
---------
Co-authored-by: yangjingting <yangjingting@yxqiche.com>
|
||
|
|
34b5b85614
|
docs(deploy): document Colima build swap helper (#967)
* docs(deploy): document Colima build swap helper Explain when Apple Silicon Colima users should prepare temporary VM swap before manual image publishing, and cover the host guard with a focused test. * fix(deploy): harden Colima swap helper Address review feedback by validating swap overrides, passing remote shell values safely, preserving configured fallback sizes, and expanding behavior coverage. |
||
|
|
ebe3513ed4
|
add typography-hierarchy and typography-hierarchy-editorial craft rules (#975)
* add typography-hierarchy and typography-hierarchy-editorial craft rules Adds two layered craft files extending typography.md: - typography-hierarchy.md: core hierarchy contract, vectors, failure modes, controlled violations, and lint checklist - typography-hierarchy-editorial.md: editorial pacing, dramatic scale jumps, whitespace hierarchy, display tracking overrides, and editorial-specific lint Both files are registered in craft/README.md with guidance on when to require them. Includes a new editorial stack example showing the layered opt-in pattern. Validation: - pnpm guard: PASSED - Universal craft knowledge (not brand-specific) - Stable slugs: typography-hierarchy, typography-hierarchy-editorial - No new dependencies or breaking changes Passes craft additions lane per code-review-guidelines.md. * wire typography base into editorial skills craft stack All three editorial skills now require the complete layered stack: [typography, typography-hierarchy, typography-hierarchy-editorial, rtl-and-bidi] The new hierarchy files (typography-hierarchy.md, typography-hierarchy-editorial.md) explicitly extend typography.md and depend on its base contract (scale ranges, tracking values, line-height guidance, weight system). Without typography in requires[], the hierarchy rules arrive at runtime without their foundational contracts, making them incomplete. Skills updated: - skills/blog-post/SKILL.md - skills/docs-page/SKILL.md - skills/digital-eguide/SKILL.md This completes the craft injection for the editorial stack as documented in craft/README.md and ensures both base typography and hierarchy extensions load together at runtime. |
||
|
|
362b92c1a6
|
fix(packaged): swallow harmless setTypeOfService EINVAL from undici (#895) (#906)
* fix(packaged): swallow harmless setTypeOfService EINVAL from undici (#895) Opening Settings → Pets → Community in the packaged desktop app surfaced a native "JavaScript error in main process" dialog with `Uncaught Exception: Error: setTypeOfService EINVAL`. Root cause: undici's socket setup tries to set the IP_TOS byte for QoS / DSCP marking on outbound sockets, and the macOS kernel refuses with EINVAL on certain configurations (VPNs, IPv6-only sockets, some firewall postures). The byte is purely advisory — the socket itself is healthy and serves traffic — so the rejection should not crash the app. Two cooperating layers: 1. **`protocol.ts`** registers the `od://` scheme that backs every renderer page load and API call in the packaged build by forwarding through Node's global `fetch` (which is undici under the hood). Pulled the inner request handler out as `handleOdRequest()` so a test can drive it with a stub fetch, and wrapped the `await fetch()` in a try/catch that returns a 502 Response on failure. Without this, every undici rejection — not just `setTypeOfService` — propagated to Electron's default uncaught-exception path. Now the renderer sees a normal error response and the main process keeps running. 2. **`logging.ts`** adds a defensive `process.on('uncaughtException')` handler with a narrow filter, `isHarmlessSocketOptionError`, that only matches the canonical undici shape (message contains `setTypeOfService` AND code is `EINVAL` or message contains `EINVAL`). For any unrecognised error the handler re-throws via `setImmediate` so Node's default crash + Electron's crash dialog still fire end-to-end — a future regression that broadens the filter to "every EINVAL" is caught by the unit tests below. Tests: 13 new tests across `tests/protocol.test.ts` (5) and `tests/logging.test.ts` (8) pin both layers — including the explicit #895 regression case (fetch rejecting with the canonical EINVAL shape returns a 502 instead of throwing) and the negative guard against the filter swallowing real bugs (a generic write EINVAL or a setTypeOfService EACCES is *not* matched). Verified locally: - `pnpm --filter @open-design/packaged vitest tests/protocol.test.ts tests/logging.test.ts` → 13/13 - packaged `tsconfig.json` and `tsconfig.tests.json` (the CI killer): both clean - the one pre-existing failure in `tests/sidecars.test.ts` (`adds custom VP_HOME/bin …`) is independent of this PR — confirmed by stashing the change and re-running * fix(packaged): break recursive rethrow + tighten EINVAL filter (#906 review) @mrcfps and @lefarcen both flagged a real P1 in the first iteration: the non-harmless branch of the new uncaughtException handler rethrew via setImmediate while the same listener was still registered, so a real bug would re-enter the handler indefinitely instead of terminating. mrcfps reproduced the loop with a minimal Node script. lefarcen also flagged that the filter trusted the message string over a contradicting structured `code`. Both fixes: 1. **Recursive rethrow (P1).** Extract the handler as a named factory, `createFatalUncaughtExceptionHandler(logger)`, that captures itself in closure. On non-harmless errors the handler now `process.removeListener('uncaughtException', self)` before scheduling the rethrow. With no listener registered, the next throw lands in Node's default crash path — which is exactly what we wanted ("preserve fail-fast for real bugs"). 2. **`code` is authoritative (P2).** When `code` is present on the error, only `code === 'EINVAL'` qualifies. A contradicting `EACCES`/`EPERM` paired with `setTypeOfService EINVAL` in the message now slips through to the crash path instead of being swallowed. Message-based detection only fires when `code` is genuinely absent (some libuv builds don't populate it on raw thrown Errors). 3 new tests pin both fixes: - `does NOT match when code contradicts the message` and the EPERM variant guard against the P2 regression. - `removes itself from uncaughtException listeners before scheduling the rethrow` uses `vi.spyOn(process, 'removeListener')` and a stubbed setImmediate to assert the call order: removeListener fires before setImmediate schedules the throw. - `does NOT re-enter itself when invoked twice` is a belt-and-suspenders loop guard — even if a future refactor dropped the removeListener call, the test would catch runaway scheduling. Verified locally: - packaged vitest: 18/18 (was 13, +3 new tests; +2 negative-guard tests for the P2 filter; -0 deletions) - packaged tsc -p tsconfig.json --noEmit: clean - packaged tsc -p tsconfig.tests.json --noEmit (the CI killer): clean |
||
|
|
32c36f44a7
|
fix(i18n): rename live artifact tab label in zh-CN and zh-TW (#969)
* fix(i18n): rename live artifact tab label in zh-CN and zh-TW Remove redundant '新建/新建' prefix from live artifact tab and title labels to match other tab naming convention (e.g., '原型', '幻灯片'). - zh-CN: '新建实时制品' → '实时制品' - zh-TW: '新建即時成品' → '即時成品' * fix(i18n): revert titleLiveArtifact to original value Only tab label should be renamed; title should keep '新建' prefix to match other title labels (新建原型, 新建幻灯片, etc.). |
||
|
|
f50c34fcd4
|
fix: ensure Settings close button is always clickable (#971)
- Increase .settings-chrome z-index from 3 to 10 to ensure it stays above all modal content - Remove pointer-events: none from .settings-chrome container to avoid click interception issues - .settings-autosave already has pointer-events: none, so it won't block clicks The close button was becoming unclickable after PR #681 introduced the new Settings layout with Orbit summaries. The issue was likely caused by either z-index stacking or the pointer-events pattern not working reliably in all scenarios. Fixes #905 |
||
|
|
b578b93f3f
|
Bug FIx: Media generation task state is volatile and lost on daemon restart #648 (#884)
* feat: implement media tasks persistence
* fix(daemon): satisfy exactOptionalPropertyTypes in media-tasks-routes test
`process.env.OD_DATA_DIR` is `string | undefined`, but `openDatabase`'s
options accept `{ dataDir?: string }`. Under the daemon tsconfig's
exactOptionalPropertyTypes the two are not assignable. Spread the key
in only when defined.
* fix(daemon): restore mcp-config / mcp-oauth / mcp-tokens imports in server.ts
The earlier 'Merge branch main into main' resolved the import-block
conflict by keeping only the new media-tasks import and dropping the
three pre-existing import blocks. server.ts still uses 13+ symbols
from those modules (PendingAuthCache, MCP_TEMPLATES, beginAuth,
readMcpConfig, getToken, etc.), so the daemon crashed at startup with
'ReferenceError: PendingAuthCache is not defined' the moment Playwright
booted it. Restore the three import blocks verbatim from main.
---------
Co-authored-by: lefarcen <935902669@qq.com>
|
||
|
|
05461c64fd
|
fix(connectors): show stable curated tool count in connector card badge (#748) (#767)
* fix(connectors): show stable curated tool count in connector card badge (#748)
The connector card's "N tools" badge in `apps/web/src/components/
EntryView.tsx` rendered `connector.tools.length` for both pre- and
post-Composio-hydration states, so the displayed count lurched
without explanation:
- Before configuring a Composio API key, GitHub showed "2 tools"
(the static fallback catalog at `composio.ts:21-53`).
- After hydration, the daemon merged in the full Composio provider
inventory (`composio.ts:725-778`, ~868 GitHub tools), and the
same badge jumped to "868 tools".
Same field name, two different concepts — `connector.tools` is
"everything the provider ships" while the agent-callable subset
is `definition.allowedToolNames` (read-only auto-approval tools
that pass `isAgentPreviewListableTool()`).
Fix: surface `allowedToolNames` on the wire `ConnectorDetail` and
have the badge use that count instead. The detail drawer below
the card still iterates over `connector.tools` to enumerate the
full inventory — the count and the list are intentionally
different surfaces.
The badge now stays close to "tools the agent can actually
invoke" (≈2-30 for GitHub, depending on auto-allowed read tools)
instead of the raw provider inventory (~868). No 800x jump on
hydration.
Wire format change:
- packages/contracts/src/api/connectors.ts: add
`allowedToolNames: string[]` to ConnectorDetail
- apps/daemon/src/connectors/catalog.ts: same field on the
daemon-internal type, populated in `connectorDefinitionToDetail()`
as a defensive copy of `definition.allowedToolNames`
- packages/contracts/src/examples.ts: extend the example fixture
- apps/web/src/components/EntryView.tsx: badge call sites switch
to `connector.allowedToolNames?.length ?? connector.tools.length`
(the `??` keeps the badge alive against any older daemon build
that hasn't shipped the field yet)
Tests:
- 4 new daemon tests in connectors-service.test.ts pin the
contract: getConnector() emits the field, the array is a
defensive copy, and the #748 regression guard simulates the
Composio post-hydration shape (tools.length=801,
allowedToolNames.length=1) to prove the badge invariant
- web EntryView.test.ts fixtures updated to satisfy the new
required field
Verified locally:
- daemon vitest: 925/925
- web vitest: 332/332
- daemon/web/contracts typecheck clean
- i18n-check passes
- Live `/api/connectors/discovery` returns the new field; pre-
hydration GitHub/Notion/Google Drive badges all read "2 tools"
(no regression vs before this change)
Fixes #748
* fix(connectors): split drawer badge vs inventory counts; fix daemon test typecheck (#767 review)
Two follow-ups for @mrcfps's review on PR #767.
1) **P1: Drawer empty-state regression.** The previous commit reused
the curated `toolCount` for both the header badge AND the inventory
section's loading gate / empty-state branch / section count. The
inventory section renders `connector.tools` directly, so a hydrated
connector with raw provider tools but an empty allowlist (e.g. a
write-only Composio surface) would render "no tools available" and
hide the actual inventory list — exactly the contradiction my own
PR description warned against.
Fix splits the two surfaces:
- `badgeToolCount` (curated, via the new exported helper
`getConnectorBadgeToolCount`) feeds the card and drawer header
badge — the summary count, where the #748 stability matters.
- `inventoryToolCount = connector.tools.length` (inline) drives the
drawer's loading gate, section count, and empty-state branch —
the surfaces describing the actual rendered list.
The card has no inventory section so it stays on the badge helper
unchanged.
2) **CI: daemon test typecheck failed.** The connectors-service test's
`provisionedTools[0].safety` index access tripped daemon
`tsconfig.tests.json`'s `noUncheckedIndexedAccess` strict setting,
even though my local `tsc -p tsconfig.json --noEmit` was clean —
that config is a separate compilation. Bind through a defined-checked
local before reading `.safety`, per @mrcfps's exact suggestion.
Tests:
- 4 new web tests in `EntryView.test.ts` pin the
`getConnectorBadgeToolCount` contract, including the explicit
regression: a connector with `allowedToolNames=[]` and
`tools.length=800` returns badge=0 but the inventory length
stays at 800 — the drawer's empty-state branch must use the
inventory count, never the badge count.
- Existing daemon test fixed without losing assertion coverage.
Verified locally:
- daemon vitest: 921/921
- web vitest: 336/336 (was 332, +4)
- daemon `tsc -p tsconfig.json` and `tsc -p tsconfig.tests.json` (the CI killer): both clean
- web `tsc -b --noEmit` clean
- i18n-check passes
Process learning baked into this PR: from now on I'll always run the
`tsconfig.tests.json` separately before pushing, since the workspace
typecheck script chains both and the second one is what CI fails on.
* fix(connectors): pin badge to curated catalog count, not the dynamic execution allowlist (#767 review v2)
@lefarcen and @mrcfps both flagged that the previous iteration of
this PR (commit
|
||
|
|
109722de3a
|
feat(desktop): export artifacts directly to PDF (#532)
* feat(desktop): export artifacts directly to PDF * fix(desktop): PDF 내보내기 기본 여백 제거 |
||
|
|
0b757c2452
|
feat(skills): add clinical-case-report skill (#581)
* feat(skills): add clinical-case-report skill
Adds a new healthcare skill for generating structured medical case
presentations (SOAP format, conference, and ward rounds).
Files added:
- SKILL.md — od: frontmatter + full agent workflow instructions
- references/checklist.md — P0/P1/P2 medical accuracy validation
- references/case-formats.md — SOAP, conference, and rounds formats
- examples/example-stemi.html — inferior STEMI with cardiogenic shock
Fills the healthcare vertical gap in the current skill catalog.
Includes physiologically consistent vitals, labs, and an
evidence-based management plan using real clinical guidelines.
* fix: address review feedback from lefarcen and mrcfps
- Add prescribing safety gate (Step 5) — warns about missing allergy,
renal, weight, and pregnancy context before drug recommendations
- Soften physiologic rules from 'must follow' to 'typical patterns' —
acknowledges afebrile pneumonia, beta-blocker-blunted shock, etc.
- Preserve user-provided values even if atypical for the diagnosis
- Remove incorrect TIMI 0-7 score (UA/NSTEMI scale) from STEMI example;
retain Killip Class III and Shock Index 1.27
- Fix troponin units: hs-troponin reported as 2400 ng/L (ref <40 ng/L)
instead of conventional 2.4 ng/mL
- Add table accessibility: <caption> and scope='col' on vital signs
and laboratory results tables
- Expand PHI checklist item to cover indirect identifiers (MRNs, dates,
locations, rare conditions, occupation, verbatim stories)
- Disambiguate format selection guide ('ward round' maps to Brief Rounds,
'formal rounds' maps to SOAP)
- Add example.html at skill root for /api/skills/:id/example resolver
* i18n: add clinical-case-report to DE/FR/RU skill fallback lists
* fix: soften checklist P0 vital signs rule to allow clinical variability
* fix: add medication safety checks block before antiplatelet section in examples
* fix: correct eGFR/age in safety block, add prescribing-safety P0 checklist items
* fix: correct age 67 to 58 in pregnancy line of safety block
* fix: defer norepinephrine dose to local protocol until weight confirmed
* fix: wire reference files into workflow; defer beta-blocker until shock resolved
* fix: close html code fence before Step 7 so checklist gate renders as prose
* fix: restrict oxygen to hypoxaemia only; generalise social history for de-identification
* fix: format-conditional P0 HPI gates; Killip III->IV for cardiogenic shock; smoking status consistent
* fix: make Step 2 and 'What you will produce' format-conditional for Brief Rounds
* fix: remove occupation detail from social history to comply with P0 de-identification rule
* fix: add 'ward rounds' plural to Brief Rounds format-selection table
* fix: gate Step 1 clarification on format; accept Killip+Shock Index as ACS risk scores
|
||
|
|
727936ecb7
|
fix: remove redundant Connect GitHub placeholder from import menu (#964)
GitHub connection is already available through the Connectors tab. The disabled 'Connect GitHub' item in the chat composer import menu was a redundant placeholder that created confusion about duplicate entry points. This removes the placeholder to streamline the UI and direct users to the primary GitHub connection flow via Connectors. Fixes #777 |
||
|
|
e3423c2b7b
|
feat: add draggable file tab reordering (#936) | ||
|
|
50b72feffd
|
docs: add Docker setup instructions to QUICKSTART.md and CONTRIBUTING.md and README.md (#935)
* feat(docs): add docker setup guide * feat(docs): add docker setup guide to README * feat(docs): add docker setup guide to CONTRIBUTING.md |
||
|
|
0701dee9b9
|
fix(desktop): allow od:// URLs in setWindowOpenHandler so live artifact previews open in child window (#911) (#933)
The Orbit panel's "Open artifact" button is an
`<a target="_blank" href="/api/live-artifacts/.../preview?projectId=...">`.
In packaged Electron builds the renderer is loaded from
`od://app/`, so the relative `href` resolves to
`od://app/api/live-artifacts/.../preview?projectId=...` by the
time `setWindowOpenHandler` sees it.
The existing handler in `apps/desktop/src/main/runtime.ts:300`
runs three checks in order:
1. `isAllowedChildWindowUrl(url)` → only matched `blob:`
2. `isHttpUrl(url)` → only matched `http:` / `https:`
3. fall through → `{ action: "deny" }`
The packaged `od://` scheme matched neither, so the click was
silently dropped — Orbit's "Open artifact" became a no-op for
every desktop user. Dev mode (`http://127.0.0.1:port/`) was
unaffected: its links resolved through the http branch and
opened in the user's external browser via `shell.openExternal`.
Fix: extend `isAllowedChildWindowUrl` to also accept the
packaged `od:` scheme. Electron then opens a child BrowserWindow
that inherits the same protocol registration + preload, so the
live artifact preview HTML (served via the `od://` proxy in
`apps/packaged/src/protocol.ts`) renders in the new window.
Behavior delta:
- Packaged: click → `od://app/api/.../preview` → child window
renders the live artifact preview (was: silent no-op).
- Dev mode: unchanged — link is `http://127.0.0.1:port/...`,
still routes to `shell.openExternal`.
- Other links inside the packaged app that still flow to
`shell.openExternal` (e.g. github.com / external docs):
unchanged — `od:` is not a wildcard, only the packaged
scheme passes the allowlist.
Tests: exposed `isHttpUrl` and `isAllowedChildWindowUrl` from
`apps/desktop/src/main/index.ts` (re-exported through
`@open-design/desktop/main`) so the packaged workspace's vitest
can pin both helpers without bringing up Electron. 8 tests
across the negative-guard surface — `od:` allowed, `blob:` still
allowed, `http(s):` NOT allowed by this branch (handled by the
sibling `isHttpUrl` path), `file:`, `javascript:`, `data:`
explicitly NOT allowed.
The pre-existing `apps/packaged/tests/sidecars.test.ts >
adds custom VP_HOME/bin to the packaged PATH builder` failure
is unrelated to this PR — confirmed by stashing the change
and re-running on the bare branch base.
Verified locally:
- packaged vitest desktop-url-allowlist: 8/8
- desktop tsc -p tsconfig.json --noEmit: clean
- packaged tsc -p tsconfig.json --noEmit: clean
- packaged tsc -p tsconfig.tests.json --noEmit (the CI killer): clean
|
||
|
|
5d7568ba2c
|
fix(web): confirm before clearing a saved Media provider API key (#875)
The Clear button on Settings → Media providers wiped the saved
apiKey + baseUrl + model in a single click with no recovery — a
fat-fingered click on the wrong row would silently delete a key
the user just pasted in.
Wrap the existing onClick in `window.confirm()` matching the same
pattern the codebase already uses for destructive actions
(conversation delete, design delete, FileWorkspace file delete).
The prompt is localized via a new `settings.mediaProviderClearConfirm`
key with `{name}` placeholders for the provider label, translated
across all 17 locales.
Updated the existing media-provider clear test to auto-accept the
prompt, plus added a sibling test asserting that dismissing the
prompt leaves the saved config intact.
Co-authored-by: Nagendhra <nagendhra405@gmail.com>
|
||
|
|
ba776aecc0
|
fix(tools-pack): mark blake3-wasm as external in mac prebundle (#844)
The packaged macOS daemon crashes on startup with: ReferenceError: __dirname is not defined in ES module scope at .../blake3-wasm/dist/wasm/nodejs/blake3_js.js esbuild bundles blake3-wasm into the daemon ESM output, but the generated chunk uses __dirname without esbuild emitting a helper for it, and the wasm asset is not copied alongside the bundle. Adding blake3-wasm to the daemonCli/daemonSidecar externals and to MAC_PREBUNDLE_RUNTIME_DEPENDENCIES lets the wasm package resolve its own assets at runtime, mirroring how better-sqlite3 is handled. |
||
|
|
dcfab797c2
|
[codex] Add stable nightly promotion gate (#962)
* Upload beta e2e spec reports to R2 * Expose beta report URLs in summary * Complete Indonesian deploy locale keys * chore: factor release workflow scripts * chore: bump packaged beta base version * test: wait for mac packaged runtime health * fix: capture mac packaged startup logs * chore: improve mac release build observability * fix: ad-hoc sign unsigned mac builds * chore: diagnose mac packaged startup * fix: relax unsigned mac launch signing * chore: improve mac launch diagnostics * chore: simplify beta mac release artifacts * fix: align packaged mac smoke launch config * fix: externalize mac daemon wasm dependency * chore: require signed stable mac releases * fix: use stable app version for nightly package builds * chore: clean release artifacts after publish * chore: publish beta reports as zip * ci: disable beta mac tools-pack cache * fix: skip mac framework binary symlinks when signing * fix: sign mac framework version bundles * ci: disable beta mac pnpm cache * chore: align stable release reports * ci: require matching nightly before stable release * ci: avoid mac pnpm cache for packaged smoke |
||
|
|
f951ccb612
|
fix: keep examples filter counts consistent (#949)
* fix: keep examples filter counts consistent * test: cover scoped examples scenario counts * test: satisfy examples fixture typing --------- Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com> |
||
|
|
ce5f20918c
|
test: cover model option rendering (#948)
* test: cover model option rendering * fix: strengthen model option regression coverage --------- Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com> |
||
|
|
2005a5b727
|
fix: add gpt-5.1 Codex picker options (#946)
* fix: add gpt-5.1 Codex picker options * test: clarify gpt-5.1 picker coverage --------- Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com> |
||
|
|
0f586c410d
|
Fix Cloudflare Pages custom domain lookup (#958)
* Support Cloudflare Pages custom domains without hiding pages.dev fallback Keep the default Pages preview as the first public link while optional owned-zone binding provisions DNS and Pages custom-domain state in parallel. Constraint: Cloudflare deploys must use the existing direct-upload API path with no Wrangler dependency. Constraint: pages.dev must stay visible even while custom-domain verification is pending. Rejected: Vercel custom-domain support | outside requested Cloudflare-only scope. Rejected: overwriting arbitrary CNAME records | risks taking over user-managed DNS. Confidence: high Scope-risk: moderate Directive: Do not expose providerMetadata through public deploy contracts; keep custom-domain DNS ownership checks conservative. Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts Tested: pnpm --filter @open-design/contracts build && pnpm --filter @open-design/contracts typecheck && pnpm --filter @open-design/contracts test Tested: pnpm --filter @open-design/web typecheck && pnpm --filter @open-design/web test -- providers/registry.test.ts components/FileViewer.test.tsx i18n/locales.test.ts Tested: pnpm i18n:check && pnpm guard && pnpm typecheck Tested: pnpm --filter @open-design/daemon build && pnpm --filter @open-design/web build && git diff --check Not-tested: real Cloudflare account/token/domain smoke test * Preserve Cloudflare fallback correctness under large accounts and races Constraint: Cloudflare Pages keeps pages.dev as the primary usable fallback while custom domains remain optional typed metadata. Rejected: Treating custom-domain DNS or binding failure as a top-level deployment failure | pages.dev can still be ready and usable. Confidence: high Scope-risk: moderate Directive: Keep custom-domain finality tied to Cloudflare Pages API active status plus URL reachability; do not expose providerMetadata. Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts; pnpm --filter @open-design/web test -- components/FileViewer.test.tsx i18n/locales.test.ts providers/registry.test.ts; pnpm --filter @open-design/daemon typecheck; pnpm --filter @open-design/web typecheck; pnpm i18n:check; git diff --check; pnpm guard; pnpm typecheck; pnpm --filter @open-design/daemon build; pnpm --filter @open-design/web build Not-tested: Real Cloudflare token/account/zone smoke test. * Keep impeccable design notes local Constraint: .impeccable.md is local assistant/design context and should not be part of the PR diff. Rejected: Keeping the file tracked while adding it to .gitignore | tracked files are not ignored by Git. Confidence: high Scope-risk: narrow Directive: Keep .impeccable.md untracked and ignored; do not rely on it for required project documentation. Tested: git check-ignore -v .impeccable.md; git diff --check Not-tested: Full workspace tests not rerun for ignore-only metadata change. * Use direct Pages domain lookup for custom bindings Cloudflare rejects list-style options on Pages custom-domain lookup in some accounts, so the deploy path now reads the selected hostname directly before creating a binding. This keeps pages.dev deployment success intact while avoiding a failed custom-domain branch caused by page/per_page query parameters. Constraint: Cloudflare Pages custom-domain lookup must not send unsupported page/per_page list options Rejected: Continue paginating /domains | Cloudflare returns invalid list options before the binding can be created Confidence: high Scope-risk: narrow Directive: Keep pages.dev as the primary deployment URL and treat custom-domain setup as a recoverable secondary branch Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/deploy.test.ts tests/deploy-routes.test.ts; pnpm --filter @open-design/daemon typecheck; pnpm guard; pnpm typecheck; git diff --check; pnpm --filter @open-design/daemon build Not-tested: Live Cloudflare deployment was not retriggered from the browser |
||
|
|
2340d38d9d
|
docs: fix stale internal links (#950)
* docs: fix stale internal links * docs: fix design sample link label --------- Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com> |
||
|
|
9f6a918796
|
Fix: windows inspect overlay (#944)
* fix(web): improve the hint overlay behaviour * feat(web): update inspect hint styling * fix(web): add title and aria-label for closing hint box * fix(web): remove the dead code from index.css * fix(web): make default state of openhintbox to true * fix(web): allow pointer events to pass through hint overlay |
||
|
|
c2facb0e02
|
fix: serve python files as text (#947)
Co-authored-by: leprincep35700 <leprincep35700@users.noreply.github.com> |
||
|
|
208f09c60e
|
fix: settle completed runs and clean up shutdown children (#924)
* fix: clean up completed and shutting down runs * fix: bound daemon CLI shutdown Generated-By: looper 0.6.0 (runner=fixer, agent=codex) * fix: harden daemon shutdown cleanup Generated-By: looper 0.6.0 (runner=fixer, agent=codex) * fix: harden daemon shutdown cleanup Generated-By: looper 0.6.0 (runner=fixer, agent=codex) * test: align acp abort fake with typed child |
||
|
|
ef9ca7baff
|
fix(daemon): typecheck core server paths (#952) | ||
|
|
32d820e4ee
|
fix(daemon): typecheck leaf modules (#943)
* update drift * fix(daemon): typecheck leaf modules * fix(daemon): decode Qoder stdout buffers Generated-By: looper 0.5.6 (runner=fixer, agent=opencode) |
||
|
|
4f647f56ba
|
[codex] Optimize Composio connector previews (#907)
* Optimize Composio connector previews * Fix partial connector tool preview hydration * Cancel pending connector authorization on daemon * Preserve Composio cached tool counts * Avoid pending state after OAuth launch failure * Preserve static tool count fallback * Fix connector preview retry state * Remove Composio auth config metrics * Hydrate unknown connector tool previews * Fix remaining connector review threads * Stop failed connector preview spinner * Hydrate only targeted agent connectors |
||
|
|
e13adf2e63
|
feat(daemon): finalize design package endpoint (closes #450) (#832)
* feat(daemon): scaffold /api/projects/:id/finalize/anthropic (refs #450) Phase C of the PR 2 plan for issue #450: scaffold the route + module shape so subsequent phases (D-I) land function bodies and tests against a stable surface that already passes typecheck. What lands here: - apps/daemon/src/finalize-design.ts: module-level constants (DEFAULT_BASE_URL, DEFAULT_MAX_TOKENS=16000, INPUT_BODY_CAP_BYTES=384KiB, LOCK_FILENAME=.finalize.lock, OUTPUT_FILENAME=DESIGN.md, DEFAULT_TIMEOUT_MS=120s); inline interfaces for the request/response shape (kept out of packages/contracts per scope rules); two error classes - FinalizePackageLockedError (mirrors PR #493's TranscriptExportLockedError) and FinalizeUpstreamError (carries upstream HTTP status for the route's error mapping); function stub that throws "not yet implemented". - apps/daemon/tests/finalize-design.test.ts: vitest harness with describe.skip placeholder so the file imports cleanly. Real cases land in phases D-I. Default-import of node:fs (per memory: vi.spyOn cannot redefine on the frozen ESM Module Namespace; CJS exports object is mutable). - apps/daemon/src/server.ts: route handler at POST /api/projects/:id/finalize/anthropic, slotted next to the existing :id/deploy* family. Validates apiKey/model non-empty, optional baseUrl via the existing validateExternalApiBaseUrl closure (forbidden -> 403, invalid -> 400), optional maxTokens positive number; calls getProject (404 on miss); calls finalizeDesignPackage (which throws, caught and mapped to 500 for now); maps known error classes (FinalizePackageLockedError -> 409, FinalizeUpstreamError -> 502) pre-emptively. Path shape rationale (Bryan-confirmed): project-scoped path matches every sibling /api/projects/:id/* route in server.ts (deploy, deployments, deploy/preflight); provider-namespaced segment leaves a clean expansion line for /api/projects/:id/finalize/openai etc. as follow-ups. Field-name rationale: apiKey, baseUrl, model, maxTokens match ProxyStreamRequest verbatim (packages/contracts/src/api/proxy.ts:8-19) so a future caller can reuse the same body shape. baseUrl is optional here (intentional divergence from the proxy at server.ts which requires it) so standard Anthropic users do not need to set it; Bedrock / self-hosted-proxy users still can. Verification: pnpm --filter @open-design/daemon typecheck exits 0; finalize-design.test.ts loads cleanly with 1 skipped placeholder; no other tests touched. Refs nexu-io/open-design#450 (PR 2 scaffold; pipeline body in subsequent commits) * feat(daemon): transcript truncation helper for /finalize prompt Phase D of the PR 2 plan for issue #450: lands the helper that bounds the transcript section of the synthesis prompt. Why this exists: real-world signal at authoring time was a local project transcript already at 3.95 MB. Anthropic's claude-opus-4-7 context cap is roughly 200K tokens (~700 KB at typical density). Inserting an unbounded transcript would 4xx upstream on the first real call. This helper keeps the on-disk .transcript.jsonl lossless (PR #493's contract) while making the prompt-inclusion bounded. Strategy: - Cap output at INPUT_BODY_CAP_BYTES (384 KiB) so the prompt has room for the system prompt + design system body + current artifact + room for the synthesis output. - Always preserve the header line - it carries projectId, schemaVersion, conversation/message counts, attachment counts; synthesis quality depends on knowing the original sizes. - Split equal byte budgets between head and tail so both project genesis and most-recent intent survive. Two thinking segments separated only by mid-session truncation lose the same kind of boundary that PR #493 preserves between thinking blocks - that's accepted; smarter semantic chunking is a follow-up. - Insert a single `{"kind":"truncated","reason":"size","omittedBytes":N}` sentinel JSON line between the head and tail so a synthesis consumer can detect the gap. omittedBytes is the difference between the original UTF-8 byte length and the output's UTF-8 byte length. - If the head + tail budgets together cover the whole body (e.g. all message lines are tiny), no marker is emitted - the output is the input verbatim. Tests: - "returns the input verbatim when the JSONL fits under the 384 KiB cap" pins that small transcripts pass through unchanged with no marker. - "head+tail truncates with a single marker line when the JSONL exceeds the 384 KiB cap" pins that output is bounded, header survives, exactly one marker emitted with non-zero omittedBytes, both ends of the body preserved, and at least one middle message omitted. Suite delta: +2 tests in finalize-design.test.ts. Refs nexu-io/open-design#450 * fix(daemon): resolve noUncheckedIndexedAccess in truncateTranscriptForPrompt D1 (0eaa123) shipped with `body[headIndex]` and `body[i]` typed as `string | undefined` under TypeScript's `noUncheckedIndexedAccess` strict mode. Local typecheck would have caught it but the prior verification piped through `tail` which masked the non-zero exit code of `tsc`. Coalesce each access via `?? ''` (the array is from `String.split('\n')` so undefined elements are not actually reachable; the coalesce is a type-narrowing convenience, not a behavior change). Verification: `pnpm --filter @open-design/daemon typecheck` exits 0; `pnpm --filter @open-design/daemon test finalize-design` shows 2/2 + 1 skipped, identical to the pre-fix run. Refs nexu-io/open-design#450 * feat(daemon): current-artifact resolver for /finalize Phase E of the PR 2 plan for issue #450: resolves which artifact (if any) accompanies the transcript + design system in the synthesis prompt. Priority order (Bryan-locked in plan §6): 1. The file referenced by tabs.is_active = 1 IF an <name>.artifact.json sidecar exists on disk. Sidecar presence is the discriminator: an inferred manifest from `inferLegacyManifest` (e.g. for a bare .html with no sidecar) does NOT count, and an active tab pointing at a non-artifact file (.md, .txt) falls through. 2. Newest project file with a real .artifact.json sidecar, sorted by manifest.updatedAt descending. Files without an updatedAt sort last so legacy pre-streaming manifests do not get accidentally promoted. 3. Returns null - "no artifact in scope". The Phase H caller will emit `artifact: null` in the response and the prompt's "Current artifact" section will read "none". Sidecar presence is checked via `existsSync` on the on-disk path, NOT via the `artifactManifest` field returned by readProjectFile/listFiles (those run inferLegacyManifest as a fallback for known kinds, which would otherwise cause a bare .html with no sidecar to look like an artifact). Tests: - "returns the active-tab artifact when its sidecar is present, even if a newer artifact exists elsewhere": pinned.html (older updatedAt) is in the active tab; newer.html (newer updatedAt) is not. Resolver returns pinned.html - intent (active tab) beats recency. - "falls through to newest .artifact.json when active tab points at a non-artifact file": README.md is the active tab (no sidecar); design.html has a real sidecar. Resolver falls through and returns design.html. - "returns null when no active tab and no .artifact.json sidecars exist": only a README.md is in the project; no tabs row. Resolver returns null. Suite delta: +3 tests in finalize-design.test.ts (5 active total). Refs nexu-io/open-design#450 * feat(daemon): synthesis prompt construction for /finalize Phase F of the PR 2 plan for issue #450: builds the system + user prompts that get sent to Anthropic's Messages API in the synthesis call. Pure function; no IO, no side effects. System prompt (literal, stored as a module-level constant): instructs Claude to emit a DESIGN.md document with a fixed 7-heading structure (# DESIGN.md / ## Summary / ## Brand & Voice / ## Information Architecture / ## Components & Patterns / ## Visual System / ## Open Questions / ## Provenance). The Provenance section is required to list project ID, design system, current artifact, transcript message count, and the UTC generation timestamp. User prompt (built at runtime): structured payload with the truncated transcript JSONL, the design system body, and the current artifact body, each under a ## heading. Missing inputs (no design system selected, no artifact in scope) produce explicit "none" headings + parenthetical placeholder body so Claude does not hallucinate content for absent sections. Truncation is the caller's concern - this function does not re-truncate. The caller (Phase H pipeline) feeds in a JSONL that has already been bounded by truncateTranscriptForPrompt. Tests: - "includes the transcript JSONL verbatim and the generation context": pins all section headings, the transcript body verbatim, the design system body verbatim, the artifact body verbatim, and every generation-context line. - "falls back to \"none\" + parenthetical when no design system is selected": designSystemId=null and designSystemBody=null -> heading reads "## Active design system: none" with the parenthetical body. - "falls back to \"none\" + parenthetical when no artifact is in scope": artifact=null -> heading reads "## Current artifact: none" with the parenthetical body. Suite delta: +3 tests in finalize-design.test.ts (8 active total). Refs nexu-io/open-design#450 * feat(daemon): Anthropic call + retry strategy for /finalize Phase G of the PR 2 plan for issue #450: lands the upstream Claude Messages API call with a single transient-error retry, plus the response extractor that turns Anthropic's content array into the DESIGN.md body. What lands here: - appendVersionedApiPath: inlined from the connectionTest helper at apps/daemon/src/connectionTest.ts:188-195 (it is not exported there). Appends /v1/messages when the base URL has no /vN segment, otherwise appends /messages directly. Same semantics; ~5 lines. - callAnthropicWithRetry: POSTs to <base>/v1/messages with the canonical Anthropic headers (content-type, x-api-key, anthropic-version: 2023-06-01) and body shape ({ model, max_tokens, system, messages, stream:false }). One retry on transient (HTTP 429 or 5xx); on terminal failure throws FinalizeUpstreamError carrying the upstream HTTP status and raw body text. The route handler in Phase I maps status to AUTH_FAILED / RATE_LIMITED / UPSTREAM_FAILED and runs the body through redactSecrets before exposing it as `details`. - extractDesignMd: concatenates content[].text for every block where type === 'text', preserving order. Throws FinalizeUpstreamError(502) on three malformed-response shapes: non-object payload, missing content array, zero text blocks. The route handler maps the throw to 502 UPSTREAM_FAILED so synthesis cannot land a half-empty DESIGN.md on disk. - Test-only `_sleepMs` injection on the call params so the retry-delay sleep is instant under vitest. Default sleep uses setTimeout. Retry posture (1 retry on transient) is opinionated; the maintainer's "standard exponential backoff" answer was directional and a single retry matches the existing daemon's posture (transcript export and connectionTest do zero retries) while staying inside the daemon's blocking-fast posture for /finalize. Tests: - callAnthropicWithRetry: throws on 401 with no retry; retries once on 429 and resolves on second 200; throws after both 5xx attempts; propagates AbortError when signal is pre-aborted. - extractDesignMd: concatenates ordered text blocks; throws on missing content array; throws on content with zero text blocks. A spurious typecheck error from `exactOptionalPropertyTypes` (signal typed as AbortSignal | undefined where RequestInit expects AbortSignal | null) was resolved by conditionally spreading signal into the RequestInit literal. Suite delta: +7 tests in finalize-design.test.ts (15 active total). Refs nexu-io/open-design#450 * feat(daemon): wire /finalize pipeline end-to-end Phase H of the PR 2 plan for issue #450: stitches together every phase D-G primitive into the full finalizeDesignPackage pipeline that the route handler in Phase I will expose over HTTP. Pipeline (in execution order, all inside a try/finally that always releases the lockfile): 1. getProject(db, projectId): defensive 404 (the route validates first; this throw catches direct CLI/script callers). 2. mkdirSync(<projectDir>, { recursive: true }): some projects have DB rows but no on-disk dir yet (PR #493's same fix). 3. fs.openSync(.finalize.lock, 'wx'): EEXIST -> FinalizePackageLockedError (mirror PR #493's TranscriptExportLockedError). 4. exportProjectTranscript(db, projectsRoot, projectId, { now }): produces .transcript.jsonl on disk; we read the body and run it through truncateTranscriptForPrompt to bound the prompt-inclusion size. 5. readDesignSystem(designSystemsRoot, designSystemId): returns null when the project has no design_system_id selected, when the design system directory does not exist, or when the DESIGN.md file is missing. 6. resolveCurrentArtifact(db, projectsRoot, projectId): active tab -> newest .artifact.json by manifest.updatedAt -> null. 7. buildSynthesisPrompt({...}): system + user prompt (per Phase F). 8. callAnthropicWithRetry({...}): one retry on 429/5xx; throws FinalizeUpstreamError on terminal failure. 9. extractDesignMd(payload): concatenates content[].text blocks; throws FinalizeUpstreamError(502) on malformed shape. 10. Atomic write: writeFileSync({flag:'wx'}) -> reopen for fsync -> rename. Errors unlink tmp before rethrowing. 11. Lock release in finally (always closeSync + unlinkSync). Bounded blocking: the function uses its own AbortController + 120s timeout when the caller does not supply a signal. Caller-supplied signal takes precedence. Type tightening: switched the local Db interface to `type Db = Database.Database` (better-sqlite3) so the function signature is compatible with `exportProjectTranscript`'s typed parameter. Source file already had a `better-sqlite3` import in claude-design-import area of the daemon, so no new dependency. Tests: - "writes DESIGN.md atomically on the happy path": end-to-end with seeded project + conversation + 2 messages + design system on disk; asserts file at exact path + body bytes match the fetch mock. - "response carries every documented field with correct types": designMdPath/bytesWritten/model/inputTokens/outputTokens/artifact/ transcriptMessageCount/designSystemId all present and typed. - "emits design system 'none' in the prompt when no design_system_id is set": fetch mock asserts on the body it receives. - "throws FinalizePackageLockedError when .finalize.lock is already held": pre-create lockfile; assert throw + DESIGN.md not written + pre-existing lock NOT unlinked (we did not own it). - "replaces an existing DESIGN.md atomically on a second finalize": inject a sentinel between two finalize calls; assert sentinel is gone after second run. - "cleans up tmp file AND lock file on every error path": mock fs.writeFileSync to throw on the tmp path; assert no DESIGN.md.tmp.* remain, no DESIGN.md, no .finalize.lock. - "uses the default https://api.anthropic.com baseUrl when baseUrl is omitted": fetch URL begins with the default; baseUrl=undefined path. vi.restoreAllMocks() now runs in afterEach so the writeFileSync spy from the cleanup test does not leak into subsequent tests. Suite delta: +7 tests in finalize-design.test.ts (22 active total). Refs nexu-io/open-design#450 * feat(daemon): /finalize HTTP route handler + error mapping Phase I of the PR 2 plan for issue #450: replaces the Phase C stub's catch-all 500 with status-aware error mapping that surfaces the right HTTP status + error code for each documented failure mode, and adds HTTP-layer tests that boot startServer to exercise the route's validation branches. Route handler changes: - :id format guard: an inline regex matching isSafeId at apps/daemon/src/projects.ts:556-558 rejects unsafe ids with 400 BAD_REQUEST before any DB or filesystem work. Without this, an id like 'bad!id' would either fail getProject as 404 (wrong code) or reach the function and throw 'invalid project id' (mapped to 500). - FinalizeUpstreamError mapping is now status-aware: - upstream 401 -> 401 AUTH_FAILED - upstream 429 -> 429 RATE_LIMITED - upstream 5xx (or our own 502 sentinel for malformed responses) -> 502 UPSTREAM_FAILED In all cases the upstream raw text is run through redactSecrets so the apiKey cannot leak through `details` even if the upstream echoes the inbound headers. - AbortError mapping: when the 120s AbortController fires (or the caller pre-aborted the signal), surface as 503 TIMEOUT. - Default case: console.error the error per daemon convention; client sees 500 INTERNAL with the message routed through redactSecrets. - Imported redactSecrets alongside the existing connectionTest imports (apps/daemon/src/server.ts:51). HTTP-layer tests (boot startServer({port:0,returnServer:true}) once in beforeAll, mirror the proxy-routes.test.ts pattern): - "400 BAD_REQUEST when baseUrl is not a valid URL (test #13)": baseUrl='not-a-url'. - "403 FORBIDDEN when baseUrl points at a private internal IP (test #14)": baseUrl='http://10.0.0.1'. Note: validateBaseUrl explicitly allows loopback (for local OpenAI-compatible servers) and only blocks non-loopback private IPs (10/8, 172.16/12, 192.168/16, fc00::/7, fe80::/10). - "400 BAD_REQUEST when apiKey is missing (test #15)": apiKey omitted. - "400 BAD_REQUEST when :id contains characters outside the safe-id regex (test #16)": id='bad!id' contains '!' which is not in [A-Za-z0-9._-]. Suite delta: +4 tests (26 active in finalize-design.test.ts). Full daemon suite: 1078/1078 pass; baseline+26 (the +5 above plan target reflects retry+extract split into more granular unit tests than originally enumerated; all real, none skipped). Refs nexu-io/open-design#450 * fix(daemon): tighten isSafeId to reject pure-dot project ids Addresses the P1 path-traversal finding from @lefarcen on PR #832 (https://github.com/nexu-io/open-design/pull/832#discussion_r3202512644). The pre-fix `isSafeId` at apps/daemon/src/projects.ts:556-558 used regex `/^[A-Za-z0-9._-]{1,128}$/` which permitted pure-dot ids (`.`, `..`, `...`) because `.` is in the character class. `projectDir` and `resolveProjectDir` both delegated to `isSafeId`, so an id of `..` would resolve to the PARENT of `.od/projects/` via `path.join`. Threat model (per @lefarcen): - An attacker creates a project row whose stored id is `..` (or another pure-dot variant) — for instance via a workflow that writes the row directly without going through the API. Subsequent finalize/write ops keyed by that id then escape the project tree. - A direct CLI / scripted caller passing `..` as the project id reaches the function without HTTP normalization saving us. (Express normalizes %2e%2e to .. and collapses path segments, which yields 404 for the URL `/api/projects/%2e%2e/...` in practice — but that's Express's protection, not ours.) Fix: - isSafeId now explicitly rejects pure-dot ids (`/^\.+$/.test(id)`) before the char-class regex check. Empty string and inputs longer than 128 chars are also rejected explicitly so the function fails closed on edge cases. - isSafeId is now exported from apps/daemon/src/projects.ts so the /finalize route handler in apps/daemon/src/server.ts can use the same validator instead of re-implementing the regex inline. This prevents drift between the route guard and the projectDir guard, which was how this hole originally appeared. Tests (in finalize-design.test.ts because that's where the threat was flagged; isSafeId is daemon-wide so a dedicated test file would also work): - isSafeId rejects `.`, `..`, `...`, `....` - isSafeId rejects ids with `/`, `\`, `!`, leading whitespace - isSafeId rejects empty string and >128 chars - isSafeId rejects non-string inputs (null/undefined/number) - isSafeId accepts plain ids, ids with mid-string dots, UUIDs, single chars Suite delta: +7 tests (33 active in finalize-design.test.ts). Full daemon suite: 1085/1085. Refs nexu-io/open-design#832 * fix(daemon): address PR #832 P1 findings — imported folders + network 502 Addresses two of the three P1 findings from @lefarcen on PR #832: 1. Imported-folder projects route DESIGN.md to metadata.baseDir (https://github.com/nexu-io/open-design/pull/832#discussion_r3202512656, also flagged independently by @chatgpt-codex-connector at #discussion_r3202430470) The pipeline previously called `projectDir(projectsRoot, projectId)` unconditionally, which resolves to `.od/projects/<id>`. For projects created via /api/import/folder the project row's `metadata.baseDir` carries the user's actual folder; without threading metadata through, finalize would silently land DESIGN.md in the hidden daemon data dir and the current-artifact resolver would miss the user's real files. Fix: switch from `projectDir` to `resolveProjectDir(projectsRoot, projectId, metadata)` in both `finalizeDesignPackage` and `resolveCurrentArtifact`. Thread `project.metadata` (from `getProject`'s normalized row) through both call paths. The resolver gets a new optional `metadata` parameter; native projects pass null and get identical behavior. 2. Network failures and JSON parse errors now map to 502 UPSTREAM_FAILED (https://github.com/nexu-io/open-design/pull/832#discussion_r3202512661) Pre-fix, only HTTP-non-OK responses were wrapped as FinalizeUpstreamError. DNS failures (ECONNREFUSED, ENOTFOUND), fetch TypeErrors, and `response.json()` SyntaxErrors fell through to the route's catch-all and surfaced as 500 INTERNAL — incorrect: those are upstream-level failures, not daemon bugs. Fix: - Wrap callAnthropicWithRetry in a try/catch that passes FinalizeUpstreamError and AbortError through verbatim, but rewraps any other thrown error as FinalizeUpstreamError(502, '', message). - Wrap response.json() in a try/catch that rewraps SyntaxError as FinalizeUpstreamError(502, '', "upstream Anthropic returned non-JSON body: ..."). - The route handler's existing FinalizeUpstreamError mapping then correctly maps these to 502 with the message in `details` (run through redactSecrets first). Tests: - "writes DESIGN.md under metadata.baseDir for imported-folder projects": inserts a project row with metadata.baseDir pointing at a user-folder temp dir; asserts result.designMdPath lands there AND the hidden .od/projects/<id> dir does NOT contain a DESIGN.md. - "rewraps fetch network rejection as FinalizeUpstreamError(502)": fetchImpl throws TypeError with cause.code='ENOTFOUND'; assert thrown error has name=FinalizeUpstreamError and status=502. - "rewraps 200 with non-JSON body as FinalizeUpstreamError(502)": fetchImpl returns 200 with text/html body; response.json() throws SyntaxError internally; assert FinalizeUpstreamError(502). Suite delta: +3 tests (36 active in finalize-design.test.ts). Full daemon suite: green at last check; will re-verify before push. Refs nexu-io/open-design#832 * refactor(daemon): move /finalize DTOs to contracts + map error codes + validate active-tab Addresses the P2 and P3 findings from @lefarcen on PR #832: P2 — Error codes + DTOs not in packages/contracts https://github.com/nexu-io/open-design/pull/832#discussion_r3202512673 Reverses my plan's locked decision #10 ("no contracts changes in this PR; inline the request/response types"). That rule came from the predecessor PROMPT brief's anti-pattern table; @lefarcen's review is fresher signal and supersedes it. Drift risk between the daemon's inline types and any future PR 3 web client is real. - New contracts module: packages/contracts/src/api/finalize.ts with FinalizeAnthropicRequest / FinalizeArtifactRef / FinalizeAnthropicResponse. Re-exported from the package root and made addressable via `@open-design/contracts/api/finalize` subpath. - Daemon source imports the canonical types from contracts and re-exports the public type names so internal references keep working without touching every call site. - Daemon-local error codes remapped to existing ApiErrorCode union members (apps/daemon/src/server.ts), per @lefarcen's suggested mapping: FINALIZE_IN_PROGRESS -> CONFLICT AUTH_FAILED -> UNAUTHORIZED UPSTREAM_FAILED -> UPSTREAM_UNAVAILABLE TIMEOUT -> UPSTREAM_UNAVAILABLE (status 503) INTERNAL -> INTERNAL_ERROR HTTP status codes are unchanged; only the `code` field in the error JSON body changed. P3 — Active-tab name not validated before sidecar probe https://github.com/nexu-io/open-design/pull/832#discussion_r3202512684 resolveCurrentArtifact now runs the active tab's name through validateProjectPath BEFORE composing it into a path.join expression. An invalid tab (traversal segments, absolute path, null byte, reserved segment) causes resolveCurrentArtifact to fall through to the newest-artifact branch rather than abort or probe outside the project directory. Tests: - "falls through (does not throw) when active tab name contains traversal segments": injects a malformed `tabs.name = '../../../etc/passwd'` row directly via SQL (bypassing production tab-creation validation), seeds a real artifact, asserts the resolver returns the real artifact rather than the malformed name. Suite delta: +1 test (37 active in finalize-design.test.ts). Full daemon suite: 1089/1089 green. Refs nexu-io/open-design#832 * fix(contracts): publish /api/finalize as standalone runtime entrypoint Addresses @mrcfps's CI-red review on PR #832 (https://github.com/nexu-io/open-design/pull/832, inline comment on packages/contracts/package.json). The previous J3 commit added `./api/finalize` as a type-only subpath: the entry had only a `types` field, no `default`. That broke the contracts package-runtime gate (packages/contracts/tests/package- runtime.test.ts:38-47) which asserts every exports entry exposes both a `.mjs` runtime and a `.d.ts` types target. mrcfps proposed two fixes; this commit takes path B — make finalize a first-class published module rather than a type-only re-export from the package root. Path B vs path A (a peer-AI second opinion via /collaborate confirmed): under NodeNext + ESM with exports-map semantics, TypeScript validates re-exported symbols against the published module-identity surface. Because the previous J3 had `./api/finalize` neither declared as an exports-map entry nor materialized as a standalone .mjs, TS omitted the re-exported names during package boundary analysis. Even at runtime `import('@open-design/contracts').FINALIZE_SCHEMA_VERSION` worked from the bundled index.mjs but the type-checker rejected it. Path B aligns the runtime and declaration surfaces. Changes: - packages/contracts/esbuild.config.mjs: add `./src/api/finalize.ts` to entryPoints so dist/api/finalize.mjs is generated as a standalone module rather than only inlined into the bundled root. - packages/contracts/package.json: re-add `./api/finalize` to the exports map with both `default: ./dist/api/finalize.mjs` AND `types: ./dist/api/finalize.d.ts`. Mirrors `./api/connectionTest`'s shape (the canonical pattern for first-class submodule entries). - packages/contracts/src/api/finalize.ts: keep the runtime export `FINALIZE_SCHEMA_VERSION = 1` (giving the standalone module a real value to emit beyond the type-only interfaces) and update the doc-comment now that the standalone .mjs is wired. - apps/daemon/src/finalize-design.ts: switch the type import from the inline declarations introduced in the prior J3 fallback to `import type { ... } from '@open-design/contracts/api/finalize'`. Re-export the names so internal references inside finalize-design.ts keep working without touching every call site. Verified: - node --input-type=module -e "import('@open-design/contracts/api/finalize').then(m=>console.log(JSON.stringify(Object.keys(m))))" prints ["FINALIZE_SCHEMA_VERSION"] — runtime resolution clean. - pnpm --filter @open-design/contracts test: 6/6 (including both package-runtime.test.ts cases on the rebuilt exports map). - pnpm --filter @open-design/daemon typecheck: exits 0. - pnpm --filter @open-design/daemon test: 1089/1089 (no regression vs the prior J3 number). Refs nexu-io/open-design#832 --------- Co-authored-by: DevForgeAI CI/CD Engineer <devforge-ai@development.ai> |
||
|
|
1e8926271b
|
Harden security scan findings and upgrade dependencies (#806)
* feat: add accent color control and launcher for Open Design * fix: remove launcher binary from PR * test: cover accent appearance edge cases * Harden security scan findings and upgrade deps * Address proxy security review * Pin jsdom for web test stability --------- Co-authored-by: ferasbusiness666 <ferasbusiness666@users.noreply.github.com> Co-authored-by: lefarcen <935902669@qq.com> |