Commit Graph
513 Commits
Author SHA1 Message Date
Sergey KozyrenkoandClaude Opus 4.8 ebeddf4e76 fix(providers): set qwen cache_read to the documented 20% implicit-cache rate
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>
2026-06-26 05:04:13 +07:00
Sergey KozyrenkoandClaude Opus 4.8 2f9e86391e feat(providers): declare reasoning efforts for the remaining xhigh openai models
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>
2026-06-26 05:04:12 +07:00
Sergey KozyrenkoandClaude Opus 4.8 de7db21448 feat(providers): declare reasoning efforts so xhigh/max surface in the UI
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>
2026-06-26 04:39:31 +07:00
Sergey KozyrenkoandClaude Opus 4.8 8bbbdbf7c5 fix(providers): correct qwen3.7-max cache_read to the official rate
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>
2026-06-26 03:56:04 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b40091d68f fix(providers): correct kimi-k2.7-code and qwen3.7-plus prices to official rates
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>
2026-06-26 03:54:31 +07:00
Sergey KozyrenkoandClaude Opus 4.8 f489e13ed4 docs(providers): drop unverifiable superlative from glm-5.2 description
"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>
2026-06-26 03:47:13 +07:00
Sergey KozyrenkoandClaude Opus 4.8 c3504b1839 test(evidence-receipts): cover unterminated and oversized tail reads
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>
2026-06-26 03:47:13 +07:00
Sergey KozyrenkoandClaude Opus 4.8 35c77ae5aa test(providers): scan the whole catalog for adaptive modes in the M7 pin
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>
2026-06-26 03:47:12 +07:00
Sergey KozyrenkoandClaude Opus 4.8 83bff62d3b refactor(evidence-receipts): drop change-narration comment on the lock pool
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>
2026-06-26 03:32:02 +07:00
Sergey KozyrenkoandClaude Sonnet 4.6 2d8bce7ccb feat(providers): add glm-5.2, kimi-k2.7-code, qwen3.7-plus to model catalogs
- 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>
2026-06-26 03:11:20 +07:00
Sergey KozyrenkoandClaude Sonnet 4.6 a69e19f751 feat(providers): allow xhigh/max reasoning_effort on the OpenAI-compatible path
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>
2026-06-26 02:18:57 +07:00
Sergey KozyrenkoandClaude Sonnet 4.6 40debd150d test(providers): pin openaicompat providers off adaptive thinking (M7)
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>
2026-06-26 00:52:49 +07:00
Sergey KozyrenkoandClaude Sonnet 4.6 4d25cf5c51 perf(evidence-receipts): read only the file tail for the previous hash
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>
2026-06-25 21:43:19 +07:00
Sergey KozyrenkoandClaude Sonnet 4.6 4b9e4b4972 fix(evidence-receipts): fsync each receipt append for durability
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>
2026-06-25 21:21:07 +07:00
Sergey KozyrenkoandClaude Sonnet 4.6 09383063ac test(evidence-receipts): pin sharded lock pool bounds and concurrency
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>
2026-06-25 21:00:27 +07:00
Sergey Kozyrenko 124a1e1084 Merge remote-tracking branch 'origin/fix/ram-consumption' into integrate/open-prs 2026-06-25 20:11:59 +07:00
Sergey KozyrenkoandClaude Sonnet 4.6 d12b984631 fix(evidence-receipts): replace per-path sync.Map with sharded mutex pool
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>
2026-06-25 20:10:32 +07:00
Dmitry Ng c543831205 feat(backend): enhance graphiti search tool with langfuse integration
- 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.
2026-06-25 12:29:14 +03:00
Dmitry Ng e909f40b2e fix(backend): implement singleton pattern for anonymizer replacer
- 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.
2026-06-25 12:28:49 +03:00
Dmitry Ng 9caa6a8775 fix(backend): update LRU cache implementation to use non-expirable version
- 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.
2026-06-25 12:28:10 +03:00
Sergey KozyrenkoandClaude Opus 4.8 2d2aea5785 fix(auth): reject OAuth logins with a provider-unverified email
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>
2026-06-25 14:43:16 +07:00
Sergey KozyrenkoandClaude Opus 4.8 497cfdd555 fix(users): require a real email on user-initiated email change
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>
2026-06-25 13:00:36 +07:00
Sergey KozyrenkoandClaude Opus 4.8 14e0a0ae71 fix(users): preserve email case on change to match login, OAuth and create
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>
2026-06-25 10:15:44 +07:00
Sergey Kozyrenko 14b3da2d6f fixup! fix(detail-navigation): re-seed the sheet search mirror from the controller on open 2026-06-25 09:56:14 +07:00
Sergey Kozyrenko ac32a8e996 fixup! fix(providers): don't crash startup when a disabled provider's external config is unreadable 2026-06-25 09:56:14 +07:00
Sergey KozyrenkoandClaude Opus 4.8 f441ec1c4d fix(providers): don't crash startup when a disabled provider's external config is unreadable
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>
2026-06-25 09:26:00 +07:00
Sergey KozyrenkoandClaude Opus 4.8 8ca83137a1 fix(detail-navigation): re-seed the sheet search mirror from the controller on open
DetailNavigationSheet keeps a local input mirror (localQuery) seeded once from
controller.searchQuery — a deliberate perf decouple (92a9e59) so typing in the
sheet doesn't re-render the whole detail page. But in the controller's documented
controlled mode (a page-level search box that owns searchQuery), an external
change made while the sheet was closed left the mirror stale: reopening the sheet
showed the old text.

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 06:45:06 +07:00
Sergey KozyrenkoandClaude Opus 4.8 3d447c582f ci: gate frontend install on frozen-lockfile drift
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>
2026-06-24 10:44:31 +07:00
Sergey KozyrenkoandClaude Opus 4.8 7867a73a6a test(providers): guard providerRegistry against AllProviderTypes drift
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>
2026-06-24 09:49:00 +07:00
Sergey KozyrenkoandClaude Opus 4.8 2f3df495fa test(pconfig): cover the adaptive-thinking backstop (refs #288)
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>
2026-06-24 09:49:00 +07:00
Sergey KozyrenkoandClaude Opus 4.8 9e8ee58786 fix(resources,flowfiles): propagate zip Close() error instead of swallowing it
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>
2026-06-24 09:48:45 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b8151c95ba fix(frontend): resolve review nits in settings (toast re-fire, chip a11y, dead export)
- settings-provider: handleTest/handleTestAgent/onSaveAndLeave now reset
  submitError before re-validating. The error toast is driven by a
  submitError-change effect, so a repeated identical validation failure
  previously set the same string and never re-fired; the await between the
  reset and the re-set splits the React batch so the toast shows every time.
- settings-prompt: the variables palette marks "used" chips with a leading
  check icon, giving a non-color signal alongside the green variant (the
  used/available distinction was conveyed by hue alone for sighted users).
- settings-sidebar: drop the unused `export` on the MenuItem interface — it has
  no importers; the extraction from settings-layout was the moment to clean it.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 06:37:12 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b8b90f8349 refactor(providers): use native adaptive thinking from the langchaingo fork
Adaptive thinking (Claude Opus 4.7+) is now implemented natively in the
vxcontrol/langchaingo fork (v0.1.14-update.6): the anthropic and bedrock clients
emit thinking.type=adaptive + output_config.effort and omit the sampling params
adaptive models reject. PentAGI no longer patches the serialized request body.

- pconfig.PrepareAdaptiveCallOptions appends llms.WithAdaptiveReasoning; the
  ctx-effort plumbing (WithAdaptiveEffort/AdaptiveEffortFromContext) is removed.
- Delete the bedrock smithy Build middleware and anthropic http RoundTripper
  (adaptive_thinking.go + tests in both packages) that rewrote the request body.
- Bump the langchaingo require to v0.1.14-update.6; refresh llms_how_to.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 00:47:32 +07:00
Sergey KozyrenkoandClaude Opus 4.8 8670ce4b1e fix(docker): add pnpm global bin to PATH in frontend build stage
pnpm 11 installs globally-added binaries to $PNPM_HOME/bin and treats that
directory being absent from PATH as a hard error, so `pnpm add -g license-checker`
in the frontend-compiler stage failed ("global bin directory is not in PATH")
whenever the frontend layer cache was invalidated. Prepend $PNPM_HOME/bin to PATH.

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 19:12:04 +07:00
Sergey KozyrenkoandClaude Opus 4.8 28036f652c refactor(providers): table-driven provider registry
The provider controller repeated every provider type across five hand-written
blocks (default-config wiring, key-gated instantiation, GetProvider fallback,
NewProvider, buildProviderFromConfig) plus the API-layer Valid() whitelist.

Introduce pkg/providers/registry.go: a providerRegistry table whose entries hold
the per-type constructors and credential gating, with small adapter helpers
(ignoreConfig/fromData) absorbing the signature variance (bedrock/ollama/custom
take *config.Config; the rest don't). The controller now wires and looks up
providers in loops over the table. Valid() validates against the new canonical
provider.AllProviderTypes list (no heavyweight import, no cycle).

Adding a provider's backend wiring drops from ~6 edits across these functions to
one registry entry. providers.go shrinks ~300 lines; behavior is unchanged
(same exported funcs, same gating) and all provider/server/graph tests stay green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 19:07:11 +07:00