Commit Graph
258 Commits
Author SHA1 Message Date
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
Sergey KozyrenkoandClaude Fable 5 5765b05c19 feat(webui): give PasswordChangeForm horizontal/vertical layouts for the forced screen
The forced password-change screen reused the compact account-card footer, so it
rendered small right-aligned buttons that clashed with the full-width "Sign in"
CTA on the same login page. Add two orthogonal props: layout
('horizontal' default | 'vertical') drives footer direction and button width;
buttonSize ('default' default, mirroring Button) drives size. The forced screen
passes layout="vertical" (full-width stacked buttons, submit on top, matching the
login button language); the account cards pass buttonSize="sm" to stay compact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 16:04:14 +07:00
Sergey KozyrenkoandClaude Fable 5 6000bc8310 refactor(webui): drop redundant isModal/showSkip flags from account forms
isModal (gated the Cancel button) and showSkip (gated the Skip button) were
redundant with the onCancel/onSkip callbacks they always paired with: every
call site that set a flag also passed the matching handler, and none passed a
handler while wanting the button hidden. Render each button from its callback's
presence instead. Removes two props and a name that misdescribed its job
(isModal gated a button, not a modal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 15:35:08 +07:00
Sergey KozyrenkoandClaude Fable 5 1cc7d26987 refactor(webui): migrate remaining route literals to registry, add drift test
The first migration pass missed call sites where the path is nested inside a
helper rather than passed straight to navigate/to: detail-navigation getHref
helpers, mergeHrefWithSearchParams row-open handlers, the knowledge create
redirect, the flow report window.open targets (wiring up the unused
routes.flowReport builder), and the OAuth return_uri. All now build from routes.

Add routes.test.ts: every builder output is asserted to match the app.tsx route
pattern that serves it via matchPath, guarding URL<->pattern sync, plus exact
checks for query encoding and the login return-url guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 14:20:04 +07:00
Sergey KozyrenkoandClaude Fable 5 6fa9077f21 refactor(webui): migrate main navigation to lib/routes
Step 4 of the route-registry migration: dashboard/flows/templates/knowledges/resources
navigation builds from lib/routes.

- routes.ts gains newKnowledge and newTemplate (sentinel ":id=new" routes)
- main-sidebar nav links + recent-flow link -> routes.* / routes.flow(id)
- app.tsx root and catch-all redirects -> routes.dashboard
- flow/flows/new-flow, knowledge/knowledges, template/templates page navigations ->
  routes.flows / routes.flow(id, { tab }) / routes.newFlow / routes.knowledges /
  routes.newKnowledge / routes.templates / routes.newTemplate

useMatch('/x/*') patterns stay literal — they are route-matching globs, not URLs the
registry builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 13:53:14 +07:00
Sergey KozyrenkoandClaude Fable 5 dc6b3a9e2c refactor(webui): route auth redirects through lib/routes
Step 3 of the route-registry migration: every /login redirect and post-login landing
fallback builds from lib/routes instead of hardcoded strings.

- protected-route, axios 401/403 interceptor, user-provider (logout, session-expiry,
  /login + public-route checks) -> routes.login(...) / routes.login()
- oauth-result error redirect -> routes.login()
- login-form / public-route / login page post-login fallback -> routes.newFlow

getReturnUrlParam now has a single in-app caller (the routes.login builder) plus the
logout suffix-override; dropped its now-unused imports from axios and protected-route.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 13:45:46 +07:00
Sergey KozyrenkoandClaude Fable 5 d71feaf854 refactor(webui): migrate settings provider/prompt page navigations to routes
Step 2 of the route-registry migration: the provider/prompt settings pages build their
navigate() targets from lib/routes instead of hardcoded strings.

- settings-providers: provider(id), newProvider({ id | type }), newProvider()
- settings-provider: routes.settings.providers (x5)
- settings-prompts: routes.settings.prompt(name)
- settings-prompt: routes.settings.prompts (x2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 13:37:39 +07:00
Sergey KozyrenkoandClaude Fable 5 ae45bd7c4f refactor(webui): add a central route registry; migrate the settings shell
Introduce lib/routes.ts as the single source of truth for client route paths, with typed
builders for parameterised routes (flow, provider, prompt, login return-url, ...). First
migration step covers the settings shell we own:

- app.tsx settings index/catch-all redirects -> routes.settings.account
- main-sidebar Settings/Profile links -> routes.settings.root / .account
- settings-layout menu items + the "Back to App" fallback -> routes.*

The rest (settings provider/prompt pages, auth redirects, main nav) migrate in follow-ups.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 13:29:32 +07:00
Sergey KozyrenkoandClaude Fable 5 4b4e93c603 fix(webui): land /settings on Account, the first menu item
Account was added as the first settings sidebar item, but the index and catch-all
routes still redirected to /settings/providers, so the gear "Settings" link dropped
the user on the second item. Point both redirects at /settings/account.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 12:43:29 +07:00
Sergey KozyrenkoandClaude Fable 5 bc80aac985 fix(webui): derive the account avatar initial by code point
(displayName).charAt(0) returns the first UTF-16 unit, so a non-BMP first character
(emoji, rare CJK) rendered as a lone surrogate (�). Spread to iterate by code point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 12:36:47 +07:00
Sergey KozyrenkoandClaude Fable 5 86c7616efa fix(webui): return "Back to App" to the page the user came from
The settings shell's only exit was a "Back to App" link hardcoded to /flows, so opening
Settings or Profile from anywhere dropped the user at the flows list on the way out.

- the Settings and Profile entry links pass the current path as location.state.from
- SettingsLayout captures it once on entry (surviving sub-tab navigation, which drops
  state) and points "Back to App" there, falling back to /flows
- tests: SettingsLayout honors the origin / falls back / preserves it across sub-tabs;
  MainSidebar's Settings and Profile links carry the origin

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 12:06:38 +07:00
Sergey KozyrenkoandClaude Fable 5 9b9a74bec0 refactor(webui): share the API-error → message mapping across account forms
The three account forms duplicated the getApiErrorCode + ERROR_BY_CODE lookup pattern
verbatim, and each carried a dead `AuthRequired` entry that the axios interceptor
already handles (hard redirect) before the form's catch can render it.

- add resolveApiErrorMessage(err, map, fallback) next to the axios error helpers
- email/name/password forms use it and drop the unreachable AuthRequired entry
- unit-test the helper's precedence (mapped code > server msg > fallback)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 11:40:39 +07:00
Sergey KozyrenkoandClaude Fable 5 789caf0dfe fix(webui): keep the session on a transient /info failure; update user cache optimistically
A network blip or 5xx on an /info refresh — after an email/name change or on plain
navigation — wrongly cleared local auth and bounced the user to /login, even though the
session cookie was still valid (the axios interceptor already handles real 401/403).

- refreshAuthInfo and the navigation refresh now keep the current session on a transient
  /info failure instead of clearing auth and redirecting to /login
- add patchUser so the email/name forms reflect the change immediately, dropping the
  dependency on a successful /info round-trip for the visible field
- tests: UserProvider keeps the session on transient failure (refresh and navigation
  paths); forms call patchUser with the new value

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 11:18:39 +07:00
Sergey KozyrenkoandClaude Fable 5 96f346e6ec test(webui): cover account page OAuth gating and the name-change form
- settings-account: local accounts expose name/email/password; OAuth accounts hide
  the password card and email editing, keep the name editable, and label the provider
  (known label, raw fallback, then generic); renders nothing without a user
- name-change-form (previously untested): seeds the current name, submits the trimmed
  value and refreshes auth, blocks an empty name, maps Users.NotFound to friendly copy

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 10:42:58 +07:00
Sergey KozyrenkoandClaude Fable 5 c14ba936fb fix(auth): accept mixed-case and long-TLD emails; normalize on change
The vmail validator rejected addresses the frontend's z.string().email() accepts
(uppercase, TLDs longer than 4 chars like .cloud), so users saw a confusing 400.

- relax the vmail regex to allow uppercase and TLDs of 2+ chars
- lowercase + trim the new address in ChangeEmailCurrentUser and in the form schema
  so storage, the duplicate check, and login lookups stay case-stable
- validator + change-email tests for mixed case and long TLDs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 19:26:44 +07:00
Sergey KozyrenkoandClaude Fable 5 06178bf868 fix(server): link OAuth logins to existing accounts by email; harden email change
- authLoginCallback matches users by email alone so an OAuth login links into an
  existing (incl. local) account instead of 500-ing on users_mail_unique
- relink on a create-branch unique-violation race instead of returning 500
- issue the session with the linked account's actual role privileges
- clear the stale OAuth provider link when a user changes their email
- map the email-change unique-violation race to 409 instead of 500
- isUniqueViolation matches Postgres and SQLite case-insensitively
- tests for link/create/blocked/role-inheritance/race, email 409, provider reset
- update auth form test selectors after the form refactor

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 19:00:26 +07:00
Sergey KozyrenkoandClaude Opus 4.8 901753e8d1 feat(webui): show the Profile menu shortcut for OAuth users too
The account page is already available to OAuth users, so drop the
local-only gate on the user-menu "Profile" item to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 03:00:36 +07:00
Sergey KozyrenkoandClaude Opus 4.8 273592ec29 feat(webui): open account page to OAuth users, edit display name, consistent forms
- Keep /settings/account available to OAuth users: drop the redirect and
  the local-only sidebar gate. Show the auth provider (Google/GitHub) on
  the summary badge, render the email read-only, and hide the password
  card for OAuth accounts. Fix the `provide` -> `provider` typo in User.
- Add a "Display name" card + NameChangeForm (PUT /user/name) for all users.
- Make the email/password forms consistent: lead with Current Password,
  unify placeholders ("Enter your ..."), drop the redundant Current Email
  field, and size form buttons to match the "Change" buttons.
- Rename the user-menu item "My Profile" -> "Profile".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 02:56:38 +07:00
Sergey KozyrenkoandClaude Opus 4.8 82b82c5f49 chore(webui): remove dead monaco-terminal component and monaco deps
monaco-terminal.tsx had no importers; drop it together with the now-unused
monaco-editor and @monaco-editor/react dependencies (~3 MB). The component is
preserved on branch feature/frontend-monaco-terminal for later work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 21:44:17 +07:00
Sergey KozyrenkoandClaude Fable 5 5a7bdca4ac fix(webui): re-check focus before stealing it in DetailNavigationSheet
Stabilizes a flaky test ("does not yank focus onto a listbox option when
filteredItems shrinks"): the rAF that moves roving focus now re-checks
document.activeElement before calling focus(), so a focus bounce between
the effect's gate check and the next frame no longer steals focus.

Provisional: the race only reproduces under JSDOM focus simulation (not in
real browsers), so the proper fix likely belongs in the test layer — to be
revisited separately.

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