Commit Graph
277 Commits
Author SHA1 Message Date
Sergey Kozyrenko 14b3da2d6f fixup! fix(detail-navigation): re-seed the sheet search mirror from the controller on open 2026-06-25 09:56:14 +07:00
Sergey KozyrenkoandClaude Opus 4.8 8ca83137a1 fix(detail-navigation): re-seed the sheet search mirror from the controller on open
DetailNavigationSheet keeps a local input mirror (localQuery) seeded once from
controller.searchQuery — a deliberate perf decouple (92a9e59) so typing in the
sheet doesn't re-render the whole detail page. But in the controller's documented
controlled mode (a page-level search box that owns searchQuery), an external
change made while the sheet was closed left the mirror stale: reopening the sheet
showed the old text.

Re-seed the mirror from the controller on each open, folded into the existing
open-transition reconciliation. The sheet is modal, so searchQuery can only
change externally while closed — on-open re-seeding suffices and never clobbers
in-progress typing. Add a controlled-mode test that reproduces the stale input
(red before, green after).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:16:26 +07:00
Sergey KozyrenkoandClaude Opus 4.8 47c1ea108b fix(report-pdf): drop dead CJK font ternary, document the Hangul gap, add tests
renderTextWithCJK selected the CJK font via `bold ? 'NotoSansSC' : 'NotoSansSC'`
— both branches identical, so the ternary was dead. Collapse it to the single
family. The CJK detection regex matches Han, kana, Hangul and Bopomofo, but the
only registered CJK font (NotoSansSC) lacks Hangul — verified with fontkit — so
Korean renders as missing-glyph boxes; document that limitation and the
NotoSansKR fix path.

splitByCJK and renderTextWithCJK had no tests; add a suite covering segmentation
(mixed Latin/CJK, kana, hangul, CJK punctuation, empty input) and per-segment
font assignment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:03:53 +07:00
Sergey KozyrenkoandClaude Opus 4.8 d440eafea9 test(detail-navigation): cover the virtualized sheet branch (>100 items)
The DetailNavigationSheet virtualized branch (items.length > 100) was never
exercised — the fixture had 4 items, always below the threshold. Add a 150-item
suite covering the virtualized path: a windowed subset of options, the roving
tabIndex on the current option, and in-window ArrowDown navigation. The
@tanstack virtualizer can't measure a viewport under jsdom, so the hook is mocked
to a fixed window (the windowing math is @tanstack's own coverage).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 08:43:14 +07:00
Sergey KozyrenkoandClaude Opus 4.8 28377ab4a4 fix(webui): reset price/reasoning when a model name is typed, not just picked
The provider model field is an Input + dropdown combobox. Picking from the
dropdown (or "Use as custom") fires onOptionSelect, which resets price/reasoning
to the new model's catalog values — but typing the model name only updated the
field, leaving stale price/reasoning from the previously-selected model. Fire
onOptionSelect from the Input's onChange when the typed value exactly matches a
catalog option, so a typed known model behaves like a picked one.

Live-verified: typing us.anthropic.claude-sonnet-4-5-... into an agent on
gpt-oss reset Input Price 0.15->3, Output 0.6->15, cache 0->0.3/3.75.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 08:35:59 +07:00
Sergey KozyrenkoandClaude Opus 4.8 49af50d4ab refactor(webui): apply Apollo Client v4 review fixes to apollo.ts
From a doc-grounded review against @apollo/client 4.2.3 + graphql-ws 6 (all
findings low/info, adversarially verified):

- streaming link: fold the throttle timestamp into the LRU StreamingLogEntry so
  it is evicted with its log — removes the unbounded lastUpdateTimestamps Map
  that leaked for streams that never receive a closing (non-appendPart) message.
- errorLink: also dispatch auth:refresh on a ServerParseError 401/403 (the narrow
  application/graphql-response+json path that ServerError.is() does not match).
- WS retryWait: add jitter so a mass reconnect (e.g. backend restart) does not
  thundering-herd the server.
- cache: log instead of silently swallowing a storeFieldName parse failure in
  matchesCacheVariant, so a future Apollo serialization change is observable.
- v4 deprecations: onError -> new ErrorLink, createHttpLink -> new HttpLink,
  FetchResult -> ApolloLink.Result, Operation -> ApolloLink.Operation.

Verified: tsc -b + vite build, 604 vitest pass, and a live smoke on the docker
stand (queries + subscription WS connect, no auth/JS errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 07:48:55 +07:00
Sergey KozyrenkoandClaude Opus 4.8 ec608d7091 fix(webui): redirect to login when the GraphQL subscription WS auth fails
The subscription WebSocket retries forever on a 403 handshake (expired/invalid
cookie), but its error handler only escalated to auth:refresh when the error
*message* contained "403"/"auth required" — and browsers never expose the
handshake HTTP status on a WebSocket error event, so that detection never fired.
An idle page with active subscriptions therefore looped on 403 with no redirect
to login (the redirect only fired once a REST/axios call hit a 403). Dispatch
auth:refresh unconditionally on any WS error and let /info decide: it redirects
on a real 401/403 and is a no-op while the session holds. Live-verified both
ways: an invalid cookie now redirects to login from an idle page, and a
transient WS drop on a valid session keeps the user logged in. Pre-existing (the
WS handler is unchanged from main); surfaced by a tester.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 06:45:06 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b8151c95ba fix(frontend): resolve review nits in settings (toast re-fire, chip a11y, dead export)
- settings-provider: handleTest/handleTestAgent/onSaveAndLeave now reset
  submitError before re-validating. The error toast is driven by a
  submitError-change effect, so a repeated identical validation failure
  previously set the same string and never re-fired; the await between the
  reset and the re-set splits the React batch so the toast shows every time.
- settings-prompt: the variables palette marks "used" chips with a leading
  check icon, giving a non-color signal alongside the green variant (the
  used/available distinction was conveyed by hue alone for sighted users).
- settings-sidebar: drop the unused `export` on the MenuItem interface — it has
  no importers; the extraction from settings-layout was the moment to clean it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 20:44:28 +07:00
Sergey KozyrenkoandClaude Opus 4.8 4214f3b26d refactor(frontend): unify provider/prompt unsaved-changes onto the shared guard
The provider and prompt edit pages each carried a bespoke unsaved-changes
mechanism: a synthetic history.pushState + popstate listener that only caught
the browser back gesture, a "Discard changes?" ConfirmationDialog, and (after
the in-page Cancel button was dropped in the redesign) a chunk of unreachable
leave-handler code. In-app sidebar navigation while dirty silently discarded
edits, and the discard dialog rendered a Trash icon via confirmIcon={undefined}.

Both pages now use the same useUnsavedChangesGuard + UnsavedChangesDialog the
knowledge editor uses:

- useBlocker intercepts in-app router navigation (the sidebar links), and
  beforeunload covers reload/tab-close — not just the back gesture.
- The dialog offers Cancel / Discard / Save & leave (the old one was Stay /
  Leave with no save path).
- Forms switch to mode: 'onTouched' so isValid drives the dialog's Save button.
- Provider keeps "Save navigates to the list" via the guard's skipNextBlock;
  prompt's Save & leave saves every dirty tab (snapshotting both before the
  refetch-driven reset can clobber the other tab).

Removes the popstate/pushState refs, the dead handleConfirmLeave/-OpenChange
branches, and the misleading Trash icon. Resolves review findings on the
removed Cancel affordance, the dead leave-guard code, and the discard icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 10:37:13 +07:00
Sergey KozyrenkoandClaude Opus 4.8 916961d68d fix(frontend): harden the prompt variables palette (used-detection, dirty, a11y, scroll)
A code review of the "Available variables" block surfaced several real issues:

- Used-detection only matched bare {{.X}} interpolations, so variables used
  only in conditionals/ranges/nested access ({{if .X}}, {{range .X}}, {{.X.Y}})
  showed as "available" and clicking them inserted a stray bare {{.X}} instead
  of jumping to the existing usage. Detection and the click-to-locate logic now
  share one regex matching a top-level field in any {{ }} action, mirroring the
  backend's variable extraction; clicking a used variable selects its real
  occurrence.
- Clicking a used variable scrolled to it by counting logical newlines, which
  undershot badly once the textarea soft-wraps long lines (the default
  templates lay out ~40% more visual lines than logical), so the jumped-to
  variable landed off-screen. Measure the true pixel offset through a hidden
  mirror element and center it, so the selection is always scrolled into view.
- Inserting a variable via a chip called setValue without { shouldDirty: true },
  so the change never marked the form dirty and the unsaved-changes guard stayed
  silent. Both insert paths now mark the form dirty, matching keyboard typing.
- The chips were click-only <div>s (no keyboard/screen-reader access). They are
  now role="button" with tabIndex, Enter/Space handling, and a state-aware
  aria-label/title ("Insert …" / "Go to …"), which also activates the Badge's
  existing focus ring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 07:47:26 +07:00
Sergey KozyrenkoandClaude Opus 4.8 161ff10315 feat(frontend): restyle the prompt variables palette
The "Available variables" block merged into the background on desktop and the
hand-rolled chips used light-theme greens (bg-green-100/text-green-800) that
clashed with the dark UI. Rebuild it on the project's primitives:

- Container: a delineated `rounded-lg border bg-card` panel with a `border-b`
  header (title + "click to insert" hint); the chips sit in a recessed
  `bg-background` tray below it, so the section reads as a distinct group with
  depth instead of a faint box.
- Chips: the shared `Badge` component — `green` (theme-aware) for variables
  already used in the template, `secondary` for the rest; monospace, normal
  weight, clickable.
- Badge: give the color variants (blue/green/orange/pink/purple/red/yellow)
  hover states to match default/secondary/destructive, so consumers don't
  hand-roll hover colors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 06:55:13 +07:00
Sergey KozyrenkoandClaude Opus 4.8 6408310950 feat(frontend): move the prompt variables palette below the editor on mobile
Extract the "Available Variables" panel out of promptMeta into its own
`variablesPanel`. Desktop keeps it in the left pane under the tab selector
(reference beside the editor); mobile now renders it after the editor instead of
between the tabs and the editor, so the template editor sits directly under the
tabs on small screens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 06:25:58 +07:00
Sergey KozyrenkoandClaude Opus 4.8 fd6185969d refactor(frontend): unify mobile layout + intro across the 2-pane edit pages
The provider, knowledge and prompt edit pages share one 2-pane shell, but
mobile and the intro block had drifted. Align them:

- Mobile: drop the wrapping Card on provider + knowledge (knowledge-form-layout,
  settings-provider) so all three render the stacked form directly in
  `flex min-h-0 flex-1 flex-col gap-4 p-4` (matches the prompt page; gap-4 to
  match the p-4 edge padding).
- Intro: use a `flex flex-col gap-2 text-center` wrapper with
  `<h2 class="text-2xl font-semibold">` + a `text-muted-foreground` description
  everywhere — knowledge was an `<h1>`, the prompt used `items-center gap-1`, and
  both leaned on a `mt-2` margin instead of the container gap.

Desktop shell (root, ResizablePanelGroup 45/55, left Card + CardContent py-6,
grip handle, per-pane scroll) was already uniform. The right-pane content stays
page-specific (accordion / markdown editor / textarea).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 06:22:28 +07:00
Sergey KozyrenkoandClaude Opus 4.8 a5633cf959 feat(frontend): redesign prompt edit page (2-pane layout, header actions, toasts)
Mirror the knowledge/provider edit pages: replace the single-column form +
sticky footer with a responsive 2-pane layout (desktop ResizablePanelGroup,
mobile single column). Left pane = centered intro + prompt identity + the
System/Human tab selector + the variables palette; right pane = the template
editor that fills the panel.

Move actions into the AppHeader: Validate + Save, plus an ellipsis menu (Reset,
Diff) shown only when a user override exists — no Cancel. Convert the inline
error alerts to a single submitError-driven toast, and the loading/error/
not-found branches to StatusCard. Drop the now-unused handleBack.

Shared UI tweaks supporting the editor:
- Textarea: add an `autoSize` opt-out (default true) so the editor can flex-fill
  its parent (the auto-size hook's inline max-height otherwise clamps it).
- TabsTrigger: add `gap-2` (matches Button) so an icon + label aren't glued.

Align the panel/Card structure with the knowledge layout (py-6 intro,
overflow-y-auto left, overflow-hidden fill-editor right, grip handle, 45/55
split); style the tab bar like the dashboard analytics tabs (bg-background list
+ bg-card active, full-width flex-1 triggers).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 05:50:43 +07:00
Sergey KozyrenkoandClaude Opus 4.8 174a0ba819 refactor(frontend): AppHeader component family + reorganize layouts by area
Introduce a shared AppHeader compound family (AppHeader / AppHeaderAction /
AppHeaderActions / AppHeaderContent / AppHeaderTitle) in a single module and
fold the page-header action button into it as AppHeaderAction (renamed from the
former HeaderButton). Every page header now composes this family instead of
copy-pasting the sticky <header> shell:

- settings pages (account, api-tokens, prompt(s), provider(s))
- main-app lists (dashboard, flows, templates, resources, knowledges, new-flow)
- detail headers (flow, template, knowledge) keep their bespoke breadcrumb
  content as AppHeaderContent children; their action clusters move into
  AppHeaderActions; sibling sheets/dialogs stay outside the header

This removes the duplicated shell across ~15 sites and the drift between copies
(the family owns the one canonical sticky shell + the SidebarTrigger/Separator).

Reorganize components/layouts into per-area subfolders:
- app/      app-layout + the AppHeader family (app-header.tsx)
- main/     main-layout, main-sidebar (+ test)
- settings/ settings-layout, settings-sidebar (+ test)
- flows/    flows-layout

Delete settings-page-header.tsx (superseded by AppHeader).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 03:01:15 +07:00
Sergey KozyrenkoandClaude Opus 4.8 8594a159a7 feat(frontend): redesign provider edit page (2-pane layout, header actions, toasts)
Rebuild the provider settings page to match the knowledge editor:
- responsive 2-pane layout — left Card with the centered intro + Type/Name,
  right scrollable panel with the agent-config accordion; mobile collapses
  to a single-column Card (via useBreakpoint). Each pane scrolls
  independently inside a fixed-height root (no whole-page scroll).
- move Save / Test / Delete into the page header (SettingsPageHeader actions);
  drop the sticky-bottom action bar and the now-unused Cancel/handleBack.
- surface mutation/validation errors as sonner toasts instead of inline
  Alerts (single submitError-driven effect).
- redesign the test-results modal: shrink-0 status icon, result/metadata as
  badges, wrapping monospace error block, colored per-agent pass count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 06:37:28 +07:00
Sergey KozyrenkoandClaude Opus 4.8 73c5500bb2 refactor(frontend): per-page headers for settings, extract SettingsSidebar
Mirror the main app layout: SettingsLayout now renders only
SidebarProvider + SettingsSidebar + SidebarInset + Outlet, with no shared
header. Each settings page renders its own header via a new shared
SettingsPageHeader (sticky h-12 bar with an optional actions slot). The
sidebar is extracted into a self-contained SettingsSidebar component that
computes the "Back to App" returnUrl from location.state; its behavior is
covered by the new settings-sidebar.test (migrated from the removed
settings-layout.test). settings-account.test now wraps the page in a
SidebarProvider since the page renders its own SidebarTrigger.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 06:37:12 +07:00
Sergey KozyrenkoandClaude Opus 4.8 9623e9b77a refactor(frontend): reuse the shared provider-icon map in settings-providers
settings-providers.tsx redeclared its own ProviderType→icon map (and imported all
11 icon components) duplicating the registry already in provider-icon.tsx. Export
that map and consume it here, dropping the duplicate map and the 11 icon imports.
providerLabels/providerTypes stay (labels, single-use). No visual change — the
icons still render monochrome at their existing sizes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 19:12:04 +07:00
Sergey KozyrenkoandClaude Opus 4.8 86b1329c43 fix(providers): surface bedrock models error, make provider type list exhaustive, trim comments (refs #288, #328)
Review nits:
- schema.resolvers.go: log (not swallow) bedrock.DefaultModels errors so an
  invalid BEDROCK_MODELS_PATH no longer yields a silently-empty model list.
- settings-providers.tsx: derive the create-provider type list from an
  exhaustive Record<ProviderType,string> so a future ProviderType is a compile
  error here instead of silently missing from the menu.
- trim two doc comments (convertModelReasoningMode, ReasoningFields) to lead
  with the load-bearing contract instead of restating the name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 18:05:26 +07:00
Sergey KozyrenkoandClaude Opus 4.8 3fc57ff7b7 fix(frontend): reasoning robustness — custom-model reset, max/xhigh→adaptive, range refines (refs #288)
Three QA-found edge cases in the provider form:
- Choosing "Use <x> as custom" set the model without running onOptionSelect, so
  reasoning/price stayed stale for the typed model. Fire onOptionSelect with the
  synthetic option so they reset like a dropdown pick.
- Selecting effort max/xhigh on an adaptive-capable model with mode unset left
  the backend with no way to route the reasoning (it dropped it). Auto-set mode
  to Adaptive, since max/xhigh are adaptive-thinking effort levels.
- Add zod cross-field refines (minLength<=maxLength, reasoning.maxTokens<=32000)
  that native HTML5 validation can't express, mirroring the new backend checks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 16:46:22 +07:00
Sergey KozyrenkoandClaude Opus 4.8 e18169f1a6 fix(frontend): clear stale reasoning.maxTokens on model change (refs #288)
onOptionSelect reset reasoning.mode and reasoning.effort when the agent model
changes but left reasoning.maxTokens untouched, so a budget token value set for
a previous model leaked into the next one (e.g. switching from a budget-capable
model to an adaptive-only model kept the stale value in the form). Reset it
alongside mode/effort. Found during comprehensive provider QA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 16:25:51 +07:00
Sergey KozyrenkoandClaude Opus 4.8 55335feb17 feat(minimax): add MiniMax provider with full app/UI/installer parity (refs #328)
Reworks external PR #328 (octo-patch). The PR added only the provider core;
this brings MiniMax to full parity with the other providers (qwen) so it is
selectable and configurable in the UI and installer.

Applied from the PR (verified against MiniMax's official API docs — M3/M2.7/
M2.7-highspeed are real current models; corrected the M3 description from the
PR's "512K" to the documented ~1M context):
- minimax provider package (OpenAI-compatible https://api.minimax.io/v1),
  config.yml, models.yml, tests; MINIMAX_API_KEY/SERVER_URL/PROVIDER env vars;
  ProviderMiniMax type + DefaultProviderNameMiniMax; providers.go wiring;
  Valid() whitelist.

Added for completeness:
- goose migration adding 'minimax' to the PROVIDER_TYPE enum + database
  ProviderTypeMinimax const.
- GraphQL: minimax in ProviderType enum, ProvidersModelsList,
  ProvidersReadinessStatus, DefaultProvidersConfig; resolvers wire default
  config/models + enabled status; gqlgen regenerated.
- Frontend: MiniMax icon (lobehub), provider-icon + settings-providers
  registration + provider type list; regenerated GraphQL types.
- Installer wizard: provider form, screen, list, registry, env-var mappings,
  locale strings + help text.
- ctester/ftester: -type/-provider minimax support.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 15:24:04 +07:00
Sergey KozyrenkoandClaude Opus 4.8 907440609c fix(frontend): control reasoning mode/effort selects so model-change reset shows (refs #288)
The ReasoningFields mode/effort selects used `defaultValue`, making them
uncontrolled — Radix reads it once on mount, so the `setValue` resets in
onOptionSelect (reset reasoning on model change) updated form state but never
the visible selection. Result: after switching models the selectors showed
stale values (e.g. mode "Adaptive" when the field was cleared to null; effort
left blank when a stale 4.7/4.8 "xhigh" had no item under opus-4-6). Switch to
controlled `value` so the display follows the reset. Also drop a dead
empty-comment block in onOptionSelect.

Found during Layer 3 live verification on the docker stack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:04:06 +07:00
Sergey KozyrenkoandClaude Opus 4.8 4910b62468 fix(frontend): type ReasoningFields control as Control<FormInput> (refs #288)
The component declared control as Control<FormData> (z.output, required
name/type) but receives the form's Control<FormInput> (z.input, optional
name/type), which `tsc -b` (the production build typecheck) rejects with
"Type 'undefined' is not assignable to type 'string'". `tsc --noEmit -p
tsconfig.json` did not catch it; `pnpm run build` (tsc -b && vite build) now
passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 05:36:54 +07:00
Sergey KozyrenkoandClaude Opus 4.8 0fa53a551e fix(frontend): reset agent reasoning when the model changes (refs #288)
Switching an agent's model left the prior reasoning.mode/effort in the form
(onOptionSelect only reset price), so a stale budget mode or an effort the new
model doesn't support could be persisted. Now the model picker also resets
reasoning: adaptive-only models lock to adaptive, others clear mode + effort.

Verified: tsc --noEmit + eslint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 14:35:33 +07:00
Sergey KozyrenkoandClaude Opus 4.8 1501aec843 feat(frontend): adaptive thinking controls in provider settings (refs #288)
Ports the per-agent reasoning UI to support adaptive thinking, gated by each
model's declared capability (the reasoning fragment) instead of a model-name
regex:
- new ReasoningFields component: a reasoning Mode select (adaptive | budget,
  shown only for adaptive-capable models, locked to adaptive for adaptive-only)
  and an effort select whose options follow the model's allowed efforts
  (incl. xhigh/max).
- getReasoningEffort handles xhigh/max; getReasoningMode + the form schema and
  transformFormToGraphQL carry the chosen mode through to the API.

Verified locally: tsc --noEmit and eslint clean. Runtime verification on the
stack (Layer 3) is the next step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:22:15 +07:00
Sergey KozyrenkoandClaude Opus 4.8 1cd8a6878c feat(frontend): fetch model reasoning capability (refs #288)
Adds reasoning { mode efforts } to the modelConfigFragment and regenerates
GraphQL types, so the provider settings UI can gate the reasoning mode/effort
controls by each model's declared capability (ModelReasoningMode incl.
adaptive_only) instead of a model-name regex.

Groundwork for the adaptive-thinking settings UI port.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 10:29:57 +07:00
Sergey KozyrenkoandClaude Opus 4.8 ca9283b93e feat(bedrock): integrate adaptive thinking mechanism backend (refs #288)
Integrates the backend half of PR #288 onto our branch (the frontend is
reworked separately in our codegen style), 3-way merged so the #233
config/models-path changes are preserved:
- adaptive_thinking.go: smithy Build middleware that rewrites the langchaingo
  Converse body from thinking{type:enabled,budget_tokens} to
  thinking{type:adaptive} + output_config.effort, wired via WithAPIOptions and
  prepareCallOptions in Call/CallEx/CallWithTools.
- pconfig: ReasoningConfig.Mode (adaptive|budget) + EffectiveMode/IsZero.
- GraphQL: ReasoningMode enum, reasoning.mode field, xhigh/max effort levels.

Drops the dead llms.WithMetadata adaptive branch from AgentConfig.BuildOptions
(nothing reads opts.Metadata on the Bedrock path; adaptive is applied per-call
by the provider) and updates the unit test accordingly.

Hardening still pending (next commit): strip temperature/top_p/top_k and set
display:summarized for adaptive requests, add Opus 4.7/4.8 catalog entries with
a model reasoning-capability descriptor + force-adaptive backstop, and a live
Bedrock repro. Until then Opus 4.7 should not be selected for an agent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 09:59:48 +07:00
Sergey KozyrenkoandClaude Opus 4.8 1f81800c37 chore(frontend): remove non-functional commitlint setup
commitlint had no config anywhere (no config file, no husky commit-msg
hook), so its rules were never loaded; the "commit" script also pointed
at an uninstalled binary. Remove @commitlint/cli and
@commitlint/config-conventional, the dead "commit"/"commitlint" scripts,
and the stale commitlint entry in README Development Requirements.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 03:39:45 +07:00
Sergey KozyrenkoandClaude Opus 4.8 5523582647 chore(frontend): remove unused dependencies
Confirmed unused via depcheck + manual verification:
- anser, js-cookie, @types/js-cookie — no imports anywhere
- rehype-raw — only referenced in a vite manualChunks regex, never
  imported as a markdown plugin (markdown.tsx uses rehype-highlight/slug
  and remark-gfm); also dropped from that regex
- @graphql-codegen/{client-preset,near-operation-file-preset,typescript}
  — codegen config uses explicit plugins (typescript-operations,
  typed-document-node), not these presets/base plugin

Also drop the dead package.json "eslintConfig" block — it is ignored by
the flat config (eslint.config.mjs) and referenced an uninstalled
storybook plugin.

Verified: graphql:generate reproduces src/graphql/types.ts unchanged,
plus build, lint, 604 tests, and --frozen-lockfile all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 03:34:34 +07:00
Sergey KozyrenkoandClaude Opus 4.8 c3f6a02f34 chore(frontend): remove unused eslint-plugin-jsx-a11y
The plugin was a direct devDependency but never enabled in
eslint.config.mjs (0 active jsx-a11y rules via --print-config). Dropping
it removes dead weight and one stale eslint-9 peer constraint. Lint still
passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 03:04:53 +07:00
Sergey KozyrenkoandClaude Opus 4.8 49d9687c75 chore(frontend): update dependencies to latest within-range
Bump ~60 minor/patch dependencies (Radix UI group, Tiptap 3.27, Apollo
4.2, vite 8.0.16, vitest 4.1.9, typescript-eslint 8.61, react 19.2.7,
react-router 7.18, graphql-codegen 7.1, etc.).

Majors intentionally held back: eslint 10, graphql 17, and @types/node 25
(kept on 24 to match the pinned Node 24.17.0 runtime).

Verified: build, lint, prettier, and 604 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 02:54:57 +07:00
Sergey KozyrenkoandClaude Opus 4.8 9b073e2183 build(frontend): pin Node 24.17.0 and pnpm 11.8.0 across all builds
- add frontend/.nvmrc (24.17.0) as the single source of truth for Node;
  GitHub CI reads it via node-version-file, Dockerfile uses node:24.17.0-slim
- bump packageManager to pnpm@11.8.0; drop "corepack prepare pnpm@latest"
  so the pnpm version derives from packageManager everywhere
- migrate pnpm onlyBuiltDependencies -> allowBuilds in pnpm-workspace.yaml
  (the package.json "pnpm" field is no longer read by pnpm 11)
- add a CI step that fails if the Dockerfile Node tag drifts from .nvmrc

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 02:50:39 +07:00
Sergey KozyrenkoandClaude Opus 4.8 11db9c2e0c feat(webui): merge file-manager folder/chevron into one hover toggle
The directory expand/collapse control now shows the folder icon by
default and crossfades to a chevron on hover (motion, reduced-motion
aware); the single element toggles expansion on click. Nesting indent
gains the icon->text gap (22px/level) so a child's icon lines up under
its parent's label, and the header expand-all control moves to the
shared Button. Folder and chevron share the text-blue-400 accent set on
the parent (header chevron picks it up on hover).

Also prunes ~46 restatement/justification comments and a dead
collectAllFilePaths export (plus its tests) to match the house style.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 05:45:00 +07:00
Sergey KozyrenkoandClaude Fable 5 b6db0b59ad ci(webui): make all frontend CI checks blocking
Prettier / Lint / Test were `continue-on-error: true` (advisory — failures did not fail CI), the same gap that let type errors pile up. Now Prettier, Lint, Type check and Test all block the lint-and-test job (which runs on every branch push).

Prerequisite: `prettier --write` on 5 pre-existing non-conformant files (pages/login.tsx, lib/report/report-pdf.tsx, 3 *.test.tsx) so the now-blocking Prettier check passes — pure formatting, no logic change. Install stays advisory (setup step); backend checks unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 22:48:05 +07:00
Sergey KozyrenkoandClaude Fable 5 52951ceb43 ci(webui): gate build and CI on TypeScript errors
vite build strips types and never type-checks, and the old build`s bare tsc (solution config, files:[]) checked nothing — so type errors reached main unnoticed (which is how 76 had accumulated). Now they fail fast everywhere.

- build: `tsc -b && vite build` — `pnpm build` (and the Docker image build, which runs `pnpm run build`) fails on any type error before bundling.

- add `typescript` script (`tsc -b`, checks both app + node project configs).

- ci.yml: blocking "Frontend - Type check" step in lint-and-test (runs on every branch push; docker-build only runs on main/tags).

Verified: a type error makes both `pnpm run typescript` and `pnpm build` exit non-zero (vite never runs); removing it restores a clean build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 22:37:18 +07:00
Sergey KozyrenkoandClaude Fable 5 895de4cadc fix(webui): resolve settings-provider type errors — frontend now 0 tsc errors
The provider form had 23 errors rooted in Zod v4: `z.preprocess` gives an `unknown` input type, which @hookform/resolvers v5 surfaces as the form field-values type, mismatching `useForm<output>` and cascading to every Control<FieldValues> site.

- Replace `z.preprocess` with explicit helper schemas (proper input/output types) and use `useForm<FormInput, unknown, FormData>`; make the reusable field components generic `<T extends FieldValues>` (Control<T> + FieldPath<T>).

- Fetch `thinking` on the model-config fragment (regenerated types.ts; backend ModelConfig.thinking exists).

- Validate the provider `type` into the ProviderType enum via `z.nativeEnum(...).parse` at the GraphQL boundary instead of an `as` cast (the form intentionally uses watch() to keep disabled fields in the payload, so its values stay input-typed).

tsc: 23 → 0; the frontend now has zero TypeScript errors. 606 tests pass; eslint + prettier clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 22:05:09 +07:00
Sergey KozyrenkoandClaude Fable 5 3b6464e710 fix(webui): resolve 53 pre-existing TypeScript errors
Hack-free fixes (no `as any`/`@ts-ignore`/`as unknown as`):

- tsconfig lib es2023 for Array.findLast; tighten DocumentTitle TitleResolver to ApolloTitleComponent (the broad ComponentType was masked by findLast returning any).

- NodeJS.Timeout → ReturnType<typeof setTimeout> in browser code; models import casing (./User → ./user).

- settings prompt/api-token forms: type against the real generated fragment types and the Zod input/output split (useForm<Input, ctx, Output>); generic FormTextareaItem<T>.

- data-table / detail-nav: noUncheckedIndexedAccess guards; React vs DOM KeyboardEvent disambiguation; drop invalid user-event delay option.

tsc: 76 → 23 (the remaining 23 are one settings-provider Zod/RHF cluster). 606 tests pass; eslint + prettier clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 21:32:21 +07:00
Sergey KozyrenkoandClaude Fable 5 d1bfe368a2 build(webui): drop deprecated baseUrl from tsconfig, rely on path mapping
TypeScript 6.0 deprecated `baseUrl` (removed in 7.0). The `@/*` alias resolves relative to each tsconfig without it, and Vite resolves `@` via its own resolve.alias, so baseUrl was vestigial. Dropping it removes the need for `ignoreDeprecations: "6.0"`, which editor-bundled TypeScript (5.x) flagged as an invalid value (TS5103).

Also drop the dead `@env`/`./env.ts` entry from tsconfig.node.json: the file does not exist and the alias is imported nowhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 17:02:11 +07:00
Sergey KozyrenkoandClaude Fable 5 962e648fe1 refactor(webui): migrate to Apollo Client v4 typed-document-node codegen
Replace the deprecated typescript-react-apollo codegen plugin (unmaintained for Apollo Client v4) with typed-document-node. All ~105 useXxxQuery/Mutation/Subscription call sites are rewritten to the generic useQuery/useMutation/useSubscription(XxxDocument, ...) form, with skipToken replacing skip + conditional-variables.

graphql-codegen.ts: plugins typescript-operations + typed-document-node; add scalars { Time: string } (was unknown); drop withHooks/apolloReact* options. tsconfig.app/node.json: ignoreDeprecations "6.0" for the baseUrl TS5101 deprecation. Regenerate src/graphql/types.ts.

Behavior-preserving: variables, fetchPolicy, error handling, and subscription onData/refetchQueries are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 16:07:57 +07:00
Sergey KozyrenkoandClaude Fable 5 8bd6c72f63 fix(knowledge): preserve document content when list query writes empty body
The /knowledges list query fetches documents with `withContent: false` to save bandwidth, writing an empty `content` to the shared normalized KnowledgeDocument cache entity. That clobbered the full body loaded by the detail query, so opening a document by direct URL (or reload) showed an empty editor.

Add a `content` field merge policy so an empty incoming value never blanks out a body already loaded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 16:07:17 +07:00
Sergey KozyrenkoandClaude Fable 5 942fb45c16 feat(knowledge): dedicated renameKnowledgeDocument mutation, lighter list
Rename a knowledge document via a dedicated mutation that rewrites only the
question in cmetadata — no re-embedding, no embedder required — mirroring the
flows renameFlow pattern instead of round-tripping the full document through
updateKnowledgeDocument.

Backend:
- renameKnowledgeDocument(id, question) mutation + resolver (admin/user split;
  ownership enforced at GetUserDocument, like the update pair)
- metadata-only query UpdateKnowledgeDocumentMetadata (no migration)
- unit + edge tests: metadata-only, missing-doc error, non-owner rejection

Frontend:
- renameKnowledge provider method; wire list and detail inline-rename to it
- drop the content "Preview" column and request the list with withContent:false
  so it no longer pulls full document bodies

Verified end to end against a local Docker backend (rename works; content and
embedding preserved) and against the remote backend (graceful failure where the
mutation is not yet deployed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 13:15:57 +07:00
Sergey KozyrenkoandClaude Fable 5 6d9453a5a2 refactor(webui): type away the any's in settings-provider
Replace 13 `any` usages with real shapes: ProviderTest / ProviderTestResults
for the test-mutation payload, Control<FieldValues> for the form control, and
inferred element types in the result maps. Collapse the two duplicated
3-level-nested error-formatting blocks into one recursive formatFormErrors
helper — validation error output is byte-identical (verified against the same
inputs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 00:35:40 +07:00
Sergey KozyrenkoandClaude Fable 5 da63ced002 refactor(webui): silence the React-Compiler incompatible-library lint
react-hooks v6 ships this rule, but it only carries signal once the React
Compiler is enabled; until then it flags every RHF watch() and useReactTable
as unactionable noise (12 hits). Turn it off project-wide and drop the
now-redundant inline disable in use-element-virtual-list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 00:35:32 +07:00
Sergey KozyrenkoandClaude Fable 5 7c90ec4f92 fix(webui): escape the CJK Unicode range in the PDF report regexes
The CJK-detection regexes embedded a literal U+3000 ideographic space inside
their character class, which ESLint flagged as no-irregular-whitespace (error).
Switch the fullwidth ranges to \u escapes ( -〿＀-￯) — identical
match behaviour (verified: Han / U+3000 / fullwidth match, ASCII does not), no
irregular whitespace in source.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 00:11:38 +07:00
Sergey KozyrenkoandClaude Fable 5 c382d35a52 refactor(webui): drop the unused virtualizer escape hatch from useWindowVirtualList
No consumer reads the returned `virtualizer` (data-table destructures only the
spacer / measure fields), and exposing the raw instance leaks the abstraction —
the same dead, over-broad return just removed from useElementVirtualList. Drop it.

Also correct a stale doc claim: react-virtual does have an `enabled` option (it
gates the scroll listeners), the hook just doesn't surface it — so the
conditional-mount guidance stands without the false "no enabled option" note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 23:56:24 +07:00
Sergey KozyrenkoandClaude Fable 5 fdb5c7b86e perf(webui): virtualize the DetailNavigationSheet listbox
The sheet rendered every filtered option (1797 on a busy flows list), so the
filter re-render and the DOM weight (~1800 nodes) made filtering jank. Above a
100-item threshold the listbox now virtualizes via a new element-scroll
useElementVirtualList hook (the inner-container analogue of useWindowVirtualList):
only the ~30-node visible window is mounted. Small lists — and JSDOM tests —
keep rendering in full.

Roving focus onto an off-screen option scrolls it into view and focuses it once
its row mounts (pendingFocusId), so arrows/Home/End and open-time focus on the
current item work across the virtualized window. Options carry aria-setsize /
aria-posinset so screen readers still see the full count and position.

Measured live (chrome-devtools, test.pentagi.net, 1797-flow sheet): DOM nodes
~1800 → ~32; typing INP 258ms → ~200ms. The remaining cost is the flow.tsx page
re-render when the controller's filtered list changes (debounce flush) — a
separate concern. Open / End / Home / typing focus all verified; full suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 23:21:35 +07:00
Sergey KozyrenkoandClaude Fable 5 92a9e595e0 perf(webui): decouple DetailNavigationSheet search from the page render
Typing in the sheet's search lagged badly (INP ~600ms on a 1797-item list): the
input bound to the controller's searchQuery, whose state lives in the page-level
hook (flow.tsx), so every keystroke re-rendered the whole detail page AND rebuilt
all ~1800 option rows before the caret could echo — characters appeared in batches.

Mirror the input value in local sheet state (instant caret echo, re-renders only
the sheet) and push to the controller in a transition (filtering + prev/next stay
correct; the heavy re-render no longer blocks the keystroke). Memoize the option
rows so a keystroke that only changes the local mirror does not rebuild them.

Measured live (chrome-devtools, test.pentagi.net, 1797-flow sheet): typing INP
606ms -> 258ms; all characters land, focus stays on the input. The residual is the
DOM reconciliation when the filter changes — addressed next by virtualizing the list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 22:09:40 +07:00
Sergey KozyrenkoandClaude Fable 5 4cb25ac469 fix(webui): move DetailNavigationSheet focus to explicit navigation only
Focus was driven off `focusedId` through an effect, so a reconciliation-driven
change (the filter shrinking and re-pinning to the first survivor) also moved
DOM focus, yanking it off the search input mid-type. Two document.activeElement
guards (the second added in 5a7bdca) papered over the timing; the rAF re-check
only ever fired under JSDOM and was dead in real browsers — verified live.

Move focus imperatively only where the user navigates: opening the sheet now
uses Radix's onOpenAutoFocus (preventDefault + focus the current option, with no
rAF race against the focus trap), arrow keys focus synchronously in the key
handler, and render-phase reconciliation only updates the roving tabindex. Both
guards, the effect and the rAF are gone, and the previously-flaky focus-steal
test is deterministic (10/10 isolated).

Verified live (chrome-devtools, test.pentagi.net, 1797-flow sheet): open focuses
the current option, typing keeps focus on the input with no dropped chars, and
arrows / ArrowDown-from-input move focus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 22:08:58 +07:00
Sergey KozyrenkoandClaude Fable 5 345c26eea7 fix(webui): let account settings sections edit independently
The account page tracked a single `editing` section, so opening one editor
unmounted any other open form and silently discarded its unsaved input. Track
an open-section set instead: opening a section no longer closes the others, so
a half-typed draft survives switching between name, email and password.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 16:17:41 +07:00