mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
agent-runtime
23
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
38a5ab69e6 |
feat(daemon): Critique Theater Phase 12 (9 Prometheus metrics + 6 log events + OTel span + Grafana dashboard) (#1485)
* 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.
* feat(daemon): rollout flag resolver (Phase 15.1)
Single decision point every caller consults to know whether the
orchestrator should wire the critique pipeline for a given run.
Priority:
1. Skill-level policy (required wins, opt-out wins inversely)
2. Per-project override from the Settings toggle
3. OD_CRITIQUE_ENABLED env override
4. Rollout phase default
M0 dark-launch false
M1 settings only false (toggle is off until the user flips it)
M2 per-skill true if skill opted in
M3 global default true
OD_CRITIQUE_ROLLOUT_PHASE parser defaults to M0 on unknown input
so a fresh install never surprises a user with the feature on.
10/10 vitest cases green covering every cell of the matrix.
* feat(web): Settings toggle hook for Critique Theater (Phase 15.2)
React hook that reads critiqueTheaterEnabled from the existing
open-design:config localStorage blob and stays in sync via:
- the platform storage event (cross-tab)
- a open-design:critique-theater-toggle CustomEvent (same-tab)
Same-tab event is the one that fires when the Settings panel saves
in the current window: the toggle and every mounted theater update
without a page reload.
setCritiqueTheaterEnabled(next) is the imperative setter the Settings
panel calls. It preserves the rest of the stored config (mode, apiKey,
etc.) and dispatches the same-tab event after the localStorage write.
The web hook reflects what the user toggled; the daemon-side
isCritiqueEnabled is the final routing authority (project override,
env, rollout phase). When they disagree, the daemon wins for backend
gating and the web reflects the toggle state.
6/6 vitest cases green covering first read, stored read, same-tab
event flip, config preservation, corrupted JSON tolerance, and
cross-tab storage event.
* test(web): Phase 15 toggle hook failure-mode coverage (PR #1320)
lefarcen P2 on PR #1320 flagged that the PR body claimed safe
behavior for disabled localStorage, non-object JSON, and missing
CustomEvent shim, but the suite only covered corrupt JSON plus
happy-path storage events. Added four failure-mode tests so the
swallowed errors are not silently traded for a throw in a future
refactor:
1. Returns false on a stored JSON value that parses to an array
(non-object). Catches a regression where the guard treats
anything truthy as a config blob.
2. Returns false on a stored JSON value of literal 'null'.
typeof null === 'object' in JS, so the guard has to check null
explicitly; this test pins that check.
3. Returns false when localStorage.getItem throws (private mode /
disabled storage / SecurityError). The hook must swallow and
return false so the rest of the app keeps rendering.
4. setCritiqueTheaterEnabled still dispatches the same-tab
CustomEvent when localStorage.setItem throws (quota exceeded /
disabled storage). The dispatch path is the in-session
broadcast that keeps every mounted hook coherent even when
persistence is unavailable; verified by mounting two probes
and asserting both flip after the setter is called with a
throwing setItem.
10/10 vitest cases green (6 existing + 4 new).
* fix(web): honor CustomEvent payload in toggle hook listener (PR #1320)
Both Siri-Ray (blocking) and lefarcen (P2 new) caught the same
real bug in the failure-mode test I added in
|
||
|
|
2a0ebea50b |
release: Open Design 0.7.0
- bump 14 monorepo package.json files to 0.7.0 (root + apps/{web,daemon,desktop,packaged,landing-page} + packages/{contracts,platform,sidecar,sidecar-proto} + tools/{dev,pack,pr} + e2e); apps/packaged was already at 0.6.1 from beta lane, all others at 0.6.0
- add CHANGELOG.md [0.7.0] - 2026-05-12 entry covering 97 merged PRs since 0.6.0:
- Critique Theater: Phase 7 web client state machine (#1307) + Phase 6.2 daemon artifact extraction (#1085)
- Web/UI: thumbs-up/down feedback widget (#1308), Cmd+, opens Settings (#1173), Finalize design package + Continue in CLI (#974), fetch models button for BYOK (#1034), provider models alphabetical sort (#1097), collapsible MCP JSON field-mapping (#1136), design file rename (#894)
- Daemon: auto-memory store with chat-protocol-aware extraction (#999), install/uninstall skills & design systems (#1003), HTTP 206 range requests for video/audio (#1105), scheduled routines (#1033), agent runtime + route registration refactor (#1063, #1043)
- HyperFrames: HTML-in-Canvas across web + skills (#866)
- Skills/design systems: generic skills + design-templates split + finalize-design API (#955), agent-browser skill (#1284), WeChat design system + login-flow skill (#1083), hud/loom/trading-terminal design systems (#1069), release-notes-one-pager skill (#873), tokens.css schema (#1231)
- Packaging: macOS Intel (x64) build (#759), official Nix flake (#402), beta packaging cache (#1095)
- Maintainer ops: tools-pr PR-duty workspace (#1259), MAINTAINERS.md (#1290), contributor card bot (#932), PR→issue linking discipline (#1263)
- Changed: conversation run isolation (#1271), default English i18n fallback (#1270), Codex CLI exit diagnostics / empty-response handling / path fallback (#1267, #1244, #1205)
- Fixed: ~30 web + desktop + daemon + packaging bugfixes
- Internal: nightly UI/desktop regression coverage (#1256), e2e/release report hardening (#1140), entry/settings automation (#954)
- catch up [Unreleased] compare link to v0.7.0 and add missing [0.6.0] release link
- add 97 PR footnote refs ([#402]..[#1330])
Verified locally: pnpm install + pre-build contracts/daemon/desktop dist + pnpm typecheck (exit 0 across all 14 packages on Node 22.22 with engine-warning).
Release workflow validation runs after merge via release-stable.
|
||
|
|
587c783dc0 |
feat(web): add Finalize design package + Continue in CLI buttons (#451) (#974)
* feat(daemon): expose resolvedDir on GET /api/projects/:id (#451 prereq)
Native projects (no metadata.baseDir) live at <projects root>/<id>, where
projects root is daemon-side state. The web client cannot reconstruct an
absolute path on its own, and shell.openPath on a relative path is
undefined behavior. Without resolvedDir, the upcoming Continue in CLI
button (#451) would render permanently disabled for native projects.
Mirrors PR #832's pattern of exposing designMdPath in its response.
Computed via the existing resolveProjectDir(...) helper. No behavior
change to existing callers; they ignore the new field.
Adds ProjectDetailResponse contract type and a focused projects-routes
test covering imported-folder, native, and unknown-id paths.
* feat(web): add parseProvenance helper for DESIGN.md staleness checks
Pure helper that extracts Project ID, design system, current artifact,
transcript message count, and generated UTC timestamp from the
`## Provenance` section emitted by the daemon's finalize synthesis
prompt (apps/daemon/src/finalize-design.ts). Used by useDesignMdState
to derive the Continue in CLI button's stale/fresh state without an
additional daemon endpoint.
Handles missing section, "none" sentinels for design system /
artifact, and malformed timestamps without throwing. Tests cover all
four branches.
* feat(web): add buildClipboardPrompt template for Continue in CLI
Inline single-source-of-truth template per #451 spec §3.4. Names the
project, the working directory, and the DESIGN.md-first operating
contract for the receiving `claude` CLI session. Trailing TODO is
the blank task slot the issue body specifies — left empty so the user
fills it in before submitting.
Also lands the shared copyToClipboard helper (jsdom-safe canonical path
+ execCommand fallback) so the new button and any future caller share
one fallback path, mirroring the inline pattern in FileViewer.tsx.
Tests cover happy-path field rendering, "none"/"unknown" sentinels
when DESIGN.md fields are absent, and both clipboard branches.
* feat(web): add useProjectDetail + useDesignMdState hooks
useProjectDetail wraps GET /api/projects/:id, surfacing the resolvedDir
field and falling back to metadata.baseDir for older daemons that don't
include it. Continue in CLI needs an absolute working directory so the
desktop bridge can openPath it; the web client never reconstructs the
path itself.
useDesignMdState fetches the project's file list, downloads DESIGN.md
when present, parses the Provenance section, and computes a stale
verdict by comparing the recorded generatedAt against the max mtime of
non-DESIGN.md files and the max conversation updatedAt. Drives the
button's three-state UI (disabled / fresh / stale) without a
daemon-side endpoint.
Tests cover happy path, fallback, and both stale branches plus the
pure computeStale helper for the null-timestamp edge case.
* feat(web): add useFinalizeProject hook with cancel + error-code mapping
Wraps POST /api/projects/:id/finalize/anthropic for the Finalize design
package button. Three concerns:
1. Lifecycle: idle → pending → success | error. Double-clicking the
button aborts the prior in-flight request before starting a new
one so the daemon never sees stacked finalize calls per project.
2. Cancellation: AbortController plumbed through fetch + a 130 s
timer (daemon timeout 120 s + 10 s buffer). Cancel returns to idle
cleanly — it's a user gesture, not an error surface.
3. Daemon error mapping: when the response is non-OK, body.error.code
drives the canonical user-facing toast string (table covers all
7 codes the daemon emits today plus a network-error catch-all).
body.error.details, when a string, surfaces alongside the category
message so account-usage-cap responses (Anthropic 400 →
UPSTREAM_UNAVAILABLE) can show the upstream's own reason instead
of just the daemon's category label — committed to lefarcen on
#450 verification reply.
Tests cover request body shape, all 8 error codes via it.each, the
network-error path, the details-surfacing branch, the cancel ⇒ idle
flow, and the unknown-code → catch-all message branch.
* feat(web): add useTerminalLaunch with electron/web detection
Capability-detected wrapper around window.electronAPI.openPath. On
desktop the bridge forwards to shell.openPath, which opens the OS
file manager at the project working directory (per Electron's
contract for directory paths — it is NOT a terminal launcher;
spawning a terminal application is deferred per #451 Non-goals). On
browser builds the hook reports web-fallback so the caller renders
a manual-instruction toast naming the working directory.
Treats any non-empty string return from shell.openPath as ok: false
so platform-specific failures surface the manual fallback toast.
Behavior is exercised end-to-end by the upcoming
ContinueInCliButton tests.
* feat(desktop): expose shell.openPath via electronAPI bridge
Adds an openPath bridge method that the Continue in CLI button (#451)
uses to surface the project working directory in the OS file manager.
shell.openPath is part of Electron's contract and resolves to '' on
success / a non-empty error string on failure; the IPC handler
forwards the result so the renderer can decide between the success
toast and the manual fallback toast without a separate error channel.
Empty / non-string inputs short-circuit to a self-describing error
string so the renderer never needs to worry about undefined-input
crashes from the main process.
Web side: extracts Window.electronAPI into a single global declaration
at apps/web/src/types/electron.d.ts so future bridge methods land in
one place. Two pre-existing inline declare-global blocks
(NewProjectPanel.tsx, providers/registry.ts) are deleted in favor of
that single source of truth — the inline ones each carried a partial
shape of the bridge and were diverging from the desktop preload.
* feat(web): add FinalizeDesignButton, ContinueInCliButton, ProjectActionsToolbar
Project-level toolbar that hosts the two new actions from #451.
Mounted between AppChromeHeader and the chat/workspace split (wiring
lands in the next commit). Per-file actions (Export PDF/PPTX/ZIP,
Deploy) stay in the FileViewer share menu.
FinalizeDesignButton has three idle labels driven by DESIGN.md
existence + staleness, plus a pending state with a spinner and a
cancel link that maps to useFinalizeProject's AbortController. Error
toasts are owned by ProjectView so the button doesn't carry its own
toast surface.
ContinueInCliButton renders disabled with a Finalize-pointing
tooltip when DESIGN.md is missing (so the workflow is discoverable
rather than hidden), enabled when fresh, and enabled with a stale
chip otherwise. Chip text is the spec's canonical "Spec is stale —
regenerate?" — N-turns-ago is deferred per spec §4.6.
Toast.tsx is a tiny transient component that mirrors
PromptTemplatePreviewModal's state-based toast pattern; supports a
secondary details line so daemon error envelopes that carry an
upstream explanation (e.g. Anthropic account-usage cap) can surface
the real reason alongside the daemon's category label.
CSS appends one block to apps/web/src/index.css mirroring the
existing app-project-title token usage; no CSS modules in this
repo (verified by grep).
* test(web): cover ContinueInCliButton states + interaction wiring
Three rendered states (DESIGN.md missing → disabled with the
Finalize-pointing tooltip; DESIGN.md fresh → enabled, no chip;
DESIGN.md stale → enabled with the canonical "Spec is stale —
regenerate?" chip), plus three onClick branches (no-op when
disabled, fires once when fresh, fires once when stale).
Click-handler integration with clipboard / shell.openPath / toast
lives in ProjectView (the button is presentational and takes the
handler in via props), so those are covered by Phase K's wiring +
the manual smoke test rather than the per-component test.
* feat(web): wire Continue in CLI + Finalize buttons into ProjectView
Mounts the new project-actions toolbar between AppChromeHeader and
the chat/workspace split, hidden when workspaceFocused so the
focus-mode artifact view stays uncluttered.
Wires the four hooks (useProjectDetail, useDesignMdState,
useFinalizeProject, useTerminalLaunch) to a single shared toast
surface. handleFinalize reads the request body from the existing
config: AppConfig prop and uses effectiveMaxTokens(config) to match
the chat-flow's maxTokens defaulting; on success it refreshes
useDesignMdState so the toolbar re-renders with the new chip state.
handleContinueInCli builds the literal clipboard prompt, copies it,
opens the working directory via shell.openPath on desktop /
falls through to a manual-instruction toast on browser, and surfaces
shell.openPath failures with a fallback toast that names the path.
Errors lift into the same toast surface (a useEffect tied to
finalize.error) so the daemon's category message + body.error.details
reach the user as the spec's two-line render — covered by hook test
16a in the prior commit.
⌘+Shift+K (mac) / Ctrl+Shift+K (others) is the keyboard
accelerator for Continue in CLI; capture-phase, platform-gated,
no-op when DESIGN.md is missing. Mirrors the existing FileWorkspace
shortcut idiom and does not collide with ⌘+P (Quick Switcher).
* fix(web): distinguish timeout abort from user cancel in useFinalizeProject
Addresses codex P2 finding on PR #974: the catch block treated every
AbortError as a user-initiated cancel and reset to idle silently. If
the internal 130 s timeout fired, users saw no failure signal but the
daemon's synthesis call may still have been in flight.
Adds a timedOutRef set inside the setTimeout callback before
controller.abort(), and branches in the catch: timeout → status
'error' with new TIMEOUT code ("Finalize timed out after 130 s. The
daemon may still be running."), user cancel → existing idle reset.
Reset the ref at the start of every trigger() so a previous timeout
doesn't poison the next call.
Adds one test using vi.useFakeTimers() that advances past 130_001 ms
and asserts the TIMEOUT error surface.
* fix(web): surface clipboard failures by rendering the prompt in the toast
Addresses codex P2 finding on PR #974: handleContinueInCli ignored
copyToClipboard's return value, so when both clipboard paths failed
(restricted browser context / insecure origin) the toast still said
"paste the prompt" though nothing had been copied — leaving users
with no manual-copy recourse in exactly the environments where the
fallback should help.
handleContinueInCli now branches on copyToClipboard's boolean return.
On failure the toast renders the prepared prompt in a scrollable
<pre> block and pins itself open (no auto-dismiss) so the user has
time to select-and-copy manually. Includes a Dismiss button + the
working directory in the secondary details line so the user has the
information needed to proceed.
The folder-open call is skipped on copy failure because there's
nothing to paste yet; the user copies first, then re-clicks Continue
in CLI when they're ready.
Toast component grows an optional Updating VS Code Server to version 41dd792b5e652393e7787322889ed5fdc58bd75b
Removing previous installation...
Installing VS Code Server for Linux x64 (41dd792b5e652393e7787322889ed5fdc58bd75b)
Downloading: 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 0% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 1% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 2% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 3% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 4% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 5% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 6% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 7% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 8% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 9% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 10% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 11% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 12% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 13% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 14% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 15% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 16% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 17% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 18% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 19% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 20% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 21% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 22% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 23% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 24% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 25% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 26% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 27% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 28% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 29% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 30% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 31% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 32% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 33% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 34% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 35% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 36% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 37% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 38% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 39% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 40% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 41% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 42% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 43% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 44% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 45% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 46% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 47% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 48% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 49% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 50% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 51% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 52% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 53% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 54% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 55% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 56% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 57% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 58% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 59% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 60% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 61% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 62% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 63% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 64% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 65% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 66% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 67% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 68% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 69% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 70% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 71% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 72% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 73% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 74% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 75% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 76% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 77% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 78% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 79% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 80% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 81% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 82% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 83% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 84% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 85% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 86% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 87% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 88% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 89% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 90% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 91% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 92% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 93% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 94% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 95% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 96% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 97% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 98% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99% 99%100%100%
Unpacking: 0% 1% 2% 3% 4% 5% 6% 7% 8% 9% 10% 11% 12% 13% 14% 15% 16% 17% 18% 19% 20% 21% 22% 23% 24% 25% 26% 27% 28% 29% 30% 31% 32% 33% 34% 35% 36% 37% 38% 39% 40% 41% 42% 43% 44% 45% 46% 47% 48% 49% 50% 51% 52% 53% 54% 55% 56% 57% 58% 59% 60% 61% 62% 63% 64% 65% 66% 67% 68% 69% 70% 71% 72% 73% 74% 75% 76% 77% 78% 79% 80% 81% 82% 83% 84% 85% 86% 87% 88% 89% 90% 91% 92% 93% 94% 95% 96% 97% 98% 99%100%
Unpacked 4009 files and folders to /home/bryan/.vscode-server/bin/41dd792b5e652393e7787322889ed5fdc58bd75b.
Looking for compatibility check script at /home/bryan/.vscode-server/bin/41dd792b5e652393e7787322889ed5fdc58bd75b/bin/helpers/check-requirements.sh
Running compatibility check script
Compatibility check successful (0) prop and the auto-dismiss
TTL is suppressed whenever code is present. CSS adds .od-toast-code
(monospace, max-height 240 with overflow-auto) and .od-toast-dismiss
styling.
Six new Toast tests cover details rendering, code rendering,
no-auto-dismiss when code is present, auto-dismiss when code is
absent, and the Dismiss button affordance.
* fix(web): make ContinueInCliButton disabled-state guidance visible
Addresses mrcfps's PR #974 review: native <button disabled> does
not fire hover/focus events in browsers we ship against, so a
`title` tooltip on the disabled button never surfaces. The only
guidance for the missing-DESIGN.md state was effectively invisible —
defeating the spec's "discoverable, not hidden" intent.
Renders the help text as a visible sibling <span> next to the
disabled button instead. Adds aria-describedby pointing the button
at the hint's id so assistive tech announces the explanation when
the disabled button gets focus. The native `disabled` attribute
stays so the button still can't be clicked or submitted.
CSS adds .project-actions-disabled-hint (muted italic, 11.5px,
matches the existing meta/secondary text style on this surface).
Test asserts the role="note" hint is in the DOM with the canonical
text and that the button's aria-describedby links to its id.
* fix(web): keep ProjectActionsToolbar at natural height inside the .app grid
The .app container was `grid-template-rows: auto 1fr` — only two
rows. Adding ProjectActionsToolbar as a third child between
AppChromeHeader and the chat/workspace split made the toolbar the
2nd grid item, so it took the `1fr` row (filling roughly half the
viewport) while the split got pushed into an implicit auto row at
its content's natural height. Surfaced as a screenshot from Bryan
showing the toolbar's background bleeding across most of the screen.
Extend grid-template-rows to `auto auto 1fr` and pin the split to
`grid-row: 3` explicitly. Now:
- Toolbar visible: row 1 = header (auto), row 2 = toolbar (auto),
row 3 = split (1fr, fills remaining viewport).
- Toolbar hidden via hidden=workspaceFocused → ProjectActionsToolbar
returns null, row 2 collapses to 0px (auto with no content), split
still fills row 3.
No JS changes; existing 609 tests still green.
* fix(web): guard useFinalizeProject state writes against superseded triggers
Addresses mrcfps's PR #974 P1 review on useFinalizeProject.ts:132
(also called out as P1.3 in lefarcen's deep-dive review).
Calling trigger() twice in quick succession aborted the first
controller and swapped abortRef to the new one, but the first
request's later AbortError catch still unconditionally called
setStatus('idle') / setError(null). That cleared the spinner and
re-enabled both toolbar buttons while the replacement finalize was
still pending — defeating the de-duplication this hook was meant to
enforce.
Adds an isCurrent() closure (`abortRef.current === controller`)
and gates every state-write site after the await: success path,
non-OK envelope path, AbortError-timeout, AbortError-cancel, and
network-error all bail early when the trigger has been superseded.
Per mrcfps: "make every state write request-scoped."
Regression test triggers twice in quick succession with a
never-resolving fetch, awaits the first promise (it rejects with
AbortError), and asserts status stays 'pending' rather than
collapsing to 'idle' under the replacement's lifetime.
* fix(desktop): allowlist-validate shell.openPath against registered project roots
Addresses mrcfps's PR #974 P1 review on runtime.ts:305 (also called
out as P1.2 in lefarcen's deep-dive review): the new
`shell:open-path` IPC handler accepted any renderer-supplied
string and forwarded it straight into Electron's `shell.openPath`,
widening the renderer→main trust boundary so XSS or a compromised
renderer dependency could open arbitrary local paths to the user.
Adds an explicit gate around the bridge:
1. validateExistingDirectory(p) — floor check that rejects empty
strings, relative paths, files, apps, and non-existent paths;
realpath-resolves so symlink games can't be used to register
one path and reach another.
2. createProjectRootGate() — Set-backed allowlist of
daemon-validated project working directories. The renderer
calls registerProjectRoot(absDir) once per project mount via
a new IPC method (preload bridge); the main process only
opens paths that pass both the floor check and the allowlist.
ProjectView wires the registration via a useEffect tied to
projectDetail.resolvedDir, so the active project's daemon-supplied
working directory is always the one being approved (not a renderer-
synthesized string).
Threat-model caveat documented in the runtime.ts comment block: an
attacker that fully controls the renderer can also call register
with arbitrary paths. Closing that gap fully requires a daemon-side
round-trip to derive the canonical resolvedDir from the daemon's
project registry, which is deferred to keep this PR focused.
Today's allowlist still defends against accidental misuse, bugs,
and common XSS payloads that don't know to call register first.
Adds apps/packaged/tests/desktop-project-root-gate.test.ts with 13
cases: floor-validation rejection cases (empty / relative / missing
/ file), happy-path resolution, symlink realpath canonicalization,
and the allowlist's register/isApproved/reset semantics. Mirrors
the existing apps/packaged/tests/desktop-url-allowlist.test.ts
pattern from PR #911 — the packaged workspace hosts the test
because apps/desktop has no vitest setup yet.
* fix(daemon): wire request-lifecycle abort signal through finalize route
Addresses mrcfps's PR #974 P1 review on
apps/daemon/src/server.ts:3831-3837 (also called out as P1.1 in
lefarcen's deep-dive review): `POST /api/projects/:id/finalize/anthropic`
called `finalizeDesignPackage(...)` without threading any
request-lifecycle abort, so cancelling the browser fetch only
aborted the UI-side request — the daemon's 60–120 s Anthropic call
kept running and still wrote DESIGN.md after the UI returned to idle.
Adds an AbortController inside the route handler, fired from
`res.on('close')`, and threads its signal into the existing
`signal?: AbortSignal` parameter on `FinalizeOptions`
(finalize-design.ts:70). `callAnthropicWithRetry` already passes
the signal through to the underlying fetch, so a client disconnect
now propagates all the way to the Anthropic SDK call.
Listener-event choice: `res.on('close')` is the canonical event
for "client disconnected before response was sent" in Express. The
common alternative `req.on('close')` fires whenever the *request*
stream finishes — for POST routes that means as soon as the
body-parser middleware drains the body, well before the route does
any work. Using req.on('close') would have flipped the abort
controller in every successful run; the test caught this empirically.
Caveat documented in the route's comment block: an abort fired
*after* the upstream response has been received but *before* the
atomic write completes still allows the write to land. The SDK
contract bounds the network round-trip, not the post-network disk
handoff.
Adds tests/finalize-route-abort.test.ts: spins up the test server,
mocks global fetch to capture the daemon-side AbortSignal at the
Anthropic call, sends the request via raw http (so we can destroy
the underlying socket), waits until the server reaches the
Anthropic call, then destroys the socket and asserts that the
daemon-side signal received an abort event within 5 s.
Three pre-existing project-watchers chokidar tests show flaky
timeouts under full-suite concurrency but pass in isolation;
unrelated to this fix.
* fix(daemon): refactor finalize-route-abort test to satisfy strict TS narrowing
The CI typecheck (`pnpm --filter @open-design/daemon typecheck`,
which runs both tsconfig.json and tsconfig.tests.json) caught what
my pre-push validation missed: TS narrowed `capturedSignal` to
literal `null` because vitest's mockImplementation closure can't
prove its callback runs, leaving the bare `let capturedSignal:
AbortSignal | null = null` permanently typed at its initial value.
At line 184 (`expect(capturedSignal?.aborted).toBe(true)`) the
right-hand side of the optional-chain became unreachable, and TS
flagged it as `Property 'aborted' does not exist on type 'never'`.
Switches to the standard ref-object pattern
(`const capture: { signal: AbortSignal | null } = { signal: null }`).
TS narrows let bindings inside closures conservatively but treats
object-property writes as opaque, so `capture.signal` reads
correctly across the closure boundary. Logic is unchanged.
(Pre-push oversight: ran `pnpm --filter @open-design/web typecheck`
but not the full repo `pnpm typecheck` after the daemon test
landed; the daemon's own typecheck would have caught this. Adding
`pnpm typecheck` back into the standard pre-push checklist.)
* fix(desktop): make shell.openPath gate daemon-controlled and reject .app bundles
Addresses lefarcen + mrcfps PR #974 P1 reviews on the previous path
allowlist (commit
|
||
|
|
b03a504da6 | release: Open Design 0.6.0 (#1080) | ||
|
|
2bb029cb58 |
release: Open Design 0.5.0 (#820)
0.5.0 已从
|
||
|
|
80416b185a |
Diagnose missing Next package during tools-dev web startup (#675)
* fix(tools-dev): diagnose missing Next package * fix(web): remove duplicate Ukrainian prompt labels |
||
|
|
ae4a08773a |
chore(release): prepare 0.4.1 (#659)
- bump remaining monorepo package.json files to 0.4.1 after apps/packaged was already bumped in #637 - add CHANGELOG.md [0.4.1] - 2026-05-06 entry covering the startup hotfix and 19 merged PRs since 0.4.0: - Added: manual edit mode (#620), Cmd/Ctrl+P quick file switcher (#556), resizable chat panel (#563), PI status/cancel updates (#618), accessibility and RTL/Bidi craft modules (#587, #595), i18n structure checks (#608) - Changed: first-PR README links now surface help-wanted issues (#605) - Fixed: packaged contracts runtime exports (#577), packaged runtime beta gating (#637), ACP/MCP/agent fixes (#604, #612, #627), conversation error recovery (#623), native mac quit (#637) - Documentation/Internal: OD_DATA_DIR migration docs (#570), Simplified Chinese QUICKSTART (#578), zh-TW/ko README syncs (#586, #619), generated metrics (#592) Release workflow validation runs after merge via release-stable. |
||
|
|
963bbf2500 | release: Open Design 0.4.0 (#454) | ||
|
|
009d7a5478 |
refactor(daemon): eliminate duplicate dist tree from two-tsconfig build (#553)
Move sidecar source under src/ so a single tsconfig produces all daemon
output. Removes the parallel dist/src/ tree that was emitted by
tsconfig.sidecar.json (it included src/**/*.ts to type-check the
`../src/server.js` cross-tree import).
Build now emits:
- dist/<flat> (cli.js, server.js, app-version.js, ...)
- dist/sidecar/{index,server}.js
`dist/sidecar/server.js` reaches the main daemon via `../server.js`
instead of `../src/server.js`, so there is no second copy of the source
tree in the published tarball.
Background — issue #534 (already fixed by #537):
The packaged Settings → About panel showed 0.0.0 because the sidecar
chain loaded the duplicated `dist/src/app-version.js`, where the fixed
`new URL('../package.json', import.meta.url)` resolved to a non-existent
`dist/package.json`. #537 patched the symptom by walking parents until a
real `package.json` is found and by writing `appVersion` into the Linux
packaged config. Both stay in place — they're sound defenses — but the
underlying duplicate-emit was never addressed; any future relative
resource lookup (templates, schemas, prompts) anchored on
`import.meta.url` would have hit the same trap.
This change removes the trap.
|
||
|
|
bbdd4e84b5 |
chore: enforce test directory conventions (#496)
* chore: enforce test directory conventions Move package, app, and tool tests out of src and add guard enforcement so source directories stay source-only. * ci: use guard and package-scoped tests Run the new repository guard in CI and keep test execution aligned with package-scoped commands after removing root aliases. * ci: align stable release guard check Use the new repository guard in stable release verification after replacing the residual-JS-only script. * chore: tighten test layout enforcement Enforce sibling tests directories, typecheck moved test suites with dedicated configs, and refresh remaining guidance that pointed at src-based tests. * chore: clarify no-emit test tsconfigs Explicitly disable declaration-only emit in test tsconfigs so review tooling sees they are no-emit typecheck configs. |
||
|
|
a719f02aa2 |
fix(web): normalize daemon proxy origins
Fix web sidecar proxy requests so same-origin browser requests reach the daemon without tripping origin validation, while unrelated origins remain rejected. Fixes #388. |
||
|
|
016c08183f | release: Open Design 0.3.0 | ||
|
|
648374d839 |
fix(platform): wrap cmd.exe shim invocations to survive /s /c quote stripping (#339)
PR #258 standardized agent spawning through `createCommandInvocation`, which on Windows wraps `.cmd` / `.bat` paths in `cmd.exe /d /s /c <line>` and quotes each argument with cmd-style doubled quotes. PR #232's follow-up fix for `shell:true` was lost in that refactor, and the new shape has its own quoting bug on argv-style spawn: 1. cmd.exe `/s /c` strips exactly one leading and one trailing `"` from the rest of the command line. 2. Node, with `windowsVerbatimArguments` unset, escapes each argv element using CommandLineToArgvW rules — so the inner `"path with space"` ends up surfacing to cmd.exe with an extra layer of `\"` escaping that cmd doesn't understand. Together these collapse `"C:\Users\Ethical Byte\...\codex.CMD" --help` into `C:\Users\Ethical Byte\...\codex.CMD --help` with no quoting preserved, and cmd.exe parses the first space as a token boundary — "`Ethical` is not recognized as an internal or external command." See issue #315 for the full repro. The fix mirrors what Node's own `child_process.spawn({ shell: true })` does internally: wrap the entire joined command line in an extra `"…"` and set `windowsVerbatimArguments: true`. The outer wrap absorbs the `/s /c` strip, leaving inner per-arg quoting intact, and the verbatim flag tells Node to pass argv through to CreateProcess unchanged. Changes: - `packages/platform/src/index.ts` - Extend `CommandInvocation` with optional `windowsVerbatimArguments`. - Extract the cmd.exe shim builder into `buildCmdShimInvocation` and apply the outer wrap + verbatim flag in both `createCommandInvocation` and `createPackageManagerInvocation`. - Forward the flag through `spawnBackgroundProcess` and `spawnLoggedProcess`. - `apps/daemon/src/server.ts` — agent spawn forwards `invocation.windowsVerbatimArguments`. This is the call site that hit #315 in the wild (Codex CLI `.CMD` shim, user dir with space). - `tools/pack/src/win.ts` — `runPnpm` and `runNpmInstall` forward the flag through `execFileAsync`. Affects the Windows packaged-build pipeline when run from a path with spaces. - `tools/dev/src/index.ts` — `runLoggedCommand` accepts and forwards the flag; `buildDesktop` propagates it from `createPackageManagerInvocation`. Affects local dev on Windows. Tests: - 9 new unit tests in `packages/platform/src/index.test.ts` stub `process.platform` so both Windows and POSIX branches run on every CI runner. Coverage: - POSIX pass-through. - Windows non-shim binary pass-through. - `.CMD` shim with spaces in the binary path (the #315 repro). - `.bat` shim parity. - Argv elements with spaces alongside the shim path. - Argv elements without whitespace stay unquoted. - `process.env.ComSpec` fallback. - `npm_execpath` short-circuit (cross-platform). - POSIX pnpm pass-through. - Windows pnpm wrapped through cmd.exe. Closes #315. |
||
|
|
62b01a6dbf | release: Open Design 0.2.0 (#297) | ||
|
|
c0589ed05e |
fix(desktop): launch reliably on Windows from Electron-based parent shells (#292)
* fix(tools-dev): strip ELECTRON_RUN_AS_NODE before spawning desktop Parent processes such as Electron-based IDEs may set ELECTRON_RUN_AS_NODE=1 in their environment for sidecar/script reuse. When tools-dev inherits this env via process.env, the spawned electron.exe runs as plain Node and fails to inject main-process APIs (app, BrowserWindow, protocol all become undefined). Explicitly drop the variable before spawning so desktop always boots in real Electron mode regardless of caller environment. * fix(desktop): ensure BrowserWindow is visible on initial load Windows focus-stealing prevention can leave detached-spawned GUI windows minimized or hidden, even when constructed with show:true. Add a small ensureWindowVisible helper that restores from minimized state and forces show+focus after the placeholder URL loads. Cross-platform safe: only acts when window is actually hidden or minimized, preserving any user window-state adjustments. |
||
|
|
a4fd4f949f |
fix(tools-dev): use junction instead of dir symlink on Windows (#231)
ensureWebDevNodeModules() called fs.symlink(target, path, "dir") to
link apps/web/node_modules into the web runtime root. On Windows,
"dir" symlinks require either Administrator rights or Developer Mode
(SeCreateSymbolicLinkPrivilege). Standard non-elevated user accounts
without Developer Mode get EPERM and tools-dev exits before web ever
starts.
Junctions ("junction") are functionally equivalent for directory-only
links on the same volume, work for any user without elevation or
Developer Mode, and are silently treated as plain symlinks on POSIX.
The existing isSymbolicLink() check on the next launch still matches
junctions on Node.
Reproduced on Windows 11 + Node 24 with a non-elevated PowerShell
session and Developer Mode off.
|
||
|
|
4510c69ba1 |
feat(tools-dev): add --prod flag and OD_HOST for headless server deployment (#222)
* feat(tools-dev): add --prod flag and OD_HOST for headless server deployment - Lazy-load electronBinaryPath so daemon+web can start without Electron - Add --prod flag to tools-dev start/run that sets NODE_ENV, OD_WEB_PROD, and OD_WEB_OUTPUT_MODE automatically for production Next.js builds - Add OD_HOST env var support to daemon and web sidecar bind addresses (defaults to 127.0.0.1, set to 0.0.0.0 for remote access) - Skip next.config.ts distDir override when OD_WEB_PROD=1 so production builds resolve the default .next directory Closes #221 * fix: address PR review — hardcode daemon bind to 127.0.0.1, cache electron path - Revert OD_HOST from daemon: daemon is a local privileged process and must only bind 127.0.0.1. Remote access goes through the web sidecar proxy. - Web sidecar resolveDaemonOrigin now always uses 127.0.0.1, separated from the web bind address (OD_HOST) so the daemon proxy works correctly even when the web listener binds 0.0.0.0. - Add OD_HOST character validation to reject clearly invalid values early. - Cache electronBinaryPath getter result to avoid repeated require() calls. --------- Co-authored-by: yamsfeer <yamsfeer@users.noreply.github.com> |
||
|
|
89722379c5 |
fix(tools-dev): normalize web dev tsconfig paths on Windows (#174)
tools-dev generated a temp web tsconfig with Windows backslash relative paths in extends, which Next/TypeScript failed to resolve in some environments. Normalize runtime tsconfig/dist path strings to POSIX separators so dev config resolution works consistently across Windows/Linux/macOS. |
||
|
|
3fb849d047 |
Fix chat runs surviving web disconnects (#146)
* fix chat runs surviving web disconnects * fix chat run create abort propagation Generated-By: looper 0.0.0-dev (runner=fixer, agent=openai/gpt-5.5) * fix daemon keepalive reconnect budget Generated-By: looper 0.0.0-dev (runner=fixer, agent=gpt-5.5) * fix daemon stream disconnect cancellation Generated-By: looper 0.0.0-dev (runner=fixer, agent=openai/gpt-5.5) * fix daemon stream abort cancellation race Generated-By: looper 0.0.0-dev (runner=fixer, agent=openai/gpt-5.5) * fix daemon run cancellation semantics * fix load * doc * 2 * add run refresh recovery * fix active run refresh status * fix reattach abort handling * fix * fix chat initial scroll * fix daemon start failures Generated-By: looper 0.2.7 (runner=fixer, agent=openai/gpt-5.5) * fix background run recovery Generated-By: looper 0.2.7 (runner=fixer, agent=openai/gpt-5.5) * fix stop run status Generated-By: looper 0.2.7 (runner=fixer, agent=openai/gpt-5.5) * fix background run recovery Generated-By: looper 0.2.7 (runner=fixer, agent=openai/gpt-5.5) * extract daemon run service * move prompt composition to daemon * fix prompt module resolution * fix project id generation * add project run status * add designs kanban view with awaiting_input status - add grid/kanban view toggle on Designs tab; persist choice in localStorage - introduce awaiting_input project display status (daemon-derived from unanswered <question-form>) so projects asking the user aren't shown as Completed; ordered between Running and Completed with amber accent - hide transient queued state from users: coerce queued/starting to running in daemon /api/projects projection and drop the queued kanban column - a11y polish on Designs cards: Space activation, aria-labels on delete, focus-visible outlines, reveal delete on focus-within and touch, prefers-reduced-motion handling - kanban layout uses flex sizing instead of viewport math; scoped icon- only pill button rule fixes view-toggle icon alignment --------- Co-authored-by: mrcfps <mrc@powerformer.com> |
||
|
|
ac9c239b1e |
Improve tools-dev native addon diagnostics (#153)
Surface daemon log tails when tools-dev waits for daemon status and add targeted guidance for native Node addon ABI mismatches. Generated-By: looper 0.0.0-dev (runner=worker, agent=gpt-5.5) |
||
|
|
86c256ad56 | Improve tools-dev web startup flow (#128) | ||
|
|
a19c866d5b |
Fix tools-dev default startup usability (#127)
Allow sidecar port zero for auto allocation and make lifecycle command output easier to read. |
||
|
|
c6d11018a0 |
Refresh desktop integration control plane (#123)
* feat(dev): add desktop tools-dev control plane * refactor(sidecar): split Open Design contracts Move Open Design-specific sidecar protocol definitions into @open-design/contracts so sidecar and platform can remain descriptor-driven primitives. * refactor(daemon): organize package sources Keep daemon app code, tests, and sidecar entrypoints in separate package directories so each layer can be built and verified independently. * chore(repo): streamline maintenance entrypoints Centralize agent guidance by directory and reduce root command chains while preserving the existing build scope. * docs: translate agent guidance to English * fix(sidecar): tolerate stale IPC sockets Remove stale Unix socket files only after confirming no listener is active, so tools-dev can restart after unclean shutdowns. |