Fixes#5365
Verified with focused RED/GREEN regression, web typecheck, pnpm guard, root typecheck, visual DOM readback, and independent Codex diff review.
* fix: add Kiro to agentLabels.ts (AGENT_LABELS + AGENT_ALIASES)
- Add kiro to AGENT_LABELS so exactAgentDisplayName('kiro') returns 'Kiro'
- Add 'kiro cli' and 'kiro-cli' aliases to AGENT_ALIASES
- Kiro icon (kiro.svg) already exists, this was purely a missing label mapping
Closes#5081
* test: pin Kiro label and alias resolution in agentLabels utility
PerishCode's review on PR #5085: mapping change is merge-safe but the
new behavior should be pinned in the existing utility test matrix.
Added 4 tests covering:
- agentDisplayName resolves 'kiro cli' → 'Kiro' (alias)
- agentDisplayName resolves 'kiro-cli' → 'Kiro' (alias)
- exactAgentDisplayName returns 'Kiro' for exact id 'kiro' (canonical label)
- exactAgentDisplayName returns null for 'kiro cli'/'kiro-cli' (aliases are
not themselves agents — substring aliases stay routed via agentDisplayName
and do not pollute the exact-id table)
Locks the fix from db025410 and prevents regression.
* test: update kiro alias assertions to match exactAgentDisplayName behavior
classifyAmrAccountFailureDetails only mapped the insufficient_balance /
recharge half of the accountAction union. When vela reports an auth
failure through ACP structured details while the protocol message is
generic, the run fell into the opaque AGENT_EXECUTION_FAILED bucket
instead of the AMR_AUTH_REQUIRED card with its relogin affordance.
Add the symmetric auth_required / relogin branch, mirroring the
structured balance branch that #5053 introduced.
Co-authored-by: tomsen-ai <230283659+tomsen-ai@users.noreply.github.com>
The Antigravity CLI (`agy`) is silent on stdout/stderr in print mode for
both missing-auth and quota-exhausted failures: it exits 0 with no
assistant text and only records the real upstream error in its
`--log-file`. The connection-test path never handed `agy` a `--log-file`
and never inspected one, so every such failure collapsed into
`kind: 'unknown'` with `detail: 'exit 0'`, which Settings renders as the
opaque "Test failed: exit 0" the user reported — with no hint that they
simply need to sign in.
Mirror the chat-run empty-output guard already in server.ts: give the
smoke test a temp `--log-file` under its throwaway cwd, and when `agy`
produces no visible text, fold the log tail into the classifier input and
route to the specific result — `agent_auth_required` (with the actionable
"open a terminal and run `agy` to sign in" guidance) or `rate_limited`
(quota) — falling back to `agent_auth_required` on a clean silent exit,
since that empirically means missing OAuth. A non-clean crash still
reports as `agent_spawn_failed` with its exit code.
Both the onboarding and Settings test surfaces already render these kinds,
so the fix reaches the UI (and the `od` CLI, which shares the endpoint)
with no contract or i18n change.
Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
* fix(daemon): preserve data dir for media wrapper env
Generated-By: looper 0.10.2 (runner=worker, agent=codex)
* fix(daemon): derive AMR home from daemon data dir
Generated-By: looper 0.10.2 (runner=fixer, agent=codex)
* fix(desktop): break renderer crash-reload loops with a circuit breaker
A renderer that crashes deterministically — a GPU/V8 CHECK failure, a
corrupt profile that aborts on every load — is reloaded forever by the
desktop poll loop: load → crash → reload → crash. One 0.14.0 device did
exactly this, emitting 26,011 desktop_renderer_crash events in a single
day (96% of all renderer crashes on the release) while its window stayed
blank the whole time.
Add RendererCrashLoopBreaker (pure, unit-tested): after 5 crashes inside
a 60s rolling window it opens. The poll loop then parks on a static,
dependency-free error screen that explains recovery is automatic, rather
than cycling the crash; markRendererFailed no-ops while open. Per-crash
telemetry is suppressed once open — the tripping crash is still reported,
flagged loop_tripped — so one wedged device can't emit tens of thousands
of identical events. The breaker re-arms after a 5-minute quiet cooldown
so a transient fault still self-heals without a reinstall.
* feat(desktop): make crash-loop recovery active + observable
Review feedback: the breaker only "waited" — its cooldown re-arm reloaded
the same URL from the same cache, and the whole flow was near-invisible.
On re-arm it now actively recovers: clears the HTTP + V8 code caches
(non-destructive — no cookies or storage) before the retry reload,
targeting the most common recoverable cause (a corrupt cached bundle or
shader) instead of hoping conditions changed on their own.
And it's now observable: a console.warn when the breaker opens (parking),
a console.info per recovery attempt, and a bounded recovery-attempt
analytics signal (reason:"recovery-attempt", recovery_attempt: N, at most
one per cooldown). Triage can tell a chronic looper (index keeps
climbing) from a device that recovered (events simply stop) without
re-introducing the per-crash spam the breaker was built to kill.
* revert(desktop): drop active cache-clear recovery, keep it passive + observable
Reviewer worry: clearing caches on every cooldown could amplify churn on a
wedged device without helping the dominant crash cause (a GPU/V8 CHECK,
exit 0x80000003), i.e. pour oil on the fire. Back the breaker off to
"stop the loop + passive retry": on re-arm it reloads without mutating any
device state and lets a transient fault clear on its own.
The observability added alongside stays: a warn on breaker-open, an info
per recovery attempt, and the bounded recovery-attempt analytics signal
(recovery_attempt: N) so chronic loopers are still visible.
* feat(desktop): add report + save-logs actions to the renderer crash screen
The recovery screen was informational only. On a renderer crash the daemon
is still alive, so give the wedged user an actionable exit — reusing IPC the
preload already exposes, no new surface:
- "Report a problem" → window.__od__.openExternal() opens a prefilled GitHub
new-issue whose title + body carry the app version, OS, and the renderer
exit code (e.g. 0x80000003) a triager needs, and ask the user to attach
the logs.
- "Save logs…" → window.openDesignDesktop.exportDiagnostics() runs the
existing diagnostics-bundle export (save dialog → zip).
Neither an issue URL nor mailto can carry a file attachment, so the flow is
save-then-attach. Each button hides itself if its bridge is missing (preload
failed to load) to avoid a dead control.
* feat(desktop): add "Email us" (support@open-design.ai) to the crash screen
Alongside "Report a problem" / "Save logs…", the recovery screen now offers a
prefilled mailto to our support address for users who'd rather email than open
a GitHub account. It reuses the existing openExternal bridge; the
shell:open-external handler now also allows a mailto addressed strictly to the
support address (isSupportMailtoUrl, unit-tested) — no other scheme or address
opens, so widening past http can't be abused into arbitrary-scheme launches.
The link hides itself if the bridge is absent, like the other actions.
* style(desktop): match crash screen to the app's monochrome brand palette
The crash screen used a stray blue accent that didn't match the app. Repaint
it with the neutral design tokens from apps/web (tokens.css): warm off-white
#faf9f7 + near-black #1a1916, a monochrome near-black primary button, and a
plain underlined (not blue) email link. Dark mode inverts the pair to a light
button on the dark bg (a near-black button would vanish), mirroring the
black/white onboarding look.
* fix(desktop): address review — tighten support mailto + reveal crash screen
Two Looper findings on the crash-screen actions:
- Security: isSupportMailtoUrl validated only protocol + pathname, so
`mailto:support@open-design.ai?bcc=attacker@example.com` (or ?to=/?cc=)
still passed and shell.openExternal would launch the mail client with
extra recipients/headers through the renderer-exposed bridge. Now reject
any query key other than the crash screen's own subject/body.
- Startup lifecycle: parking on the crash screen left `revealed` false and
the splash open when the loop tripped before revealWhenReady(), so the
runtime's show() kept focusing the splash — a user re-focusing during a
startup crash loop was sent back to the boot splash. Route the park
through revealMainWindow() so the recovery screen becomes the revealed,
focused window and the splash is torn down (no-op after normal reveal).
* feat(diagnostics): make the support bundle root-cause native renderer crashes
Audit of "Save logs…" for the crash-recovery screen: on a renderer crash the
bundle collected text logs but NOT the one artifact that can explain an opaque
native abort like 0x80000003 (a V8/Chromium CHECK) — a minidump. crashReporter
was never started (no dumps existed anywhere), the crash-report collector was
macOS-only, and there was no GPU info (the usual suspect for such crashes).
Close the gap end to end:
- apps/desktop: start crashReporter (uploadToServer:false — on-device only) and
relocate Electron's crashDumps into <logs/desktop>/crashes, inside the log
tree the export already bundles; snapshot GPU + feature-status +
runtime info to <logs/desktop>/gpu-info.json at startup.
- packages/diagnostics: add a `binary` LogSource kind (copied verbatim, no text
decode/redaction so a minidump isn't corrupted) and findCrashDumps() to
collect .dmp files; buildDiagnosticsZip gains a `crashDumps` input.
- apps/daemon: bundle logs/desktop/gpu-info.json and the relocated crash
minidumps (cross-platform, capped, within 14 days).
The renderer crash context (reason/exit code/url) is already captured — the
main-process console.error lands in logs/desktop/latest.log — so a bundle now
carries stack (minidump) + GPU state + context: enough to root-cause.
* fix(desktop): mkdir crashDumps dir before setPath so first-crash dumps land in the log tree
Review (PerishCode): app.setPath('crashDumps', dir) throws when the dir does
not exist; on a fresh runtime logs/desktop/crashes won't exist yet, so setPath
threw, we fell back to Electron's default crashDumps location, and the daemon
export (which only scans <logs/desktop>/crashes) missed the minidump for
exactly the first crash loop this is meant to root-cause. mkdirSync the dir
(recursive) before setPath.
* fix(desktop): reject CR/LF in the support mailto subject/body
Review (PerishCode): the key allowlist rejected to/cc/bcc keys, but a decoded
CRLF in an allowed value (subject=ok%0D%0ABcc:attacker@…) could still inject a
mail header once the mailto reaches the mail client. Reject any subject/body
value containing CR or LF.
A PR ejected from the merge queue by the needs-validation gate leaves no
trace on the PR itself: the gate fails on the queue's transient ref, the
PR's own checks stay green, and mergeQueueEntry silently goes null. The
only way to learn why the PR fell out of the queue is to dig through
merge_group workflow runs, which nobody watches (observed on #5329).
Producer: the merge_group gate in ci.yml now emits a handoff/comment
artifact targeting the labeled PR right before it blocks, and uploads it
on the failure path. Consumer: comment.atom.yml accepts merge_group
workflow_run events; since a merge_group run's head_sha is the queue's
synthetic merge commit, artifacts from those runs are bound to their
producing run by run_id, and the base-freshness skip is waived (PRs
ahead in the queue move the base while the run completes). The live-PR
head check still guards staleness.
Topology coverage pins the new boundary: comment.atom consumes
pull_request + merge_group; autofix/report stay pull_request-only.
* fix(daemon): reap the failed attempt's process group on same-run retry
A same-run retry tore down the failing attempt with run.child.kill('SIGTERM') —
a signal to the direct child PID only. Killing a parent does not kill its
children on POSIX, so any descendants the CLI spawned (MCP servers, tool
subprocesses, internal runners) were orphaned (re-parented to PID 1) and kept
running. One process group leaked per retry, accumulating over the daemon's
lifetime. The cancel path already disbands the whole group via
process.kill(-pgid); the daemon even spawns detached and captures
run.processGroupId, but the retry teardown never used it.
Add signalProcessGroup(pgid, signal) — a group signal that works even after the
direct child object has exited (grandchildren outlive it), kept SEPARATE from
signalChildProcess so the shared cancel/escalation path keeps its childHasExited
guard against the cross-generation kill fixed in #5202. reapProcessGroup(pgid)
does SIGTERM then a SIGKILL escalation, both bound to the CAPTURED pgid.
tearDownAttemptForRetry now snapshots the attempt's child + pgid, reaps its
group by the captured pgid (falling back to a direct-child signal on win32 / no
pgid), and clears run.processGroupId so the next attempt captures cleanly. The
escalation targets the captured pgid, never the shared field the next attempt
overwrites — so it cannot hit the healthy new attempt's group (#5202).
Fixes#5462.
Regression: tests/retry-orphan-process-group.test.ts drives a full run over the
HTTP API with a fake CLI that spawns a long-lived grandchild then stalls into a
retryable timeout; it asserts the grandchild is dead after the run finishes. Red
on origin/main (grandchild survives, PPID=1), green with this fix. Verified live
on a real daemon: a 20x retry stress leaked 20/20 grandchildren before the fix
and 0/20 after, with every run still completing 'succeeded'.
* fix(daemon): reap the process group on cancel/shutdown when the child already exited
Secondary case from #5462: when the parent CLI has already exited but a
grandchild still holds the inherited stdio pipe (run still running), cancel
returned 200 and finished the run canceled, but the grandchild survived — the
same childHasExited short-circuit in signalChildProcess that the retry teardown
hit. killChild (used only by the terminating cancel / shutdownActive paths, never
by a re-spawning retry) now falls back to signalProcessGroup by pgid when the
direct child has already exited, so cancel/shutdown reap surviving descendants
too. No cross-generation risk: both callers terminate and never re-spawn into
the pgid (cf. #5202).
Covered by the existing cancel/shutdown/lifecycle suites staying green (no
regression); the retry-orphan regression exercises the shared signalProcessGroup
helper end-to-end. A dedicated cancel-after-parent-exit HTTP regression can be
added if preferred — the forensics harness confirms this case manually.
---------
Co-authored-by: tomsen-ai <230283659+tomsen-ai@users.noreply.github.com>
A design-generation run that ended its turn with TodoWrite tasks still
pending / in_progress / stopped — or was truncated by max_tokens — was
stamped `succeeded` and read "Completed" on the Pet task center and the
project pill, while the chat footer simultaneously read "Stopped with
unfinished work". Users trusted "Completed", assumed the work was done, and
re-requested, creating duplicate generations and eroding trust in the
run-state UI (#1247 / #1060).
Root cause: the daemon equates "the turn/process ended" with "succeeded" and
never consults the agent's own declared work; multiple web surfaces then map
`succeeded` -> "Completed" without any completeness signal, even though the
chat footer already distinguishes unfinished work.
Fix — one canonical signal, consumed everywhere so the surfaces cannot drift:
- packages/contracts/api/run-completeness.ts defines the canonical
"unfinished declared work" predicate (status !== 'completed', so pending /
in_progress / stopped all count), matching the web footer's
unfinishedTodosFromEvents exactly.
- Daemon folds the last TodoWrite snapshot + a max_tokens truncation off the
agent event stream (every runtime, via the single emitAgentEvent choke
point) and derives run.endedWithUnfinishedWork once at finish().
- The project-status projection derives `incomplete` from the already
persisted events_json — no schema migration — so it survives reload/reattach
instead of flashing "Completed" once the in-memory run ages out.
- Contracts carry endedWithUnfinishedWork on ChatRunStatusResponse and the SSE
end payload (backward compatible: absent = finished), plus a new
`incomplete` ProjectDisplayStatus.
- Web surfaces (Pet task center, project pill, DesignsTab) render `incomplete`
as amber needs-attention, never the success color; the footer now sources
the same shared predicate.
Truncation uses the display-only option (spec Option A): the run stays
`succeeded` but is flagged incomplete, avoiding the code===0 / stdin-close
hazards of reclassifying a truncated turn to failed.
Regression coverage, red on main / green here: an HTTP-boundary e2e
(e2e/tests/dialog/task-completeness.test.ts) drives the interactive chat path
for unfinished / stopped / truncated / finished runs; daemon + contracts +
web unit tests pin the projection and the canonical predicate.
Ref: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/011f6f51-7a25-4043-ae94-182e0b8b0931
Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
Generated-mode design systems used to unconditionally rewrite every
derived file on each update path (metadata edit, revision accept,
brand re-extract, and the README-missing read-path regeneration),
silently clobbering user-created components/directories and edited
docs back to scaffold values. Users then had to redo their work.
Introduce a hidden `.od-generated.json` fingerprint manifest recording
what the generator last wrote per derived file, and a shared
`filterGeneratedWritesPreservingUserEdits` helper that only overwrites
a file when it is still byte-identical to that fingerprint. Anything
the user edited — or any pre-existing file with no fingerprint (legacy
systems) — is preserved; absent files are written and fingerprinted.
The filter lives inside `writeGeneratedDesignSystemFiles`, so all three
callers (create/update/ensure) inherit non-destructive semantics; the
revision-accept atomic-write path is filtered separately and drops
fileChange-owned paths from the manifest so hand-authored revision
content survives later regenerations.
Refs: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/50579eb2-c64b-4a2d-8fda-d10f4d4af626
Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
The claude stream-json parser read `message.stop_reason` off any
`type:'assistant'` wrapper and, when it was not `tool_use`, emitted
`turn_end` without inspecting the top-level `parent_tool_use_id`. Under
`--verbose`, a Task sub-agent's messages stream inline with a non-null
`parent_tool_use_id`, and its internal turn ends with its own
`stop_reason: 'end_turn'`. That sub-turn boundary was therefore treated
as the run's turn completion: applyClaudeStreamJsonRunBookkeeping set
`run.turnCompletedCleanly = true` and closed stdin while the main turn
was still running. If the main agent then exited non-zero without a
result frame (crash / OOM / connection-drop abort), the close handler
found `turnCompletedCleanly === true` and classifyChatRunCloseStatus
translated the non-zero exit into `succeeded` — a silent false success
that hid the failure, skipped the same-run retry, and surfaced the
sub-agent's internal scratch as the answer.
Guard the emit on `parent_tool_use_id == null` so only a main-turn frame
fires `turn_end`. Suppressing at the parser keeps the invariant in one
place: the same branch also resets the per-turn artifact-echo dedup state
(`recentWriteContents` / `wroteHtmlFileThisTurn`), which likewise must
not fire on a sub-agent boundary. The loose `== null` preserves existing
behavior for any main-turn frame that omits the field.
Fixes#5487
Co-authored-by: tomsen-ai <230283659+tomsen-ai@users.noreply.github.com>
Deleting a conversation or project left its active agent run's CLI
subprocess alive — it kept consuming provider quota, stayed non-terminal
in GET /api/runs with no UI trace, and for a project delete kept writing
into a directory that had been removed out from under it.
Both DELETE handlers now cancel the non-terminal runs they own (reusing
the existing design.runs.cancel) before the row is removed, through a
shared cancelRunsOwnedBy helper.
* feat(chat): surface daemon failure_detail to sharpen run-error guidance
The daemon already classifies each run failure into failure_category /
failure_detail (run-failure-classification.ts) for retry policy and
telemetry, but kept it daemon-internal — so the chat could only key error
guidance off the coarse errorCode. That collapses distinct causes that
share one code: a hard quota and a transient 429 both arrive as
RATE_LIMITED, and a text-detected missing CLI leaks in as the opaque
AGENT_EXECUTION_FAILED.
Propagate the classification to the client (contract DTO + SSE end frame,
mirroring the proven `resumable` path) and let resolveRunFailureUi take
`detail` as a first-class input that can override a too-coarse code:
- hard_quota / workspace_credits_exhausted -> a distinct "Quota exhausted"
card that drops the futile Retry (primaryAction 'none') and steers to the
hosted-AMR switch card instead.
- cli_not_installed (text-detected) -> the same install-the-CLI card the
AGENT_UNAVAILABLE code path already renders.
A transient 429 without a hard-quota detail still offers Retry, and the
Antigravity terminal switch-model flow still wins over the generic detail
override.
Stacks on #5308 (the code->guidance mapping this detail layer refines).
* chore(i18n): rebrand user-facing "AMR" to "Open Design Cloud"
The hosted model service is now Open Design Cloud, but the run-error
cards this PR sharpens (switch / auth / balance), plus settings account +
wallet labels, avatar console, onboarding gate, and the handoff website
link, still showed the old "AMR" (or bare "Open Design") name. Normalize
every user-visible brand mention to "Open Design Cloud" across all 19
locales. Value-only: no keys, agent ids, URLs, or raw daemon error
strings touched.
* fix(chat): persist daemon failure classification onto assistant message
The daemon emits the terminal `error` SSE frame from the child-close
handler before a run is finalized, so `failureCategory` / `failureDetail`
(computed at finalize) weren't known when that frame was first persisted
to the assistant message — the stored `status:error` event kept only
`{ detail, code }`. A failure persisted without the live web error
handler saving the message, or a conversation reloaded before that save
landed, therefore fell back to the coarse errorCode UI and lost the
specific fix guidance this PR adds.
Stamp the finalize-time classification onto the persisted assistant
message: enrich the last `status:error` event in place (append one only
if a failed run somehow never persisted an error frame). Covered by a
daemon regression that reads the stored assistant message via the
conversation-messages API after a failed hard-quota run.
* fix(chat): keep daemon failure classification on the live web reload/recovery paths
Two live-propagation gaps let the sharpened run-error card degrade back to
the coarse/raw UI even though the daemon now sends the classification:
- appendErrorStatusEvent() no-op'd whenever the message already had a
trailing status:error with the same detail. In the reload/reattach race
(reload reads the daemon-persisted error frame first, then the run
finishes and onError arrives with the finalize-time classification) that
dropped the new code / failureCategory / failureDetail. Merge those into
the existing event in place instead; no-op only when nothing is added.
- The error-frame-then-status recovery probe in providers/daemon.ts broke
out on a terminal failed status without copying failureCategory /
failureDetail (unlike the post-loop status fallback), so markErrorRunFailure
stamped null and the failureDetail mapping fell back to the generic card on
the reconnect path. Mirror the two assignments before the break.
Covered by chat-events (in-place merge) and daemon-sse-reconnect (error
frame + terminal status fetch, no end frame) regressions.
* feat(chat): map long-tail failure_detail values to specific run-error cards
#5321 pipes the daemon's failure_detail to the chat but only mapped the
high/medium-frequency causes (hard_quota, workspace_credits, cli_not_installed).
This wires the recoverable long tail the daemon already classifies — timeout,
inactivity_timeout, empty_output, session_resume_expired, git_bash_missing —
each to a named failure type + one-line fix with a plain Retry.
These are engine-neutral outcomes, so they resolve in a new
AGENT_AGNOSTIC_DETAIL_FAILURE_UI layer BEFORE the AMR/Antigravity agent
branches (which would otherwise return a generic retry for AMR). Adds the
title/message i18n keys across all 19 locales and unit coverage for the
new mappings.
---------
Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Disable Grok plan mode and auto-approve tool calls for non-interactive runs so permission requests cannot become successful no-ops. Add regression coverage for the required argv flags.\n\nFixes #5507
Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>
* fix(daemon): guard AMR proxy stream bodies against error crashes
The AMR/vela API proxy pipes the upstream response body to the client
(`upstreamRes.pipe(res)`) and the client upload body to the upstream
(`req.pipe(upstream)`). Only the outbound ClientRequest had an `'error'`
handler; neither piped stream body did. `.pipe()` does not forward a
source `'error'`, and a stream that emits `'error'` with no listener
throws — an unhandled exception that crashes the privileged daemon.
Both legs have real error paths: the upstream response body can
ECONNRESET mid-stream (a routine network drop), and the inbound request
body errors when a client aborts an upload. Either one takes the whole
daemon down today.
Route both pipes through `pipeProxyStreamWithGuard`, which attaches the
missing source-error handler: an upstream response error 502s (or
destroys the response once headers are sent), and a client-upload error
destroys the upstream request. Same class as #5126 (unhandled read-stream
error in the library asset route), different site.
Regression test pins the crash vector — a bare pipe throws on a source
error — against the guard routing it to the handler instead.
* chore: re-trigger fork-pr approval workflow (transient blip)
---------
Co-authored-by: tomsen-ai <230283659+tomsen-ai@users.noreply.github.com>
The plugin devloop tokens_used reconciliation in firePipelineForRun (server.ts,
reached from routes/runs.ts when a run resolves a plugin snapshot with a
pipeline) calls scanRunEventsForUsageAnalytics(run.events, ...), but server.ts
never imported it. Every reconciliation pass therefore threw
`ReferenceError: scanRunEventsForUsageAnalytics is not defined`, which the
surrounding .catch swallows into a `[plugins] devloop tokens_used reconciliation
failed` warning — so run_devloop_iterations.tokens_used has silently never been
backfilled since the call was added.
Regression source: PR #4644 (24298737c) added the call and the UPDATE but not
the import. Fix adds the missing name to the existing run-analytics-observability
import.
The call site is a private closure inside startServer reached only by a completed
plugin-devloop pipeline run, so a faithful end-to-end red spec would need a
bespoke pipeline-plugin fixture + fake agent + full HTTP devloop run — dispro-
portionate for a missing-import ReferenceError that is certain by static analysis
and git-blame. Verified instead by: the absent import (grep), the guaranteed
throw, git blame to #4644, daemon typecheck (both tsconfigs), pnpm guard, and the
existing plugins-pipeline-runner / plugins-headless-run suites staying green.
Co-authored-by: tomsen-ai <230283659+tomsen-ai@users.noreply.github.com>
* feat(contracts): agent-ready capability contracts + design RFCs (#5398)
Slice 1 of the "agent-ready OD" proposal (#5398): pure-TypeScript contract
shapes for the assistant's capability layer, plus the design RFCs. No behavior
code.
- packages/contracts/src/agent-tools/: descriptor (declare), actions
(call/result), manifest (list + paged search), registry (port), task
(cross-project AgentTask). Discriminated on `surface`; dual-track law is
structural; action identity is a daemon-minted `invocationId`.
- docs/rfc-drafts/agent-ready.md: umbrella design + slice roadmap, the
MCP/WebMCP-congruent capability model, tool-search options, and
"options considered -> recommendation" for the load-bearing decisions.
- docs/rfc-drafts/agent-ready-cross-project.md: the maintainer's separate
reviewable permissions writeup (AgentTask + permission options).
Registry runtime, endpoints, and behavior are later slices per the roadmap.
* fix(contracts): narrow AgentToolSearchQuery.surface to 'api'
Search is the PULL path over the persistent registry, which per the RFC
holds only api-surface tools — browser availability is PUSH-advertised
per-session and cannot be answered by a session-less search query. Narrow
the searchable surface to 'api' so the contract does not encode a
browser-tool discovery path the runtime would have to special-case or
break. Addresses the blocking review on #5400.
* fix(contracts): address review — narrow search result to api-only; distinct browser vs api call paths
Resolves the two remaining blocking findings on PR #5400:
- manifest.ts: narrow `AgentToolSearchResult.tools` from the full
`AgentToolDescriptor` union to `ApiToolDescriptor[]`, enforcing the
PULL/api-only search invariant on the response side too (the query
`surface` was already narrowed to `'api'`). A registry impl can no
longer type-check while leaking `browser` descriptors into a
session-less search result.
- agent-ready.md: stop presenting `BrowserActionRequest` as the generic
`tools/call` for every capability. The two surfaces now have distinct
request paths: `api` tools are invoked through their declared `/api/*`
route + `od` subcommand (no round-trip envelope), while
`BrowserActionRequest`/`Result` is the browser-surface path only — the
genuinely new inbound seam.
- agent-ready-cross-project.md: reconcile the slice-5 cross-project
routing fields (`taskId?`/`targetProjectId?`) with the distinct-paths
model — added on both the browser envelope and the api-call request,
not bolted onto `BrowserActionRequest` for api tools.
Validation: contracts typecheck pass; pnpm guard pass.
---------
Co-authored-by: LA <la@LAs-MacBook-Pro.lan>
Export-surface-stable split of packages/platform/src/index.ts (~1111 lines,
one file, ~5 unrelated concerns) into cohesive sibling modules, with index.ts
kept as a pure barrel of explicit named re-exports.
- command.ts cross-platform command-invocation construction
- process.ts process lifecycle, stamps, snapshots, stop escalation
- proxy-env.ts system proxy discovery + proxy-aware env merging
- fs.ts path containment, atomic copy, removal, log tails
- http.ts HTTP readiness polling
- toolchain.ts user-level toolchain bin discovery
Function bodies moved byte-identically; only import paths adjusted. Added a
@module docblock per file and JSDoc to every exported function. Tiny private
error-extraction helpers (errorCode/errorMessage) are duplicated where used to
keep the public surface identical (no new exported names). Public export set
unchanged (42 names); typecheck + 76 tests + build all green.
Tracked by #5165.
Co-authored-by: LA <la@LAs-MacBook-Pro.lan>
A Claude CLI error termination (error_during_execution, error_max_turns,
resume failures) emits a result frame with is_error true, stop_reason
null, and the human-readable cause in errors[] — then exits 1. Three
layers each washed that signal away:
- claude-stream.ts forwarded the frame as a plain usage event, dropping
is_error and errors[] (qoder-stream already honors this contract).
- applyClaudeStreamJsonRunBookkeeping treated the null stop_reason as a
clean terminal turn and set turnCompletedCleanly.
- classifyChatRunCloseStatus translated the non-zero exit into
'succeeded' via turnCompletedCleanly, skipping the CLI failure
diagnostics entirely.
Net effect: the run reads as succeeded with nothing surfaced — a silent
failure.
The fix keeps one authority per decision:
- The parser emits the error (message from errors[]) and flags the
usage event isError; success frames keep their exact shape.
- Bookkeeping still closes stdin (the turn IS over) but no longer marks
an error termination clean. A SessionEnd-hook non-zero exit after a
normal result (#3373) carries no is_error and keeps the clean path.
- The chat path suppresses the error event during a resume attempt so
the close handler's transparent reseed (#4438) stays invisible,
mirroring the existing ACP resume_failed suppression.
- The connection test skips terminal result errors — its own result
frame parse (#4501) stays the sole authority for the output_parse
phase classification.
Co-authored-by: tomsen-ai <230283659+tomsen-ai@users.noreply.github.com>
The main window's render-process-gone handler read
window.webContents.getURL() unconditionally. During app quit / teardown
the window and its webContents can already be destroyed when the event
fires, so getURL() throws "Object has been destroyed" as a fatal
uncaught exception. It is most visible right after launch when the
auto-updater triggers a quit-to-apply, making the app look like it
crashes seconds after startup.
Guard the same way sendUpdaterStatus already does a few lines below:
short-circuit once the window or its webContents is destroyed, and skip
crash-report / recovery work when the renderer is exiting as part of
shutdown.
Add a source-assertion red spec (mirroring renderer-recovery-poll-loop
and window-chrome, since createDesktopRuntime can't be instantiated
under vitest) that pins the teardown-guard invariant.
Fixes#5095
Move the conversation count from centered position between the
header title and '+ New' button to immediately after the title.
The '+ New' button is pushed to the right via margin-left: auto.
* i18n(web): add built-in skill user-override strings
Add skillsOverrideCreate / skillsOverrideSave / skillsBuiltInOverrideWarning
to the typed dictionary and every locale, ahead of wiring the built-in
skill edit flow through them.
* fix(web): localize the built-in skill override confirmation
The built-in-skill edit warning was hardcoded English, so it stayed
English under non-English locales even though the rest of the dialog
followed the active language. Read it from the i18n dictionary instead.
* fix(web): label built-in skill editing as create/save user override
Editing a built-in skill silently produced a user override, but the
affordances read "Edit" / "Save" — so the built-in → user type change
was a surprise. The edit action, confirmation, and form heading now read
"Create user override" and the submit reads "Save as user override".
* test(web): lock built-in skill override wording across locales
Cover the regression gap raised on #4793: assert the built-in skill edit
flow renders the user-override i18n keys end to end — the edit tooltip, the
confirmation warning, the confirm button, the form heading, and the submit
button — in the default en locale and a translated zh-CN path, and that a
user skill keeps the plain Edit/Save affordance with no override warning.
Assertions compare against the locale dictionaries rather than hardcoded
literals, so a copy tweak in a locale file stays green while reverting to
the old Edit/Save keys (or a hardcoded-English warning) goes red.
The plugin-preview bake renders one preview at a time — a headless-Chrome
screencast + ffmpeg encode per plugin, ~16s each, fully serial. When a PR
touches the deck renderer (as #5083 did), every deck example's served preview
HTML changes, so the content-hash skip re-bakes them all: 80 clips × ~16s ≈
19 min of pure serial render, dwarfing the ~3 min of setup. The bake is the
single long pole on those PRs (24m vs ~5m for everything else).
Parallelize the render with a bounded worker pool (PREVIEW_CONCURRENCY, default
4 = the standard GitHub runner's vCPUs). Local A/B on 6 forced re-bakes: 83s
serial -> 32s at concurrency 3 (~2.6x); the 80-item CI sweep amortizes launch
cost across ~20 clips/worker, so it should land closer to ~4x — dropping the
render from ~19 min to ~5 min. Shared by the pre-merge, post-merge, and
release-cut bake workflows, so all three get faster from this one change.
Three things had to hold for concurrency to be correct, each verified locally:
- One browser PER worker, not a shared browser with N pages. CDP screencast
only follows the ACTIVE tab, so a page captured while another is foreground
is starved of frames — I reproduced both failure modes (a frames-0 skip and
a 5KB near-empty clip). Browser-per-worker keeps every captured page the sole
foreground tab; clip sizes then match the serial baseline exactly. Launch is
lazy so an all-cached sweep (the common post-merge case) pays no browser.
- ffmpeg/ffprobe moved off execFileSync onto promisified execFile: a
synchronous encode blocks the event loop and stalls every other page's
screencastFrameAck (Chrome only sends the next frame after the ack).
- Clamp inter-frame durations to >=0. CDP frame timestamps are occasionally
non-monotonic, and the resulting negative concat `duration` makes ffmpeg
reject the whole clip ("Invalid data found when processing input") — which
fails the strict pre-merge job. Pre-existing, but CPU contention from
concurrent renders makes the blips more frequent. Reproduced serially
(example-deck-swiss-international) and confirmed the clamp turns it green.
Also exit explicitly at the end: puppeteer leaves a lingering handle after
browser.close(), and with a browser per worker that keeps the CLI alive after
all work is done — on CI the step would hang to the 90-min job timeout. Drain
stdout/stderr first so the piped CI summary is never truncated.
Verified against a real daemon (127.0.0.1 /api/plugins): 6/6 baked, 0 skipped,
clean exit (0 lingering processes), clips byte-comparable to serial output.
Clicking Continue on the discovery question form only queues the answer
send; the parent's busy/answered signals arrive a beat later. Until then
the CTA stayed enabled with no feedback, so an impatient user (thinking it
was stuck) — or the auto-continue countdown racing the click — could fire
the same submit again and again, piling dozens of identical
"[form answers - discovery]" sends into the queue.
Give the panel a single-submit invariant: on the first commit it
optimistically locks (a synchronous ref guards the chokepoint), disables
Continue/Skip, and shows a spinner + "Submitting…" so it never looks
stuck. The lock spans the async file upload and is released if that upload
fails so the user can retry; otherwise it hands off to submitDisabled once
the turn is busy and releases on answered, so an idle form stays
actionable.
* feat(deck): enhance slide deck functionality with speaker notes and navigation improvements
- Added a switch for including speaker notes in slide decks, defaulting to true.
- Updated keyboard navigation to include 'R' for resetting to the first slide.
- Implemented click navigation for half-slide transitions.
- Enhanced the deck framework to support hidden programmatic prev/next controls.
- Improved handling of file inputs in question forms to support file submissions.
This update aims to improve user experience in creating and presenting slide decks, ensuring better accessibility and functionality.
* feat(artifacts): enforce semantic naming for user-facing deliverables
- Updated guidelines to require semantic filenames derived from user briefs for new artifacts, moving away from defaulting to `index.html`.
- Enhanced documentation to clarify naming conventions for HTML files, emphasizing the use of descriptive names like `investor-pitch-deck.html`.
- Adjusted prompts and hints across the system to reflect these changes, ensuring consistency in user-facing outputs.
- Improved test coverage to validate the new naming conventions and ensure proper implementation in various contexts.
This update aims to enhance clarity and usability in project file management, promoting better organization and user experience.
* feat(file-viewer): enhance speaker notes display and improve file workspace functionality
- Updated the speaker notes section to include a styled placeholder for empty notes, improving user experience.
- Changed the icon in the file workspace from "slides" to "panel-left" for better clarity.
- Introduced new sub-category filtering options in the file workspace, enhancing organization and navigation.
- Improved styling for the page creator search input and added visual feedback for loading states.
These changes aim to enhance usability and visual consistency across the file viewer and workspace components.
* test(web): align workspace tests with pages switcher
* test(e2e): open design files through pages menu
* fix(deck): keep source mode reachable
* fix(workspace): restore uploaded file tabs
* feat(deck-thumbnail): implement virtualized thumbnail rail for improved performance
- Introduced a new `DeckThumbnailRail` component to optimize the rendering of deck thumbnails, reducing memory usage and improving load times.
- Enhanced the `FileViewer` to integrate the new thumbnail rail, allowing for lazy loading of thumbnails based on viewport visibility.
- Added tests for the `DeckThumbnailRail` to ensure correct functionality and caching behavior.
- Updated styles for better visual consistency and performance during thumbnail rendering.
These changes aim to enhance user experience by providing a smoother and more efficient way to navigate through deck slides.
* feat(deck): enhance deck rendering with thumbnail support and keyboard shortcuts
- Integrated `DeckSlideThumbnail` for rendering individual slides in a shadow root, improving performance and reducing main-thread load.
- Updated `DeckThumbnailRail` to utilize the new thumbnail component, allowing for efficient slide navigation.
- Implemented keyboard shortcuts for deck navigation, enhancing user experience during presentations.
- Added tests for `buildDeckRenderInput` to ensure proper functionality of the deck stage fallback.
- Improved styling for loading states in the viewer, providing a smoother visual experience.
These changes aim to optimize the deck viewing experience by reducing resource consumption and enhancing interactivity.
* feat(deck): improve thumbnail loading and speaker notes editing experience
- Refactored `DeckSlideThumbnail` to enhance loading states with a new shimmer effect, improving visual feedback during thumbnail rendering.
- Updated `DeckThumbnailRail` to manage loading states for thumbnails, ensuring a smoother user experience.
- Simplified the speaker notes editing interface by removing the toggle switch, allowing direct editing from the notes body.
- Enhanced accessibility by adding roles and focus states to speaker notes elements.
- Added tests to verify the new loading states and speaker notes functionality.
These changes aim to optimize the user experience in deck navigation and speaker notes management, providing a more intuitive and responsive interface.
* feat(file-viewer): enhance version export functionality and speaker notes interaction
- Introduced new export options for file versions, including PDF and image formats, improving the ability to download and share content.
- Implemented a streamlined process for exporting version content, with progress indicators and error handling for a better user experience.
- Enhanced speaker notes editing by refining event handling to prevent unintended focus shifts, ensuring a smoother editing experience.
- Updated styles for improved visual consistency in the file version download interface.
- Added tests to verify the functionality of version exports and speaker notes interactions.
These changes aim to optimize the file viewer's usability and enhance the overall experience when managing and exporting project files.
* fix(web): polish export menus and creator filters
* fix(file-viewer): fall back to preview exports
* test(file-viewer): cover version export fallbacks
* test(file-viewer): assert loading skeleton by role
* fix(file-viewer): restore html source mode affordance
* fix(file-viewer): capture full version fallback snapshots
* docs: trim slides handoff eof
* test(exports): cover full preview snapshot requests
* test(file-viewer): account for snapshot options arg
* refactor(file-viewer, file-workspace): streamline export functions and enhance page creator dialog visibility
* test(file-workspace): enhance slide template creation and mock plugin responses
* feat(export): add support for versioned exports and enhance HTML export functionality
* feat(export): implement versioned export support across multiple formats and enhance related tests
* feat(file-viewer): enhance manual edit functionality with commit sequence tracking and add test for inline text session saving
* style(speaker-notes): update filmstrip and thumb-frame styles for improved responsiveness and add corresponding tests
* feat(file-viewer): enhance comment composer functionality with portal metrics and improve draw toolbar layout for mobile views
* feat(analytics): track slide deck viewing experience
Add analytics for the new deck/slides experience introduced in this PR:
the deck viewer surface, in-deck slide navigation, thumbnail rail, and
speaker-notes authoring. Presentation entry stays on the existing
present_popover click to avoid double-counting.
- contracts: DeckViewerSurfaceViewProps (surface_view),
DeckViewerClickProps (ui_click), SpeakerNotesSaveResultProps
(new speaker_notes_save_result event), wired into the event unions.
- web: track* wrappers + fire helpers in FileViewer — deck_viewer
surface_view on deck mount, slide_prev/next/reset, thumbnail_select,
thumbnail_rail_toggle (expand/collapse), speaker_notes_edit, and
speaker_notes_save_result (preview + presenter surfaces).
- tests: deck analytics suite asserting the emitted (event, props) pairs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(file-viewer, file-workspace): enhance export toast functionality and improve localization for page creation
* feat(home-view, plugins-home-section, agent-skill): update default facet preference, enhance category selection handling, and improve localization support
- Set `preferDefaultFacet` to true in HomeView component for better user experience.
- Introduced `handlePickCategory` function in PluginsHomeSection to manage category selection based on card layout visibility.
- Updated PRIMARY_CATEGORIES to reflect the new order and added localization for titles and descriptions in adaptAgentSkill.
- Enhanced tests to verify the correct handling of display copy and tags from SKILL frontmatter.
* feat(plugins-home, file-workspace): introduce commercial category labels and enhance deck handling
- Added a new module for resolving and localizing commercial category labels for plugins.
- Updated the FileWorkspace and PluginCard components to display the commercial category label on deck cards.
- Enhanced the PageCreatorPresetFrame to adjust frame classes based on deck presets.
- Refactored subcategory handling in facets to align with the new commercial category taxonomy.
- Improved localization support for new category labels across multiple languages.
* feat(deck-preview): implement deck preview scaling and enhance home hero layout
- Introduced `useDeckPreviewScale` hook to manage scaling of deck previews within a fixed 1280×720 logical viewport, ensuring proportional rendering across various templates.
- Updated `FileWorkspace`, `HomeHero`, and `PluginCard` components to utilize the new scaling functionality for improved visual consistency.
- Enhanced the layout of the home hero section to accommodate new subtype chips and descriptions, including a "More" dropdown for overflow handling.
- Improved localization support for new UI elements across multiple languages.
* fix(PreviewModal): clarify sidebar prop documentation and update test description
- Updated the documentation for the `PreviewSidebar` interface to specify that the sidebar's stage-edge handles are not shown when the sidebar prop is absent.
- Modified the test suite description for `PreviewModal` to be more concise and relevant.
* test(e2e): select prototype visual card
---------
Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>