From .cursor/rules/CODE-REVIEW-prompt-provider-commits.md (settings-prompt, code-editor):
- rename abbreviations currentIdx->currentIndex, pos->position
- drop the cycle-ternary restatement comment
- gate the green "go to occurrence" badge affordance to plain view (code view only inserts)
- move code-editor's byte-faithful note into an actionable JSDoc warning on the component
Cross-component uniformity: collapse the four scoped-useWatch wrappers (DiffContent,
VariablesPanelContainer, KnowledgeFormHeader, DeleteProviderDialog) to one consistent
"Don't hoist this useWatch..." warning, dropping the duplicated narration + provenance.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The provider `name` field was watched at the top of SettingsProvider but only
fed the delete dialog's itemName, so every keystroke re-rendered the whole form
tree (Accordion + 12 agent configs + resizable panels). Move the watch into a
small DeleteProviderDialog wrapper that subscribes on its own. Measured: typing
10 chars now re-renders SettingsProvider 2x (the one-time isDirty flip) instead
of once per keystroke; the per-keystroke re-renders land on the tiny dialog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Code/Plain toggle in the ⋯ menu swaps the autosize textarea for the
lazy-loaded CodeMirror CodeEditor, mirroring the prompt-template editor, so
flow templates with {{VAR}} placeholders edit byte-faithfully (no markdown
normalization). The toggle is available for new templates too; the editor
nests in the existing InputGroup shell so the in-input save button is shared
across both modes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typing in the knowledge markdown editor re-rendered the whole KnowledgeForm on
every keystroke: form.watch('content') — used only to toggle the anonymize
button's disabled state — subscribed the top-level component, dragging the form
body, layout, and metadata fields through a re-render per character. Measured on
a multi-KB document: max input processing 58ms -> 21ms, keystrokes over 25ms
20 -> 0, long tasks 1 -> 0.
- Move the content subscription into a small module-scope KnowledgeFormHeader
wrapper (scoped useWatch), so only the header reacts to typing; the form body,
layout, and editor stay put. Mirrors the existing pattern in
knowledge-form-controls.tsx.
- Removing the watch let the React Compiler optimize KnowledgeForm, which then
flagged the skipNextBlockRef latest-ref (react-hooks/immutability). Rather than
suppress it, untangle the performSave<->guard cycle the ref existed to break:
performSave now returns the result and the *caller* owns navigation. The form
Save button (defined after the guard) does the CREATE redirect via the stable
skipNextBlock; the unsaved-changes dialog's "Save and leave" saves and lets the
guard proceed the navigation the user initiated. No eslint-disable, no memo.
Behavior note: on CREATE, the Save button lands on the new document; the
"Save and leave" dialog now lands on the destination the user was navigating to
(it previously raced between the two). Verified live on the local stack:
create/update via button and dialog, delete on a dirty form, anonymize toggle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SettingsPrompt called form.watch('template') on both forms at the top level, which
subscribes the whole ~1000-line component to re-render on every keystroke (header,
tabs, panels, meta, all four dialogs). Move the live-template reads into small
useWatch children so only they re-render:
- VariablesPanelContainer wraps the variables panel (needs the live ×N counts).
- DiffContent wraps the diff viewer — live while the dialog is open, and Radix
unmounts it (and its subscription) when closed.
- handleValidate and the variable-insert callback read form.getValues() at call
time instead of the watched vars; the diff dialog's derived currentTemplate is
dropped.
Mirrors the existing knowledge-form-controls.tsx pattern. Verified with render-count
instrumentation that the parent no longer renders per keystroke (only the scoped
children do), and that the live count, variable insert/cycle, validate, diff (live
while open), reset, code-editor insert, and dual-tab dirty flows are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prompt templates are Go templates with markdown tables, <xml-tags>, and
significant whitespace — content a markdown (Tiptap) editor normalizes and
corrupts on round-trip. Add a CodeMirror-based view instead, toggled from the
prompt page actions menu, that edits the raw template verbatim.
- New shared CodeEditor (CodeMirror 6 + markdown highlighting, line wrapping,
theme-aware), lazy-loaded so its chunk only loads when code view is opened.
- "Code editor" / "Plain text" toggle in the prompt actions menu.
- Available-variables click inserts {{.Var}} into the code editor at the caret.
- Fix a pre-existing tsc error in the variable-cycling code
(noUncheckedIndexedAccess on the matched-occurrence lookup).
Round-trip verified byte-exact end to end: original -> code edit -> plain, and
save -> DB -> reload, all identical to source (tables, tags, whitespace intact).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clicking an in-use variable in "Available variables" jumped only to the first
occurrence. Now it cycles: from the occurrence the caret is on it advances to
the next (wrapping past the last), else jumps to the first at/after the caret.
Each badge also shows a ×N count when the variable is used more than once.
Verified live (vite dev → docker backend) on the adviser prompt: a ×7 variable
steps through all 7 occurrences in order and wraps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FormModelComboboxItem manually glued an Input and a dropdown Button by
stripping adjacent borders/corners and juggling z-index on focus/hover.
Replace that with the project's InputGroup primitive (single border, ghost
trigger, native group focus-ring) — matching InputPassword and the ~20 other
input-group usages. Also gives the icon-only trigger an accessible name.
Verified live (vite dev against the docker backend): input editable, dropdown
opens via the asChild trigger, option/price autofill intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
anthropic, bedrock, and gemini also carry per-agent prices in config.yml but
were not covered. Adding them confirms no current drift and guards them going
forward. bedrock's loaders take a *config.Config, so they're wrapped with an
empty config to read the embedded catalog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audited all 133 catalog models across 9 providers against official pricing
pages, each delta independently re-verified. openai/anthropic/gemini/glm/kimi
were already correct (0 changes). Corrections:
- deepseek-v4-pro: 1.74/3.48/0.0145 -> 0.435/0.87/0.003625 (catalog was 4x the
official DeepSeek rate).
- bedrock mistral-large-3-675b: 4.0/12.0 -> 0.5/1.5 (8x overstatement; old
Mistral Large 24.07 rate).
- minimax M3 0.6/2.4 -> 0.3/1.2, M2.7 0.4/1.1 -> 0.3/1.2, M2.7-highspeed
0.4/1.1 -> 0.6/2.4, all +0.06 cache (permanent-50%-off effective rates).
- qwen3.6-35b-a3b: 0.248/1.485 -> 0.375/2.25 (catalog had the China-mainland
price, not the International endpoint the rest of the catalog uses).
- qwen: add cache_read = 0.20 x input (implicit-cache rule) to 16 open-source
models that were missing it.
config.yml agent prices synced for deepseek-v4-pro and MiniMax-M3 (guard test
keeps them aligned).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified per the official OpenAI model pages:
- gpt-5.2-codex: [low, medium, high, xhigh]
- gpt-5-pro: [high] (the page states it "only supports reasoning.effort: high")
Other openai reasoning models (gpt-5, gpt-5.1 = no xhigh; gpt-5-mini/nano,
codex variants, o-series) keep the default — their official pages do not
enumerate an accepted effort set.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Alibaba Model Studio's context-cache doc bills cached input tokens at 20% of the
input price (implicit hits, International endpoint). Most of the qwen catalog
already used 20%; align the outliers and guard against future drift.
- qwen3.7-max 1.25 -> 0.5, qwen3.7-plus 0.2 -> 0.08: corrects an earlier change
that read a "50% discount" off the model-pricing page; the authoritative rate
is 20%, so the original 0.5 for qwen3.7-max was already correct.
- qwen3.6-{max-preview,plus,flash}, qwen3.5-{plus,flash}: 10% -> 20% stale
outliers, with matching config.yml agent-price updates.
Add TestAgentConfigPricesMatchCatalog: GetPriceInfoForType returns the agent
price with no catalog fallback, so config.yml drift silently mis-prices cost.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified per the OpenAI model pages (reasoning.effort support):
- gpt-5.4-mini, gpt-5.4-nano, gpt-5.2: [low, medium, high, xhigh]
- gpt-5.2-pro: [medium, high, xhigh] (no low)
gpt-5 / gpt-5.1 keep the default [low, medium, high] (no xhigh; gpt-5 uses
"minimal", not representable in the UI enum). Codex variants are unverified
and left for a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The settings effort dropdown is driven by a model's reasoning.efforts and
falls back to [low, medium, high] when absent, so xhigh/max were never
selectable for OpenAI-compatible models even though the backend accepts them.
Declare the verified accepted levels:
- glm-5.2: [high, max] (Z.AI: GLM-5.2 reasoning_effort accepts only high/max,
so the default low/medium were also wrong for it)
- gpt-5.4: [low, medium, high, xhigh] (OpenAI GPT-5.4/5.5 effort enum)
Other openai/gpt-5.x entries still need per-model effort verification.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cache_read was 0.5; the Alibaba Model Studio official price (International)
is 1.25 per 1M tokens (a 50% discount on the 2.5 input rate). Pre-existing
inaccuracy, unrelated to the qwen3.7-plus addition.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the placeholder estimates with verified list prices (USD per 1M tokens):
- kimi-k2.7-code: 0.95 / 4.00 / 0.19 cache (platform.kimi.ai official) —
identical input/output to k2.6, only the cache rate differs.
- qwen3.7-plus: 0.4 / 1.6 / 0.2 cache (Alibaba Model Studio official,
International base <=256K tier) — the earlier estimate conflated it with
the qwen3.7-max tier and was ~2.5x too high.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"the only GLM model that honors reasoning_effort" is an unenforceable
prose claim; state the capability the entry actually carries instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tail reader's no-trailing-newline branch (a torn/truncated final
append, the case M3's fsync defends) and the over-window error branch
had no coverage. Add cases for a newline-free last line, a torn final
append that must be rejected, and a single line exceeding the 64KiB
window.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The M7 pin only checked models actually assigned to an agent type, so an
adaptive reasoning mode declared on an unassigned catalog model would pass
silently. Add a second loop over every catalog model so any openaicompat
provider that declares adaptive/adaptive-only fails loudly — matching the
guard's stated intent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The comment narrated the replaced sync.Map design and defended the
choice; the fixed-stripe array plus FNV indexing is self-evident and
the rationale already lives in commit d12b984.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- glm/models.yml: add glm-5.2 (only GLM model supporting reasoning_effort=high/max)
- kimi/models.yml: add kimi-k2.7-code (code-specialist above k2.6)
- qwen/models.yml: add qwen3.7-plus (cost-efficient tier below qwen3.7-max)
Prices for kimi-k2.7-code and qwen3.7-plus are estimated from adjacent
catalog entries and should be verified against official pricing pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The per-agent options builder only emitted WithReasoning for low/medium/high,
and the langchaingo fork clamped xhigh/max to high in GetEffort (treating them
as adaptive-only). OpenAI GPT-5.5 and GLM-5.2 now expose xhigh/max as real
reasoning_effort levels, so that clamp encodes a stale invariant.
Extend the effort switch to pass xhigh/max through; the fork's GetEffort clamp
is removed in the local langchaingo (propagated via vendor) so the OpenAI
transport emits the real level. Adaptive (anthropic/bedrock) and budget paths
are unaffected — GetEffort is called only by the OpenAI transport. Validity is
gated per model by ModelReasoningInfo.Efforts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adaptive thinking (thinking.type=adaptive + output_config.effort) is an
Anthropic-protocol mode the langchaingo OpenAI transport cannot emit, and
the openaicompat Call path never threads PrepareAdaptiveCallOptions. So an
adaptive reasoning mode declared in an openaicompat provider's models.yml
would silently no-op.
Assert that none of the openaicompat-backed providers (qwen, glm, deepseek,
kimi, minimax) resolve to adaptive thinking for any agent type. The day
someone adds an adaptive model/mode to one of their configs, this fails
loudly instead of dropping the thinking budget in silence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
record() called readLastEvidenceReceiptHash on every append, which re-read
and re-hashed the entire receipts.jsonl just to get the last hash and verify
the whole chain — O(N^2) over a flow's receipts, on the tool-call hot path
under the per-path lock (M2).
Read only the last line instead (windowed ReadAt from the end), failing
closed if that line is missing, wrong-schema, or hash-mismatched. This is
O(1) per append and stays correct across the multiple recorder instances
that write one path (L6): the file stays the single source of truth, so
there is no per-instance cache to go stale. Full-chain verification belongs
in a separate read-time verify tool, not on every write.
Off by default (EVIDENCE_RECEIPTS_ENABLED=false). Measured: 1024 concurrent
appends under -race dropped from ~49s to ~7.5s.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The append wrote the receipt line and returned without flushing, so a crash
between the write and the OS flushing its page cache could lose or truncate
the last receipt, after which the fail-closed chain check halts all further
writes until the file is repaired (M3). Sync the file before returning.
Off by default (EVIDENCE_RECEIPTS_ENABLED=false), so standard deployments are
unaffected; the cost lands only when the feature is explicitly enabled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three guards for the L6 sharded mutex pool (d12b984):
- bounded: 100k distinct flows resolve to <= 256 mutexes, so reverting to a
per-path map (the original unbounded leak) fails the test.
- high concurrency: 16 cross-executor writers on one path keep the hash chain
intact under -race.
- stripe collision: two flows that hash to the same stripe keep their own
chains intact, confirming shared stripes add contention but not corruption.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A sync.Map holding one *sync.Mutex per flow path grew unbounded for the
whole server uptime. A fixed 256-stripe array keyed by FNV-32a hash of
the path caps memory at a constant cost while preserving per-path append
serialization required by the hash-chain invariant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added langfuse observability support to the graphiti search tool, allowing for detailed tracking of search operations.
- Introduced a mapping of search types to human-readable titles for better traceability in logs.
- Implemented a new method to build the input payload for langfuse retrievers, encapsulating relevant search parameters.
- Enhanced error handling to include retriever status updates on success and failure, improving observability and debugging capabilities.
- Introduced a process-level singleton for the anonymizer replacer to enhance performance and ensure thread safety.
- The replacer is built once using patterns loaded at startup, allowing it to be shared across all flow executor instances.
- Updated the flow tools executor to utilize the shared replacer, simplifying the creation process and improving efficiency.
- Replaced expirable LRU cache with non-expirable lru.Cache in multiple files for improved performance and reduced resource usage.
- Introduced a new function, newSummarizerCache, to create a fixed-size LRU cache for summarizer results, eliminating the need for background goroutines.
- Updated related code to ensure compatibility with the new cache implementation.
The OAuth callback links/creates an account by the email from ResolveEmail, and
Google/GitHub only ever return a verified address — but that invariant lived
inside each provider and was invisible at the callback. A future OAuth provider
that omitted the verified check would let an attacker register a victim's email
there (unverified), "sign in", and be linked straight into the victim's account:
instant takeover.
Make verification part of the contract: ResolveEmail now returns
(email, verified, err), and the callback refuses to proceed when !verified.
Because Go's bool zero value is false and the compiler forces the new return, a
provider that forgets to report verification fails closed (its own login breaks)
rather than opening a takeover. Google reports claims.EmailVerified; GitHub only
selects verified addresses, so reports true. Added a callback test: a provider
reporting an unverified email is rejected (no link, no session) — red before the
gate, green after.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EmailChange.Mail validated with `vmail`, which accepts not just a real address
but also the literal "admin" and any UUID — escape hatches that exist so the
seeded admin row (mail "admin") passes User.Mail's Valid(). The account's
change-email form (added this branch) reaches it, so a user could save "admin"
or a UUID as their own email — a non-deliverable value (no privilege gain: roles
come from role_id, and UNIQUE(mail) blocks colliding with the real admin row).
Add a strict `realemail` validator (same address regex as vmail, minus the
hatches) and use it for EmailChange.Mail; User.Mail keeps `vmail` so the seeded
admin still validates. Tests cover the validator (admin/UUID rejected, real
address accepted) and the handler (changing to a UUID or "admin" now 400s).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ChangeEmailCurrentUser was the only path that lowercased the email
(strings.ToLower), while local login, the OAuth callback and CreateUser all
store/compare it raw against a case-sensitive UNIQUE(mail). That lone
normalization let a changed address (now lowercased) miss a later raw-case
login, and let the uniqueness pre-check (run on the lowercased value) skip an
existing mixed-case row. Drop the ToLower so every path is consistently
case-sensitive again — the pre-branch invariant. The email validator already
rejects surrounding whitespace, so the paired TrimSpace was dead.
Update the test that codified the old lowercasing to assert case preservation.
A fully case-insensitive scheme (normalize everywhere, or citext /
UNIQUE(lower(mail))) is tracked separately as a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NewProviderController loaded every provider's default config (NewConfig) in an
ungated loop that treated any error as fatal, while actual provider construction
(New) is correctly gated on Enabled. A disabled provider pointed at an unreadable
BEDROCK_CONFIG_PATH (or the equivalent custom/ollama external config) therefore
aborted startup, even though that provider was never going to be used.
Extract the config-loading loop into buildDefaultConfigs and gate the failure on
Enabled: a disabled provider whose external config can't be read is logged and
skipped; an enabled provider still fails loudly. Tests cover both paths
(disabled + bad path non-fatal, enabled + bad path fatal).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
The "Frontend - Install dependencies" step ran pnpm install --frozen-lockfile
with continue-on-error: true, swallowing the ERR_PNPM_OUTDATED_LOCKFILE that the
flag exists to raise. The rest of the frontend pipeline (prettier, lint,
type-check, test) already gates strictly, so a stale pnpm-lock.yaml would only
surface later as a confusing downstream "command not found". Drop the flag so
lockfile drift fails loudly at the install step. Backend steps left as-is.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A type present in only one of the two hand-maintained lists fails silently:
accepted by the API then erroring "unknown provider type" at construction, or
rejected 422 despite working. Assert set-equality and canonical names so drift
fails in CI instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UsesAdaptiveThinking/PrepareAdaptiveCallOptions had no test coverage after the
native-langchaingo migration removed the old transport-layer tests. Add table
tests: an adaptive-only model forces adaptive over an agent's budget choice or an
absent reasoning block, while budget-only models stay untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The four Zip* builders deferred zw.Close() with an unnamed return, dropping the
error from the central-directory flush. After the resources download moved to
direct socket streaming, a dropped connection during that final flush could
return a truncated archive under HTTP 200. Capture Close() via a named return so
streamZipArchive logs and aborts the partial response. Regression tests in both
packages drive the Close() error path with a failing writer.
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>