When the agent emits an HTML artifact with no `data-od-id` /
`data-screen-label` annotations (a freeform PRD → HTML pass through a
Claude-Code-compatible CLI without going through a skill, for
example), the existing Inspect / Picker affordances no-oped silently:
- The bridge's click handler walks up to <html>, finds nothing tagged,
and bails before emitting `od:comment-target` — by design, since
posting a synthetic id here would change save-to-source semantics
for inspect overrides (the persisted CSS keys off the same
elementId).
- The host then sat at "Click any element with `data-od-id` to tune
its style" — phrased as if the user just hadn't found the right
element, when the page in fact had nothing matching at all.
- Picker mode (Tweaks → Picker) had no hint at all.
The bridge already broadcasts `od:comment-targets` with the full list
on every mode toggle and DOM mutation, but the host's existing
listener was gated on `boardMode` only — Inspect mode never learned
the artifact's annotation count.
Two surgical fixes:
1. `FileViewer.tsx`: a dedicated `od:comment-targets` listener that
installs whenever Inspect OR Comments mode is active, mirroring
the bridge's broadcast into `liveCommentTargets`. The
comment-mode-only listener still owns its hover / click / pod
events; this new listener only handles the targets list.
2. `FileViewer.tsx`: the inspect-empty-hint banner now dispatches on
`liveCommentTargets.size === 0`. Empty: a clear "this artifact has
no `data-od-id` annotations yet — ask the agent to add them"
message that names the missing attribute. Populated: existing
instructive copy. Mirrored across Inspect and Picker modes so the
failure surface gives the same calibration signal in both.
Tests:
- `tests/runtime/srcdoc-bridge-empty-targets.test.ts` (3 cases): pin
the bridge contract this fix depends on. Run the IIFE in jsdom and
assert (a) `allTargets()` posts an empty list for unannotated DOM,
(b) clicks on unannotated elements do NOT post `od:comment-target`
(regression pin against future "synthetic id" fallbacks that would
silently change save-to-source semantics), (c) clicks DO still
resolve to an annotated ancestor when one exists.
- `tests/components/FileViewer.inspect-empty-hint.test.tsx` (3
cases): pin the host dispatch — empty state in Inspect mode, the
switch back to instructive copy when targets show up, and the
mirrored affordance in Picker mode.
Out of scope (flagged in the design comment so it isn't lost):
- The follow-up scenario from #890 ("parent has data-od-id, target
child does not → adjustments hit the parent") is a different bug
that needs either synthetic-id fallback or a UI affordance to
descend into the click target. Leaving that to a follow-up so this
PR stays narrow.
- i18n: the existing inspect-empty-hint copy is hardcoded English;
rolling it into the 17-locale Dict is a separate cleanup.
* fix(orbit): scope last run to selected template
* fix(orbit): preserve legacy last run on upgrade
* fix(orbit): pin legacy last-run fallback on refresh
* fix(orbit): pin template id at run start
* test(web): sync orbit fixtures with skill summary
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>
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>
* 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
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>
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.
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.
* 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>
* 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
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>
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>
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>
* 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>
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>
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>
* 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
* 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>
* 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.
* 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
* 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.).
- 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
* 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>
* 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 447a270) used `allowedToolNames` as the badge
source. That field isn't actually stable across Composio hydration:
`apps/daemon/src/connectors/composio.ts:758-761` extends it with
every provider-discovered tool whose classified safety is
read+auto, so a connector like GitHub goes from 2 → ≈52 on
hydration — better than 2 → 868, but still a 26x jump and still
the kind of unexplained number-lurch the issue is about. My
regression test only covered write-classified tools so this
read-path inflation slipped through.
Fix: introduce `curatedToolNames` as a separate field that is
locked to the static catalog and never extended by discovery.
- `packages/contracts/src/api/connectors.ts` and
`apps/daemon/src/connectors/catalog.ts`: add
`curatedToolNames: string[]` to `ConnectorDetail` (required on
the wire) and as an optional field on
`ConnectorCatalogDefinition` (defaults to allowedToolNames at
serialize time).
- `apps/daemon/src/connectors/composio.ts`:
`definitionFromToolkit()` now sets
`curatedToolNames = [...staticDefinition.allowedToolNames]` —
the catalog set, no extension. The runtime `allowedToolNames`
keeps its dynamic auto-allow behavior so the execution gate is
unchanged end-to-end.
- `apps/daemon/src/connectors/catalog.ts`:
`connectorDefinitionToDetail()` populates
`curatedToolNames: [...(definition.curatedToolNames ?? definition.allowedToolNames)]`,
so non-Composio connectors (no discovery layer) trivially mirror
the two fields.
- `apps/web/src/components/EntryView.tsx`:
`getConnectorBadgeToolCount` becomes a 3-tier fallback —
`curatedToolNames` first, then `allowedToolNames`, then
`tools.length`. The order means a half-deployed daemon (allowed
but not curated) still produces a more meaningful number than
the raw inventory; only when both are missing do we fall back
to the original buggy behavior.
Tests added:
- daemon `connectors-service.test.ts`: 3 new tests pin the
contract — wire shape, hydration stability (catalog stays at 1
while allowedToolNames grows to 51), and defensive copy.
- web `EntryView.test.ts`: 5 tests on `getConnectorBadgeToolCount`
including the @lefarcen scenario explicitly: tools=800,
allowedToolNames=52, curatedToolNames=2 → badge=2.
Verified locally:
- daemon vitest: 924/924 (was 921, +3)
- web vitest: 337/337 (was 332, +5 net)
- daemon `tsconfig.json` and `tsconfig.tests.json` (the CI
killer): both clean
- web `tsc -b --noEmit` clean
- contracts typecheck clean
- i18n-check passes
- Live wire format: `/api/connectors/discovery` ships
`curatedToolNames: ['github.github_search_repositories', 'github.github_get_issue']`
for GitHub (length 2)
* fix: make ConnectorDetail.{allowed,curated}ToolNames optional in shape
After merging main, several existing test fixtures (EntryView,
SettingsDialog.orbit, ConnectorsBrowser) declared `ConnectorDetail`
inline without the new `allowedToolNames` / `curatedToolNames` keys
the PR introduced. Daemon-built payloads always populate both fields
(see `connectorDefinitionToDetail` and the Composio normalization
path), so making the wire shape `?`-optional is safe for runtime
consumers and avoids touching every fixture. Updates the daemon-side
mirror type to match and adds non-null assertions in the daemon tests
that read these fields directly.
---------
Co-authored-by: lefarcen <935902669@qq.com>
* 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
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
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
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>
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.
* 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
* 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