mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
codex/visual-draw-annotation-context
138
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
51fab74993 |
Merge remote-tracking branch 'origin/main' into codex/visual-draw-annotation-context
# Conflicts: # apps/web/src/components/ChatComposer.tsx |
||
|
|
e26e8d08e5 | Fix concurrent visual annotation IDs | ||
|
|
086be271d4 |
fix: hide preview chrome in source view (#1556)
* fix: hide preview chrome in source view * fix: keep source-view edit controls |
||
|
|
660c5b88b4 | fix(web): auto-scroll feedback form (#1566) | ||
|
|
fa2bb59ab3 |
fix(test): align FileViewer tests with merged component
- Drop FileViewer.inspect-empty-hint.test.tsx (deleted on main; should have been gone after the merge but the modify/delete conflict left it in the tree). - Take main's FileViewer.test.tsx so the manual-edit interaction tests match the merged FileViewer.tsx behaviour, then patch every <FileViewer> render with projectKind="prototype" so it satisfies the prop requirement release added in #1509. |
||
|
|
4096d65125 |
fix(web): align Icon names and FileViewer tests with merged state
- SettingsDialog: use 'sparkles' for Pet section nav icon (main's choice; the merge picked up release's 'paw' which is not in main's IconName union). - FileViewer.test.tsx: take release's version which passes projectKind on every render (release added that prop in #1509 analytics; main's tests predate that prop). - FileViewer.manual-edit{,-history}.test.tsx: keep main's tests but pass projectKind="prototype" so they satisfy the merged FileViewer Props. |
||
|
|
5172e37217 |
Merge origin/main into release/v0.7.0 to prepare merge-back PR
Resolves 7 conflicts via hybrid strategy: - apps/web/src/components/EntryView.tsx: take main (Discord+X pills are forward feature) - apps/web/src/components/Icon.tsx: take main (switch-case refactor) - apps/web/src/components/NewProjectPanel.tsx: take release (preserve #1514 dropdown UX validated in 0.7.0 acceptance) - apps/web/src/index.css: take main (project-target-platforms / instructions chip styles) - apps/web/tests/components/FileViewer.inspect-empty-hint.test.tsx: accept main's deletion - nix/package-daemon.nix, nix/package-web.nix: take main pnpmDepsHash Non-conflicting hunks from #1519 (AppChromeHeader), #1428 (PostHog analytics call sites), and #1540 (release light background) are preserved via auto-merge. |
||
|
|
77aaf8a824 | Fix visual draw annotation staging | ||
|
|
9040088f1c | fix(web): remove redundant bulk-select button (#1550) | ||
|
|
4f76e836ae |
feat(audio): add ElevenLabs audio support (#1384)
* docs: add ElevenLabs audio support design * docs: add ElevenLabs audio implementation plan * feat(daemon): add ElevenLabs speech renderer * feat(daemon): add ElevenLabs sound effects renderer * fix(daemon): preserve ElevenLabs sfx durations * feat(web): expose ElevenLabs media providers * feat(daemon): document ElevenLabs audio contract * feat(audio): add ElevenLabs voice selection * chore: ignore superpowers scratch docs * fix(daemon): cache ElevenLabs voice options * fix(audio): expand ElevenLabs voice and SFX selection * fix(audio): align ElevenLabs SFX controls * fix(audio): tighten ElevenLabs SFX prompt budget * fix(audio): preflight ElevenLabs SFX prompt length * fix(audio): surface ElevenLabs lookup failures * fix(audio): sanitize ElevenLabs prompt errors |
||
|
|
7aa1b516c2 | feat(web): add visual draw annotation context | ||
|
|
11b4750677 | Update release light background (#1540) | ||
|
|
cd68c8a80a |
fix(daemon+web): emit conversation-created SSE event when routine run starts (#1523)
* fix(daemon+web): emit conversation-created SSE event when routine run starts When a Routine fires in "Reuse an existing project" mode, the daemon creates a new conversation in the project and writes a queued/running assistant message to the database, but the open `ProjectView` has no way to learn that anything happened: the project events SSE stream only carries `file-changed` and `live_artifact*` events, and `ProjectView` reloads conversations only when `project.id` changes. The result is the user's own routine "Run now" appears to do nothing until they exit and re-enter the project (#1361). Fix: - Add a `conversation-created` payload type to the existing project events stream in `apps/web/src/providers/project-events.ts`. The payload carries `projectId`, `conversationId`, `title`, and `createdAt`. Mirror the existing `file-changed` listener pattern with explicit malformed-payload handling. - In `apps/daemon/src/server.ts`, after `insertConversation` runs in the routine `setRunHandler` (both reuse-an-existing-project and new-project paths), broadcast a `conversation-created` event through the existing `activeProjectEventSinks` map. The function body was already generic so it was renamed from `emitProjectLiveArtifactEvent` to `emitProjectEvent` and the two pre-existing callers updated. - In `apps/web/src/components/ProjectView.tsx`, when `handleProjectEvent` receives a `conversation-created` event whose `projectId` matches the currently-viewed project, refetch the conversation list via `listConversations`. The active conversation is intentionally NOT changed — per maintainer guidance on #1361, auto-switching is a separate UX decision left for a follow-up. - `projectEventToAgentEvent` returns null for `conversation-created` so it doesn't get routed into the live-artifact path. Tests (`apps/web/tests/providers/project-events.test.ts`): - A single `conversation-created` event reaches the consumer with the parsed payload. - Two consecutive `conversation-created` events from concurrent routine runs both reach the consumer (covers the multiple-concurrent-runs case reported in #1502). - Malformed `conversation-created` payloads are swallowed without throwing, matching the existing `file-changed` / `live_artifact` defensive behavior. Manual verification: - Built locally with `pnpm exec tools-pack mac build --to app --portable` and installed. - Created a routine in `Reuse an existing project` mode targeting an existing project. - With the project view open, clicked `Run now`. The new "Routine" conversation appeared in the project's conversation list within about a second, without exiting and re-entering the project, and the active conversation was not changed. - Clicked `Run now` twice in quick succession; both new conversations appeared in the list, covering the concurrent-runs case in #1502. - `pnpm guard` and `pnpm --filter @open-design/web typecheck` clean; full web test suite is 1016/1016 passing. Fixes #1361 * fix(#1523 review): share SSE type via contracts; guard conversation refresh against project-switch and reordering races Addresses Codex P1 + lefarcen P2 inline review on #1523 (#1361): 1. Move `ProjectConversationCreatedSsePayload` to `@open-design/contracts` (`packages/contracts/src/sse/chat.ts`) so the daemon producer and the web consumer share one type. The web provider re-exports it under the local `ProjectConversationCreatedEvent` name to keep the existing import shape stable for callers; the daemon emit site picks up the same shape via a JSDoc typedef so producer and consumer can't drift as this stream grows. (Addresses lefarcen P2 on project-events.ts:17.) 2. Guard the `conversation-created` async refresh in `ProjectView` against two distinct races: - Project-switch race: capture `project.id` at dispatch time and re-check it via a live `projectIdRef` after `listConversations` resolves; bail if the user switched projects while the request was in flight. The existing project-load effects use the same cancellation pattern. (Addresses Codex P1 on ProjectView.tsx:767.) - Concurrent-refresh re-ordering race: bump a monotonic `conversationsRefreshTokenRef` on every dispatch and capture each request's token; only the request whose captured token still equals the live ref at await-return applies its result. Two rapid `conversation-created` events (the #1502 concurrent Run-now case) can no longer drop the newest conversation when the earlier request resolves last with a stale, shorter list. (Addresses lefarcen P2 on ProjectView.tsx:767.) Both guards are documented inline with comments that point back at the review threads. The existing project-events tests (single delivery, concurrent delivery, malformed payloads) are unchanged — the new guards are defensive logic on the consumer, not new event shapes. `pnpm guard`, `pnpm --filter @open-design/web typecheck`, `pnpm --filter @open-design/daemon typecheck`, and the full web test suite (1016/1016) remain green. |
||
|
|
eda182c8a1 |
refactor(web): UI polish for v0.7.0 — neutralised palette, official brand glyphs, lucide (#1522)
* refactor(web): adopt lucide-react for the inline Icon component
The hand-rolled `<Icon>` set drifted in stroke weight and proportion across
its 50+ glyphs as new icons were added. Swap the implementation to dispatch
to `lucide-react` while keeping the same `<Icon name="..." size={X} />` API
so the 246 existing call sites stay untouched.
- Adds `lucide-react` as a dependency (tree-shaken; ~30KB gzipped for the
~50 icons we actually import).
- `discord` and `x-brand` keep their bespoke inline SVG paths since lucide
intentionally does not ship brand artwork.
- `spinner` continues to use the existing `.icon-spin` className for its
rotation; under the hood it now renders lucide's `Loader2`.
- New `paw` glyph (lucide `PawPrint`) so the Pets nav item stops sharing
the `sparkles` icon with External MCP.
No behaviour change: the prop surface is identical, fill follows
`currentColor` exactly as before, and aria-hidden / focusable defaults are
preserved. Visual deltas are limited to the strokes themselves (slightly
finer endcaps, more consistent baseline weights) — exactly the
consistency upgrade lucide gives us.
* feat(web): bundle official brand assets for agent icons
`AgentIcon` previously approximated each agent's brand with hand-drawn
SVG (orange Anthropic-ish sparkle, OpenAI-knot ellipses, etc). Replace
those approximations with the real, vendor-published artwork shipped as
static assets under `apps/web/public/agent-icons/`.
- 13 SVG marks sourced from `@lobehub/icons-static-svg` (MIT) — color
variants where the vendor published one (Claude, Codex, Gemini,
Copilot, Qwen, Qoder, DeepSeek, Kimi, Mistral/Vibe), monochrome marks
for the rest (Cursor, OpenCode, Hermes, MiMo, Pi, Kilo).
- 1 PNG mark (Devin) sourced from devin.ai/icon.png, resized to 96×96
via `sips` since Cognition doesn't publish an SVG.
- Each SVG was cleaned (stripped `<title>` brand text and the library's
internal `style="flex:none;..."` ; dropped `width/height="1em"` so
`viewBox` governs sizing) and run through `svgo --multipass`. Total
bundle footprint: ~36 KB for all 17 files, only loaded on the agent
cards that render them.
- `AgentIcon` now resolves brands via a small `ICON_EXT` table and
renders `<img src="/agent-icons/<id>.<ext>">`. Agents without an asset
(`devin` is the lone outlier removed in this commit because PNG; new
agents with no shipped artwork at all) fall back to an initial-letter
pill that reads as "no official mark yet" rather than inventing
brand artwork.
- Removes the `simple-icons` dependency from a previous iteration since
`AgentIcon` was its only consumer.
Public-API stable: `<AgentIcon id={a.id} size={X} />` still accepts the
same prop shape; `AvatarMenu`'s small-size usage continues to work.
* refactor(web): polish entry view + Settings dialog UI for v0.7.0
A sweep over the two surfaces that have the most visual surface area in
the app (the entry sidebar / New Project panel on the left, and the
Settings modal). The work converged on a single neutral palette + a
small set of shared dimensional standards documented in CSS, so future
sections that get added slot into the same rhythm.
New Project panel (apps/web/src/components/NewProjectPanel.tsx +
.newproj* rules in index.css)
- Adds a spec comment block at the top of the .newproj rules listing
the canonical heights (input 30, dropdown 38, compact toggle 36,
popover item 38) and the neutral colour rules.
- Rebuilds PlatformPicker as a DS-picker-style dropdown trigger +
popover (the previous 6-card 2×3 grid was ~280px tall; the dropdown
collapses to a single 38px row with the same multi-select semantics).
- Replaces SurfaceOptions' two heavy `ToggleRow` cards with the new
compact one-line `CompactToggle`; the descriptive hint moves to a
native `title` tooltip.
- Compresses the Fidelity card grid (thumb aspect 16/7 → 16/5, tighter
padding, smaller label).
- Neutralises every selected/active state inside the panel: removes the
orange accent fills and rings from `.newproj-card.active`,
`.newproj-title-badge`, `.compact-toggle.on`, `.toggle-row.on`, the
DS picker popover items + radio/check marks, the trigger open border
and shadow, and the search-bar background. The Create CTA stays the
only orange element on the panel.
- Aligns the project-name input focus state across the sidebar:
border `var(--text)` + 8% black halo (rgba is written out because the
CSS pipeline collapses `color-mix(... 8%, transparent)` down to a
solid `var(--text)`, which would render as a 3px solid black band).
- Switches the body card from `flex: 1 1 auto` to `flex: 0 1 auto` so a
short form variant doesn't leave a white void at the bottom of the
card, and disables overscroll-bounce on the card so a fast scroll
doesn't briefly expose the page-level gray under the white surface.
- Pins the privacy footer below the card with a fixed 0 margin-top +
shorter padding-top so it reads as a label of the card rather than a
centred dialog footer.
Entry sidebar footer (apps/web/src/components/EntryView.tsx +
.entry-side-foot* rules)
- Replaces the X social pill's `external-link` placeholder glyph with a
bespoke filled `x-brand` SVG that mirrors the `discord` mark already
in the icon set.
- Wraps Discord + X in `.entry-side-foot-social` and lets that group
flex-margin to the right of the row, so the two social pills read as
a tight pair instead of a fourth pill stuck to the Pet pill.
- Drops the "unadopted" red dot on the Pet pill (it duplicated the call
to action that the label already carried).
- Shrinks the footer icons to 10px and dims them to 55% / 75% opacity
on hover so the labels are clearly the focal point — `currentColor`
on the lucide-rendered SVGs would otherwise make the glyphs full
black on hover.
- Tightens the env-pill version text cap (180 → 142) so the top row
ends close to the right edge of the Language + Pet group below it.
Settings dialog (apps/web/src/components/SettingsDialog.tsx +
.modal-settings / .settings-* / .seg-* / .agent-* rules)
- Removes the "SETTINGS" kicker eyebrow above each section title (the
big-typography title and modal context already make it redundant).
- Switches the sidebar from a card-per-item layout to ChatGPT-style
single-line pills: hides the `<small>` description, swaps the
sidebar bg from gray to white, makes the active item a gray pill (no
border, no shadow) so all items keep a consistent row height
regardless of state.
- Drops the modal-body's top border (already separated by the
whitespace between modal-head and the body grid) and pins
`.modal-settings { height: min(720px, 100vh - 64px) }` so the
dialog no longer resizes when the user switches between short and
long sections.
- Compresses the Local CLI / BYOK seg-control from a 2-line ~52px card
pair to a 1-line ~42px segmented pill that height-matches the active
sidebar nav-item, and aligns the `.settings-content` padding-top
with `.settings-sidebar` (22 → 16) so the first content row sits
level with the first sidebar item.
- Neutralises agent-card selected state, install/docs link colour, and
protocol-chip active state — same accent-stripping pattern as the
New Project panel.
- Uniform agent-card height via `min-height: 64px` so installed cards
(icon + name + version) align with unavailable cards (icon + name +
not-installed + Install/Docs row).
No prop-API changes, no business-logic edits — this is a pure visual
refactor. Existing tests, providers and daemon contracts are untouched.
|
||
|
|
b2841f6045 |
Fix user chat message bubble styling (#1517)
Co-authored-by: Haoyuan Yu <haoyuan.yu@shopee.com> |
||
|
|
6736310a01 |
Implement manual edit inspector (#1448)
* feat(web): tweaks palette popover with HSL hue-shift recoloring Adds a Tweaks color-palette popover to the HTML preview toolbar. Selecting a palette re-skins the iframe in place via a srcDoc-side bridge that walks the DOM and shifts every chromatic paint to the target hue while preserving each color's saturation and lightness — pale tints stay pale, bold CTAs stay bold, just in the new color family. Mono-noir desaturates instead of shifting. - runtime/srcdoc: new injectPaletteBridge + paletteBridge / initialPalette options - file-viewer-render-mode: paletteActive flips URL-load back to srcDoc so the bridge can be injected - FileViewer: state, popover, postMessage wiring, srcDoc + useUrlLoadPreview integration - PaletteTweaks: popover UI with Original + Coral / Electric / Acid forest / Risograph / Mono noir - PreviewDrawOverlay: stub pass-through until the draw branch lands * feat(web): hide finalize-design toolbar from project header * test(e2e): skip project actions toolbar flow after toolbar removal * Polish manual edit inspector * Implement manual edit inspector * Fix manual edit review regressions * Fix FileViewer CI regressions * Fix remaining manual edit review issues * Flush manual edit styles before draw exit * Restore Critique Theater styles * Accept pixel line-height manual edits --------- Co-authored-by: qiongyu1999 <2694684348@qq.com> |
||
|
|
0f0d2879ff | Make de/fr/ru content i18n optional (#1511) | ||
|
|
dc7791ef9d |
feat(analytics): add project_id + project_kind to studio/artifact events (#1509)
Product tracking doc 260513 added project_id + project_kind to studio_view (artifact), studio_click (share_option), and artifact_export_result. The Studio funnel can now group by project type without joining run_created on the back end. - contracts: 3 props gain required project_id + project_kind - ProjectView → FileWorkspace → FileViewer: thread projectKind down, converting metadata.kind via projectKindToTracking once at the top - FileViewer + HtmlViewer: populate the three call sites |
||
|
|
c16297f10c |
Refine preview and project dropdown controls (#1514)
* Refine preview and project dropdown controls * fix(web): gate OS widget metadata Generated-By: looper 0.7.4 (runner=fixer, agent=codex) * fix(web): mark platform picker listbox multi-select Generated-By: looper 0.7.4 (runner=fixer, agent=codex) |
||
|
|
e2f409579d |
docs: Critique Theater Phase 14 (user guide + 2 AGENTS module maps) (#1319)
* feat(web): pure reducer for Critique Theater states (Phase 7.1)
Pure CritiqueState reducer driven by the contracts-level PanelEvent
(the same shape both the live SSE stream and the recorded transcript
emit), so a single reducer powers both the in-flight panel and the
rerun replay. Lifecycle covers run_started → running → (shipped /
degraded / interrupted / failed), with panelist_open / dim /
must_fix / close / round_end events building per-round
CritiquePanelistView entries as they arrive.
Defensive behaviour that surfaced while writing the spec tests:
- Terminal phases (shipped / degraded / interrupted / failed) are
sticky against further lifecycle events for the same run, except
for parser_warning which can land late and is recorded in a side
channel without changing phase.
- A new run_started for a different runId at any time discards the
prior state and reboots, so the UI can launch consecutive runs
without an explicit reset action.
- Events whose runId does not match the active run return the same
state reference, so React's useReducer doesn't re-render
subscribers on stray traffic.
- Round bookkeeping keys by round number rather than "always last",
so an out-of-order panelist_dim for round 1 arriving after a
round 2 dim does not corrupt the round 2 bucket.
Test coverage: 18 cases covering each transition, the runId guard,
sticky-terminal behaviour, the out-of-order round invariant, and
the stable-identity guarantee. Sets up Phase 7.2 and 7.3 to wire
SSE + replay into the same reducer.
* feat(web): useCritiqueStream hook subscribes to SSE and feeds reducer (Phase 7.2)
createCritiqueEventsConnection is a pure connection manager that
mirrors apps/web/src/providers/project-events.ts: opens an
EventSource at /api/projects/:id/events, listens for every name in
CRITIQUE_SSE_EVENT_NAMES, decodes each frame back into a PanelEvent
(stripping the critique. prefix and merging the data payload), and
hands it to the caller's onEvent. Reconnect uses exponential
backoff (1s → 30s) and resets on `ready`; malformed payloads drop
with a dev-mode warning rather than tearing the stream.
useCritiqueStream wraps the manager in a useReducer that owns the
CritiqueState. enabled=false or a null projectId tears down the
connection cleanly; switching projectId closes the old connection
and opens a fresh one. The returned dispatch lets local UI
synthesise actions (e.g. an Esc keypress firing a synthetic
interrupted while a kill request is in flight); production traffic
comes from the SSE stream.
Test coverage:
- sse.test.ts (10 cases, node env): subscription set covers every
CRITIQUE_SSE_EVENT_NAMES channel; payload decoding lifts the wire
shape back to PanelEvent; malformed JSON is swallowed and does
not stop the stream; exponential backoff schedule and ready-reset
semantics are pinned with a setTimeout seam; close() cancels
pending reconnects and shuts the live source; no-op fallback
when EventSource is unavailable.
- useCritiqueStream.test.tsx (6 cases, jsdom env): idle pre-event,
reducer driven by synthetic actions, no connection when disabled
or projectId is null, clean close on unmount, projectId change
reopens cleanly.
* feat(web): useCritiqueReplay hook drives reducer from transcript file (Phase 7.3)
Fetches the per-run NDJSON transcript (one PanelEvent per line),
parses every line via the shared isPanelEvent predicate, and
dispatches into the same CritiqueState reducer the live SSE stream
uses. A single reducer means the UI rendering a replay can be
identical to the live panel, and a UI mounting both
useCritiqueStream and useCritiqueReplay in parallel does not have
to reconcile two state shapes.
speed knob is `paused | instant | live | { intervalMs: N }`.
- instant flushes every event synchronously, useful for opening a
finished run already at its terminal state.
- intervalMs paces dispatches at a fixed cadence so the reviewer
can watch the run unfold.
- paused parses the transcript but holds events back until the
caller advances speed (consumers can drive a scrubber later).
- live is reserved for the future "playback at original cadence"
feature, currently treated as instant; replay timestamps are not
yet persisted with each event so honest pacing requires a
follow-up Phase 7+ task.
gunzip seam handles `.ndjson.gz` transcripts via
DecompressionStream when present; the production fetch path picks
between text and arrayBuffer based on the URL extension. Both seams
are injectable so the unit tests don't need to spin up a real
network or a real gzip pipeline.
Test coverage (8 cases, jsdom env):
- Idle status before any URL is provided.
- speed=instant flushes the full transcript synchronously to
shipped state.
- speed={intervalMs:N} paces with the setTimeout seam, reaching
done after the last tick.
- speed=paused leaves status=playing with no dispatches.
- Empty transcript reports done with state still idle.
- Fetch rejection surfaces an error status with the message.
- Malformed NDJSON lines are skipped; valid events around them
still land.
- .gz transcripts route through the gunzip seam.
Closes the Phase 7 plan tasks 7.1 / 7.2 / 7.3 (reducer + stream +
replay), all on one branch ready for review. Phases 8+ (Theater
components) consume these from this PR.
* fix(web): close payload-override gap + paused-resume bug in Critique Theater hooks (Phase 7 review)
Two P1 fixes from lefarcen's review on PR #1307:
SSE payload override
`sseToPanelEvent` previously spread `data` after the channel-derived
`type`, so a payload-provided `type` could override the channel and
route a `critique.run_started` frame into the reducer as a `ship`
action. Reversed the spread so the channel-derived `type` is
authoritative, and revalidated the resulting object through the
contracts-level `isPanelEvent` predicate before returning. Frames
that fail validation (missing runId, empty runId, unknown type) are
dropped, so a malformed or compromised SSE frame can no longer
dispatch a wrong-shape action into the reducer.
Three new sse.test.ts cases pin the regression: hostile `type:'ship'`
in the payload still resolves to `run_started`, missing runId is
dropped, empty runId is dropped.
Replay pause/resume
`useCritiqueReplay` had one big effect keyed on `transcriptUrl`
only, so flipping `speed` from `paused` to `instant` never re-fired
and the held events sat undispatched. Split into a parse effect
(depends on URL, fetches and stores events in state) and a pace
effect (depends on parsed-events + speed, owns the cursor + timers).
The playback cursor lives in a ref that survives pause/resume
cycles, so flipping `paused` -> `instant` flushes from the current
position rather than restarting (which would double-dispatch
`run_started` and reset the reducer).
Two new useCritiqueReplay.test.tsx cases:
- paused-then-instant transitions from `playing` to `done` and
reaches the shipped terminal phase
- intervalMs paced playback dispatches one event, pauses to drain
the next scheduled timer, flips to instant, and confirms the
remaining transcript drains exactly once (cursor was preserved)
Doc consistency
The earlier source comment in useCritiqueReplay.ts claimed `live`
"paces by recorded timestamps" while the impl used zero-delay
timers and the PR body said it behaves like `instant`. Aligned to
reality: `live` currently behaves like `{ intervalMs: 0 }` (events
drain on successive microtasks via setTimeoutFn) because transcripts
do not yet carry per-event timestamps. Honest timestamp-driven
pacing is queued as a Phase 7+ follow-up.
Validated: pnpm guard, pnpm --filter @open-design/web typecheck,
Theater suite 47/47 (up from 42, +3 sse + 2 replay), full web suite
96 files / 888 tests.
* feat(i18n): seed Critique Theater key block (en + zh-CN; other locales fall back via spread)
* feat(web): Theater PanelistLane component (Phase 8.1)
* feat(web): Theater ScoreTicker component (Phase 8.2)
* feat(web): Theater RoundDivider component (Phase 8.3)
* feat(web): Theater InterruptButton component with Escape keybind (Phase 8.4)
* feat(web): Theater TheaterDegraded chip (Phase 8.5)
* feat(web): Theater TheaterCollapsed post-run summary (Phase 8.6)
* feat(web): Theater TheaterTranscript replay surface (Phase 8.7)
* feat(web): Theater TheaterStage top-level container (Phase 8.8)
* feat(web): Theater CSS using existing semantic tokens (no hex literals)
* feat(web): Theater public exports barrel
* fix(web): resolve P2 + P3 review feedback on Phase 8 (PR #1314)
Addresses all 4 P2 + 3 P3 items from codex, Siri-Ray, and lefarcen.
State-lifecycle fixes (3 x P2)
1. Reducer learns a synthetic `__reset__` action (`CritiqueResetAction`).
Host hooks dispatch it when their gating prop changes so a stale
run from a prior project / transcript cannot bleed into the next
context. Reset is idempotent on idle (returns the same reference).
2. `useCritiqueStream` dispatches `__reset__` at the top of its
connection effect, so a workspace switch from project A (which
streamed a critique) to project B clears the reducer before the
new EventSource opens. enabled=false also clears.
3. `useCritiqueReplay` dispatches `__reset__` at the top of its
parse effect, so transcriptUrl swaps (including swap-to-null after
a replay reached `shipped`) lift the reducer back to idle before
the new fetch starts.
SSE validation (1 x P2)
4. `sseToPanelEvent` now runs a per-variant `hasValidVariantShape`
check after the cheap `isPanelEvent` predicate. A
`critique.ship` frame missing `composite` / `round` / `status` /
`artifactRef` is rejected before reaching the reducer, so
TheaterCollapsed can no longer crash on `undefined.toFixed(1)`.
Every variant's required fields are validated: run_started
(protocolVersion, non-empty cast, maxRounds, threshold, scale),
panelist_* (round, role, plus variant-specific shape), round_end
(round, composite, mustFix, decision in {continue,ship}, reason),
ship (round, composite, status, artifactRef.{projectId,artifactId},
summary), degraded (reason, adapter), interrupted (bestRound,
composite), failed (cause), parser_warning (kind, position).
Reducer correctness (1 x P2)
5. `panelist_open` now materializes the round + an empty panelist
view (`{dims: [], mustFixes: []}`) so TheaterStage can highlight
the in-progress lane the instant the tag opens. Before this, a
stream that emitted only `panelist_open` after `run_started` left
`rounds = []` and the UI rendered no current round until a later
`panelist_dim` arrived.
Polish (3 x P3)
6. Brand role tint swaps from `var(--magenta, var(--accent))` to
`var(--purple, var(--accent))`. `--purple` is actually defined
across the design systems; `--magenta` is not, so Brand was
silently falling through to `--accent` and looking identical to
Designer.
7. New i18n key `critiqueTheater.interruptedSummary` for the
interrupted-collapse copy ("Interrupted at round N, best
composite X.X"). Previously the interrupted branch reused
`shippedSummary` and the UI read "Shipped at round..." for a run
that specifically did not ship. Native value in en + zh-CN; other
locales fall back via `...en` spread.
8. `TheaterDegraded` heading id comes from `useId()` instead of a
hardcoded `theater-degraded-heading`, so two chips rendered on
the same page (chat history with multiple completed runs) keep
their aria-labelledby references unambiguous.
Tests (15 new cases)
- reducer.test.ts (+5): __reset__ on running/terminal/idle, panelist_open materializes round, panelist_open does not stomp prior panelist data.
- sse.test.ts (+6): variant-level rejection for ship without required fields, degraded without adapter, run_started with empty cast, panelist_dim with non-numeric score, round_end with unknown decision, plus a positive fully-formed ship.
- useCritiqueStream.test.tsx (+2): state reset on projectId change, state reset on enabled flip false.
- useCritiqueReplay.test.tsx (+1): state reset on transcriptUrl swap to null after a replay reached shipped.
- TheaterCollapsed.test.tsx (text-pinning update): asserts the interrupted branch reads "Interrupted at round 1" + "best composite 7.9", and explicitly NOT "Shipped at round...".
- TheaterDegraded.test.tsx (+1): two chips on the same page get unique aria-labelledby ids that each resolve to an `<h3>`.
Validated
- pnpm guard clean
- pnpm --filter @open-design/web typecheck clean
- Theater suite: 13 files, 101 tests (was 86 on the first Phase 8 push, +15 new)
- tests/i18n/locales.test.ts 5 of 5 across 18 locales
* feat(web): CritiqueTheaterMount wires SSE + reducer into a single drop-in (Phase 9.1)
* feat(i18n): Critique Theater strings for de + ja + ko + zh-TW (Phase 9.2)
* fix(web): resolve P1 + P2 review feedback on Phase 9 (PR #1315)
Addresses every blocker from codex, Siri-Ray, and lefarcen. The
three state-lifecycle and SSE-validation issues they also flagged
inherit fixes from PR #1314's review pass that this branch now sits
on top of after rebase.
Real daemon kill on Interrupt (P1)
- CritiqueTheaterMount now POSTs to
/api/projects/:id/critique/:runId/interrupt alongside the
optimistic local dispatch. Before this fix, clicking Interrupt
only flipped the React state to interrupted while the daemon job
kept running. The fetch is best-effort: a 404 (endpoint not wired
yet, lands in Phase 15) is swallowed with a dev-mode console.warn
so the UI still moves to the collapsed badge.
- New fetchInterrupt test seam lets RTL assert on the URL / method
and simulate the "daemon not ready yet" path. Two tests pin both:
the happy URL proj-42/critique/run-abc/interrupt POSTs, and a
rejected fetch still flips the UI.
interruptPending reset on new run (P2)
- A ref-backed effect compares the current runId against the last
one we saw; when it changes, interruptPending is cleared. A user
who interrupts run-1 and then triggers run-2 from the same mount
now gets a fresh, enabled kill button instead of one stuck in
"Interrupting…". Pinned by a new mount test.
Escape keybind scope (P2)
- InterruptButton now checks the keydown target. Escape inside an
input, textarea, select, or contenteditable element is ignored
(and any ancestor of those via closest() is treated the same
way). Body-level focus still fires the keybind so the Theater
area's affordance keeps working. Four new tests cover textarea,
input, contenteditable, and the body-focus positive case.
userFacingName i18n key (P2)
- The spec at specs/current/critique-theater.md:6 mandates a single
critiqueTheater.userFacingName key so the "Design Jury" label can
be renamed without touching code. Phase 8 introduced
critiqueTheater.title by mistake; renamed across types.ts, en.ts,
zh-CN.ts, de.ts, ja.ts, ko.ts, zh-TW.ts, and the lone consumer
TheaterStage.tsx. The locale alignment test stays green.
Validated
- pnpm guard clean
- pnpm --filter @open-design/web typecheck clean
- Theater suite: 14 files, 112 tests (was 101 before, +11 new for
the Phase 9 review pass: 3 mount + 4 InterruptButton focus scope;
the rest were already in #1314's review fix).
- tests/i18n/locales.test.ts 5 of 5 across 18 locales.
* feat(daemon): adapter-degraded registry with TTL (Phase 10.1)
In-memory registry recording adapters that produced malformed or
oversize transcripts so the orchestrator can skip them for a TTL
window (default 24h) instead of cycling through known-bad providers
on every run.
Records carry reason (malformed_block | oversize_block |
missing_artifact), source label, and expiresAt. The test-only
clock seam lets the suite advance time deterministically and prove
that an expired entry stops counting as degraded without anyone
calling clearDegraded.
7/7 vitest cases green.
* feat(daemon): synthetic good + bad adapter fixtures (Phase 10.2)
Two test-only adapters that read the existing v1 transcript
fixtures (happy-3-rounds and malformed-unbalanced) and replay them
as either a full string or a 512-byte chunked stream. The chunked
form is what the conformance harness uses to prove the parser
holds together when the transcript arrives in arbitrary network
slices, not as one buffered blob.
* feat(daemon): adapter conformance harness (Phase 10.3)
runAdapterConformance pulls a transcript through the same
parseCritiqueStream pipeline the orchestrator uses and classifies
the outcome as shipped, degraded, or failed. On a degraded
outcome it forwards the matched reason to the adapter-degraded
registry, so a single nightly conformance run is what populates
the skip list rather than the orchestrator learning each adapter
is broken at request time.
5/5 vitest cases green covering shipped, malformed degraded,
oversize degraded, no-ship failure, and the harness-thrown
failure path.
* test(e2e): Critique Theater Playwright suite (Phase 11)
Six tests, one viewport per visual case, deterministic SSE
fixtures stubbed via page.route(). Adds the suite to
test:ui:extended so the existing extended-UI lane picks it up.
Coverage:
1. Happy path: a single mounted theater plays the full
fixture (1 run_started, 5 panelists open / dim / must_fix /
close, 1 round_end, 1 ship) and ends on the score badge.
2. Interrupt mid-run: the panelist that is open at the time
the interrupt button is clicked closes with an interrupted
marker and the transcript freezes there.
3. Visual regression at 375x720 mobile.
4. Visual regression at 768x1024 tablet.
5. Visual regression at 1280x800 desktop.
6. A11y role tree: the theater region exposes a labelled
landmark, each panelist lane is a group with an accessible
name, the score is a status live region.
All SSE traffic is stubbed by page.route so the suite runs in CI
without a daemon. The toggle is seeded via localStorage by
bootAppWithCritiqueEnabled so the gate behaves as if Settings
flipped it on. typecheck clean; playwright --list reports 6.
* test(web): reducer p99 bench at 10k iterations (Phase 13.1)
Locks the documented 2ms budget for the Critique Theater reducer
on a representative SSE script (27 actions, one full happy run)
behind a regression gate. Asserts p99 stays under 4ms (2x the
documented budget) so CI runners with a noisy neighbour do not
flake while a real regression to 20ms or 200ms still trips.
The bench is a vitest case rather than a bare microbenchmark so
it runs in the same CI lane as every other web test and does not
need a parallel runner.
* test(web): critique surface coverage walker (Phase 13.2)
Walks the public critique surface (11 SSE event names, 5 panelist
roles, 6 lifecycle phases, 9 named i18n keys) and asserts each
named symbol appears in both the src corpus and the test corpus.
The walker is the gate that catches a rename in one half of the
codebase without a matching update in the other half: a future
PR that drops 'panelist_must_fix' from the reducer without also
removing its test reference fails this suite.
62 assertions, one per symbol per corpus.
* docs: Critique Theater user guide (Phase 14.1)
Seven sections aimed at end users (not contributors):
1. What is Design Jury
2. How it works (the five panelists, auto-converging rounds,
the composite formula)
3. Settings (the M1 toggle and what it does)
4. Reading the score badge
5. Replay surface
6. Troubleshooting (degraded, interrupted, failed)
7. FAQ
The composite formula is documented as
designer * 0 + critic * 0.4 + brand * 0.2 + a11y * 0.2 + copy * 0.2
because anyone trying to reverse-engineer the score is going to
search for those weights and the docs are the place they should
land first.
* docs(daemon): critique module AGENTS map (Phase 14.2)
Daemon-side wayfinder for the apps/daemon/src/critique directory.
Tables every file, what owns what invariant, and the 'when you
change anything here' guide so a future contributor does not
have to reverse-engineer the rollout resolver before adding a
new SSE event.
* docs(web): Theater module AGENTS map (Phase 14.3)
Web-side mirror of the daemon AGENTS map. Same file table, same
invariants section, same change-impact guide, sized to the
Theater component package.
* docs: tighten Phase 14 reasoning from lefarcen review (PR #1319)
Four content gaps lefarcen flagged in the Phase 14 docs review,
addressed inline rather than deferred. The fifth item (scope-drift
between 'docs only' PR body and the cumulative stacked diff) is
handled by rewriting the PR body, not the docs.
1. Round exit conditions (lefarcen P2-1).
docs/critique-theater.md §2 'Auto-converging rounds' now lists
the five conditions that stop a run (threshold reached, round
budget exhausted, per-round timeout, total timeout, user
interrupt) with their default values. A user debugging a run
that stopped at round 1 with composite 5.4 can read this list
and find the matching cause without spelunking the orchestrator.
2. Prior-art comparison (lefarcen P2-2).
New §1.5 'Why an in-CLI panel and not a third-party design lint'
pre-answers the 'why not Figma lint / Adobe checker / Material
You conformance' question. Three differences: rule engines vs
generative reviewers, post-hoc vs in-loop, external service vs
same-CLI-session.
3. Composite formula rationale (lefarcen P2-4).
§2 now explains why each weight is set the way it is: critic
gates correctness so it gets 0.4; brand / a11y / copy are
secondary quality dimensions at 0.2 each; designer is at 0.0
in v1 because aesthetic preference is not a ship gate. The slot
stays in the schema so notes flow into the transcript and a v2
config release can bump the weight without a wire-shape change.
4. v2 cast-config ownership (lefarcen P2-3).
Both AGENTS.md files (daemon + web) now declare a 'Designer
weight frozen at 0.0 until v2 cast config' invariant. The
daemon side calls out where the SKILL.md frontmatter resolver
lands (apps/daemon/src/critique/config.ts); the web side calls
out where the Settings surface lands (apps/web/src/components/
Settings/). A contributor reading either AGENTS.md before
implementing v2 sees which module to touch first.
* docs(web): mirror the Designer-weight invariant in Theater AGENTS.md (PR #1319)
lefarcen P1 follow-up on PR #1319: the daemon AGENTS.md already
declares 'Designer weight is frozen at 0.0 until v2 cast config
lands' as an invariant, but the web AGENTS.md's parallel bullet
led with 'Composite weights are read-only on the web side' which
buried the Designer-specific constraint. A web contributor
reading that bullet would not realise the v1 weight distribution
is wire-shape (changing it mid-v1 invalidates persisted
critique_runs composite values).
Rewrote the bullet to lead with the same 'Designer weight is
frozen at 0.0 until v2 cast config lands' phrasing the daemon
side uses, and added an explicit cross-link to the daemon
AGENTS.md so the two halves of the invariant read as one rule.
Web-side specifics retained: ScoreTicker / TheaterCollapsed read
composite off the wire (no client recompute), v2 lands as a
Settings surface at apps/web/src/components/Settings/, do not
add a 'weights' prop to any component in this directory until
the contracts package carries the v2 cast type.
* docs: replace deferred metrics endpoint reference + refresh Theater module map (PR #1319)
Two carryover items lefarcen flagged across the PR #1319 + #1320
reviews.
1. docs/critique-theater.md was sending users to
/api/metrics/critique as the conformance-status check on
malformed_block, but the Phase 12 metrics endpoint is
explicitly deferred until after orchestrator wiring lands.
Replaced the link with the pnpm conformance-harness command
that DOES exist today (pnpm --filter @open-design/daemon
vitest run tests/critique-conformance.test.ts) and noted
that the dashboard surfaces this status as a series once
Phase 12 ships.
2. apps/web/src/components/Theater/AGENTS.md module map was
stale after Phase 15: the index.ts row said 'only two hooks
are exported' but the barrel now exports
useCritiqueTheaterEnabled too (plus the setCritiqueTheaterEnabled
setter). Updated the row to list all three hooks + the
setter + the reducer-derived contract types, and added a
new row for hooks/useCritiqueTheaterEnabled.ts in the file
table so a web contributor scanning the table sees the new
hook without inferring it from the index.ts blurb.
* fix(web): restore wait-for-daemon-ack pattern on Theater interrupt
Same regression as flagged on PR #1316 post-main-merge: the
optimistic local dispatch fired before the POST resolved, so a
daemon 404 / 409 still terminalized the UI and the real SSE
terminal event got ignored by the sticky interrupted phase.
Snapshot runId / bestRound / composite at click time, dispatch
interrupted only on res.ok, clear interruptPending on rejection or
non-2xx so the user can retry. Tests cover rejection + 404 leaving
the run on the live stage; the 204 path waits for the ack.
---------
Co-authored-by: Nagendhra <nagendhra405@gmail.com>
|
||
|
|
e2952acd05 |
Revert "fix(web): restore consistent app header layout (#1432)"
This reverts commit
|
||
|
|
342ba44383 | fix memory extraction history affordance (#1447) | ||
|
+14 |
3d3119333c |
fix(web): restore consistent app header layout (#1432)
* docs: add NotebookLM GitHub export script (#1062) * docs: add NotebookLM GitHub export script * fix: make NotebookLM export TOC anchors work * fix: escape TOC link text markdown chars * fix: include merged PRs when exporting --prs all * fix: allow --prs merged mode * fix: treat --limit as total export budget * fix: avoid starving buckets under global --limit * fix: support --issues none and handle repos w/ issues disabled * fix: avoid underfilling export when buckets empty * fix: keep disabled-issues fallback quiet * fix: silence disabled issues fallback * fix: satisfy script typecheck * prevent duplicate saves and add template deletion (#1294) * prevent duplicate template entries on repeated save * add delete button to saved template list Templates can now be removed from the template picker via a hover x button, calling the existing DELETE /api/templates/:id endpoint. * add missing onDeleteTemplate prop in test fixtures * add template deletion flow test for NewProjectPanel * reject template names longer than 100 characters * preserve original createdAt on template update * feat: add FAQ page skill (#1162) * fix: set writable OD_DATA_DIR default for nix run Fixes #1157 When running via 'nix run github:nexu-io/open-design', the daemon attempted to create runtime state under the Nix store package path: /nix/store/.../lib/open-design/.od/projects The Nix store is read-only at runtime, causing startup to fail with ENOENT when mkdir() tried to create the projects directory. This commit updates the nix run wrapper to export OD_DATA_DIR with a writable default ($HOME/.od) when the variable is unset. Users can still override it by setting OD_DATA_DIR before running. The Home Manager and NixOS modules already set OD_DATA_DIR, so they are unaffected by this change. * feat: add FAQ page skill Add a new skill for generating Frequently Asked Questions pages with: - Collapsible accordion sections for Q&A pairs - Real-time search functionality - Category filtering (Billing, Account, Technical, General) - Smooth animations and transitions - Keyboard navigation support - Mobile-friendly responsive design - Semantic HTML with proper ARIA attributes The skill includes: - SKILL.md with triggers, workflow, and output contract - example.html demonstrating a complete FAQ page with 12 questions Use cases: help centers, support pages, product documentation * fix: address PR review feedback for FAQ page skill - Fix craft slugs: use accessibility-baseline and state-coverage instead of non-existent slugs - Remove overly broad 'questions and answers' trigger - Add edge case handling for insufficient/excessive FAQs - Remove search highlighting requirement (XSS risk) - Update self-check to reflect filtering instead of highlighting Addresses review comments from @lefarcen and @chatgpt-codex-connector * feat: add localized copy for faq-page skill Add German, French, and Russian translations for the FAQ page skill example prompt to fix validation test failure. - DE: FAQ-Seite mit Akkordeon-Abschnitten, Suchfunktion und Kategoriefilterung - FR: Page FAQ avec sections accordéon, recherche et filtrage par catégorie - RU: Страница FAQ со складными секциями-аккордеонами, поиском и фильтрацией * fix: escape apostrophe in French translation Use double quotes to avoid syntax error with d'auth * fix(platform): add legacy ~/.fnm path to wellKnownUserToolchainBins (#1110) * fix(platform): add legacy ~/.fnm path to wellKnownUserToolchainBins fnm legacy installations use ~/.fnm/node-versions. Closes #1102 * fix: remove stray .fnm token from type declaration * docs: add Windows troubleshooting guide (#478) (#1170) * docs: add Windows troubleshooting guide (#478) Add docs/windows-troubleshooting.md with step-by-step fixes for the most common native-Windows setup errors: - Node 24 / nvm-windows gotchas (fake nvm file in System32) - pnpm not found after installation - Build scripts blocked by pnpm 10 (better-sqlite3, sharp) - Visual Studio / gyp build errors - Starting the dev server - Optional OpenCode CLI setup Also update CONTRIBUTING.md and QUICKSTART.md to link to the new guide instead of the vague "file an issue if it doesn't" note. * docs: fix Windows guide command accuracy (#1170) Address all 6 inline review comments from lefarcen: - Pin npm-global pnpm install to @10.33.2 (matches packageManager field) - Use where.exe instead of bare where (PowerShell alias conflict) - Fix OpenCode package: opencode-ai (not opencode), binary is opencode - Add EPERM fallback note for corepack enable on protected installs - Add Python check for gyp ERR! find Python - Expand diagnostic checklist with corepack, python, execution policy Also remove redundant corepack pnpm --version from checklist. * feat(daemon): inject compiled design-system tokens + fixture into prompts (#1385) * feat(daemon): inject compiled design-system tokens + fixture into prompts Follow-up to #1231. The prior PR landed the structured form of two brands (`default` + `kami`) and codified the schema; this PR teaches the daemon to actually consume those files when assembling the system prompt, so agents stop having to re-derive token names from DESIGN.md prose every turn. Gated behind `OD_DESIGN_TOKEN_CHANNEL=1` for the smoke-test phase — flag-off keeps the daemon byte-equivalent to today's behavior, flag-on appends two new prompt blocks (the brand's `tokens.css` :root contract and its `components.html` reference fixture) right after the existing DESIGN.md block. Brands without those sibling files (every brand except `default` and `kami` today) skip silently in either mode. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(daemon): only swallow ENOENT/ENOTDIR in readFileOptional, rethrow rest Reviewer feedback (nettee, #1385). The prior catch-all hid permission errors, EISDIR, and broken packaged-resource paths behind the same "undefined = absent" branch the legacy ~138-brand fallback uses, which would let `OD_DESIGN_TOKEN_CHANNEL=1` silently degrade to the DESIGN.md-only prompt while reporting success. That corrupts the exact signal the smoke-test rollout depends on. Now `readFileOptional` only returns undefined for ENOENT / ENOTDIR (real "file does not exist" cases) and rethrows everything else. Added a focused test that plants a directory at the tokens.css path to exercise the EISDIR branch, plus a partial-presence regression test to confirm the stricter contract preserves the legacy fallback. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: chaoxiaoche <chaoxiaoche@192.168.10.16> Co-authored-by: Cursor <cursoragent@cursor.com> * feat(daemon): make connection-test timeouts configurable (#1222) * feat(daemon): make connection-test timeouts configurable Provider and agent connection tests had hardcoded 12s / 45s budgets, which are too tight for slow networks or distant providers (the user sees "timeout" in Settings with no way to extend the budget). - Add OD_CONNECTION_TEST_PROVIDER_TIMEOUT_MS (default 12_000) - Add OD_CONNECTION_TEST_AGENT_TIMEOUT_MS (default 45_000) - Invalid values (non-numeric, zero, negative, fractional) emit a console.warn and fall back to the default, so a typo in the env never silently disables the safety timeout. - Export resolveConnectionTestTimeoutMs for unit testing; cover the three resolution paths (fallback / honored override / invalid). 41 connection-test tests pass (+3 new), full daemon suite 1170/1170. * fix(daemon): reject connection-test timeout overrides above Node's setTimeout maximum Node's `setTimeout` silently clamps any delay above `2^31-1` ms (2_147_483_647) to ~1 ms with a TimeoutOverflowWarning. The previous `Number.isInteger(n) && n >= 1` check accepted oversized values unchanged and passed them straight to `setTimeout`, so an override that *intended* to raise the budget — e.g. `OD_CONNECTION_TEST_AGENT_TIMEOUT_MS=3000000000` — instead caused every connection test to fail almost immediately. The safety timeout was effectively disarmed. Add `MAX_CONNECTION_TEST_TIMEOUT_MS = 2_147_483_647` and switch the guard to `Number.isSafeInteger(n) && n >= 1 && n <= MAX...`. The boundary value is still accepted; one millisecond past it falls back with a warn. Regression test exercises `3_000_000_000`, `2_147_483_647`, and `2_147_483_648`. Addresses #1222 review feedback from @chatgpt-codex-connector, @mrcfps, and @lefarcen. * fix(security): strip trailing dot in normalizeBracketedIpv6 (FQDN SSRF bypass) (#1122) * fix(security): strip trailing dot in normalizeBracketedIpv6 (FQDN bypass) new URL('http://192.168.1.5./').hostname returns '192.168.1.5.' — the trailing dot is the RFC 1034 absolute-FQDN form and resolves identically to '192.168.1.5'. parseIpv4 fails on the dotted form, so 169.254.169.254. slips past the metadata-service block, 192.168.1.5. slips past the LAN block, and localhost. slips past the loopback identification. Strip trailing dots in normalizeBracketedIpv6 so all downstream checks (isLoopbackApiHost, isBlockedExternalApiHostname, isBlockedIpv4, IPv6 range tests) see the canonical form. Adds 6 vitest cases covering loopback FQDN forms (localhost., foo.localhost., 127.0.0.1.) and SSRF FQDN bypasses (169.254.169.254., 192.168.1.5., 10.0.0.5.). Refs nexu-io/open-design#1119 review feedback (P2 from @lefarcen). * test(connectionTest): tighten trailing-dot coverage per #1122 review Two issues from #1122 review: 1. (P2 from @mrcfps + codex bot) The original `foo.localhost.` case asserted error===undefined on validateBaseUrl, which only proves the URL passed validation — not that the host is identified as loopback. Replaced with direct isLoopbackApiHost(...) assertions on the actual loopback FQDN forms (localhost., 127.0.0.1., 127.0.0.5.) so the test exercises the loopback path the comment claims. 2. (P3 from @lefarcen) Original blocked-FQDN tests covered only 3 of 7 ranges that isBlockedIpv4 handles. Added a dedicated case per range (0.0.0.0/8, 10/8, 100.64/10, 169.254/16, 172.16/12, 192.168/16, multicast >=224) so future regressions in normalizeBracketedIpv6 surface against the full coverage. * docs: drop misleading foo.localhost./endsWith claim in normalizer comment @lefarcen review feedback: isLoopbackApiHost only accepts exact 'localhost', '::1', loopback IPv4, and mapped loopback IPv4 — there's no subdomain or endsWith handling, so referencing 'foo.localhost.' overstates what the trailing-dot strip enables. Rewrite the comment to match actual call sites (isLoopbackApiHost equality + isBlockedIpv4 numeric parse). * feat(daemon): export self-contained HTML via /export/*?inline=1 endpoint (#1312) * test(daemon): add Red unit tests for inlineRelativeAssets helper 14 cases pinning the behavior contract for the upcoming apps/daemon/src/inline-assets.ts helper: - link/script inlining with verbatim body preservation - non-src script attrs preserved (type=module, defer, crossorigin) - relative path resolution (root + nested + deep-nested owners) - self-closing and single-quoted attr forms - negative cases: missing rel, rel=preload, absolute/data/blob/leading-slash - escaping: </style and </script inside body - null-fileReader graceful degradation - duplicate identical tags fully replaced (diverges from apps/web/src/components/FileViewer.tsx:5313's first-match-only; locked decision per plan §3.3) - HTML-escaped data-od-inline-asset attr Tests intentionally Red — module ../src/inline-assets.js does not yet exist. Phase B-G of plan declarative-roaming-gosling.md will turn them green by porting FileViewer.tsx:5248-5354 server-side. Refs nexu-io/open-design#368. * feat(daemon): port inlineRelativeAssets server-side for export endpoint Adds apps/daemon/src/inline-assets.ts — a pure helper that takes (html, ownerFileName, fileReader closure) and returns the HTML with every relative <link rel=stylesheet> and <script src> contents inlined into <style data-od-inline-asset="…">/<script>…</script> blocks. The fileReader closure keeps the helper free of fs/Express coupling so the route handler owns the filesystem boundary. Port source: apps/web/src/components/FileViewer.tsx:5248-5354 — five functions (inlineRelativeAssets, resolveProjectRelativePath, baseDirFor, readHtmlAttr, escapeHtmlAttr). The fetch hop becomes the fileReader closure; replace-all replaces first-match-only per locked design decision §3.3 (inline comment in inline-assets.ts cites the divergence from FileViewer.tsx:5313 and notes the web inline path is on a deprecation track since PR #384 made URL-load the default). Phase B-G of plan declarative-roaming-gosling.md. All 14 unit cases from the Red commit ( |
||
|
|
e1bc83a476 |
feat(analytics): PostHog product analytics (P0 events, consent-gated, packaged) (#1428)
* feat(analytics): scaffold PostHog product-analytics integration
- Add @open-design/contracts/analytics subpath with the 17 P0 event
payload types, header constants, and code↔CSV enum mapping helpers.
- Add apps/daemon/src/analytics.ts with env-gated posthog-node client,
request-scoped analytics context reader, and artifact-id anonymizer.
- Expose GET /api/analytics/config so the web bundle never embeds the
PostHog key at build time; daemon owns POSTHOG_KEY / POSTHOG_HOST.
- Add apps/web/src/analytics module (identity + lazy posthog-js client
+ React provider) and mount it under <I18nProvider> in app/layout.
No event wiring yet — that lands in the next commit alongside trigger
points (App.tsx, EntryView, NewProjectPanel, SettingsDialog, FileViewer,
runs.ts).
* feat(analytics): wire app_launch, home_view, home_click, project_create_result
- App.tsx: fire app_launch once after first effect tick. handleCreateProject
now emits project_create_result on both success and failure paths.
- EntryView.tsx: home_view (page) gated on agents loading so
has_available_cli isn't transiently false; home_view (asset_panel) fires
per top-tab change with the right result_count.
- NewProjectPanel.tsx: home_click create_button fires before delegating to
the parent; a fresh request_id is generated here and threaded through
onCreate so the matching project_create_result stitches via $insert_id.
- contracts/analytics: tighten createTabToTracking and topTabToTracking
for the worktree branch's renamed tabs (live-artifact, templates).
* feat(analytics): wire settings_view + 3 settings_click events
- settings_view fires on dialog mount and on every section switch,
carrying the active section (mapped via settingsSectionToTracking
for the 16-section worktree layout), execution_mode, and the
selected CLI provider id when present.
- settings_click execution_mode_tab: setMode now emits before/after
values whenever the user toggles between Local CLI and BYOK.
- settings_click cli_provider_card: agent card onClick reports
cli_provider_id via agentIdToTracking (kiro → other).
- settings_click byok_field: onFocus added to api_key, model select,
and base_url inputs; provider_id widened to include google so the
worktree's Gemini protocol slot type-checks.
* feat(analytics): wire studio_view + studio_click chat, studio_view artifact
- packages/contracts/src/analytics/artifact-id.ts: FNV-1a 64-bit helper
produces a 16-hex anonymized id for (projectId, fileName). Stable
cross-platform so the daemon and the web bundle resolve the same id
without a Web Crypto round-trip; daemon now re-exports it.
- ChatComposer: studio_view chat_panel fires once per project mount,
studio_click chat_composer fires on attachment + send buttons with
estimated user_query_tokens (length/4) and has_attachment.
- FileViewer: studio_view artifact fires once per (project, file) at
the dispatcher level, before any sub-viewer renders, with
artifact_kind derived from the renderer registry / file.kind table.
- Widen TrackingExportFormat to include markdown and cloudflare_pages
so the worktree branch's full share menu can emit verbatim.
* feat(analytics): wire studio_click share_option + artifact_export_result
HtmlViewer's share menu now emits both events per click via a
fireShareExport helper:
- studio_click share_option fires immediately on click with the chosen
export_format and a fresh request_id.
- artifact_export_result fires when the export resolves — success for
sync exporters (html, markdown, template) the moment the call
returns, success/failed for async exporters (pdf, zip, deploy)
via .then/.catch. The same request_id threads both events so
PostHog stitches click → result via $insert_id.
DEPLOY_PROVIDER_OPTIONS maps to the CSV's vercel / cloudflare_pages
slots; markdown is now a first-class export_format value.
Also ignore .env.local so local POSTHOG_KEY / .env-style secrets
don't get committed.
* feat(analytics): emit run_created and run_finished from the daemon
POST /api/runs now reads the analytics context off the
x-od-analytics-* headers the web client sets on every fetch, then:
- Captures run_created with project_id, conversation_id, run_id,
model_id, agent_provider_id (mapped via agentIdToTracking),
skill_id, design_system_id, plus the token_count_source marker.
- Schedules a run_finished capture on runs.wait(run) resolution,
mapping succeeded/canceled/failed to success/cancelled/failed and
reporting total_duration_ms.
Both events use a stable insert_id derived from the same uuid so
PostHog dedupes the daemon-side mirror against any future
web-side capture without double-counting.
Token sub-fields (user_query_tokens/system_prompt_tokens/...) stay
omitted in v1 — the claude-stream parser only exposes input/output
totals today. See tracking-doc-issues.md §3.2.
* feat(analytics): emit settings_cli_test_result + settings_byok_test_result
The original BLOCKING-list assumed these CSV P0 events were not
implementable in this branch because main lacked Test buttons. The
worktree HEAD actually wires `handleTestAgent` and `handleTestProvider`
in SettingsDialog, so both events are now in scope.
- handleTestAgent emits settings_cli_test_result on success and
failure paths with cli_provider_id mapped via agentIdToTracking,
result drawn from result.ok / catch branch, error_code from
result.kind or the thrown error name, and duration_ms timed via
performance.now().
- handleTestProvider emits settings_byok_test_result analogously,
using apiProtocol (anthropic|openai|azure|ollama|google) directly
as provider_id — wider than the CSV's 5-value enum, documented in
tracking-doc-issues.md §2.5.
Contracts: add SettingsCliTestResultProps / SettingsByokTestResultProps
plus matching track* helpers. AnalyticsEventName union now covers all
14 P0 events this branch supports.
* feat(analytics): gate PostHog on the existing telemetry.metrics consent
The integration now reuses the same first-launch privacy banner +
Settings → Privacy toggle that gates Langfuse, so a single user
decision controls both telemetry sinks.
- /api/analytics/config now consults the persisted AppConfigPrefs:
it returns enabled=true only when POSTHOG_KEY is set AND the user
has chosen "Share usage data" (telemetry.metrics === true). The
response also echoes installationId so the web client uses the
same anonymous id Langfuse keys off of — one identity per install,
shared across both sinks.
- Web AnalyticsProvider:
- Bootstrap fetch resolves installationId and threads it through
the x-od-analytics-anonymous-id header on every /api/* fetch,
so daemon-side captures (run_created / run_finished /
project_create_result) land on the same person record.
- Exposes a setConsent(granted) method that calls posthog-js's
opt_in_capturing / opt_out_capturing, wired from App.tsx via a
useEffect watching config.telemetry?.metrics. Toggling Privacy
→ metrics now stops/resumes events immediately, no reload.
- app_launch additionally gates on telemetry.metrics so a freshly-
declined user fires nothing, and a freshly-opted-in user fires on
the next reload.
* feat(packaging): bake POSTHOG_KEY into packaged daemon spawn env
Wires PostHog product analytics through the same Langfuse-style build-
secret pipeline so official Open Design builds ship with the key while
fork builds compile without it (the integration short-circuits cleanly
when POSTHOG_KEY is absent).
tools/pack
- resolveToolPackConfig reads POSTHOG_KEY / POSTHOG_HOST from
process.env at packaging time, validates them (no whitespace in the
key, http(s) URL for host, trailing-slash strip), and stamps them on
ToolPackConfig. Fork builds without the env vars simply omit the
fields; the daemon-side gate keeps things off in that case.
- Mac, Windows, and Linux packaged-config writers each append the two
fields to open-design-config.json next to the existing
telemetryRelayUrl entry.
apps/packaged
- RawPackagedConfig / PackagedConfig surface posthogKey / posthogHost
so the Electron entry and headless entry both forward them to the
daemon sidecar.
- buildPackagedDaemonSpawnEnv emits POSTHOG_KEY / POSTHOG_HOST into
the daemon child env when present. The daemon's existing analytics
module reads these via process.env — no daemon-side changes needed.
- The headless packaged path falls back to process.env for fields the
builder hasn't injected, mirroring how OPEN_DESIGN_TELEMETRY_RELAY_URL
is read there.
CI
- release-beta.yml and release-stable.yml expose POSTHOG_KEY (secret)
and POSTHOG_HOST (var) at workflow-env scope so every packaging job
inherits them. PR / fork builds without these set simply skip the
bake step.
Tests
- tools/pack: config.test.ts covers bake-through, fork-build omission,
whitespace rejection, invalid-URL rejection, and trailing-slash
normalization.
- apps/packaged: sidecars.test.ts covers buildPackagedDaemonSpawnEnv
forwarding the keys when present and omitting them when null.
* feat(analytics): enable PostHog autocapture + perf + exceptions
Flip on the PostHog SDK's automatic diagnostic features so we capture
click paths, page transitions, web vitals, dead clicks, and browser
exceptions without scattering instrumentation through the codebase.
Privacy defense lives in one place — apps/web/src/analytics/scrub.ts —
wired in via posthog-js's `before_send` hook so every outgoing event
passes through the same audit point:
- $autocapture / $rageclick / $dead_click / $copy_autocapture:
strips $el_text and value/placeholder/aria-label attrs from any
input, textarea, password input, or contenteditable element. PostHog
autocapture does not capture input.value by default, but $el_text
on a <textarea> reflects the typed content — that's the prompt
body for us, so it has to be scrubbed every time.
- $pageview / $pageleave: drops query string and fragment from
$current_url / $referrer so any future ?q=… can't leak.
- $exception: rewrites file:// and absolute filesystem paths in
stack frames to app://apps/<repo-relative> so we don't ship the
user's home directory.
- Suppresses $opt_in entirely — duplicate of our explicit
setConsent toggle in App.tsx.
Element-level defense in depth is limited to the single most sensitive
surface: the chat composer textarea gets `ph-no-capture` so PostHog
never even generates an event for clicks inside that subtree. Every
other input relies on scrub.ts — sprinkling the class through every
form would be noisy and easy to forget on new surfaces.
The existing Privacy → "Share usage data" toggle continues to gate
every new feature: posthog-js's opt_out_capturing() halts autocapture,
$pageview, $exception, web vitals, and dead clicks alongside the
explicit capture() calls — one global switch.
11 unit tests pin the scrub rules in apps/web/tests/analytics-scrub.test.ts.
* ci(nix): bump pnpmDepsHash for posthog-js + posthog-node additions
Adding posthog-js to apps/web and posthog-node to apps/daemon changed
pnpm-lock.yaml, which Nix's fixed-output pnpmDeps derivation pins by
sha256. The CI nix flake check failed with:
specified: sha256-KF3Mld72/iau+pJmA7HvnanRx8VLtDP0N624SKrtrrc=
got: sha256-PGFgX4lYyeH2TRAXfUq52A3EOa6bb1gO59hPsXhEk3s=
Copy the new hash into both nix/package-web.nix and
nix/package-daemon.nix per the procedure documented in nix/README.md
§"First-build hash pinning".
* feat(analytics): unify PostHog identity with Langfuse installationId
PostHog's distinct_id is the installationId stamped by /api/analytics/
config; Langfuse already reads the same id off app-config.json to
populate trace.userId. With both sinks keying off the same anonymous
identity, dashboards can correlate user actions (PostHog events) with
LLM runs (Langfuse traces) without re-identifying.
Two gaps closed:
1. applyConsent(false) — clear posthog-js's persisted ph_*_posthog
localStorage entry on opt-out via posthog.reset(). Without this, a
user who opts out, then clicks Delete my data, then re-opts in
would see PostHog stitch their new session to the deleted identity
because bootstrap.distinctID only takes effect on first init.
2. applyIdentity(newInstallationId) — Delete my data rotates the
installationId in app-config; App.tsx now watches config.installationId
and calls posthog.reset() then identify(newId) so the next event
batch is fully decoupled from the deleted one. Idempotent on
same-id re-renders so benign config refreshes don't churn PostHog
identities.
The fetch wrapper's x-od-analytics-anonymous-id header also flips to
the new id on rotation so daemon-side captures (run_created /
run_finished) land on the same person record from the very next API
call, not after a reload.
The end-to-end rotation flow is verified against a live PostHog
project; these unit tests pin the safety guards (no-client paths, null
inputs) since stubbing posthog-js's init-loaded callback chain is
brittle.
* fix(langfuse): require both metrics AND content consent for trace reports
Tightens the Langfuse gate so a user who shares anonymous metrics but
NOT conversation content stops emitting Langfuse traces entirely —
Langfuse is used for turn-quality evals which only make sense with
prompt/output bodies. PostHog (product analytics, content-free) stays
gated on `metrics` alone and is unaffected.
i18n: "Conversation content" → "Conversation and tool content" with
hints expanded to mention tool inputs/outputs so the consent surface
matches what the trace actually carries (en + zh-CN).
Bundled here per PR scope — change originated outside this PostHog
PR but lands cleanly on the same files; gating Langfuse strictly
on `content` makes the dual-sink consent model (PostHog = metrics,
Langfuse = metrics + content) symmetric across both i18n locales and
the daemon-side gate.
* feat(analytics): wire byok_provider_option + fix PR review P1s
Adds the BYOK protocol-chip click event (5-value provider_id mirroring
the apiProtocol Settings UI) and resolves four P1 review threads on
PR #1428.
byok_provider_option:
- New SettingsClickByokProviderOptionProps in contracts (provider_id =
anthropic|openai|azure|google|ollama; maps to CSV's 5 values per
tracking-doc-issues.md §2.5).
- trackSettingsClickByokProviderOption helper in apps/web/src/analytics.
- SettingsDialog hooks it on the protocol-chip onClick alongside the
existing setApiProtocol call; is_selected reflects whether the chip
was already active.
Review fixes:
1. client.ts (Siri-Ray): clear `initPromise` when the resolution is
null so a Privacy → metrics opt-in after a previous decline triggers
a fresh /api/analytics/config fetch. Without this, the disabled
response was cached forever — first-session opt-in needed a reload
to start sending PostHog events.
2. provider.tsx (Siri-Ray): replace `url.includes('/api/')` with a
strict same-origin + /api/ pathname check (shared
`isSameOriginApiCall` helper). Outbound third-party URLs containing
`/api/` (e.g. provider.example.com/api/x) no longer receive our
x-od-analytics-* headers.
3. provider.tsx (codex-connector, lefarcen): gate header injection on
`resolvedAnonId` being non-null. When Privacy → metrics is off,
/api/analytics/config returns enabled=false → resolvedAnonId stays
null → wrapper never installs → daemon can't read consent-bearing
headers → no daemon-side PostHog event. setConsent now also clears
resolvedAnonId on opt-out and re-fetches on opt-in.
4. daemon/analytics.ts (defense in depth): createAnalyticsService now
takes dataDir and capture() re-reads app-config to check
telemetry.metrics inside the fire-and-forget wrapper. Even if a
stale header somehow reaches the daemon after opt-out, the capture
is dropped before posthog-node.capture is called.
* fix(web): place "Share usage data" on the right in privacy consent banner
Swap button order in PrivacyConsentModal and the in-settings ConsentCard
so the affirmative "Share usage data" lands on the right and "Not now"
on the left. Matches the OK-on-the-right pattern users expect for
primary actions.
Both buttons keep equal visual prominence (same .privacy-consent-action
styling) so the swap doesn't change the EDPB equal-prominence stance
called out in the original Langfuse telemetry spec.
* feat(analytics): populate run_finished token totals from claude-stream usage
Daemon's claude-stream parser already emits agent usage events with
input_tokens / output_tokens totals; the run service buffers them in
run.events and Langfuse reads them out the same way. The run_finished
PostHog event was leaving these fields empty.
Scan run.events for the most recent agent usage frame on terminal
transition and emit input_tokens / output_tokens / total_tokens when
present. token_count_source flips to 'provider_usage' only when at
least one count landed; runs without provider-side usage data keep
'unknown'.
Provider does not break the input down into the 7 sub-fields the
tracking doc lists (memory / context / attachment / system_prompt /
…); those stay omitted until a parser change exposes them.
* feat(analytics): estimate user_query_tokens from prompt length
The user_query_tokens field for run_created / run_finished was hardcoded
to 0. We can't tokenize without bundling a model-specific tokenizer, but
the character/4 heuristic is the industry-standard estimate when one
isn't available and is enough for funnel analysis (prompt-length cohorts,
short-vs-long-query conversion rates).
Extracted from req.body via the same telemetryPromptFromRunRequest
pattern the daemon already uses for langfuse-bridge (currentPrompt then
message fallback). Only the integer count goes to PostHog — the prompt
text itself never leaves the daemon.
token_count_source flips appropriately:
- run_created with a prompt: 'estimated' (was 'unknown')
- run_created with no prompt: 'unknown'
- run_finished with provider usage: 'provider_usage' (overrides
baseProps' 'estimated' value)
- run_finished without provider usage: inherits 'estimated' or 'unknown'
from baseProps so input/output absent doesn't mask the estimate.
|
||
|
|
49ea2499ac |
[codex] Add draw annotation workflow (#1435)
* feat(web): tweaks palette popover with HSL hue-shift recoloring Adds a Tweaks color-palette popover to the HTML preview toolbar. Selecting a palette re-skins the iframe in place via a srcDoc-side bridge that walks the DOM and shifts every chromatic paint to the target hue while preserving each color's saturation and lightness — pale tints stay pale, bold CTAs stay bold, just in the new color family. Mono-noir desaturates instead of shifting. - runtime/srcdoc: new injectPaletteBridge + paletteBridge / initialPalette options - file-viewer-render-mode: paletteActive flips URL-load back to srcDoc so the bridge can be injected - FileViewer: state, popover, postMessage wiring, srcDoc + useUrlLoadPreview integration - PaletteTweaks: popover UI with Original + Coral / Electric / Acid forest / Risograph / Mono noir - PreviewDrawOverlay: stub pass-through until the draw branch lands * feat(web): hide finalize-design toolbar from project header * test(e2e): skip project actions toolbar flow after toolbar removal * Add draw annotation workflow * Restore project actions toolbar |
||
|
|
2d405fae96 | fix:align artifact preview exit button (#1445) | ||
|
|
09a8fa8d64 |
feat(web): Critique Theater Phase 8 (8 Theater components, barrel, role-keyed CSS) (#1314)
* feat(web): pure reducer for Critique Theater states (Phase 7.1)
Pure CritiqueState reducer driven by the contracts-level PanelEvent
(the same shape both the live SSE stream and the recorded transcript
emit), so a single reducer powers both the in-flight panel and the
rerun replay. Lifecycle covers run_started → running → (shipped /
degraded / interrupted / failed), with panelist_open / dim /
must_fix / close / round_end events building per-round
CritiquePanelistView entries as they arrive.
Defensive behaviour that surfaced while writing the spec tests:
- Terminal phases (shipped / degraded / interrupted / failed) are
sticky against further lifecycle events for the same run, except
for parser_warning which can land late and is recorded in a side
channel without changing phase.
- A new run_started for a different runId at any time discards the
prior state and reboots, so the UI can launch consecutive runs
without an explicit reset action.
- Events whose runId does not match the active run return the same
state reference, so React's useReducer doesn't re-render
subscribers on stray traffic.
- Round bookkeeping keys by round number rather than "always last",
so an out-of-order panelist_dim for round 1 arriving after a
round 2 dim does not corrupt the round 2 bucket.
Test coverage: 18 cases covering each transition, the runId guard,
sticky-terminal behaviour, the out-of-order round invariant, and
the stable-identity guarantee. Sets up Phase 7.2 and 7.3 to wire
SSE + replay into the same reducer.
* feat(web): useCritiqueStream hook subscribes to SSE and feeds reducer (Phase 7.2)
createCritiqueEventsConnection is a pure connection manager that
mirrors apps/web/src/providers/project-events.ts: opens an
EventSource at /api/projects/:id/events, listens for every name in
CRITIQUE_SSE_EVENT_NAMES, decodes each frame back into a PanelEvent
(stripping the critique. prefix and merging the data payload), and
hands it to the caller's onEvent. Reconnect uses exponential
backoff (1s → 30s) and resets on `ready`; malformed payloads drop
with a dev-mode warning rather than tearing the stream.
useCritiqueStream wraps the manager in a useReducer that owns the
CritiqueState. enabled=false or a null projectId tears down the
connection cleanly; switching projectId closes the old connection
and opens a fresh one. The returned dispatch lets local UI
synthesise actions (e.g. an Esc keypress firing a synthetic
interrupted while a kill request is in flight); production traffic
comes from the SSE stream.
Test coverage:
- sse.test.ts (10 cases, node env): subscription set covers every
CRITIQUE_SSE_EVENT_NAMES channel; payload decoding lifts the wire
shape back to PanelEvent; malformed JSON is swallowed and does
not stop the stream; exponential backoff schedule and ready-reset
semantics are pinned with a setTimeout seam; close() cancels
pending reconnects and shuts the live source; no-op fallback
when EventSource is unavailable.
- useCritiqueStream.test.tsx (6 cases, jsdom env): idle pre-event,
reducer driven by synthetic actions, no connection when disabled
or projectId is null, clean close on unmount, projectId change
reopens cleanly.
* feat(web): useCritiqueReplay hook drives reducer from transcript file (Phase 7.3)
Fetches the per-run NDJSON transcript (one PanelEvent per line),
parses every line via the shared isPanelEvent predicate, and
dispatches into the same CritiqueState reducer the live SSE stream
uses. A single reducer means the UI rendering a replay can be
identical to the live panel, and a UI mounting both
useCritiqueStream and useCritiqueReplay in parallel does not have
to reconcile two state shapes.
speed knob is `paused | instant | live | { intervalMs: N }`.
- instant flushes every event synchronously, useful for opening a
finished run already at its terminal state.
- intervalMs paces dispatches at a fixed cadence so the reviewer
can watch the run unfold.
- paused parses the transcript but holds events back until the
caller advances speed (consumers can drive a scrubber later).
- live is reserved for the future "playback at original cadence"
feature, currently treated as instant; replay timestamps are not
yet persisted with each event so honest pacing requires a
follow-up Phase 7+ task.
gunzip seam handles `.ndjson.gz` transcripts via
DecompressionStream when present; the production fetch path picks
between text and arrayBuffer based on the URL extension. Both seams
are injectable so the unit tests don't need to spin up a real
network or a real gzip pipeline.
Test coverage (8 cases, jsdom env):
- Idle status before any URL is provided.
- speed=instant flushes the full transcript synchronously to
shipped state.
- speed={intervalMs:N} paces with the setTimeout seam, reaching
done after the last tick.
- speed=paused leaves status=playing with no dispatches.
- Empty transcript reports done with state still idle.
- Fetch rejection surfaces an error status with the message.
- Malformed NDJSON lines are skipped; valid events around them
still land.
- .gz transcripts route through the gunzip seam.
Closes the Phase 7 plan tasks 7.1 / 7.2 / 7.3 (reducer + stream +
replay), all on one branch ready for review. Phases 8+ (Theater
components) consume these from this PR.
* fix(web): close payload-override gap + paused-resume bug in Critique Theater hooks (Phase 7 review)
Two P1 fixes from lefarcen's review on PR #1307:
SSE payload override
`sseToPanelEvent` previously spread `data` after the channel-derived
`type`, so a payload-provided `type` could override the channel and
route a `critique.run_started` frame into the reducer as a `ship`
action. Reversed the spread so the channel-derived `type` is
authoritative, and revalidated the resulting object through the
contracts-level `isPanelEvent` predicate before returning. Frames
that fail validation (missing runId, empty runId, unknown type) are
dropped, so a malformed or compromised SSE frame can no longer
dispatch a wrong-shape action into the reducer.
Three new sse.test.ts cases pin the regression: hostile `type:'ship'`
in the payload still resolves to `run_started`, missing runId is
dropped, empty runId is dropped.
Replay pause/resume
`useCritiqueReplay` had one big effect keyed on `transcriptUrl`
only, so flipping `speed` from `paused` to `instant` never re-fired
and the held events sat undispatched. Split into a parse effect
(depends on URL, fetches and stores events in state) and a pace
effect (depends on parsed-events + speed, owns the cursor + timers).
The playback cursor lives in a ref that survives pause/resume
cycles, so flipping `paused` -> `instant` flushes from the current
position rather than restarting (which would double-dispatch
`run_started` and reset the reducer).
Two new useCritiqueReplay.test.tsx cases:
- paused-then-instant transitions from `playing` to `done` and
reaches the shipped terminal phase
- intervalMs paced playback dispatches one event, pauses to drain
the next scheduled timer, flips to instant, and confirms the
remaining transcript drains exactly once (cursor was preserved)
Doc consistency
The earlier source comment in useCritiqueReplay.ts claimed `live`
"paces by recorded timestamps" while the impl used zero-delay
timers and the PR body said it behaves like `instant`. Aligned to
reality: `live` currently behaves like `{ intervalMs: 0 }` (events
drain on successive microtasks via setTimeoutFn) because transcripts
do not yet carry per-event timestamps. Honest timestamp-driven
pacing is queued as a Phase 7+ follow-up.
Validated: pnpm guard, pnpm --filter @open-design/web typecheck,
Theater suite 47/47 (up from 42, +3 sse + 2 replay), full web suite
96 files / 888 tests.
* feat(i18n): seed Critique Theater key block (en + zh-CN; other locales fall back via spread)
* feat(web): Theater PanelistLane component (Phase 8.1)
* feat(web): Theater ScoreTicker component (Phase 8.2)
* feat(web): Theater RoundDivider component (Phase 8.3)
* feat(web): Theater InterruptButton component with Escape keybind (Phase 8.4)
* feat(web): Theater TheaterDegraded chip (Phase 8.5)
* feat(web): Theater TheaterCollapsed post-run summary (Phase 8.6)
* feat(web): Theater TheaterTranscript replay surface (Phase 8.7)
* feat(web): Theater TheaterStage top-level container (Phase 8.8)
* feat(web): Theater CSS using existing semantic tokens (no hex literals)
* feat(web): Theater public exports barrel
* fix(web): resolve P2 + P3 review feedback on Phase 8 (PR #1314)
Addresses all 4 P2 + 3 P3 items from codex, Siri-Ray, and lefarcen.
State-lifecycle fixes (3 x P2)
1. Reducer learns a synthetic `__reset__` action (`CritiqueResetAction`).
Host hooks dispatch it when their gating prop changes so a stale
run from a prior project / transcript cannot bleed into the next
context. Reset is idempotent on idle (returns the same reference).
2. `useCritiqueStream` dispatches `__reset__` at the top of its
connection effect, so a workspace switch from project A (which
streamed a critique) to project B clears the reducer before the
new EventSource opens. enabled=false also clears.
3. `useCritiqueReplay` dispatches `__reset__` at the top of its
parse effect, so transcriptUrl swaps (including swap-to-null after
a replay reached `shipped`) lift the reducer back to idle before
the new fetch starts.
SSE validation (1 x P2)
4. `sseToPanelEvent` now runs a per-variant `hasValidVariantShape`
check after the cheap `isPanelEvent` predicate. A
`critique.ship` frame missing `composite` / `round` / `status` /
`artifactRef` is rejected before reaching the reducer, so
TheaterCollapsed can no longer crash on `undefined.toFixed(1)`.
Every variant's required fields are validated: run_started
(protocolVersion, non-empty cast, maxRounds, threshold, scale),
panelist_* (round, role, plus variant-specific shape), round_end
(round, composite, mustFix, decision in {continue,ship}, reason),
ship (round, composite, status, artifactRef.{projectId,artifactId},
summary), degraded (reason, adapter), interrupted (bestRound,
composite), failed (cause), parser_warning (kind, position).
Reducer correctness (1 x P2)
5. `panelist_open` now materializes the round + an empty panelist
view (`{dims: [], mustFixes: []}`) so TheaterStage can highlight
the in-progress lane the instant the tag opens. Before this, a
stream that emitted only `panelist_open` after `run_started` left
`rounds = []` and the UI rendered no current round until a later
`panelist_dim` arrived.
Polish (3 x P3)
6. Brand role tint swaps from `var(--magenta, var(--accent))` to
`var(--purple, var(--accent))`. `--purple` is actually defined
across the design systems; `--magenta` is not, so Brand was
silently falling through to `--accent` and looking identical to
Designer.
7. New i18n key `critiqueTheater.interruptedSummary` for the
interrupted-collapse copy ("Interrupted at round N, best
composite X.X"). Previously the interrupted branch reused
`shippedSummary` and the UI read "Shipped at round..." for a run
that specifically did not ship. Native value in en + zh-CN; other
locales fall back via `...en` spread.
8. `TheaterDegraded` heading id comes from `useId()` instead of a
hardcoded `theater-degraded-heading`, so two chips rendered on
the same page (chat history with multiple completed runs) keep
their aria-labelledby references unambiguous.
Tests (15 new cases)
- reducer.test.ts (+5): __reset__ on running/terminal/idle, panelist_open materializes round, panelist_open does not stomp prior panelist data.
- sse.test.ts (+6): variant-level rejection for ship without required fields, degraded without adapter, run_started with empty cast, panelist_dim with non-numeric score, round_end with unknown decision, plus a positive fully-formed ship.
- useCritiqueStream.test.tsx (+2): state reset on projectId change, state reset on enabled flip false.
- useCritiqueReplay.test.tsx (+1): state reset on transcriptUrl swap to null after a replay reached shipped.
- TheaterCollapsed.test.tsx (text-pinning update): asserts the interrupted branch reads "Interrupted at round 1" + "best composite 7.9", and explicitly NOT "Shipped at round...".
- TheaterDegraded.test.tsx (+1): two chips on the same page get unique aria-labelledby ids that each resolve to an `<h3>`.
Validated
- pnpm guard clean
- pnpm --filter @open-design/web typecheck clean
- Theater suite: 13 files, 101 tests (was 86 on the first Phase 8 push, +15 new)
- tests/i18n/locales.test.ts 5 of 5 across 18 locales
* fix(web): tighten isPanelEvent in contracts so enum + numeric fields are checked end-to-end (Siri-Ray round-3 P1 on PR #1314)
The variant validator on the web SSE path previously accepted any
`typeof === 'string'` for closed-enum fields (ship.status,
panelist_*.role, degraded.reason, failed.cause, parser_warning.kind,
run_started.cast[]) and any `typeof === 'number'` for numeric fields,
which let NaN / Infinity through. Downstream components index i18n
tables by enum value, so an unknown status or role would land
`SHIP_BADGE_KEY[final.status]` on undefined and crash the translator.
The replay parser had a separate gap: `useCritiqueReplay.parseTranscript`
called the cheap `isPanelEvent` header check directly, so a recorded
line like `{"type":"ship","runId":"r"}` reached the reducer with
composite, status, round, artifactRef, summary all undefined and
TheaterCollapsed then called `final.composite.toFixed(1)` on undefined.
Resolution: move all wire-side validation into the contract guard.
- Export const arrays for the closed enums:
SHIP_STATUSES, DEGRADED_REASONS, FAILED_CAUSES, PARSER_WARNING_KINDS,
ROUND_DECISIONS (PANELIST_ROLES already existed).
- Rewrite `isPanelEvent` in packages/contracts/src/critique.ts to be the
single deep validator: header (known type + non-empty runId) plus
every variant-specific required field plus closed-enum membership
plus Number.isFinite on every numeric field. Documented as the wire
source of truth.
- Drop the local `hasValidVariantShape` from web/sse.ts; sseToPanelEvent
now relies entirely on the contract guard, and parseTranscript in
useCritiqueReplay (which already uses isPanelEvent) gets the deeper
validation for free.
Tests (TDD, red-first):
- packages/contracts/tests/critique.test.ts: 13 new cases pinning the
strict guard directly (well-formed across every variant, every
rejection path: unknown type, empty/non-string runId, unknown enum,
non-finite numeric, missing variant field).
- apps/web/tests/components/Theater/state/sse.test.ts: 9 new cases for
each closed-enum rejection on the wire path plus a positive sweep
across every legal enum value across every variant.
- apps/web/tests/components/Theater/hooks/useCritiqueReplay.test.tsx:
2 new cases for incomplete and unknown-enum transcript lines.
Verified:
- pnpm --filter @open-design/contracts test 4 files / 30 tests green.
- pnpm --filter @open-design/contracts build clean.
- pnpm --filter @open-design/web typecheck clean.
- pnpm --filter @open-design/web test 107 files / 976 tests green.
* fix(contracts): enforce numeric domains in isPanelEvent (lefarcen P2 on PR #1314 round 4)
The strict guard from PR #1314 round 3 enforced enum membership and
Number.isFinite, but accepted any finite number where the contract
intends a specific domain: scale: 0 (ScoreTicker divides by it),
negative thresholds, fractional rounds, negative mustFix, etc.
ScoreTicker.tsx writes `var(--scale, ${state.scale})` into inline
CSS and divides by it for tick width, so a guard-passing scale: 0
shipped Infinity into the rendered style. Negative composite /
score values reached downstream code that assumes >= 0.
Resolution: mirror the daemon-side Zod domain constraints in the
runtime guard.
Three new helpers in packages/contracts/src/critique.ts:
- isPositiveInt(v): integer with v > 0. Used for round, maxRounds,
scale, protocolVersion (all 1-indexed in the orchestrator).
- isNonNegativeInt(v): integer with v >= 0. Used for mustFix,
position, bestRound. bestRound: 0 is the valid sentinel for
'interrupted before any round closed'.
- isNonNegativeFinite(v): finite number with v >= 0. Used for
composite, score, dimScore, threshold. Threshold may be
fractional (e.g. 8.5 on a scale of 10).
Cross-field check inside run_started: threshold <= scale (the daemon
Zod schema enforces this with an epsilon refine, the wire guard
matches the same intent).
Tests (TDD, red-first) added in packages/contracts/tests/critique.test.ts:
- 22 new rejection cases across every numeric field that
previously slipped through: scale: 0, negative scale, fractional
scale, maxRounds: 0, fractional maxRounds, protocolVersion: 0,
fractional protocolVersion, negative threshold, threshold > scale,
round: 0, fractional round, negative dimScore / score, negative /
fractional mustFix, negative composite, ship round: 0, negative /
fractional bestRound, negative interrupted composite, negative /
fractional parser_warning position.
- 3 positive boundary cases that must still pass: threshold == scale,
fractional threshold within [0, scale], interrupted with
bestRound: 0 (no round completed before interrupt), parser_warning
with position: 0 (start of stream).
Verified:
- pnpm --filter @open-design/contracts build clean.
- pnpm --filter @open-design/contracts test: 4 files / 59 tests green
(was 37 before the new domain cases).
- pnpm --filter @open-design/web typecheck clean.
- pnpm --filter @open-design/web test: 110 files / 1004 tests green;
no regression on Theater suite, sse validator, replay parser, or
assistant-feedback widget tests.
---------
Co-authored-by: Nagendhra <nagendhra405@gmail.com>
|
||
|
|
7b191b5f85 |
fix: load Orbit templates from design templates (#1442)
(cherry picked from commit
|
||
|
|
a7e6e0dc3d |
fix(web/AssistantMessage): update status block detail to latest value instead of skipping (#1413)
When using Open Design with an ACP agent (e.g. Devin for Terminal), after selecting a non-default model in the picker, the model badge under the conversation header kept showing the agent's initial default (e.g. `model swe-1-6-fast`) instead of the running model. The conversation header text and the agent itself reflected the selected model correctly — only the badge UI was stale. Root cause: `buildBlocks()` in this file de-duplicated consecutive status events with the same label by SKIPPING the new one rather than updating the existing block. The daemon emits two `status: label='model'` events per turn — once after `session/new` returns with the agent's default model, then again after `session/set_config_option` succeeds with the user-selected model (see `apps/daemon/src/acp.ts` ~lines 495 and 587). The dedupe kept the first and skipped the second, so the badge stayed stuck on the default. Fix: update the existing block's detail to the latest value instead. "Most recent detail wins" is more accurate than "first one wins forever" for every status label that reaches this code path (`'model'`, `'initializing'`, etc.; the filter at lines 1156-1162 already drops the labels we don't want to surface as badges: streaming, starting, requesting, thinking, empty_response). Adds two regression tests in apps/web/tests/components/AssistantMessage.test.tsx: - Two sequential `status: 'model'` events with different details render the second detail and not the first (the Bug A scenario). - Two sequential events with the SAME detail still collapse to a single badge (no regression in the existing dedupe behavior). `pnpm guard` clean; AssistantMessage tests pass 22/22. Manually verified working in a local build against Devin for Terminal `2026.5.6-4` with #1208 applied — badge now updates to the selected model on every turn. |
||
|
|
e6c5560884 | Fix appearance accent color persistence (#1439) | ||
|
|
23218eacd9 |
refactor(settings): use tiled language picker instead of dropdown (#1406)
The Language section in Settings rendered a single-button dropdown trigger that opened a floating menu. With one visible label and lots of empty panel space, the layout misled users into thinking only one language existed. Replace the dropdown trigger + portaled menu with an inline tile grid that shows every locale at a glance and clicks directly to switch. Side effects of the new layout: the languageOpen / languageMenuRect state, the dynamic placement effect, the resize-close effect, the mousedown click-outside handler, and the languageRef are gone. The global Escape handler no longer needs to guard against the menu being open. CSS for .settings-language-picker, .settings-language-button, .settings-language-menu, and .settings-language-option is replaced by .settings-language-grid (auto-fill 180px minmax columns) + .settings-language-tile. Tests in SettingsDialog.execution.test.tsx that drove the dropdown (click trigger → click menuitemradio → assert menu closed) are rewritten to drive the tiles directly via the radio role. Refs #1347 |
||
|
|
0220124e0f |
test: add Memory and Routines coverage (#1400)
* test: align extended Playwright coverage with current UI behavior * test: address extended suite review feedback * test: fix Codex fallback config hydration in e2e * test: add Memory and Routines coverage * test: fix Memory and Routines component test typing * test: include Memory and Routines e2e in extended suite |
||
|
|
6c3fd86642 |
fix(daemon/acp): terminate ACP child after clean prompt completion (#1286)
* fix(daemon/acp): terminate ACP child after clean prompt completion (Bug B / #1265) Some ACP agents (notably Devin for Terminal) keep the child process alive after stdin closes, waiting for the next prompt. Open Design spawns a fresh agent per chat turn and relies on child.on('close') to finalize the run, so without an explicit signal-driven shutdown the chat sits stuck in the 'working' state indefinitely. Three small, targeted changes: - apps/daemon/src/acp.ts: After a clean session/prompt response we schedule a 500ms grace period and then SIGTERM the child. This mirrors the pattern detectAcpModels() already uses after model discovery. The grace period leaves well-behaved agents that exit on stdin.end() unaffected. - apps/daemon/src/acp.ts: New completedSuccessfully() method on the session handle reports whether the prompt resolved without a fatal error or abort, so the consumer can distinguish 'clean signal exit' from 'genuine signal failure'. - apps/daemon/src/server.ts: child.on('close') now treats a SIGTERM exit as 'succeeded' when acpSession.completedSuccessfully() is true. - apps/web/src/providers/daemon.ts: Trust the server's authoritative endStatus; the signal/non-zero-code safety net no longer overrides an explicit 'succeeded' status, so the chat doesn't surface a fake 'agent exited with signal SIGTERM' error after a clean ACP run. Daemon tests cover the SIGTERM grace timer, clean early-exit (timer cleared), and completedSuccessfully() abort/error states. Manual UI test on plain main + this fix confirms Devin chats now return to ready automatically after Done · ... * fix(daemon/connectionTest): treat ACP clean SIGTERM as success Codex review on #1286 caught that the new SIGTERM in attachAcpSession breaks ACP connection tests for agents that don't shut down on stdin.end() (the exact Devin behavior the patch targets). attachAgentStreamHandlers() in connectionTest.ts now also respects acpSession.completedSuccessfully(), mirroring the same check we apply in server.ts. Without this, a clean prompt response followed by our SIGTERM would set winner.signal === 'SIGTERM', flip exitedCleanly to false, and the connection test would report 'agent_spawn_failed' even when the agent had returned a healthy response. Also widened the AgentSpawnHandle type so completedSuccessfully is visible on the structural type used inside connectionTest.ts. All 56 daemon tests still pass; typecheck + guard clean. * fix(daemon/acp): narrow ACP success-on-signal override to forced-SIGTERM Looper review on #1286 caught that the success predicate was broader than the SIGTERM case it was meant to handle. `completedSuccessfully()` flips to true as soon as the ACP `session/prompt` response is processed, but it does not say why the child later closed. With the broad predicate, an ACP agent that returned a prompt result and then exited with code 1 (or was killed by SIGKILL/SIGSEGV) was still marked 'succeeded', regressing the existing close-status behavior for genuine post-response process failures. Scope the override to the exact forced-shutdown shape this PR introduces: code === null && signal === 'SIGTERM' && acpCleanCompletion Applied to both `server.ts` (chat run finalization) and `connectionTest.ts` (connection-test classification). Any other post-response failure now falls through to 'failed' / 'agent_spawn_failed' as before. All 59 daemon tests still pass; typecheck + guard clean. * fix(web/daemon): only bypass exit-code safety net on explicit server success Looper review on #1286 caught that the previous web change trusted `endStatus === 'succeeded'` absolutely, but `endStatus` can become 'succeeded' in two distinct ways: 1. The SSE end event explicitly carries `status: 'succeeded'` (authoritative server declaration). 2. The end event omits or has an invalid `status` field and the handler silently falls back to 'succeeded' as a local default. Both produced `endStatus === 'succeeded'` in the existing code, so the new safety-net bypass treated them identically. That regressed backward compat: a compatible or older daemon emitting an end event like `{code:1}` or `{code:null,signal:"SIGTERM"}` with no `status` would suddenly skip the failure banner. Track explicit success separately via `serverDeclaredSuccess`, set true only when: - The SSE end event has `status === 'succeeded'`, or - The fallback `fetchChatRunStatus` REST path returns `status === 'succeeded'` (which the existing `isChatRunStatus()` guard already proves is explicit). The safety net is now bypassed only on that explicit signal; the local-fallback success path still reaches the exit-code/signal check so real failures surface as before. Adds three web-side regression tests in `apps/web/tests/providers/sse.test.ts`: - Explicit `status: 'succeeded'` + SIGTERM → onDone called, no error - End event with `{code:1}` and no `status` → onError surfaces 'agent exited with code 1' as before - End event with `{code:null,signal:'SIGTERM'}` and no `status` → onError surfaces 'agent exited with signal SIGTERM' as before `pnpm guard` + daemon typecheck clean; 27/27 SSE tests pass (up from 24). |
||
|
|
5ff578dc8d |
fix: support object-style question-form options (#1293)
* fix: support object-style question-form options * fix: preserve stable option values in form submissions |
||
|
|
2f51f3c1ae |
feature: refine assistant artifact feedback (#1379)
* feature: refine assistant artifact feedback * fix: clear hidden custom feedback reason * test: update assistant feedback expectations |
||
|
|
aeb6cde923 |
prevent duplicate saves and add template deletion (#1294)
* prevent duplicate template entries on repeated save * add delete button to saved template list Templates can now be removed from the template picker via a hover x button, calling the existing DELETE /api/templates/:id endpoint. * add missing onDeleteTemplate prop in test fixtures * add template deletion flow test for NewProjectPanel * reject template names longer than 100 characters * preserve original createdAt on template update |
||
|
|
9c489aa045 |
feat(web): redesign Designs tab cards — covers, tags, overflow menu, multi-select (#1161)
* feat(web): redesign Designs tab cards — covers, tags, overflow menu, multi-select - Render real previews on project cards: HTML iframe / image / video / hashed gradient fallback with project initial; lazily fetches the project's primary file when metadata.entryFile is unset, prefers index.html → newest html → image → video. - Live artifact card thumbnails embed the rendered artifact URL via sandboxed iframe. - Replace the per-card close button with a `…` overflow menu (Rename, Delete) that opens on hover/click; click-outside and Esc close it. - Add multi-select mode (toolbar toggle → checkbox per card → "N selected · Delete · Cancel" pill) with batch delete via the existing onDelete prop. - Add a category tag to every card (Prototype / Live Artifact / Slide / Media) derived from project.metadata.intent / kind / skillId. - Replace browser prompt() and confirm() with custom modals (rename input + danger-confirm) reusing the existing .modal shell. - Add `more-horizontal` icon and 16 new i18n keys across all 18 locales (zh-CN/zh-TW localized; others fall back to English). * test(e2e): update home delete flow for overflow menu + custom confirm modal The previous flow targeted a per-card X button labelled "delete project <name>" and asserted on a native `dialog` event. The card UI now exposes a `…` overflow menu and a styled confirm modal, so reach delete via the menu and assert against the modal's Cancel / Delete buttons instead. * fix(web): harden Designs tab preview sandbox * fix(web): hide Designs select mode in kanban |
||
|
|
77f69257a7 |
feat(web): in-context comment thread for the artifact preview (#1276)
* feat(web): free-pin fallback in comment mode for unannotated artifacts When the artifact has no data-od-id annotations, clicking in Comment mode now posts a synthetic position-based target so the host opens a popover at the click location. Daemon upsert validation requires a non-empty selector/label, so the pin uses [data-od-pin=ID] and label 'pin'. Coordinates are document-space (viewport + scrollY) so pins stay anchored after scroll/reload. Clicks on interactive elements (a/button/input/textarea/select/label/contenteditable) keep their native behavior and are not pinned. * feat(web): tighten comment popover layout for free-pin and element targets The popover header used to dump the raw elementId verbatim — fine for data-od-id targets like 'hero-cta' but jarring for free-pins where elementId is a synthetic 'pin-...' string. Branch on the prefix and show 'Pin · at X, Y' for free-pins; keep the label + selection kind for real element / pod targets. Replace the text 'Close' button with an icon-only close affordance to match the popover-as-card visual. Action row is now two right-aligned buttons (Comment + Send to Claude) for element targets and (Add note + Send to Claude) for pod targets, eliminating the three-button row that wrapped onto two lines at narrow widths. The 'Remove' affordance for existing comments stays left-aligned. * feat(web): drop comments tab from chat sidebar The chat sidebar's 'Comments' tab listed saved/attached preview comments but duplicates the per-element popover already shown in the artifact viewer. Hide the tab and its content while the right-side comment thread panel takes over the same surface in-context. The CommentsPanel / CommentSection components stay defined as dead code for the moment so callers and translation keys remain valid; a later pass can delete them. * feat(web): right-side comment thread panel in board mode Render a 320px CommentSidePanel anchored to the right of the artifact preview whenever board (comment) mode is on. The panel lists every saved preview comment for the current file with an avatar initial, the element label (or 'Pin' for free-pin synthetic ids), an Xd/Xh/Xm-ago timestamp, the note body, a Reply link, and a checkbox. Reply focuses the comment's element via liveSnapshotForComment so the popover opens at the right anchor. Selecting one or more comments via the checkboxes surfaces a 'N selected · Clear · Send to Claude' action bar above the list; Send to Claude reuses the existing onSendBoardCommentAttachments pipeline via commentsToAttachments. The panel takes the place of the chat sidebar's removed Comments tab so the thread lives next to the artifact instead of behind a tab switch. * feat(web): styles for right-side comment thread panel Floating 320px panel anchored to the right edge of the artifact preview with a scrollable comment list and a coral selection bar that appears when one or more comments are checked. Selected items get a coral tint; the reply / check / send-to-claude controls match the popover's coral primary tone. * feat(web): toast confirmation on comment save, close popover After savePersistentComment succeeds, close the popover via clearBoardComposer and surface a transient 'Comment saved' (or 'Pin saved' for free-pin targets) toast for 2.2s. Replaces the previous behavior where the popover stayed open with an empty draft after save, which left users uncertain whether the save landed and forced an extra click to dismiss. * feat(web): position the comment-save toast at the top of the preview * feat(web): allow editing saved comment notes via the side panel Rename the per-item 'Reply' affordance to 'Edit' (no thread model exists yet, so reply was misleading) and pre-fill the popover with the existing note when clicked. The save path goes through onSavePreviewComment which the daemon implements as an upsert keyed on (project, conversation, filePath, elementId), so the edit overwrites the existing row's note without spawning a duplicate. Also fall back to a snapshot synthesized from the saved comment's own fields when the corresponding live target is no longer in the iframe DOM (e.g. free-pin parents that were re-rendered), so the edit path still works after artifact reloads. * feat(web): hide already-sent comments from the side panel After Send to Claude, the daemon flips the comment status from 'open' to 'applying' (and then 'needs_review' / 'resolved' / 'failed' depending on the run). Filter the side panel to status === 'open' so sent comments visibly leave the list — the user gets clear feedback that the send landed and the panel stays focused on actionable, un-sent items. * feat(web): drop single-tab bar and conversation count badge After the Comments tab was removed the chat header still rendered a one-tab 'tablist' just for the Chat tab, which read as visual noise without a sibling to switch between. Drop the tabs wrapper entirely; the chat content stays mounted and the header now hosts only the conversation-history affordance. Also drop the numeric badge that overlaid the conversation history button: counting open conversations next to a generic history icon was easy to mistake for an unread / notification count. The dropdown itself remains the canonical place to see and switch between past conversations. * feat(web): right-align chat header actions after tab bar removal With the tabs wrapper gone, chat-header-actions sat flush left because nothing was pushing it across the header. Add margin-left: auto so the history / new-conversation / collapse buttons land at the right edge, matching the design files / index.html tab row's own right-aligned controls. * feat(web): rename board-mode toggle to Comment with comment icon The artifact preview toolbar's board-mode entry was labeled 'Tweaks' with the tweaks icon, which collided with the palette Tweaks button next to it and hid the comment capability behind a generic label. Rename to 'Comment' with the comment icon and switch to the viewer-action class so the button matches the surrounding toolbar items (Edit/Draw) and the coral active state lands on the right surface. * fix(web): pass designTemplates to ProjectView in api-empty-response test The test props for ProjectView were missing the designTemplates prop that was added to Props in #955 (generic skills split). CI's strict typecheck (tsc -b --noEmit) caught it; local runs that hit project references differently did not. Pass an empty SkillSummary array — matches the empty skills fixture for the same reason. |
||
|
|
928079daf5 |
feat(web): consolidate Image/Video/Audio entries into a Media tab (#1167)
Reduces the New Project panel's top-level tab count by collapsing the three media surfaces into a single Media tab with an inner segmented control, and polishes the controls inside that tab so they stop dominating the panel: - Media tab + segmented (Image / Video / Audio) inside the panel body. Underlying ProjectKind branches and submission contract unchanged — the daemon still receives kind=image/video/audio. - Model picker rewritten as a combobox: one trigger row + searchable, provider-grouped popover with Recommended badges. Replaces the flat grid of provider-grouped cards that scrolled past the fold once the fourth provider landed. - Aspect picker compressed from a 5-card grid to a single row of segmented pills with mini ratio glyphs. - Image surface no longer carries a free-form Style notes field; it was redundant with the prompt template + main prompt input. - Live artifact tab locks fidelity to high-fidelity (the wireframe option is now hidden) — a wireframe live artifact doesn't make sense and the picker added noise. i18n: adds tabMedia / titleMedia / model* keys across all 18 locales, removes imageStyleLabel / imageStylePlaceholder. Tests + e2e selectors updated to drive the new Media tab + segmented surface flow. |
||
|
|
140a4e1ff6 |
Improve responsive preview and design handoff outputs (#1224)
* feat: improve responsive design handoff * feat: refine cross-platform design outputs Changelog:\n- Add auto-fit responsive preview behavior for tablet/mobile frames.\n- Add landing page and OS widgets metadata options with project header chips.\n- Strengthen prompt contracts for modern breakpoints, app-specific modules, CJX-ready UX, and final product surfaces.\n- Require cross-platform outputs to use separate platform files instead of tabbed demo selectors.\n- Add DESIGN-MANIFEST.json plus richer handoff guidance to daemon/client exports.\n- Update archive/export tests for manifest and responsive viewport matrix. * feat: enforce screen-file design outputs Changelog:\n- Enforce screen-file-first generation for landing pages, app screens, platform surfaces, and OS widgets.\n- Update design handoff and manifest exports so coding tools map each screen file to separate routes/surfaces.\n- Strengthen minimal-brief visual guidance to avoid monochrome or unstyled design outputs. * fix: address responsive handoff review feedback * fix: address handoff review blockers * fix: preserve proxy auth and normalized export entry * fix: narrow frame wrapper filter to directory paths only * fix: make artifact save failure banner generic --------- Co-authored-by: Huy Hoàng <macos@MacBook-Pro-Hoang.local> |
||
|
|
dbc94b83ed |
feat(web): add thumbs-up/down feedback widget under completed assistant turns (#1288) (#1308)
* feat(web): add thumbs-up/down feedback widget under completed assistant turns (#1288) Adds a lightweight feedback widget that surfaces under each assistant turn whose run succeeded. Users can submit positive or negative feedback in one click; the negative path opens an optional free-text comment area. The widget never blocks the message composer and only mounts after the run has produced its final artifact, matching the acceptance criteria. What ships - `<MessageFeedback>` (apps/web/src/components/MessageFeedback.tsx) renders the three states: idle (prompt + thumbs), submitted positive (confirmation + Change), submitted negative (confirmation + optional comment textarea + Send + Change). - AssistantMessage.tsx slots the widget under AssistantFooter, gated on `runSucceeded && !hasEmptyResponse`, so failed and empty-response turns don't ask the user to rate something that never finished. - The full record shape leaves room for the future analytics metadata the issue calls out (rating, comment, submittedAt; artifactRef / runId derivable from the surrounding message whenever the analytics pipeline lands). Persistence (v1 = localStorage) Lefarcen's clarifying comment on the issue asked whether v1 should be daemon-persisted or in-memory while the analytics pipeline is defined. The daemon's messages table is column-strict, so daemon persistence would require a SQLite migration plus a contract bump on `ChatMessage`; locking that shape in before the analytics pipeline is designed risks reworking it twice. localStorage is the middle ground: feedback survives reload (so the "feedback state is visually clear after submission" criterion holds across tabs and sessions) without committing the wire shape. The hook surface is just `(value, setter)`, so a future PR can swap the storage layer for a daemon mirror or an analytics shipper without touching the React surface. The store handles corrupted JSON, unknown future rating values, disabled storage (private-mode browsers), and broadcasts changes across listeners in the same tab via a CustomEvent so two mounts of the hook for the same messageId stay in sync. i18n 11 new keys under `feedback.*` (prompt, thumbsUp/Down, two confirmation chips, comment label/placeholder/submit/saved, change). English source values authored alongside the keys; zh-CN translations added in the same pass so the locale alignment test stays green and Chinese users see Chinese strings from day one. The other 16 locales pick up English fallbacks via their existing `...en` spread. Test coverage - `tests/state/message-feedback.test.ts` (8 jsdom cases) — round-trip, null-clear, corrupted JSON, missing rating, unknown rating, key collision across messages. - `tests/components/MessageFeedback.test.tsx` (7 jsdom cases) — idle state, positive submit, negative submit, comment save, blank-comment Send disabled, Change unsticks the rating, rehydration from pre-populated storage. The locale alignment test continues to enforce that every locale declares the new keys (5/5 across 18 locales). Validated - pnpm guard clean - pnpm --filter @open-design/web typecheck clean - tests/i18n/locales.test.ts 5/5 - tests/state/message-feedback.test.ts 8/8 - tests/components/MessageFeedback.test.tsx 7/7 - Full web suite: 98 files, 903 tests * fix(web): tighten feedback widget gate + storage sync + textarea, add styles (PR #1308 review) Addresses every P2/P3 from the codex + Siri-Ray + lefarcen reviews on PR #1308, plus a couple of polish items the review surfaced indirectly. Visibility gate (lefarcen P2) The gate was `runSucceeded && !hasEmptyResponse`, which also matched text-only acknowledgements and question-form replies. The issue scopes feedback to turns that produced a final artifact, so the gate now also requires `produced.length > 0`. New AssistantMessage suite (5 jsdom cases) pins: artifact -> shown, no-artifact -> hidden, streaming -> hidden, failed run -> hidden, empty_response -> hidden. Storage sync (codex P2 + lefarcen P2) The previous broadcast contract was: write storage, dispatch a bare CustomEvent, listeners re-read storage. That had two failure modes: - setItem throwing (private mode / quota / disabled storage) left the listener seeing null and clobbering the in-memory state the user just confirmed. - The clear path early-returned after removeItem and never dispatched, so a second mount of the same messageId stayed in the submitted state when the user clicked Change. New contract: every successful OR failed write dispatches a CustomEvent whose `detail.value` carries the new feedback record (or null). Listeners apply the value directly without re-reading. Same- tab sync survives storage failures and the clear path no longer early-returns. Cross-tab still re-reads on the platform `storage` event since that event has no detail. Two new storage tests pin the new broadcast contract (positive + null) and the failed-setItem path; two new component tests pin in-session confirmation under setItem failure and two-mount Submit + Change synchrony. Textarea draft fix (lefarcen P3) The textarea used `draftComment || feedback.comment || ''` as its controlled value, so erasing a saved comment snapped it back. The draft is now exclusively the source of truth; a ref-backed effect re-seeds the draft from feedback.comment whenever the rating transitions (mount, idle -> negative, cross-mount sync). Send is now enabled when `draftComment !== savedComment`, which lets the user both edit and clear a saved comment. New component test pins erase+ Send actually removing a previously-saved comment. Accessibility The confirmation chip and "Comment saved" tag both gain `role="status"` + `aria-live="polite"` so screen readers announce the state transition. The thumb buttons keep their `aria-label`. CSS (lefarcen P3) The widget's `.message-feedback*` class set had no rules in index.css, so it rendered with default browser controls. Added a ~130-line block that mirrors the surrounding chat pill/chip vocabulary: bg-subtle background, border-pill confirmation chip, accent-tinted positive state and amber-tinted negative state to match the assistant-footer's data-unfinished pattern. Comment area sits below the chip and wraps on narrow widths so the composer isn't pushed off-screen on small panes. Validated - pnpm guard clean - pnpm --filter @open-design/web typecheck clean - tests/state/message-feedback.test.ts 10/10 (was 8, +2 broadcast) - tests/components/MessageFeedback.test.tsx 10/10 (was 7, +3 sync / storage-failure / clear-saved-comment) - tests/components/AssistantMessage.test.tsx 5/5 (new file) - tests/i18n/locales.test.ts 5/5 - Full web suite: 866 tests --------- Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
1df3eca161 |
feat(web): Critique Theater Phase 7 — reducer + useCritiqueStream + useCritiqueReplay (#1307)
* feat(web): pure reducer for Critique Theater states (Phase 7.1)
Pure CritiqueState reducer driven by the contracts-level PanelEvent
(the same shape both the live SSE stream and the recorded transcript
emit), so a single reducer powers both the in-flight panel and the
rerun replay. Lifecycle covers run_started → running → (shipped /
degraded / interrupted / failed), with panelist_open / dim /
must_fix / close / round_end events building per-round
CritiquePanelistView entries as they arrive.
Defensive behaviour that surfaced while writing the spec tests:
- Terminal phases (shipped / degraded / interrupted / failed) are
sticky against further lifecycle events for the same run, except
for parser_warning which can land late and is recorded in a side
channel without changing phase.
- A new run_started for a different runId at any time discards the
prior state and reboots, so the UI can launch consecutive runs
without an explicit reset action.
- Events whose runId does not match the active run return the same
state reference, so React's useReducer doesn't re-render
subscribers on stray traffic.
- Round bookkeeping keys by round number rather than "always last",
so an out-of-order panelist_dim for round 1 arriving after a
round 2 dim does not corrupt the round 2 bucket.
Test coverage: 18 cases covering each transition, the runId guard,
sticky-terminal behaviour, the out-of-order round invariant, and
the stable-identity guarantee. Sets up Phase 7.2 and 7.3 to wire
SSE + replay into the same reducer.
* feat(web): useCritiqueStream hook subscribes to SSE and feeds reducer (Phase 7.2)
createCritiqueEventsConnection is a pure connection manager that
mirrors apps/web/src/providers/project-events.ts: opens an
EventSource at /api/projects/:id/events, listens for every name in
CRITIQUE_SSE_EVENT_NAMES, decodes each frame back into a PanelEvent
(stripping the critique. prefix and merging the data payload), and
hands it to the caller's onEvent. Reconnect uses exponential
backoff (1s → 30s) and resets on `ready`; malformed payloads drop
with a dev-mode warning rather than tearing the stream.
useCritiqueStream wraps the manager in a useReducer that owns the
CritiqueState. enabled=false or a null projectId tears down the
connection cleanly; switching projectId closes the old connection
and opens a fresh one. The returned dispatch lets local UI
synthesise actions (e.g. an Esc keypress firing a synthetic
interrupted while a kill request is in flight); production traffic
comes from the SSE stream.
Test coverage:
- sse.test.ts (10 cases, node env): subscription set covers every
CRITIQUE_SSE_EVENT_NAMES channel; payload decoding lifts the wire
shape back to PanelEvent; malformed JSON is swallowed and does
not stop the stream; exponential backoff schedule and ready-reset
semantics are pinned with a setTimeout seam; close() cancels
pending reconnects and shuts the live source; no-op fallback
when EventSource is unavailable.
- useCritiqueStream.test.tsx (6 cases, jsdom env): idle pre-event,
reducer driven by synthetic actions, no connection when disabled
or projectId is null, clean close on unmount, projectId change
reopens cleanly.
* feat(web): useCritiqueReplay hook drives reducer from transcript file (Phase 7.3)
Fetches the per-run NDJSON transcript (one PanelEvent per line),
parses every line via the shared isPanelEvent predicate, and
dispatches into the same CritiqueState reducer the live SSE stream
uses. A single reducer means the UI rendering a replay can be
identical to the live panel, and a UI mounting both
useCritiqueStream and useCritiqueReplay in parallel does not have
to reconcile two state shapes.
speed knob is `paused | instant | live | { intervalMs: N }`.
- instant flushes every event synchronously, useful for opening a
finished run already at its terminal state.
- intervalMs paces dispatches at a fixed cadence so the reviewer
can watch the run unfold.
- paused parses the transcript but holds events back until the
caller advances speed (consumers can drive a scrubber later).
- live is reserved for the future "playback at original cadence"
feature, currently treated as instant; replay timestamps are not
yet persisted with each event so honest pacing requires a
follow-up Phase 7+ task.
gunzip seam handles `.ndjson.gz` transcripts via
DecompressionStream when present; the production fetch path picks
between text and arrayBuffer based on the URL extension. Both seams
are injectable so the unit tests don't need to spin up a real
network or a real gzip pipeline.
Test coverage (8 cases, jsdom env):
- Idle status before any URL is provided.
- speed=instant flushes the full transcript synchronously to
shipped state.
- speed={intervalMs:N} paces with the setTimeout seam, reaching
done after the last tick.
- speed=paused leaves status=playing with no dispatches.
- Empty transcript reports done with state still idle.
- Fetch rejection surfaces an error status with the message.
- Malformed NDJSON lines are skipped; valid events around them
still land.
- .gz transcripts route through the gunzip seam.
Closes the Phase 7 plan tasks 7.1 / 7.2 / 7.3 (reducer + stream +
replay), all on one branch ready for review. Phases 8+ (Theater
components) consume these from this PR.
* fix(web): close payload-override gap + paused-resume bug in Critique Theater hooks (Phase 7 review)
Two P1 fixes from lefarcen's review on PR #1307:
SSE payload override
`sseToPanelEvent` previously spread `data` after the channel-derived
`type`, so a payload-provided `type` could override the channel and
route a `critique.run_started` frame into the reducer as a `ship`
action. Reversed the spread so the channel-derived `type` is
authoritative, and revalidated the resulting object through the
contracts-level `isPanelEvent` predicate before returning. Frames
that fail validation (missing runId, empty runId, unknown type) are
dropped, so a malformed or compromised SSE frame can no longer
dispatch a wrong-shape action into the reducer.
Three new sse.test.ts cases pin the regression: hostile `type:'ship'`
in the payload still resolves to `run_started`, missing runId is
dropped, empty runId is dropped.
Replay pause/resume
`useCritiqueReplay` had one big effect keyed on `transcriptUrl`
only, so flipping `speed` from `paused` to `instant` never re-fired
and the held events sat undispatched. Split into a parse effect
(depends on URL, fetches and stores events in state) and a pace
effect (depends on parsed-events + speed, owns the cursor + timers).
The playback cursor lives in a ref that survives pause/resume
cycles, so flipping `paused` -> `instant` flushes from the current
position rather than restarting (which would double-dispatch
`run_started` and reset the reducer).
Two new useCritiqueReplay.test.tsx cases:
- paused-then-instant transitions from `playing` to `done` and
reaches the shipped terminal phase
- intervalMs paced playback dispatches one event, pauses to drain
the next scheduled timer, flips to instant, and confirms the
remaining transcript drains exactly once (cursor was preserved)
Doc consistency
The earlier source comment in useCritiqueReplay.ts claimed `live`
"paces by recorded timestamps" while the impl used zero-delay
timers and the PR body said it behaves like `instant`. Aligned to
reality: `live` currently behaves like `{ intervalMs: 0 }` (events
drain on successive microtasks via setTimeoutFn) because transcripts
do not yet carry per-event timestamps. Honest timestamp-driven
pacing is queued as a Phase 7+ follow-up.
Validated: pnpm guard, pnpm --filter @open-design/web typecheck,
Theater suite 47/47 (up from 42, +3 sse + 2 replay), full web suite
96 files / 888 tests.
---------
Co-authored-by: Nagendhra <nagendhra405@gmail.com>
|
||
|
|
64510b790b |
fix(web): translate Design Files refresh strings instead of hardcoding English (#1254) (#1300)
* fix(web): translate Design Files / live artifact refresh strings instead of hardcoding English When the app language was set to Chinese, the Design Files refresh flow showed Chinese for the surrounding chrome but kept English for every label and message originating in describeRefreshStatus, describeEventPhase, and the refresh-event timeline body of LiveArtifactRefreshHistoryPanel. Same-screen mixed-language UX, the exact symptom reported in #1254. Root cause: those three sites bypassed i18n entirely. describeRefreshStatus returned hardcoded English label + description strings for the running / succeeded / failed / idle / never statuses; describeEventPhase returned hardcoded Started / Succeeded / Failed labels; the timeline body inlined "Refresh started…", "<n> source(s) updated", and "Refresh failed." string literals; and the empty-timeline copy ("No refresh activity yet in this session. Trigger Refresh to record a timeline…") was hardcoded too. Fix: thread the existing TranslateFn through both helpers, swap every hardcoded string for a t() lookup, and pull the empty-timeline copy and the failure-fallback through the same path. Added 13 new keys under liveArtifact.refresh.* — statusRunning, the five *Description keys, three event-phase labels (eventStarted/Succeeded/Failed), eventStartedDetail, sourcesUpdatedOne/Many with an {n} placeholder, and timelineEmpty. Status labels for succeeded / failed / ready / never already had keys (statusSucceeded / statusFailed / statusReady / statusNever) so those are reused unchanged. Locales: full Chinese translations added to zh-CN.ts (the locale directly named in the issue). The other 16 locales pick up English fallbacks through their existing ...en spread, so the locale-key alignment test stays green; native translations for those locales can land via the usual locale-team passes without re-touching the source code. * fix(web): cover the rest of the refresh panel under i18n + add a zh-CN render test Lefarcen's review on #1254 / PR #1300 surfaced that the first pass only translated three helpers (describeRefreshStatus, describeEventPhase, session timeline body) and left the rest of the panel in English. Under a Chinese UI the panel still mixed languages, which was exactly the regression the issue was filed for. This commit threads t() through every user-visible refresh-panel string the user would see in the Chinese flow: - Hero block: "Last refreshed" label + "Never" empty state. - Created / Last updated facts + their "Unknown" empty label. - Persisted refresh history header, hint, empty-state copy. - Persisted timeline status badge: succeeded / running / failed / cancelled / skipped now resolve through describePersistedStatus, which uses an exhaustive switch off LiveArtifactRefreshLogEntry's status union so a future contract addition trips tsc. - Session activity header, hint. - Document source header, hint, Type / Tool / Connector field labels. - Advanced debug metadata summary + note line. - "just now" relative-time fallback in the persisted timeline. 22 new i18n keys total (23 with the new heroLastRefreshedNever distinct from statusNever); zh-CN strings authored alongside the English source, every other locale picks them up via its existing ...en spread and the locale-key alignment test stays green. Intentionally untranslated surfaces: raw daemon payloads inside the <details> debug panel (event.step / refreshId / error.message and the JSON.stringify dump), since those are agent / connector identifiers and stack-trace style strings, not localised copy. The debug summary heading itself is translated; if the debug section should be hidden in localised primary flows, that is a separate UX call worth its own issue. Test coverage: new render test wraps LiveArtifactRefreshHistoryPanel in I18nProvider initial="zh-CN" and pins the Chinese rendering of every translated label, plus negative assertions that the formerly hardcoded English literals are NOT present in the markup. With the no-provider fallback returning English, the existing static-markup tests can't observe the regression this PR is meant to fix; the zh-CN render test is the only one that would have caught the original gap and will catch the next one. Validated: pnpm guard, pnpm --filter @open-design/web typecheck, locales.test.ts (5/5), FileViewer.test.tsx (69/69, +1 new zh-CN test), full web suite (92 files, 841 tests). * fix(web): route formatRelativeTime through Intl.RelativeTimeFormat so units localise Lefarcen's second pass on PR #1300 caught the remaining hardcoded English path: formatRelativeTime() still emitted units like `5s ago` and `45m ago`, so Chinese users would see those strings inside the otherwise-translated refresh panel. The function now takes the active locale + TranslateFn and routes through Intl.RelativeTimeFormat with style: 'narrow', numeric: 'always'. That preserves the historical `5s ago` shape for English while producing locale-correct output for every other locale (zh-CN gets `5秒前` / `45分前`, with the right past / future suffix and word order). The `just now` carve-out (abs < 5s) keeps using t('liveArtifact.refresh.justNow') since Intl's narrow output for zero-delta reads awkwardly. A try/catch around the RTF constructor falls back to 'en' if the runtime rejects the locale, so the function is safe on engines with limited ICU data. Callsites threaded through: - LiveArtifactRefreshHistoryPanel hero metric (`lastRefreshedAt`) - Session timeline event row (`event.startedAt`) - Session timeline event time (`event.at`) - LiveArtifactRefreshFact for the created / last-updated facts; the component now accepts optional `locale` + `t` props and the panel passes them in. Test coverage extension: - The existing zh-CN render test sets a real lastRefreshedAt (now - 45s) and real session-event timestamps, then asserts the Chinese past-tense suffix `前` appears AND the legacy English `Xs ago` / `Xm ago` shapes do NOT. That was the gap lefarcen pointed at: setting `lastRefreshedAt: undefined` couldn't see the regression because no relative-time formatting ran. - Added a small second test for the lastRefreshedAt-undefined empty hero so the original `从未` coverage still pins. Validated: pnpm guard, pnpm --filter @open-design/web typecheck, FileViewer.test.tsx (70/70, +1 new test), locales.test.ts (5/5), full web suite (92 files, 842 tests). --------- Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
5bd9763181 |
[codex] Improve Claude Code exit diagnostics (#1267)
* fix daemon claude diagnostics * fix claude custom endpoint auth diagnostics * fix project view api empty response test props * fix claude diagnostic review gaps * fix silent custom endpoint claude diagnostics * fix claude diagnostic credential redaction * fix quoted api key redaction * fix claude diagnostic tail redaction * fix silent claude configured profile diagnostics |
||
|
|
87a95b7fb4 | Fix conversation run isolation (#1271) | ||
|
|
3524a43d18 |
fix: pretty-print JSON file previews (#1206)
* fix: pretty-print JSON file previews * fix: avoid formatting JSON with unsafe numbers * fix: preserve precision-sensitive JSON previews * fix: preserve signed zero in JSON previews * fix: scan JSON numbers without repeated slicing --------- Co-authored-by: Kael S <YOUR_GITHUB_EMAIL_HERE> |
||
|
|
a0316d2599 |
fix(web): suppress autosave indicator for draft-only Connector key edits (#1232)
When the user typed a replacement Composio API key, the global Settings autosave loop persisted `buildPersistedConfig(cfg)` — which intentionally strips the in-flight secret — and then advanced the indicator through 'saving' -> 'saved' despite the key never actually being written. The "All changes saved" status then contradicted the section-local "Save key" gesture and eroded trust in the saved-state badge for a sensitive field. The autosave effect now tracks the snapshot at the last successful save (or the initial cfg on mount) and compares the next snapshot's persisted shape against it via a new `isAutosaveDraftOnlyChange` helper. When the only diffs since last save are fields that `buildPersistedConfig` strips (today the Composio API key, generalizing to any future save-on-explicit-confirm secret), the persist call is skipped and the indicator settles to 'idle' instead of flashing 'saved'. The forced media-provider sync path still runs because that is a real outbound effect even when the persisted shape hasn't changed. Refs #1187 |
||
|
|
be77dc0394 | Default English resource i18n fallback (#1270) | ||
|
|
fb079d8115 |
Add reliable agent-browser skill (#1284)
* Add reliable agent browser skill * Fix ProjectView delete conversation test props |
||
|
|
1eb20e3807 | fix(web): keep tweaks selection usable without annotations (#1268) |