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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>