Commit Graph
711 Commits
Author SHA1 Message Date
Sergey KozyrenkoandClaude Opus 4.8 cbfad0853c fix(settings): contrast the variables count badge, scope the tab overrides to the split
The count badge sat on a bg-secondary button, where every neutral surface token
is within 1.08–1.23 contrast of it: secondary painted the pill invisible and
outline drew its border in --border, at 1.18. Only --primary separates (2.76), so
the badge is default.

The tab overrides exist because the desktop panel sits on its own surface. Below
the split they fought the shadcn defaults, so they are xl:-scoped now — the same
1280 the layout branches on. Stacked widths get bg-muted with a bg-background
active tab again; 1280+ keeps bg-background with a bg-card active tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 22:05:06 +07:00
Sergey KozyrenkoandClaude Opus 4.8 fae61b2607 feat(settings): show the variables count on the narrow-width trigger
The trigger is a compact secondary button carrying just its label and, on the
right, the variable count — same count-badge shape the sheet and template
headers already use.

The badge is outline, not secondary: a secondary badge paints bg-secondary onto
a bg-secondary button, which renders the count as bare text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:57:37 +07:00
Sergey KozyrenkoandClaude Opus 4.8 4c1ee3177e fix(settings): make the variables header itself the narrow-width trigger
The header and its hint were rendered twice below the split breakpoint: once as
the trigger label and again at the top of the popover it opened.

The header belongs to the wrapper, not to the shared part. On desktop it stays
the card header; on narrow widths it becomes the trigger, and the popover now
carries only the chips. Title and hint move to constants so the two wrappers
cannot drift apart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:51:27 +07:00
Sergey KozyrenkoandClaude Opus 4.8 f89eacb990 refactor(settings): give Badge asChild, let the variables panel own its breakpoint
Three follow-ups to the variables palette.

The chips composed badgeVariants onto a raw <button>, going around the primitive,
because Badge renders a div. Badge now accepts asChild through the same Slot
switch button.tsx already uses, so a chip is <Badge asChild><button>: the badge
keeps owning the styling, the button keeps native semantics, and the hand-rolled
role/tabIndex/Enter-Space handler stays gone. className has to stay on Badge —
Slot concatenates classes, so only cn() resolves font-normal against
badgeVariants' font-semibold.

Variables reads useBreakpoint itself rather than taking isDesktop from the page.
Every other consumer here does that, sidebar.tsx included, and it keeps the panel
droppable into the next page that needs one.

The narrow-width trigger is a full-width labelled button now. It carries no
chevron and no count badge: those, not the width, are what make a control read as
a value select.

Also size the popover by --radix-popover-trigger-width, the way autocomplete.tsx
does. It was 100vw wide and spilled across the sidebar between 768 and 1280.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:40:32 +07:00
Sergey KozyrenkoandClaude Opus 4.8 fed1a020c6 feat(settings): collapse the prompt variables panel into a popover on mobile
In the stacked layout the "Available variables" cloud sat between the tabs and
the editor, pushing ~530px of chips ahead of the content it annotates. The panel
now keeps its slot and its desktop rendering, and below the split breakpoint the
same cloud moves behind a { } trigger, leaving the editor at the top.

The cloud itself is shared by both wrappers; only the wrapper differs (card vs
non-modal popover). The popover must not trap focus: insert and cycle act on the
editor's stored selection, and cycling only helps if its highlight stays visible.

Chips are real buttons now — they were divs with role=button and a hand-rolled
Enter/Space handler that the native element provides for free.

Verified live at 500px: opening keeps the caret, clicking a used variable selects
the next occurrence in view, the popover closes as the editor takes focus back,
and the desktop split is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:08:46 +07:00
Sergey KozyrenkoandClaude Opus 4.8 a66ac97a9b refactor(prompts): render the prompt meta and variables as one panel block
On narrow viewports the editor was rendered between the prompt meta/tabs
and the "Available variables" panel, splitting one logical block in two and
forcing a different composition per breakpoint. Merge them into a single
panel block that the desktop split and the mobile stack both render as a
unit, so the variables stay attached to the tabs they belong to.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 18:29:14 +07:00
Sergey KozyrenkoandClaude Opus 4.8 a3dcd053bc refactor(ui): replace custom StatusCard with the shadcn Empty primitive
Consolidate every empty / loading / error / not-found / error-boundary
state off the bespoke StatusCard onto the canonical shadcn Empty (already
used by DataTable) and delete components/ui/status-card.tsx. Empty renders
borderless and centered, so the wide-viewport "letterbox" look is fixed at
the root instead of capping a solid card.

Also in this pass:
- DataTable: truncate column headers so plain-string headers in narrow
  fixed-size columns (e.g. "System Prompt") stop wrapping to two lines.
- Settings > Providers / API Tokens: move the Create action into the page
  header (icon-collapses on mobile) and the API-token Playground/Swagger
  links into a header overflow menu; drop the cramped description row that
  needed the earlier truncate/flex-wrap workarounds. Those links use a
  relative href since the API base path is root-relative.
- Settings > Prompts: drop the fixed size on the Name column of both tables
  so it flexes to fill (matching Providers) and truncates.
- Settings > Account: keep "Member since" on one line at mobile widths
  (min-w-0 + truncate, badge shrink-0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 17:48:22 +07:00
Sergey KozyrenkoandClaude Opus 4.8 001c6c0d9d feat(settings): render three-state reasoning control driven by capability
Add an Off option to the per-agent Reasoning Mode control, gated on the model's
cannotDisable capability so it appears only where a disable actually takes effect;
relax the adaptive-only lock for disablable models; disable effort/max-tokens when
Off. Reconcile a stale Off when the selected model can't disable (e.g. after typing
a custom model name) so it cannot silently persist. Map the off mode through the
form->GraphQL conversion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 19:51:03 +07:00
Sergey KozyrenkoandClaude Opus 4.8 2853c97549 feat(reasoning): add three-state On/Off/Default reasoning control
Add an explicit reasoning Off to the per-agent config, backed by the langchaingo
tri-state API. Off emits llms.WithReasoningDisabled() (the provider disable wire)
via BuildOptions, and UsesAdaptiveThinking is guarded so Off overrides the
adaptive-only auto-adaptive.

Per-model capability is derived at runtime from llms.ReasoningSupportFor and
surfaced through GraphQL ModelReasoningInfo, so the UI only offers Off where it
actually disables: cannotDisable reports when Off would be rejected (always-on
models) OR a silent no-op (an unclassified default-on model whose disable wire is
omitted), and capability is surfaced for any thinking-capable model (e.g. Gemini,
which declares thinking without a reasoning block). A new
ProviderType.ReasoningProvider() supplies the provider to the resolver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 19:50:43 +07:00
Sergey KozyrenkoandClaude Opus 4.8 ffbbabb517 fix(docker,flows): surface truncated/systemerr exec streams, bound ftester drain, tidy failure logging
- demuxExecStdout errors on a header cut short mid-frame (was a silent EOF that
  dropped the tail) and surfaces a docker systemerr frame instead of discarding it.
- ftester drains telemetry on exit through the bounded observer.Drain instead of
  two unbounded ForceFlush calls, so an unreachable collector can't hang it at exit.
- container-listing failures are sorted for a deterministic skipped-entries preview,
  logged per-entry at Debug (the detail is already in the response and the endpoint
  is hit on every navigation) with names quoted so control bytes in a hostile
  filename can't inject into a log line; corrected the stat-failure comment
  (a dangling symlink lstats fine, it doesn't fail).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:06:21 +07:00
Sergey KozyrenkoandClaude Opus 4.8 7cd22ccbc9 test(docker,flows): cover the listing cap paths, bound the container-list path count
The truncation slice and the demux stdout byte-cap had no docker-layer tests —
only the handler's Truncated wiring was exercised through the fake, so a
mis-slice or a dropped cap would have gone unnoticed. Extract find-output
parsing into a pure parseFindEntries and take the byte cap as a demuxExecStdout
parameter, then unit-test both boundaries (at cap / cap+1 / over-limit stream).

Also bound how many paths one container-files request may list
(maxContainerListPaths), so the per-path entry cap can't be multiplied by an
attacker-chosen path count into a large fan-out or response body.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 23:22:33 +07:00
Sergey KozyrenkoandClaude Opus 4.8 18234f05a3 docs(docker): refresh the container-listing doc, drop a stale telemetry test comment
The container-listing section still described the removed `ls -1` parse,
`pkg/queue`, and the old `[]container.PathStat` return. Update it to the current
`find -print0` + errgroup fan-out returning `ContainerDirListing{Files, Failures,
Truncated}`. Also remove the stale `WithBlock`/`DefaultDialTimeout` paragraph left
above the non-blocking `grpc.NewClient` test — its own next paragraph already
contradicted it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:45:34 +07:00
Sergey KozyrenkoandClaude Opus 4.8 7b67b1c0ea fix(flows): return partial container listings instead of failing; stop the refetch skeleton flash
Container-listing polish on top of the partial-success work:

- A directory whose entries all fail to stat no longer 500s. The find exec
  already proved the container alive, so ListContainerDir returns the readable
  entries (possibly none) plus the per-entry failures, and the handler serves a
  200 partial listing; only a cancelled request or a dir that can't be listed at
  all still errors.

- Over the entry cap, list the first page and set a Truncated flag rather than
  erroring with end-user copy from the docker layer. The flag flows through to
  the Pull dialog, which now warns the user the directory was truncated instead
  of silently showing a subset.

- The Pull dialog no longer flashes a skeleton on an in-place refetch (Refresh,
  or after a Pull): the listing hook clears its rows only when the path actually
  changes, so a same-path reload keeps the current rows under the loading guard.

Tests: an all-entries-failed listing returns 200 with the failures; the
Truncated flag surfaces in the response. Swagger regenerated for the new field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:38:17 +07:00
Sergey KozyrenkoandClaude Opus 4.8 2c524faea9 fix(observability,flows): drain telemetry sinks in parallel, stop leaking docker errors, dedup listing failures
Three defects on the shutdown and container-listing paths:

- Telemetry drain ran the langfuse and otel sinks serially under one deadline.
  langfuse's ForceFlush/Shutdown ignore the caller ctx and block on their own
  timeout, so a langfuse outage burned the whole budget and the healthy otel
  sink's final batch was never flushed. Add observer.Drain, which flushes then
  shuts down each sink in its own goroutine bounded by the deadline; main uses
  it, retiring the inline flush-then-shutdown dance.

- The partial-success listing put the raw stat/list error into the 200 body's
  Failures[].Message, exposing docker-layer detail (container ids, the daemon
  address) that response.Error only reveals under develop mode. Send a generic
  message to the client and keep the raw text dev-gated, logged server-side.

- A path read successfully by one query could also appear in Failures when
  another query failed on it, order-dependent. Reconcile at assembly: drop any
  failure whose path was read successfully, so no path is in both arrays.

Also bound the exec stdout buffered before parsing so a compromised sandbox
can't stream unbounded output into memory.

Tests: Drain delivers a healthy otel sink while a langfuse sink blocks and
returns within its deadline; the listing failure message is dev-gated; a path
never lands in both Files and Failures. Live: unreachable collector -> shutdown
stays bounded (~5s) and exits cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:24:02 +07:00
Sergey KozyrenkoandClaude Opus 4.8 bc8714db6e fix(observability): flush telemetry on graceful shutdown, drain every sink
The graceful-shutdown branch only logged "cleaning up resources" and exited
without flushing telemetry, so the last buffered batch was dropped on every
SIGTERM. Drain it, with three corrections:

- main: flush then shut down the observer on shutdown, bounded by a 5s
  deadline in a goroutine so an unreachable collector can't stall exit.
  Flush must precede Shutdown because the langfuse observer's Shutdown
  cancels its context without flushing; its final batch drains only via
  ForceFlush while the context is still live.
- observer Flush/Shutdown: drain both the langfuse and otel sinks and join
  their errors instead of returning after the first, which skipped the
  second sink entirely when both were configured.
- telemetryClient Flush/Shutdown: attempt all three providers and always
  close the grpc connection, joining errors, so a stalled provider flush
  no longer skips the rest or leaks the connection.

Tests: the langfuse observer test proves Shutdown drops the buffered batch
while ForceFlush drains it to the sink; observer-level tests prove both
sinks drain and errors aggregate. Live: with an unreachable collector,
shutdown stays bounded (~5s) and exits cleanly instead of stalling ~10s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 17:30:31 +07:00
Sergey KozyrenkoandClaude Opus 4.8 c0660b8398 fix(observability): connect the telemetry collector lazily, tear down cleanly
The startup dial used grpc.WithBlock with a 10s timeout, so a set-but-
unreachable collector stalled main() for up to 10s, and once that dial failed
the client was nil forever — a collector that came up later never connected
without a restart. Switch to grpc.NewClient (non-blocking): startup never
stalls and gRPC re-establishes the connection in the background on its own.
Also build all three exporters before any provider, so the exporter-error
cleanup (closing the connection) happens before any batch/reader goroutine
starts and can't leak one. Drops the deprecated WithBlock/WithReturnConnectionError.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:45:41 +07:00
Sergey KozyrenkoandClaude Opus 4.8 345b91e4c1 docs(swagger): regenerate for the container-files Failures field
ContainerFiles gained a Failures list when the container listing switched to
partial-success; regenerate the Swagger docs (swag v1.8.7, the pinned version)
so generated clients see the new field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:37:18 +07:00
Sergey KozyrenkoandClaude Opus 4.8 a306b156a1 chore: gitignore backend/vendor, drop stale pkg/queue doc row, self-contained test messages
- Ignore the local `backend/vendor/` tree (187 MB, present only for the
  vendored-fork build) so it can't be accidentally staged.
- Remove the architecture-table row for `pkg/queue/`, which was deleted when
  the container stat fan-out moved to errgroup.
- Drop the internal "H4" report-id labels from the telemetry test — the
  sentences already say what they guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:37:17 +07:00
Sergey KozyrenkoandClaude Opus 4.8 780c2a722e fix(docker): drop the TTY from the dir listing to keep newline filenames intact
find -print0 already delimits with NUL, but the exec still ran under a TTY,
whose onlcr translation rewrites every \n in the stream to \r\n — including a
\n that is part of a filename — so a file literally named with a newline was
stat'd under the mangled name and reported unreadable. Run the exec without a
TTY and demux the resulting multiplexed stdout/stderr stream by hand (no new
dependency), so filename bytes reach the parser untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:20:36 +07:00
Sergey KozyrenkoandClaude Opus 4.8 1535a7a700 fix(flows): don't show a previous directory's listing under a new path
Navigating to a new directory in the Pull dialog kept showing the old files
and old "N entries could not be read" banner until the response landed (the
hook never cleared its data at fetch start), and when every entry failed the
per-entry messages were shown nowhere. Clear files/failures when a fetch
starts, and render the first few failure messages in the all-unreadable
empty state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:12:40 +07:00
Sergey KozyrenkoandClaude Opus 4.8 93d36c9827 fix(flows): keep serving readable paths when one container path fails
The multi-path container listing still fail-fasted: a single bad top-level
path (nonexistent, not a directory, list failed) returned HTTP 500 and
blanked every path that had already listed. Record each such path as a
failure and keep going so the readable paths are still returned; only a
cancelled request aborts, and if every path fails at the directory level the
request is failed as a whole. Also drop a per-entry failure whose path was
already read successfully by an overlapping query.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:12:40 +07:00
Sergey KozyrenkoandClaude Opus 4.8 ac9a16517a fix(docker): list container dirs with find -print0, not ls parsing
Parsing `ls -1` output was unsafe: under the TTY the exec allocates, GNU
coreutils shell-quotes names — so a readable file with a space, quote or
non-ASCII byte was stat'd with the quotes and reported unreadable — and
busybox wraps every entry in ANSI colour codes, so on an alpine/busybox
image the whole listing came back unreadable. Switch to `find -maxdepth 1
-print0`: literal bytes, portable across GNU and busybox, and the NUL
delimiter survives filenames containing newlines. Cap the entry count so a
directory with hundreds of thousands of files can't fan out into that many
Docker API calls, and classify a cancelled request or an all-entries-failed
listing as a directory-level error instead of a misleading empty 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:12:38 +07:00
Sergey KozyrenkoandClaude Opus 4.8 56e8035b8a feat(flows): surface partial container listings in the Pull dialog
The container file endpoint now returns HTTP 200 with the readable entries
plus a Failures list when some entries can't be stat'd, instead of failing
the whole request. Plumb those failures through useFlowContainerFiles and
show a non-blocking warning banner above the readable files ("N entries
could not be read"), reserving the full error empty-state for a true
listing failure. When nothing is readable, the empty-state says so rather
than "Directory is empty".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 13:38:24 +07:00
Sergey KozyrenkoandClaude Opus 4.8 1300f60a2b feat(docker): partial-success container listing instead of fail-fast
A single unreadable directory entry (dangling symlink, a file removed
between ls and stat, a transient /proc entry) used to fail the whole
listing with HTTP 500, blanking the file browser and discarding every
readable sibling. The frontend already expects /proc/sys to not fail
spuriously, but the backend did the opposite.

ListContainerDir now returns a ContainerDirListing{Files, Failures}: per-
entry stat errors no longer abort the batch. GetFlowContainerFiles serves
the readable entries as HTTP 200, carries the failures back in a new
ContainerFiles.Failures field, and logs each skipped entry (capped) plus a
degradation summary. Directory-level faults (not a dir, ls failed, container
gone) still return 500 — there is no partial to show. statContainerEntries
returns successes + failures instead of the lowest-index error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 13:32:48 +07:00
Sergey KozyrenkoandClaude Opus 4.8 8212802c84 fix(docker): clamp non-positive worker count in the stat fan-out
The deleted queue normalized workers<=0 to a default; the errgroup helper
passed the count straight to SetLimit, where 0 deadlocks the first Go() on an
unbuffered semaphore and a negative value disables the bound entirely. The
sole caller hardcodes 20 so this was latent, but clamp it back so a future
config-driven count can't hang the listing or flood the Docker daemon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 03:03:57 +07:00
Sergey KozyrenkoandClaude Opus 4.8 40eeea93c0 refactor(docker): replace the custom queue with a bounded errgroup fan-out
ListContainerDir was the only consumer of pkg/queue and discarded its ordered
delivery (the caller dedups and re-sorts). Replace the fan-out with a small
errgroup helper — SetLimit keeps the same 20-way bound on concurrent stats
against the Docker daemon, the caller's context propagates into each call,
results stay in input order, and the lowest-index failure is surfaced
deterministically — then delete pkg/queue. The helper was verified byte-
identical to the previous queue implementation across 600 randomized directory
shapes (sizes, failure positions, out-of-order completion) before removal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 02:05:24 +07:00
Sergey KozyrenkoandClaude Opus 4.8 d44a33f5dd test(observability): cover the telemetry success path
Adds a fake OTLP collector so NewTelemetryClient can be exercised against a
reachable endpoint: it builds all three providers and both ForceFlush and
Shutdown export and tear down without error. Previously only the
not-configured and unreachable-dial paths were covered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 10:02:58 +07:00
Sergey KozyrenkoandClaude Opus 4.8 745c8ae6d6 test(queue): cover the default-workers, nil-process, and Instance paths
Adds scenarios for a non-positive worker count (must fall back to the
default, else nothing drains the queue and delivery hangs), a nil process
function (logs and drops each item without panicking), and a stable
instance id. Brings queue.go to full statement coverage and kills the
mutants that previously survived on the workers<=0 fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 10:02:56 +07:00
Sergey KozyrenkoandClaude Opus 4.8 d49e740821 test(observability): cover dial-timeout + ErrNotConfigured; close conn on exporter error
Unit coverage for NewTelemetryClient: an unset OTEL_HOST yields ErrNotConfigured,
and an unreachable collector returns within the dial timeout instead of hanging
(a TCP-accepting, silent listener drives the WithBlock path).

Also close the grpc.ClientConn on the exporter-creation error paths — a
successful dial followed by a failed exporter New() previously leaked it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 09:09:38 +07:00
Sergey KozyrenkoandClaude Opus 4.8 62a1445d32 refactor(queue): drop the running-context for a flag; reject restart-without-stop
The two-context design in ddd3916 was correct but fragile: the field named
`ctx` was the liveness signal goroutines must NOT bail on, while the real
cancellation signal was `stopCtx`. That inversion is the exact trap that
produced the earlier happy-path regression (bailing on `ctx`, which a normal
input-close cancels, dropped in-flight results). It also left a latent
restart-without-Stop hazard: a second Start() after input-close orphaned the old
stopCtx and shared wg, deadlocking a later Stop() (with a data race).

Collapse `ctx` into a plain `running` bool (it was only ever read via .Err(),
never awaited), keep `stopCtx` as the single cancellation signal, and guard
Start() on stopCtx so a restart requires a prior Stop() — removing both the
naming trap and the restart hazard.

Comprehensive tests (queue_scenarios_test.go): boundary-N full delivery,
contiguous-prefix-on-abandon, a randomized prefix-invariant fuzz, goroutine-leak,
restart-reject, process-error, double-stop, Running() transitions. The happy-path
tests hang on the pre-rework code and pass here (side-by-side under -race). Live:
a flow's non-empty container dirs of 1/40/100/500 files each deliver every entry;
the error path returns 500 without hanging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 09:09:36 +07:00
Sergey KozyrenkoandClaude Opus 4.8 ddd3916e9c fix(queue): only hard-stop workers on Stop(), not on normal input-close
The previous fix (97e5730) had workers and the reader bail on q.ctx, but the
reader also cancels q.ctx on a normal input-close — so the ListContainerDir
happy path regressed: after input closed (buffer names, close, read all N),
workers dropped still-undelivered results and the consumer hung waiting for the
last one. Any non-empty container directory that didn't error early hung the
file-manager request.

Split the signals: q.ctx still tracks "running" (cancelled by input-close or
Stop), and a new q.stopCtx is cancelled only by Stop(). Workers and the reader
bail on q.stopCtx, so a normal input-close drains and delivers every result
while a hard Stop() still unblocks a consumer that abandoned output. Stop()'s
already-stopped guard now checks q.stopCtx (input-close alone must not
short-circuit it, or blocked workers leak).

Tests: DeliversEveryResultAfterInputClose (hangs on the old fix, verified
side-by-side under -race) + StopHardStopsAfterInputClose; deadlock + ordering
still green under -race.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 00:02:51 +07:00
Sergey KozyrenkoandClaude Opus 4.8 d6fa58e5d4 fix(observability): don't let an unreachable telemetry collector down the app
A set-but-unreachable OTEL collector hung startup: NewTelemetryClient dialed
with grpc.WithBlock() and no timeout on the deadline-free signal context, so
the process blocked forever before it ever served. And a non-ErrNotConfigured
init error from either observability client was log.Fatalf, killing the
process. Both let an OPTIONAL integration take the whole app down.

Bound the dial with a 10s timeout, and on init failure degrade to a no-op
observer with a logged warning instead of exiting. Verified live: a bad
OTEL_HOST that hung startup indefinitely now boots in ~14s with a warning and
runs a full flow; a reachable collector still boots in ~4s with no warning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 23:20:43 +07:00
Sergey KozyrenkoandClaude Opus 4.8 97e5730a53 fix(queue): don't deadlock Stop() when the consumer stops reading output
ListContainerDir aborts on the first stat error and stops reading the queue's
output channel, leaving workers blocked on the unbuffered `q.output <- result`
send and the reader blocked on a full `q.queue`; Stop() -> wg.Wait() then hung
forever. Select every pipeline send/wait on q.ctx so a stopped queue unwinds.
On stop a worker returns without msg.cancel(), so later workers also bail via
q.ctx and output ends at a contiguous prefix instead of developing gaps.

Regression test drives the exact hang (unread output -> Stop must return),
verified with -race.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 21:29:07 +07:00
Sergey KozyrenkoandClaude Opus 4.8 9876c13ea8 docs(providers): restore the DeepSeek legacy reasoning-format contract
The openai-compat consolidation dropped the comment warning that DeepSeek needs
the legacy top-level "reasoning_effort" string form; without it a maintainer
could add openai.WithModernReasoningFormat() to the shared opts and silently
break DeepSeek thinking mode. Restore it at the shared opts choke point.

Also fix two stale pointers: glm/qwen config.yml cited
WithPreserveReasoningContent() "in glm.go/qwen.go", but it moved to
openaicompat.go during that same consolidation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 17:02:21 +07:00
Sergey KozyrenkoandClaude Opus 4.8 a87ac2db18 test(api-tokens): cover the token-name length cap
Export tokenNameSchema and pin the 100-character boundary (100 accepted, 101
rejected) plus its trim/default behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:34:05 +07:00
Sergey KozyrenkoandClaude Opus 4.8 f5f7974826 test(validation): cover REST prompt validation and knowledge length limits
- prompts_test.go: PatchPrompt rejects a syntax error, an undeclared variable,
  and a whitespace-only template over REST (the path a raw client hits when the
  UI is bypassed) and does not persist them; valid templates still create/update.
- validation_test.go: validateKnowledgeFieldLengths accepts each field at its
  max length and rejects one character over.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:34:04 +07:00
Sergey KozyrenkoandClaude Opus 4.8 72da224033 feat(prompts): validate template syntax on the REST update endpoint
The GraphQL createPrompt/updatePrompt mutations run validator.ValidatePrompt
(Go text/template parse + declared-variable check + trial render), but the
REST PUT /prompts/:type handler only checked the field was present, so a
prompt with a syntax error or an undeclared variable could be stored over
REST and later break rendering. Mirror the GraphQL check in PatchPrompt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:01:36 +07:00
Sergey KozyrenkoandClaude Opus 4.8 f92aafb09a fix(api-tokens): align token-name length cap with the backend (100)
The create/edit form allowed 255 characters, but the backend caps token
names at 100 — a longer name cleared client validation and was rejected on
save. Lower the zod cap to 100 to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:44:20 +07:00
Sergey KozyrenkoandClaude Opus 4.8 0cf85ef029 feat(graphql): validate knowledge and API-token mutation inputs
The REST handlers reject empty or oversized knowledge fields and over-long
API-token names through their request-model validate tags, but the GraphQL
mutations — the path the web UI uses — accepted them unchecked, so the same
entity could be stored past its documented limits depending on the caller.

Mirror the REST caps at the resolver boundary:
- createKnowledgeDocument / updateKnowledgeDocument: require content (and
  question on create), and cap content/question/description/codeLang lengths.
- createAPIToken / updateAPIToken: cap the token-name length.

Limits are kept in sync with server/models via mirror comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:44:19 +07:00
Sergey KozyrenkoandClaude Opus 4.8 bcef80de99 fix(graph): reject empty flow title, flow input, and assistant input
The renameFlow / putUserInput / callAssistant GraphQL resolvers passed the
title/input straight to the controller with no non-empty check, while the
equivalent REST handlers reject them with 400. An empty flow title in
particular then fails the Flow model's `required` invariant and breaks the
REST GET /flows listing for that user. Mirror the REST guard at the resolver
boundary, matching the existing createFlow "... is required" checks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:16:33 +07:00
Sergey KozyrenkoandClaude Opus 4.8 e1b7d8e5d9 fix(resources): pre-flight blob stats so a ZIP download can't truncate under 200
The streaming ZIP refactor made `ZipResources` open each blob inline while
`streamZipArchive`'s writer commits HTTP 200 on the first byte. A blob missing
on disk (a DB record whose blob file is gone) failed mid-stream, so the client
received a 200 with a central-directory-valid but incomplete archive — the
missing files silently dropped (regression vs main, which buffered then sent).

Stat every blob up front; a missing one now returns before any byte is written,
so `streamZipArchive` emits a clean structured error instead. Keeps the
streaming memory benefit.

Tests (both proven fail-on-unfixed / pass-on-fixed): a unit test asserts the
writer stays empty when a blob is missing, and a download-handler test asserts
a missing blob in a multi-file ZIP returns a clean 500, not a truncated 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 13:51:54 +07:00
Sergey KozyrenkoandClaude Opus 4.8 9bda603de3 refactor(frontend): prune restatement comments
Drop comments that restate the code they sit on (public-route auth block,
app.tsx catch-all-route label, image-handle overlay description) per the
project's default-zero comment policy. Kept the load-bearing inline note that
password_change_required is local-users-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 13:10:35 +07:00
Sergey KozyrenkoandClaude Opus 4.8 dc1ebef25f fix(file-manager): use OKLCH token directly for the multi-drag badge
The multi-drag "N items" drag image set `hsl(var(--primary))`, but the theme
tokens are OKLCH values — `hsl(oklch(...))` is invalid CSS and dropped, leaving
a transparent badge with default text. Reference the tokens directly via
`var(--primary)` / `var(--primary-foreground)` (theme-aware, valid).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 13:10:35 +07:00
Sergey KozyrenkoandClaude Opus 4.8 cd9f45d69c fix(oauth-result): drive status text through state so it updates in production
`updateMessage` only mutated a ref, so the visible status ("Authentication in
progress...") never changed in production — it appeared to work only in dev via
StrictMode's double-mounted useLayoutEffect. Call setStatusMessage directly and
drop the dead ref + layout-effect machinery. The OAuth flow itself was already
correct (close/redirect use locals); only the visible text was frozen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 13:10:34 +07:00
Sergey KozyrenkoandClaude Opus 4.8 ee9393d4c0 fix(a11y): name sidebar new-actions + mark active editor-menu options
- main-sidebar.tsx: the three icon-only "New" quick-action links wrapped a bare
  <Plus/> with no accessible name (WCAG 2.4.4/4.1.2) — add aria-label
  "New flow"/"New template"/"New knowledge".
- editor toolbar heading/list/table-align menus: single-select options marked
  the active one with only a visual <Check>. Add role="menuitemradio" +
  aria-checked (mirroring the shipped Header-row menuitemcheckbox) so screen
  readers announce which option is active. Visual affordance unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 13:10:34 +07:00
Sergey KozyrenkoandClaude Opus 4.8 933a80147e perf(context): memoize providers + system-settings context values
Both provider components built their context `value` as a fresh object literal
each render (providers-provider also re-sorted into a new array and recreated
its setter), forcing every consumer to re-render on each provider render.
Memoize the sorted array, the setter, and both value objects — the two
non-memoized outliers among the app's context providers. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 13:10:14 +07:00
Sergey KozyrenkoandClaude Opus 4.8 605b271214 perf(settings-provider): stop per-keystroke full-form validation
Subscribing to `formState.isValid` via useFormState flips RHF's internal
`_proxyFormState.isValid`, which makes the zod resolver re-run over the WHOLE
form (13 agent accordions × ~19 fields + refines) on every keystroke — even
in onSubmit mode. `isValid` is only needed to gate the unsaved-changes dialog,
so validate lazily when that dialog opens instead of subscribing. Live INP on
the Name field: steady-state per-keystroke processing 152ms worst-case → ~5ms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 13:10:13 +07:00
Sergey KozyrenkoandClaude Opus 4.8 c517ccbb1e chore(frontend): remove dead code (monaco chunk, setSelection, isNew branch)
- vite.config.ts: drop the manualChunks 'monaco' branch — monaco was removed
  in the monaco→tiptap migration (82b82c5), so the regex never matches.
- use-file-manager-selection.ts: remove `setSelection` — no caller destructures
  it (file-manager.tsx never did); all writes go through rawSelectedPaths.
- settings-prompt.tsx: remove the unreachable `isNew` (promptId === 'new')
  branch — nothing routes to /settings/prompts/new, and it would render a
  "Create Prompt" header over a "Prompt not found" card. The real create path
  is `isUpdate === false`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 13:10:12 +07:00
Sergey KozyrenkoandClaude Opus 4.8 bfd5b9d42f fix(markdown-editor): give the editor contenteditable an accessible name
The rich editor is a contenteditable div with role="textbox", which is
NOT a labelable element — a sibling `<FormLabel htmlFor>` can't name it,
and two of the three consumers (settings-prompt, template) render no
visible label at all, so the field had no accessible name (Lighthouse
A5). Thread `aria-label` through MarkdownEditorField to the contenteditable
(via the same view.dom passthrough as aria-describedby/aria-invalid) and
to the raw textarea, with each consumer supplying its name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 03:46:33 +07:00
Sergey KozyrenkoandClaude Opus 4.8 44b5fef6dc fix(api-tokens): clear the table filter when opening the create row
The inline "Create Token" row is a data row prepended at index 0, so an active
table filter (globalFilterFn) or a non-first page hid it — clicking Create Token
appeared to do nothing. handleCreateNew now calls setFilter(''), which clears the
filter and resets pageIndex to 0 (clearPageOnFilterChange default), so the create
row is always visible.

Live-verified (docker/HEAD backend): with a non-matching filter, Create Token now
shows the create row and clears the filter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 01:37:49 +07:00