Commit Graph
773 Commits
Author SHA1 Message Date
Sergey KozyrenkoandClaude Fable 5 26189c65e0 refactor(e2e): drop the contrast helper commentary
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 14:23:46 +07:00
Sergey KozyrenkoandClaude Fable 5 569d95e8dd test(e2e): gate every badge and destructive fill on measured AA contrast
The palette fixes had no guard: no scanned page renders a colored badge or a
destructive fill, so a shade edit could silently drop a variant under AA and
nothing would fail. Two comments were standing in for that coverage.

Mount one probe per variant from the component's own cva — a class list copied
into the spec would only ever re-test itself — and measure what the browser
actually paints, so oklch tokens and translucent fills are read as rendered.
Every variant is checked at rest and on hover, in both themes.

The measurement is deliberately unrounded: green-700 comes out at 4.4991, which
two decimals report as a passing "4.5". With rounding in place the guard passed
against a deliberately regressed green; without it, it fails and names the
variant, the state and the ratio.

Specs can now import component modules, so the e2e tsconfig gains jsx.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 14:05:28 +07:00
Sergey KozyrenkoandClaude Fable 5 421074e583 refactor(ui): drop the badge and button palette commentary
Three comments added with the contrast fixes were mostly narration: two restated
the class strings they sat above, and two defended the implementation rather than
naming a contract. The one fact that is not visible from the code — green sits a
step darker than its siblings — belongs in a test that fails when it regresses,
not in prose asking the reader not to touch it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 13:53:48 +07:00
Sergey KozyrenkoandClaude Fable 5 9f40102d6a fix(ui): keep white on the destructive fill readable in both themes
Four states were under AA, not one: the dark badge and button at rest (3.71), the
light badge on hover (4.34) and the dark button on hover (4.40), plus the dark
destructive menu item (4.01). Badge and button had also drifted apart — one faded
to 80% on hover, the other to 90% — so each failed in a different theme.

The token itself cannot be retuned: sweeping its lightness finds no value that
serves both roles, because the same red is a fill under white text and the text
colour on the page. Darkening it for the fill would break the twenty-four
text-destructive call sites.

Tune the fill's opacity per theme instead, keeping white text and adding no tokens.
A translucent red composites toward what is behind it, so the same fade darkens it
on the dark ground and lightens it on the light one: full strength on light fading
to 90% on hover, 80% on dark dropping to 70%. Hover now raises contrast in both
themes. All four states land between 5.04 and 6.28, verified on a built bundle
against every dark surface token (worst case 5.13 on secondary).

The dark destructive menu item drops its focus wash from 20% to 10%, matching light
and restoring the label to 4.52 at the cost of a fainter highlight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 13:42:11 +07:00
Sergey KozyrenkoandClaude Fable 5 f88427c264 fix(ui): give colored badges a per-theme text shade so they meet AA
Every colored Badge variant carried a single text shade for both themes, and no
one shade clears WCAG AA on both grounds. Measured against the current tints,
five of the seven failed on light and four on dark, with red and pink failing on
both — the badge text is 12px semibold, so the bar is 4.5:1.

Use the darker shade on the light tint and the lighter one on dark:
text-{color}-700 with a dark:text-{color}-400 override. Green takes 800 instead
of 700 because a shade number is not a constant lightness across hues — green-700
measures 4.4991, just under the bar.

Every variant now lands between 4.6 and 9.7. The knowledges a11y waiver is gone
and both theme scans pass without it; the knowledges dark visual baseline is
regenerated for the lighter blue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 12:58:45 +07:00
Sergey KozyrenkoandClaude Fable 5 e8aa8df192 docs(e2e): correct the badge contrast waiver note — both themes fail
Measuring the variants (browser-resolved sRGB, the 10% tint composited over the
real card surface, WCAG formula) showed the defect is not dark-only: five of the
seven colored variants fail AA on light and four on dark, and no single text shade
clears both grounds. Blue happens to pass on light and fail on dark, which is why
only the dark scan flags it here. The waiver itself is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 12:21:41 +07:00
Sergey KozyrenkoandClaude Fable 5 43387a7f7b test(e2e): cover the query-backed flow detail tabs
Only the Terminal tab had a spec; Tasks, Agents, Searches and Vector Store — all
fed by the FlowDocument query — were uncovered. Add a populated flow cassette (one
entry per tab) and a spec that opens each tab and asserts its seeded content
renders. Files and Screenshots are REST-backed and left for a follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 05:04:40 +07:00
Sergey KozyrenkoandClaude Fable 5 17d34161cd test(e2e): add a populated settings-providers visual baseline
The route-manifest visual sweep only captured the empty providers state. Add a
populated cassette (enabled defaults + a user-defined provider row) and a dedicated
visual spec with light/dark baselines, generated in the pinned container. Dates
render deterministically because the visual project inherits the mock clock via the
backend fixture.

Also bind --update-snapshots=all in run-visual.sh so a trailing spec-file filter is
not swallowed as the option's optional [mode] argument.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 04:54:34 +07:00
Sergey KozyrenkoandClaude Fable 5 dcea92e17b docs(e2e): document the spec tag taxonomy and the gate's reserved tags
The suite tags every spec but the docs only hinted at "@smoke, …". List the real
tags (@smoke/@flows/@crud/@coverage/@cross/@visual/@real/@stand) with their meaning
and how to filter, and spell out the two conventions the mock gate reserves:
@quarantine to isolate a flaky spec (excluded via --grep-invert) and @generated for
recorder-derived cassettes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 04:49:53 +07:00
Sergey KozyrenkoandClaude Fable 5 3d092f989c test(e2e): scan a11y in dark theme too, waiving the known badge contrast debt
axe only ran in the light theme, so dark-only contrast regressions were invisible.
Parametrize every a11y scan over light and dark. The dark pass surfaced a real,
pre-existing issue: colored Badge variants use text-{color}-600 with no dark
override and fail contrast on the dark background. Waive it narrowly on the
knowledges list (the blue badge) as named debt — a palette fix is queued
separately — so the rest of the dark surface is now gated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 04:43:17 +07:00
Sergey KozyrenkoandClaude Fable 5 16e926614c test(e2e): assert the dashboard period switch renders a distinct range
The period-switch test only proved the usageStatsByPeriod refetch fired; it would
still pass if the UI rendered stale data. Key the cassette's usageStatsByPeriod by
period (week vs month) with distinct date ranges, and assert the Token Usage chart
x-axis shows the month range after the switch and drops the week range.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 04:33:57 +07:00
Sergey KozyrenkoandClaude Fable 5 f5070667de test(editor): state the real byte-integrity invariant for GFM cell whitespace
The comment claimed no non-backslash byte is lost, but GFM legitimately trims
cell-edge and collapsed whitespace, so the test asserts the weaker true invariant:
every byte other than backslashes and that trimmed whitespace survives in order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 03:59:17 +07:00
Sergey KozyrenkoandClaude Fable 5 179c814bb3 chore(e2e): scope Tier 2 networks and gate pentagi on a healthy mock LLM
The base compose file pins fixed network names, so any network left un-overridden
in this e2e override joins the stack to the developer's live observability and
langfuse networks. Give all three networks e2e-scoped names. Add a healthcheck to
the mock-LLM service and make pentagi depend on service_healthy, so a crash-looping
mock can never read as a started dependency and silently mask itself.

Update frontend/docs/e2e.md to match reality: the stand tier runs @stand only
(flow-run drives a real paid agent and stays local), every mock run rebuilds its
own bundle by design, and the trend/affected-routes notes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 03:59:07 +07:00
Sergey KozyrenkoandClaude Opus 4.8 25b0229282 docs(frontend): move the list/detail guide to frontend/docs and correct its drifted API
The co-located components/shared/README.md documented hooks that no longer
exist. `usePagination` was never a hook, and the writer `useTableQueryFilter`
was replaced by the unified `useTableState`, so the copy-paste recipe it shipped
would not compile. Detail pages had likewise moved from spreading a `toolbarProps`
object to passing a `controller` prop.

Rewrite it as frontend/docs/list_detail_pages.md, matching the flat
per-subsystem layout backend/docs already uses: exact signatures for every
hook, component and storage helper, the wiring the five live list pages
actually use, and an explicit table of removed names so they are not
reintroduced. Drop the source-adjacent README — no other directory under
frontend/src or backend/pkg carries one.

Fix the comments that still named the removed hook, plus two claims in them
that contradict the code: the controlled filter is URL-backed only (DataTable
persists sorting, column visibility, page size and search columns, never the
filter), and `busy` on InlineEditInput disables the Save and Cancel buttons
while leaving the input editable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 03:28:58 +07:00
Sergey KozyrenkoandClaude Fable 5 7ff38f2da6 fix(e2e): locate dashboard cards by data-slot, not an uncommitted test id
The dashboard partial-errors spec matched cards via getByTestId('dashboard-card'),
but that attribute lives only in an uncommitted working-tree change, so on a clean
checkout the locator matched nothing and the spec failed. Match the data-slot="card"
attribute the shadcn Card primitive already emits. Also lift the inline xterm buffer
type in the message-rendering spec into named types so it satisfies prettier and the
union/module sort lint rules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 19:30:43 +07:00
Sergey KozyrenkoandClaude Fable 5 7acd0b6cca test(e2e): cover the rejected-login path
The smoke only exercised a successful login. Adds a cassette whose
/auth/login answers an error envelope and asserts the surfaced error plus
that Sign in stays disabled (react-hook-form holds it until a field
changes) — the negative path a live run confirmed but no spec covered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 16:11:45 +07:00
Sergey KozyrenkoandClaude Fable 5 6b493a391a test(e2e): cover the type-specific message render paths
The message cassettes were all Answer-typed, so flow-message.tsx's
type-specific branches never ran under test. A live QA run against a real
Bedrock flow showed real streams emit thoughts/report/terminal/input.
Adds a cassette with one message per path and asserts each: the
thinking toggle (thinking + message), a report that auto-expands its
details, a Terminal-format report that mounts the xterm renderer with
its output, and an input message's right alignment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 16:06:31 +07:00
Sergey KozyrenkoandClaude Fable 5 4b6dd0423e fix(e2e): harden real-tier specs, CI, and mock-gate rigor after review
Real-tier (Tier 2/3) fixes, the blocking set:
- stand-smoke asserts a per-route anchor (breadcrumb title span) instead
  of a shared sidebar link that 0-matches on /settings/* and double-matches
  on /templates — the smoke was structurally red on 3 of 5 routes.
- stand-tier trace/video forced off (they embedded the session cookie and
  the stand password in a public-repo artifact) and the stand project
  selects @stand via config grep (the CI step selected zero tests).
- run-local-tier's sandbox filter is anchored to the seeded 9xxxx range
  (`^/?pentagi-terminal-9[0-9]{4,}$`) so it can't force-remove a dev's
  flow-9 sandbox; sandbox cleanup moved behind the keep-stack gate; compose
  logs are captured before teardown.
- mock-LLM wraps the request handler (a non-JSON body crashed the process);
  its transcript now issues a real terminal exec via a delegate chain and
  flow-run asserts the terminal output; OpenAI-shaped error envelope.

Mock-gate rigor + coverage:
- order-insensitive deep-subset variable matching; unmatched WS subscribes
  are now accounted at teardown; REST/GraphQL entries can pin a body/vars
  subset; favorite-star asserts the persisted (not optimistic) value.
- new flows/create smoke (the primary journey had no mock coverage), a
  graphql-ws protocol unit test against the real client, and a
  route-manifest completeness test.
- affected-routes no longer under-scopes shared component dirs.

Hygiene: .mjs tools now linted/prettier'd; dead gitignore line; JSDoc/
sheet aria-label/schema-compat/trend/serve-dist/report-comment fixes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 15:59:30 +07:00
Sergey KozyrenkoandClaude Fable 5 2d1e677e75 fix(e2e): match the stand-smoke URL by pathname, not a string-built regex
new RegExp(route.replace(/\//g,'\\/')) tripped CodeQL
js/incomplete-sanitization (only slashes escaped). The routes are
constants, so it was a false positive — but the predicate form is
cleaner and asserts the exact pathname.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 06:24:54 +07:00
Sergey KozyrenkoandClaude Fable 5 6426bb9923 fix(e2e): don't disable TLS validation in committed code
schema-compat set NODE_TLS_REJECT_UNAUTHORIZED=0 process-wide (CodeQL
js/disabling-certificate-validation, high). A real stand has a valid
cert; only the local self-signed Tier-2 stack needs it, so the operator
now opts in via their own shell env, never in code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 06:20:53 +07:00
Sergey KozyrenkoandClaude Fable 5 419c56317a feat(e2e): phase 3 infra — schema-compat, trends, diff-scoping, stand job
Lays the Phase-3 substrate on top of the three tiers.

- schema-compat pre-flight (e2e/tools/schema-compat.mjs): introspects a
  target backend's live GraphQL schema and validates every frontend
  operation against it, so deploy skew (a renamed/removed field) fails
  once, readably, instead of as dozens of red specs. Verified against the
  live Tier-2 backend both ways: 105 operations pass, an injected bogus
  field is caught with the exact location.
- trend aggregation (trend.mjs): turns a run's results.json into one JSONL
  record (p50/p95 spec duration, slowest three, pass/flaky/fail) so slow
  regressions are visible, not just green/red; CI appends it to a
  90-day-retained artifact.
- diff-scoping (affected-routes.ts + affected.ts): maps a diff to the
  manifest routes it touches via each route's owning sources — the
  substrate for selective runs and for scoping the exploratory agent.
  Pure mapping fn, unit-tested (backend-only → none; shared infra → all;
  owned → that route).
- CI: an e2e-stand job (label-gated + a protected Environment whose
  reviewers approve before secrets are exposed; schema-compat runs first),
  a trend step, and an LLM-independent @stand smoke.
- docs: the stand tier, trend/affected tools, and the LLM advisory recipe
  (playwright-mcp + init-agents + guardrails) — the recipe the
  deterministic tiers plug into, not a bespoke bot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 06:17:59 +07:00
Sergey KozyrenkoandClaude Fable 5 dc66898bc8 feat(e2e): visual snapshots — route×theme matrix in the pinned container
18 baselines (9 manifest routes × light/dark) compared only inside
mcr.microsoft.com/playwright:v<version>-noble, so pixels are identical on
every machine and CI; the wrapper derives the tag from the installed
@playwright/test version, making the pin drift-proof, and a CI guard
fails loudly if the workflow's container tag falls behind.

macOS hosts cannot run the visual project directly (parallel darwin
baselines) nor mount their node_modules into the container (native vite
binaries): the wrapper builds dist on the host and the container serves
it with a dependency-free static server — route mocks intercept API
calls before the network, so no proxy is needed. The xterm canvas is
masked (SwiftShader pixels are driver-dependent). Determinism proven by
back-to-back container runs. The e2e-visual CI job is advisory, never a
required check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 04:02:29 +07:00
Sergey KozyrenkoandClaude Fable 5 1a4f603216 feat(e2e): tier 2 — real backend run driven by an OpenAI-compatible mock LLM
specs/real/** now exercise the actual agent loop end to end: create flow
-> image/language/title -> tool-call-ID sampling -> subtask_list plan ->
done barrier -> subtask_patch refine -> report_result — with messages
streamed over the real GraphQL websocket and the flow settling in
Waiting. The mock LLM is ~150 lines of Node driven by a deterministic
first-match transcript; the custom provider env seam means zero backend
changes.

The stack is fully isolated from a developer machine:
- own compose project/network/ports (8444/5433), coexists with a dev stack
- --env-file /dev/null so the developer's .env (live keys, DOCKER_HOST,
  config paths) never leaks into the e2e backend
- flow ids seeded from 90001: sandbox containers are named
  pentagi-terminal-<flowId> on the shared docker daemon, so the range
  keeps them clear of dev flows and makes runner cleanup unambiguous
  (down -v cannot remove them — the backend spawns them outside compose)
- pentagi healthcheck via busybox wget (the alpine image has no curl),
  since the base service has none and up --wait returns too early

Auth for live tiers moved to the canonical setup-project + storageState;
the localStorage seed is now mock-tier-only (a forged client session has
no cookie behind it and the first 401 wipes it). CI gains a nightly/
dispatch-only e2e-local job.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 03:54:05 +07:00
Sergey KozyrenkoandClaude Fable 5 c2577777e8 fix(editor): give the backslash/pipe property test the heavy-generative timeout
The suite's generative tests run with timeout 30000 — the new one missed
it and tripped vitest's 5s default on slower CI runners.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 03:11:04 +07:00
Sergey KozyrenkoandClaude Fable 5 6b22e28c3a fix(ci): drop the duplicate Go cache restore and pin token permissions
setup-go's cache:true already caches the build and module caches keyed
on go.sum; the extra actions/cache step restored the same read-only
~/go/pkg/mod on top and failed with "Cannot open: File exists" whenever
both caches hit. Also sets the workflow's GITHUB_TOKEN to contents:read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 03:06:25 +07:00
Sergey KozyrenkoandClaude Fable 5 6badfb90db fix(editor): keep table cells intact when cell text ends a backslash run with a pipe
marked's cell splitter honors \| only after an odd backslash run and
truncates rows that split into extra columns, so escaping the pipe alone
in text like a\|b produced \\| — a live delimiter that dropped the
trailing cells on the next load. An odd run + pipe has no exact GFM
encoding: the serializer now pads the run by one backslash, trading a
one-character gain inside the cell for structural integrity, byte-stable
from the first save. Covered by direct round-trips and a random \/|
payload property test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 03:02:25 +07:00
Sergey KozyrenkoandClaude Fable 5 5cb0522818 feat(e2e): dashboard, settings, resources coverage + route manifest sweep
Completes the scenario-catalog coverage on the mock tier (48 specs).

- dashboard: analytics/overview tabs (lazy mount), period switch verified
  on the wire, and the per-card degradation path — a failed stats query
  renders "Couldn't load" in its own card while neighbours stay live
  (first use of error-response cassette entries)
- settings: prompts (the largest typed cassette — 15 agent + 12 tool
  prompt configs), providers empty state, create form opens
- resources: REST-seeded file tree, and the mkdir journey proving the
  full chain — REST mutation raises a world flag, the flag releases a
  resourceAdded frame, the subscription cache merge grows the tree
- e2e/routes.ts: route manifest (paths imported from src/lib/routes so
  a rename breaks compile) driving a data-driven NAV sweep over 9
  routes; entries carry owning source dirs for future diff-scoping

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 03:01:59 +07:00
Sergey KozyrenkoandClaude Fable 5 99829bff0b fix(a11y): give the built-in dialog X a name distinct from page Close buttons
The token-reveal dialog (and any dialog with an explicit Close/Cancel)
exposed two controls named "Close" — ambiguous for screen readers and
for role-based locators in strict mode. The shared X is now "Dismiss
dialog".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 01:58:24 +07:00
Sergey KozyrenkoandClaude Fable 5 b2990538a7 feat(e2e): flow streaming/reconnect/lifecycle, entity CRUD, and cross-cutting specs
Builds on the mock gate: 26 new specs across flows, CRUD, and cross
concerns, all driven by typed cassettes against the production bundle.

- flows: no-duplicate and exact-set-disjoint message streaming across
  concurrent flows (ID sets attached to the report), reconnect that
  reconciles the missed delta exactly once, and the rename/finish/
  favorite/delete lifecycle chained mutation->subscription-frame
- mock engine: streams move to one-driver-broadcast (flowUpdated is
  opened by two providers at once; reconnect re-joins past the cursor,
  delta-only) and frames can wait on world flags
- CRUD: templates, knowledges (required-validation proves no mutation
  fired via the zero-unmatched teardown), api-tokens (inline create,
  one-time secret reveal)
- cross: themes (seeded + toggled), responsive (Pixel 7 touch profile,
  1279/1280 split boundary), a11y with a policy - critical/serious
  floor plus a committed per-route allowlist
- terminal content is asserted through the xterm buffer (the WebGL
  canvas has no DOM text): the hook now exposes the instance on its
  host element
- app fixes surfaced by the sweep: the rows-per-page select had no
  accessible name; the favorite star is useOptimistic and only
  persists via the settingsUserUpdated frame - the spec now asserts
  the real persistence path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 01:24:18 +07:00
Sergey KozyrenkoandClaude Fable 5 bae81a9b22 chore: ignore Playwright MCP page-snapshot artifacts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 00:31:26 +07:00
Sergey KozyrenkoandClaude Fable 5 67266c9316 feat(e2e): Playwright scaffold with a hermetic mock gate
The default tier runs the production bundle (vite build + preview)
against a cassette-driven mock of the whole API surface — GraphQL over
HTTP, graphql-transport-ws subscriptions, and REST — so the suite needs
no backend, no keys, and no secrets, and fork PRs can run it.

- mock engine: (operationName, variables) matching with sequenced
  entries and world flags (login flips /info guest->user without
  call-order coupling); unmatched calls answer 501 and fail the test,
  so nothing leaks through the vite preview proxy to a live backend
- ws mock follows the graphql-transport-ws contract the app's client
  needs: immediate ack, nothing before ack, streams stay open, delta
  cursors survive reconnects, drops use retryable close codes
- clock and timezone pinned on the mock tier: formatDate branches on
  isToday/isThisYear, so unpinned cassette dates rot within a day
- cassettes are TS modules typed against the generated GraphQL types,
  so schema/operation drift fails the existing tsc gate at compile time
- CI: fork-safe e2e.yml (read-only token, no secrets, no write steps)
  plus e2e-report.yml posting a sticky PR comment via workflow_run,
  resolving fork PRs by head SHA
- smoke specs (login redirect, form login, authenticated /flows render)
  green on the mock tier; e2e/ wired into tsc, eslint, and prettier

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 00:30:18 +07:00
Sergey KozyrenkoandClaude Opus 4.8 f3972bb9d1 fix(ui): guard error/loading surfaces and the ws reconnect tail
List pages and the flow detail page no longer replace live data with a
full-page error on a failed background refetch — the error/redirect branches
now gate on the absence of data, not on the error alone. On a cold-cache load
failure the flow detail page renders an in-page ErrorState + Retry instead of
silently redirecting to the list, while a genuine not-found still redirects.
Retrying after a failed initial load shows the spinner rather than flashing
the empty state.

Reconnect reconcile: catch the aggregate refetch rejection (the per-query
wrapper doesn't cover it) so a transient failure during the sweep can't
surface as an unhandledrejection, and re-hydrate the REST-backed resources
slot — which the observable-query sweep skips — via a ws:reconnected event.
The flow provider gates its blocking spinner and subscription teardown to the
initial load only, so a background reconcile no longer overlays the page or
bounces the live subscriptions.

Terminal: clear() cancels any in-flight chunked write so its trailing chunks
can't land after the buffer was cleared; the search Clear (X) resets only the
query and keeps the task/subtask filter.

Smaller fixes: role="alert" on ErrorState, a keyboard-operable per-agent Test
control, the correct prompt-validation Alert variant, and dashboard error
props. Shared LoadingState and DashboardError components collapse duplicated
loading/error blocks; terminal write cancellation and the Apollo cache
policies both gain test coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 03:34:46 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b4b640b36b fix(flows): reconcile the cache after a websocket reconnect
The GraphQL subscriptions are delta-only: the server registers a subscriber
for future events and never replays what it published while a client was
disconnected (and it drops events for a disconnected subscriber outright).
Nothing on the client refetched after the socket came back, so a websocket
drop during an active flow — a network blip, a laptop waking, a proxy timeout
— left a permanent hole in the streamed logs/messages/status until the user
manually reloaded the page.

Refetch the active queries on reconnect: graphql-ws hands `wasRetry` to the
`connected` handler, so on a retry (not the initial connect) call
`refetchObservableQueries()`, which re-runs the flow's queries and merges the
full current state back into the cache.

Verified live with a faithful drop (patched WebSocket, real `ws.close()` mid-
flow while the agent kept producing): before the fix the messages created
during the outage stayed missing after reconnect and only a reload recovered
them; with the fix they reappear automatically on reconnect, no reload. (A
CDP "offline" emulation does NOT reproduce this — it buffers the socket rather
than closing it, so use a real close when testing.) 1006 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 15:47:30 +07:00
Sergey KozyrenkoandClaude Opus 4.8 c2287ad90c test(apollo): cover the assistant-log streaming link
Round out the subscription coverage with createStreamingLink — the piece that
coalesces streamed assistant token updates. Export it (test-only) and drive it
with a controllable source plus a mocked clock.

Locks the three behaviors that make token streaming feel right: append parts
inside the 50ms throttle window are accumulated but not re-emitted (so the UI
isn't hammered), the next emission past the window carries the full running
message rather than the latest delta, and the final non-append part flushes the
accumulated total and clears the per-id cache so the same id can stream again.
Also confirms a non-assistant-log result passes straight through untouched.

1006 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 14:04:22 +07:00
Sergey KozyrenkoandClaude Opus 4.8 e3d9999c17 test(apollo): cover the subscription cache merge-link
The link that folds live subscription events into the Apollo cache
(updateCacheForSubscription) had no coverage — MockedProvider replaces the
whole link chain, so it can't exercise this. Export it and drive it directly
against a real InMemoryCache configured like production (flow-scoped list
fields keyed by flowId).

Locks the routing rules: *Added appends + de-dups by id, *Created prepends
(newest first — not re-sorted by id), *Deleted removes by id, *Updated merges
an entity's fields in place without reordering (and appends if it's not yet
cached). Also covers the two subtle bits: flowId variant isolation (an event
for one flow leaves another flow's list untouched) and type-tolerant id
de-dup (a numeric subscription id matches a string id from REST hydration).

Also exports createSubscriptionCacheLink for future frame-level tests. Pure
additive exports, no runtime change; app verified still loading. 1003 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 13:30:15 +07:00
Sergey KozyrenkoandClaude Opus 4.8 c43c05c29b fix(flows): don't bounce off a working flow on a partial load error
The flow detail query runs with errorPolicy:'all', so a failed sibling field
(the flow's log lists are separate, nullable root query fields — terminalLogs,
messageLogs, etc.) surfaces an Apollo error while `flow` itself resolves fine.
Both the redirect effect and the provider's load-error toast keyed off "any
error present", so one flaky log resolver would kick the user back to /flows
and toast "Failed to load flow" over a flow that had actually loaded.

Gate both on the flow genuinely being absent (`!flowData?.flow`) instead of on
the presence of an error. A missing/invalid flow still redirects and toasts
(its non-null `flow` field propagates to a null result); a partial failure now
renders the flow and lets the affected panel show its own empty state.

Reachability confirmed from the schema (sibling log fields are `[X!]`, nullable;
`flow` is `Flow!`, non-null) — end-to-end confirmation folded into the P2-S5
live run. 996 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:10:06 +07:00
Sergey KozyrenkoandClaude Opus 4.8 140578fd7c fix(terminal): stop garbling logs after a search filter is cleared
The terminal wrote incrementally by diffing on array length alone: if the
new logs were at least as long as what had been written, it appended the
tail from the old length onward. That assumes the on-screen prefix never
changes — but the same component is fed filtered subsets, so narrowing a
search then clearing it grew the array back past the filtered length and
appended the full-log tail on top of the still-visible filtered lines,
producing a scrambled, partly-duplicated buffer.

Track the exact lines last rendered and only take the append fast-path when
they are a true prefix of the new array; otherwise clear and rewrite. Pure
streaming stays incremental (no clear); any non-prefix change rewrites.

Verified with a repro test (red before, green after): full -> filtered ->
cleared restores the full set, streaming appends never clear, and a
different filter rewrites. 996 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 11:55:28 +07:00
Sergey KozyrenkoandClaude Opus 4.8 cf68f3a675 fix(prompts): tell the user when a reset fails instead of only logging it
Resetting a prompt to its default on the prompts list swallowed a failed
delete into console.error — sonner wasn't even imported — so the row stayed
Custom with no hint anything went wrong. Toast it, matching how every other
mutation in the app reports failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 03:14:12 +07:00
Sergey KozyrenkoandClaude Opus 4.8 f089f01aff fix(flows): show the load error on the page instead of a stacking toast
The flows list did what the other lists used to: on a failed load the provider
fired toast.error with no id — so each repeated failure while the backend was
down stacked another copy — and the page, never reading the error, fell through
to its "No flows" empty state, indistinguishable from an empty account.

The provider already exposed flowsError; read it on the page and render the
shared ErrorState with a Try again button (refetch, now also exposed), and drop
the toast effect entirely. Flows now matches templates/knowledges/resources.

Verified live: three failed refetches leave zero stacked toasts and one error
state with the button (previously three toasts + a false empty state); clicking
Try again after recovery restores the list. 993 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 03:02:45 +07:00
Sergey KozyrenkoandClaude Opus 4.8 33191b4c3c feat(ui): add a Try again button to the load-error state
The Empty error state named the failure but left the user stranded — the only
way back was a full browser reload. Give ErrorState an optional onRetry that
renders a "Try again" button, and wire every load-error site to re-run its own
query: templates, knowledges (the active list-or-search query), resources, the
three settings lists, and the two settings detail pages. The templates and
knowledges providers now expose refetch for it; the rest already had one.

Verified live: failing the templates query shows the error with the button,
and clicking it after the backend recovers clears the error and renders the
list — no page reload. 993 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 02:52:30 +07:00
Sergey KozyrenkoandClaude Opus 4.8 84c55969d5 refactor(ui): toast settings mutation failures instead of a standing banner
A failed delete/create/update on the provider and API-token lists parked a
persistent destructive Alert above the table, which lingered until the next
action and, for tokens, dumped the raw Postgres constraint text into a banner.
Every other mutation in the app reports failure with a toast; these two lists
were the exception.

Toast them too — the create/update handlers already swallowed the error into a
console.error, so this is the first real feedback they give — and drop the
banner, its error state (deleteErrorMessage plus the unread useMutation error
tuples), and now-unused ErrorAlert component.

Verified live: creating a token whose name already exists surfaces "Failed to
create token" as a toast (no second token written), with no standing banner
left behind. 993 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 02:42:28 +07:00
Sergey KozyrenkoandClaude Opus 4.8 866dac1b61 refactor(ui): render list load errors as a full Empty state, not a thin banner
A failed list query showed a one-line destructive Alert stranded at the top of
an otherwise-empty page — it read as leftover chrome rather than the page's
state. The detail pages and the route error boundary already render a failure
as a centered Empty: a large icon, a title, the message. The lists now match.

Add ErrorState (the shared Empty-error the detail pages were inlining) and use
it for the load errors on templates, knowledges, resources, and the three
settings lists, plus the two detail pages that had inlined the same markup —
eight call sites, one shape.

Resources previously only toasted its load failure from the provider while the
page kept showing "No resources yet"; it now shows the error state like the
others, and the provider drops the toast (and the now-unused sonner import).

The settings mutation-error banners stay on the inline Alert — those sit above
rendered content, where a full Empty would be wrong; they're a separate change.

Verified live: a failed templates query (GraphQL) and a failed resources fetch
(REST) both render the centered Empty error instead of a banner or a false
empty state; the healthy paths are unchanged. 993 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 02:31:14 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b54c09954c fix(dashboard): show a per-card load error instead of a banner over lying zeros
The banner told the user "something failed" but left every card showing its
value — a down backend rendered Total Cost as $0 and each chart as "No data
for this period", numbers indistinguishable from a genuinely quiet account. And
when a single stat query failed while the other eleven succeeded, the banner
alarmed over an otherwise-healthy dashboard.

Give MetricCard and ChartCard an error state alongside their loading/empty ones
— a muted "—" / "Couldn't load", quiet rather than alarming — and wire each of
the dashboard's queries to its own card and table. A failed query now says so
in its own tile; the rest keep rendering their real data. The top-of-page
banner is gone.

Verified live: failing one period query leaves that chart as "Couldn't load"
while its neighbours draw real bars; a full stats outage fills every card and
table with the quiet error state and no lying zeros; the healthy path is
unchanged. 993 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 02:11:08 +07:00
Sergey KozyrenkoandClaude Opus 4.8 164597c77a refactor(ui): fold the duplicated load-error alerts onto one component
Five copies of the same destructive Alert — same variant, same AlertCircle,
same title-and-message shape — sat inline across the settings pages, and the
preceding load-error fix had just added four more call sites of it.

Use the shared component at all nine, and rename it to ErrorAlert on the way:
the name it shipped with an hour ago, DataLoadError, was too narrow for two of
the five, which report a failed delete rather than a failed load.

Its `message` now also accepts null, because that is what the delete-error call
sites already had in hand and what the inline JSX happily rendered as nothing.

Left as they are: the prompt-validation dialog swaps variant between success
and error and carries a structured body, and the flow-files notice is a default
Alert — a warning, not an error. Neither is this shape.

No behaviour change. Verified by re-failing the providers and prompts queries
and confirming the same alert still renders and the page still recovers;
993 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 18:44:15 +07:00
Sergey KozyrenkoandClaude Opus 4.8 cada8ffbe8 fix(ui): tell the user when a page fails to load instead of showing "nothing here"
A backend that was down rendered as a pristine empty account: every dashboard
chart said "No data for this period" and every metric card showed 0, while the
templates and knowledges lists showed their "nothing here yet" empty states —
all indistinguishable from a genuinely empty account, with no toast, no banner
and no other hint that anything had failed.

None of the three pages read `error` from their queries. The templates and
knowledges providers destructured only `{data, loading}` and never exposed it,
and every dashboard query dropped it on the floor. A failed query leaves `data`
undefined, which the empty-state branch cannot tell apart from an empty result.

Expose `error` from the two providers and render a load-error state distinct
from the empty state: a full-page alert on the lists, a banner above the cards
on both dashboard tabs. The settings pages already did exactly this, so the
shared `DataLoadError` gives those call sites one shape instead of four copies.

Templates also gained the loading state it never had — its empty state used to
show while the very first fetch was still in flight.

Verified by failing each query at the network layer and re-running the check
that caught it: templates and knowledges now surface "Error loading ..." rather
than their empty state, and the dashboard shows the banner on both tabs against
a cold cache. Happy path unchanged; 993 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 18:09:52 +07:00
Sergey KozyrenkoandClaude Opus 4.8 9952f7d6b0 chore(frontend): let vite preview reach the API
The proxy was only declared under `server`, so `vite preview` served the
production build with no route to the backend: every /api/v1 call 404'd and
the built app could not get past the login screen.

That left the production bundle effectively untestable, which is where
chunking — and the bug that made every route download recharts — only ever
shows up; `vite dev` does not chunk at all.

Reuse the dev proxy for preview, on VITE_PORT+100 so it cannot collide with
the dev server. Verified: preview serves on 8100 and /api/v1/info returns 200
through the proxy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 15:44:45 +07:00
Sergey KozyrenkoandClaude Opus 4.8 6a032c74db chore: ignore the installer's log.json
`cmd/installer/wizard/logger` opens log.json from its init(), so importing the
package is enough to create one in the process's working directory. Running
`go test ./cmd/installer/...` therefore drops a log.json into every package
directory it runs from — four of them were sitting untracked in the tree,
one step away from an accidental `git add -A`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 15:44:43 +07:00
Sergey KozyrenkoandClaude Opus 4.8 fa370eacb4 perf(build): stop every route from downloading recharts and highlight.js
Loading any page — /login included — fetched the `markdown` and `charts`
chunks, ~198KB gzip that the page never used.

Vite 8 bundles with rolldown, where `manualChunks` is a compatibility shim
over code-splitting groups, and a group captures its modules' dependencies
as well as the modules themselves. So `charts` (recharts) swallowed clsx and
`markdown` (react-markdown) swallowed react/jsx-runtime — both of which every
component needs, which dragged the two chunks into the entry graph and onto
index.html's modulepreload list. Returning 'react-vendor' from manualChunks
for those modules changed nothing: the group that captured them as
dependencies won regardless of the order the branches were written in.

Use rolldown's own `codeSplitting.groups` instead and rank the shared modules
above the heavy libraries that depend on them.

/settings/account now fetches 305KB of JS rather than 502KB; /dashboard 378KB
rather than 474KB, still loading `charts` because it draws charts. Verified
against the production build served locally: 10 routes render, console clean,
charts intact, 993 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 15:06:27 +07:00
Sergey KozyrenkoandClaude Opus 4.8 29300b4df9 fix(ui): let the list empty states centre themselves like the primitive intends
Empty declares `flex-1 … justify-center` (empty.tsx:9), but the list pages wrapped
it in a plain `flex flex-col`, so it collapsed to its natural height and hugged the
header while api-tokens — same primitive, same "nothing yet + CTA" shape — centred
in its `flex-1` wrapper. At 1920 the CTA jumped ~350px between the two pages.

The five wrappers on knowledges, templates and flows (loading + empty states) now
carry flex-1 like the eight that already did. SidebarInset is `min-h-svh flex-1
flex-col`, so the height is there to fill. Measured at 1920: knowledges and
templates now report the same 564 centre / 24px offset as api-tokens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 12:56:46 +07:00
Sergey KozyrenkoandClaude Opus 4.8 d2489fd2c7 fix(sidebar): restore icon sizing the container cannot supply
The size-4 sweep stripped four icons the sweep had no right to touch, and they
rendered at lucide's default 24px: the avatar's UserIcon and the theme
switcher's Monitor/Sun/Moon in the sidebar user menu.

The sweep assumed every listed container sizes its icons at any depth. Only
Button/Toggle/CommandItem do, via [&_svg]. SidebarMenuButton and DropdownMenuItem
use [&>svg], which reaches a DIRECT child only — and these four sit deeper, inside
an AvatarFallback and inside TabsTrigger, so nothing sized them.

Found by measuring rendered icon width across every route with the menus open,
not by re-reading the selectors: the avatar icon was the only one a closed-menu
pass could see. Verified after: no icon inside a button or menu row renders over
16px except EmptyMedia's, which is meant to be large.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:24:25 +07:00