-
[OPIK-6425] [FE] feat: Explain (Ollie) on trace cells — bridge, store, button & popover (#7132)
发布于
2026-06-30 12:04:40 +00:00 - [OPIK-6425] [FE] feat: add Explain bridge contract and explain store
Host-side foundation for the Ollie "Explain" feature (no UI yet).
- Bridge contract: 7 explain/chat/console events + ExplainTarget in
assistant-sidebar.ts; bump BRIDGE_PROTOCOL_VERSION 1->2; add the three
new host events to createHostListeners() and route the four shell->host
events into the explain store. Must stay byte-identical with ollie-console. - Plugin-scoped explain store (src/plugins/comet/explain): caches results
per cell (kind:entityId) so reopening a popover shows the cached answer or
its still-streaming text; an explainId->cell route map multiplexes parallel
streams over the single bridge; in-flight cap of 4; capabilities from
console:ready; ownership-guarded host->shell emit registration.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] fix: address review + extract emitter hook
- Scope explain cache/route key by projectId (no cross-project collision).
- Drop routes[explainId] on done/error so the route map can't grow unbounded.
- Extract the emit-registration effect into a named useRegisterExplainEmitter hook.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] refactor: tidy explain store and bridge routing
- DRY the three stream handlers behind pure patchEntry/removeEntry helpers.
- Move shell->host explain event routing into the explain module
(handleConsoleEvent) so the sidebar bridge stays generic and owns no
explain-specific cases/casts. - Rename keyOf -> cellKey for intent.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
-
[OPIK-6425] [FE] add ExplainButtonProps core type
-
[OPIK-6425] [FE] add explain BI events
-
[OPIK-6425] [FE] add continueChat store action
-
[OPIK-6425] [FE] add explain kind registry
-
[OPIK-6425] [FE] add ExplainPopover
-
[OPIK-6425] [FE] add ExplainButton
-
[OPIK-6425] [FE] add ExplainButton plugin entry files
-
[OPIK-6425] [FE] register ExplainButton plugin slot
-
[OPIK-6425] [FE] wire Explain button into the Errors cell
-
[OPIK-6425] [FE] fix: report explain completion BI from the store; hoist readiness gate
Review follow-ups:
- Fire EXPLAIN_COMPLETED/ERRORED (+TTFT) from the store once per stream, not
the popover — streams outlive popovers in the cache model, so UI-side
reporting double-counted on reopen and missed completions while closed. - Hoist pod readiness into the store (
ready, synced by the sidebar) so each
per-row button reads one selector instead of calling useAssistantBackend. - Only offer "Continue" when the kind has a configured question.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] add per-kind explain target builders
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] register cost + duration explain kinds
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] add generic ExplainableCell wrapper
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] wire Explain into Error, Duration and Cost cells
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] align Explain button & popover 1:1 with Figma + interactions
- Button: round Ollie-owl IconButton (gradient/shadow per Figma), pinned
top-right/centered, grows RIGHT to "Explain" on hover, stays expanded while
the popover is open; reveals on cell-content hover. - Popover: Ollie header + divider, green-dot "Thinking..." (pulsing),
"Continue conversation ->" underlined link, Ubuntu Mono. - Streaming: fake typing caret (caret-blink animation).
- Click no longer opens the trace panel (propagation stopped on the button).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] finalize Explain UI + add test coverage
Polish the Explain pill and popover to match Figma: resting owl circle vs. expanded -18deg gradient pill, shared for Retry/Continue (border-b underline, no default Button padding), 12px markdown-rendered output, and owl optical centering. Add Retry + mid-stream cancel handling, an aria-live region for streamed text, and unit tests for the button, popover, store, useCanExplain and assistant-sidebar types.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] address review: route Explain accent colors through design tokens
Replace hard-coded Ollie hex/rgba in ExplainButton/ExplainPopover with design-system CSS variables: reuse the existing --color-ollie (#f46e41) for the orange accent/border/gradient, and add --color-ollie-amber, --color-ollie-live and --shadow-ollie to main.scss (:root + .dark) for the gradient amber stop, the streaming dot, and the brand shadow. No visual change. (baz-reviewer: hard-coded theme colors)
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] make Explain thinking dot pulse like ollie-assist beacon
Replace the near-imperceptible ollie-breathe (scale 1.04 / opacity .95) on the 8px 'Thinking…' dot with a beacon-pulse: an outward box-shadow ring in currentColor + opacity fade (1→.7), 1.4s ease-out infinite — matching ollie-assist's status dot. The dot sets text-[--color-ollie-live] so the ring inherits the green, and uses motion-safe: for reduced-motion (same pattern as OwlArt).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] Extend Ollie Explain to Spans & Threads + stream robustness
Extend the contextual Explain feature beyond the Traces table:
- Widen ExplainKind with span.{error,duration,cost} and thread.{duration,cost};
AI_EXPLAIN_REGISTRY stays a total Record so a missing config is a compile error. - Make the Traces/Spans builders entity-aware via shared factories: the Spans
view emits span.* targets (entityId = span id; the backend resolves the parent
trace), the Traces view emits trace.*. Wrapping moved into a per-view useMemo. - Wire Explain onto the Threads table (duration + cost; threads have no error).
- Extract the generic withExplain HOC to a shared LogsPage/explain module so the
Traces/Spans and Threads tabs share one wrapper.
Stream robustness (host-only):
- Watchdog for a stalled stream (no chunk yet): ~10s -> "waking" state, ~30s ->
retryable timeout error + explain:cancel. A live pod streams/pings well before
then, so this only catches a request that never reached a live pod. - Fail in-flight cells when the pod/bridge goes away (setReady(false)/clearEmit)
so open popovers can't hang on "Thinking…/waking". - Optional structured explain:error.code mapped to contextual copy (falls back
to the raw message); host-generated timeout/pod-loss set their own codes.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] Show Explain on N/A cost/duration + make popover modal
- Drop the value>0 guard on duration/cost target builders so the Explain
button also appears for N/A / zero cells (error stays gated on error_info,
since an empty error cell has nothing to explain). - Make the Explain Popover modal so the dismissing outside-click is swallowed
instead of passing through to the table row and opening the sidebar.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] Address review: dedup thread targets + guard explain readiness teardown
- Factor buildThreadDuration/CostTarget through a shared threadTargetBuilder
so the guard + number_of_messages branch live in one place (mirrors the
Traces/Spans builders). Addresses Baz review comment. - Drop the unconditional setReady(false) cleanup in AssistantSidebar: on a
surface switch (sidebar<->page) the new instance mounts and sets ready=true
before the old one's cleanup runs, so the unguarded reset clobbered the live
instance and never restored it, silently hiding every Explain button until
reload. Teardown is now owned solely by the ownership-guarded clearEmit.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- [OPIK-6425] [FE] Address review: BI dedup, aria-busy, align popover shadow
- ExplainButton: fire EXPLAIN_CLICKED only on a fresh dispatch — reopening a
popover on a cached/in-flight cell reuses the entry and no longer recounts,
so the CLICKED→COMPLETED funnel isn't inflated. Locked in with a test. - ExplainButton: drop the hard-coded rgba PopoverContent shadow and inherit the
shared shadow-md, matching all 44 other PopoverContent call sites (the popover
was the only one overriding it; modal + onCloseAutoFocus already match
FilterChipPopover/PromptLibraryMenu). - ExplainPopover: add aria-busy on the live region so screen readers announce
the settled answer once instead of re-reading every streamed token; collapse
the duplicated Thinking/waking blocks into one with a derived label.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- [OPIK-6425] [FE] refactor(explain): share target builder across tabs, null-safe payloads
Factor a single createExplainTargetBuilder(kind, buildPayload) shared by
the Traces/Spans and Threads explainTargets so the project_id guard + target
envelope can't drift between tabs. buildPayload returns null to veto explicitly.
Add finiteOrNull so N/A duration/cost serialize as null (not dropped undefined),
keeping "why is there no cost/duration?" explainable. Lives under LogsPage/ (not
the comet plugin) so OSS table code never imports the plugin.Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] refactor(explain): restructure store, extract watchdog, recover on retry-after-error
Rewrite explainStore into clear layers: pure entry transitions (startEntry /
withChunk / ...), pure cache ops over { entries, routes } (no dropRoute/keepRoute
flag soup), then a thin store. Extract the waking/timeout timers into
streamWatchdog.ts. Public API + behavior unchanged.Fix the sleeping-pod bug: when the console retries a transient 503 and streams
chunks under the SAME explainId AFTER its explain:error, the host used to retire
the route on error and drop the recovery, leaving the popover stuck on "Couldn't
load". explain:chunk is now authoritative ("stream is alive") and resurrects an
errored cell; the route survives a console error (retired only on done / pod
loss / timeout). A done after an error with no chunks keeps the error rather than
blanking it.Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] refactor(explain): extract assistant bridge module + button style helpers
Move the bridge machinery (createBridge / createHostListeners / emitHostEvent /
useRegisterExplainEmitter / useLatestRef + HostListeners/BridgeRefs types) out of
AssistantSidebar into assistantBridge.ts, leaving the component focused on
rendering. Lift ExplainButton's inline className blobs into named, Figma-annotated
owlTriggerClass/owlLabelClass helpers. Pure code moves — no behavior change.Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] harden finiteOrNull: clamp negatives + coerce numeric strings
Review follow-up. finiteOrNull is the single coercion point for explain metric
payloads, so it must only emit a backend-validnumber | null(backend models
arefloat | Nonewithge=0):- negatives now → null. lodash isFinite is true for negatives, so a corrupt
negative duration/cost (e.g. clock-skew) used to pass through and 422 against
the backend'sge=0— the same broken Explain button the fix removed. - numeric strings now coerced. A non-conforming API value ("5") collapsed to
null ("not recorded") for a row that has a real value; coercing preserves the
pre-fix behaviour where the backend parsed it.
Direct unit test added pinning the helper's contract (0/positive pass, negatives
/ NaN / Infinity / absent → null, numeric strings coerced).Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- [OPIK-6425] [FE] debug: verbose explain bridge/store tracing (temporary)
Adds host-side
[EXPLAIN]logging at the bridge chokepoint and the store so
a test-env capture pins down where the spuriousexplain:errororiginates:
whether it arrives INBOUND from the console runner or is host-generated
(watchdog/timeout/pod-loss), plus the live explain:run subscriber count
(>=2 => duplicate runner). Gated by EXPLAIN_DEBUG; remove with the helper file.Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
- [OPIK-6425] [FE] fix(explain): evict orphaned dead-realm explain subscribers
Root cause of the spurious "Couldn't load the explanation." flash (proven by
the test-env [EXPLAIN] capture): a pod readiness flap toggles isBackendReady,
which remounts the Ollie下载附件