Remove/Purge/FactoryReset failed with "file does not exist" for stacks
that were never extracted (no docker-compose-*.yml). Now such stacks are
skipped instead of aborting the whole operation.
- Add composeFileExists check before destructive compose commands
- Add isDestructiveComposeOperation helper
Problem:
In the installer processor, post-operation state refresh failures from
checker.GatherUpdatesInfo were silently discarded, so apply/install/
update/remove/purge could report success even when the refresh failed.
Root cause:
Two distinct shadowing/fall-through patterns wrapped the error into a
variable that was never returned:
- In the deferred closures of applyChanges/install/update/purge, the
inner `if err := GatherUpdatesInfo(ctx); err != nil` shadowed the
named return `err`, so `err = fmt.Errorf(...)` assigned the shadow and
the wrap never reached the `sendCompletion(stack, err)` defer.
- In the ProductStackWorker cases of remove/purge, the block-local err
was wrapped but the case fell through without returning, while every
sibling check in the same switch returns on error.
Solution:
- Deferred closures: wrap into a `gatherErr` local and assign the named
return `err`, so the failure propagates to sendCompletion.
- Switch cases: `return` the wrapped error, matching the sibling checks.
Testing:
go build ./cmd/installer/... -> OK
go vet ./cmd/installer/processor/... -> OK
go test ./cmd/installer/processor/... -> ok
golangci-lint (ineffassign) on logic.go -> clean
Fixes#312
Root cause:
During flow creation the first LLM call selects the primary Docker
image. When the configured LLM backend is unsupported or returns 404,
prv.Call fails, but the wrapped error read "failed to get primary
docker image". Users repeatedly interpreted this as a Docker problem
(see #312, #309, #203) and debugged Docker instead of their LLM
provider configuration.
Solution:
Reword the wrapped error to "failed to select primary docker image
via llm call" so the LLM provider is identified as the failing
component while still mentioning the image selection step.
Testing:
go build ./pkg/providers/ -> OK
Added new test cases to validate containment barriers in the `ResolvePulledStagedTarget` and `ZipRelativePaths` functions, ensuring that paths escaping the designated directories are properly rejected. Updated the `knowledge_test.go` to include scenarios for handling nil embedder and error propagation during document creation, improving overall test coverage and robustness.
Enhanced the flowWorker's task processing by introducing a cancellable context for task execution. This change ensures that tasks can be properly cancelled without reporting false success states. The `runTask` method has been refactored to utilize a new `execTask` method, which centralizes task execution logic and maintains context integrity. This update improves flow control and error handling during task creation and execution.
Updated the Qwen agent configuration to include `extra_body` parameters for thinking control across various models. Added `enable_thinking` and `preserve_thinking` options for reasoning agents, while utility agents have `enable_thinking` set to false. Adjusted the Qwen client initialization to support these new configurations. Updated test report to reflect changes in success rates and latencies.
Lint warnings 44 → 25. The remaining 14 `any` warnings sit in
settings-provider.tsx (provider config form + test results dialog,
needs domain knowledge — deferred per scope). The other 11 warnings
are React Compiler "Compilation Skipped" notices, not code issues.
- lib/log.ts: any → unknown (5) — logger accepts arbitrary input
- shared/markdown.tsx: typed the recursive ReactNode walker with a
`hasChildrenProp` type guard around `isValidElement`; the components
map now uses react-markdown's `Components` type so `code` / `pre`
overrides infer their props (5)
- settings-prompt.tsx: `ControllerProps.control: Control<FieldValues>`,
`field: ControllerRenderProps<…>`, validation result typed against
`ValidatePromptMutation['validatePrompt']`, dropped a stray
`as any` on `removeEventListener` options (4)
- settings-prompts.tsx: row sub-component params typed via
`Row<T>` from @tanstack/react-table (2)
- settings-providers.tsx: provider icon map typed against
`SVGProps<SVGSVGElement>`, row sub-component via `Row<Provider>`,
recursive `getFields(obj)` narrowed from `any` to `unknown` with a
single boundary cast for `Object.entries` (3)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Baseline 8/10 fail (80% flake) → 20/20 pass. Root cause was `user.type`
racing the Radix Sheet focus trap, not the `useDebounce` macrotask
theory from the prior investigation: with `defaultOpen=true` Radix sets
`body { pointer-events: none }`, swallowing user-event's internal click,
so keystrokes route to whatever element currently holds focus (the
SheetContent close button) instead of the input. `inputValue` stays
empty and the listbox never narrows.
`fireEvent.change` mirrors the sidestep already used in the
URL-filter AND test in the same file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Followed up on the "no war-story comments" rule (memory file added a
few commits ago, then promptly broken in the X-button addition).
Pass through every comment with the test "remove it — does a senior
reading the code for the first time understand less?".
Removed:
- "Same two-cell shape as DataTableFilter, see its docstring" — cross-
file rationale belongs in commit history.
- "Hint shown over the collapsed trigger. Kbd self-themes…" — Kbd
configuration detail, not part of this component's contract.
- "Mirror size='sm' button geometry used by HeaderButton" — micro
implementation detail.
- "Collapsed trigger sits flush… has-[>button]:ml-[-0.45rem] handles
the analogous adjustment" — CSS quirk story.
- "The trigger is a real <button> — never a <div> with onClick" —
generic HTML/a11y factoid, not specific to this file.
- "The component is controlled — parent owns searchQuery" — visible
from the props shape.
- "Belt-and-braces for programmatic clears" — defensive-style war
language, replaced with one line stating the effect's purpose.
- "Initialise expanded if the parent already has a non-empty query" —
duplicates `useState(() => searchQuery.trim().length > 0)`.
Kept (in single-line / sub-3-line form):
- `COMMIT_DEBOUNCE_MS` shares timing with DataTableFilter.
- `External → local sync` effect intent.
- `rAF` before focus so motion's transform has started.
- `handleInputBlur` reads `localValue`, not the prop (subtle invariant).
- Auto-collapse effect purpose.
- `inputRef.current?.focus()` in `handleClear` (otherwise auto-collapse
fires and the field disappears under the user).
- Top-level JSDoc shrunk from 28 lines to 5 — what it does plus the
two equivalent clear paths.
Behaviour unchanged. 15/15 tests still pass, lint clean. File: 340 → 260
lines (-24%).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`DataTableFilter` already ships a conditional trailing `<X>` clear
button; `InputSearch` (the collapsible variant used for /knowledges
semantic search and the same shape sitting in flow detail searches)
only offered keyboard Esc to clear. Unify so every search field across
the app has the same shape.
Implementation:
- Extract a shared `handleClear` used by both the new X button and the
first-Esc branch. The path cancels any pending debounce, emits `''`
upstream, and explicitly returns focus to the input — this matches
the Esc-clear path (where focus is already on the input) and
side-steps the programmatic-clear collapse effect, so the field
stays open and ready for fresh typing.
- Render the X only while `isExpanded && localValue` so it can never
appear on a collapsed/empty field. `aria-label` is contextual:
"Clear search knowledge documents" / "Clear semantic search" / etc.,
derived from the host page's `ariaLabel`.
- Update the JSDoc to describe both clear entry points.
Tests (input-search.test.tsx, 15 total — 10 baseline + 5 new):
- absent on empty mount
- present on deep-linked non-empty mount
- shows/hides as the user types and clears
- click clears value, emits `''` upstream, keeps focus on input,
prevents the trigger from re-appearing
- click mid-typing drops any pending debounce — no leaked emit
Browser verified on /knowledges semantic search:
beforeClear { value: 'jwt', url: '?qs=jwt', xPresent: true }
afterClear { value: '', url: '', xPresent: false, focusOnInput: true }
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the `chore(frontend): upgrade graphql-codegen toolchain` upgrade
(8a3d4e9) the generated `src/graphql/types.ts` carried every base type
twice — once as `export enum Foo` (from `typescript`) and once as
`export type Foo = 'a' | 'b'` (from `typescript-operations`). Vite's
dependency scan failed on the duplicate declarations with 29
`Identifier 'X' has already been declared` parse errors, which made
the dev server occasionally die on cold start.
Root cause: this was a stale v5-style config running on v6 plugins.
The v6 migration guide is explicit — `preResolveTypes` has been
removed, and `typescript-operations@6` now emits base types itself,
so the canonical setup is `plugins: ['typescript-operations']` rather
than `['typescript', 'typescript-operations']`. Keeping both meant the
two plugins independently emitted the full type set in different
shapes.
Bisect confirmed the split: `typescript` alone → 1327 lines, no
duplicates; `typescript-operations` alone → 1311 lines, no
duplicates; the pair → 2639 lines with 29 duplicate type names.
With `typescript-react-apollo` also in the mix the regenerated file
ballooned to 8819 lines, half of which was the shadow set.
Fix:
- Drop the `typescript` plugin from the codegen config.
- Drop `preResolveTypes: true` (removed in v6).
- Add `enumType: 'native'` so the call sites that use enum-style
access (`KnowledgeDocType.Answer`, `AgentConfigType.Adviser`, …)
keep compiling — without it, v6 defaults to string-literal unions.
Result:
- `src/graphql/types.ts` shrinks 8819 → 7586 lines, zero duplicate
declarations.
- `KnowledgeDocType`, `AgentConfigType`, etc. remain real TS `enum`s
so `KnowledgeDocType.Answer`-style access keeps working.
- `pnpm run build` clean, lint clean, 531/531 tests passing.
- Vite dev start no longer prints `[PARSE_ERROR] Identifier X has
already been declared`. `/knowledges` (the page that exercises
enum access) loads with 10 rows and zero console errors.
Migration guide:
https://the-guild.dev/graphql/codegen/docs/migration/operations-and-client-preset-from-5-0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reviewing 17451e7: the dedupe was added in reaction to a runtime
`Cannot read properties of null (reading 'useRef')` that I attributed
to dual React copies via pnpm hoist. But I applied two changes at
once — clearing `node_modules/.vite` and adding dedupe — and never
isolated which one actually fixed it.
Empirical re-check just now: production build, dev server, and
the affected page all work with dedupe removed. The real cause was
the stale Vite pre-bundle (`.vite/deps/`), which doesn't auto-rebuild
after `pnpm add` in a running dev server. Cache clear alone fixes it.
That makes the dedupe defensive hygiene at best, and the 7-line
war-story comment alongside it actively misleads — it pins a problem
on a mechanism that wasn't the cause. Removing both keeps the config
honest. QA report updated to record the misdiagnosis as a process
note instead of an architectural rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run `pnpm prettier:fix` brought three files into compliance:
- data-table.test.tsx — new race-scenario suite added in the previous
commit; prettier preferred a slightly different multi-line shape for a
couple of `render(<FilterHost />)` calls.
- input-search.test.tsx — new file in the previous commit; same minor
multi-line reshaping for `render(<SearchHost />)`.
- settings-api-tokens.tsx — pre-existing single-line nested ternary that
predates this branch, now collapsed to one line per prettier.
No behavior change. Format-only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After landing the imperative-timer fix and extracting `useDebouncedCallback`
into our own hook, the use-site count climbed past three and the case for
a single, battle-tested contract beat the case for in-tree code. Drops
both our `useDebouncedValue` and `useDebouncedCallback` and routes every
caller through `use-debounce@10.1.1` (1M+ weekly, ~3KB gzip).
API mapping at each use-site:
- value debounce: `useDebouncedValue(v, ms)` → `const [v] = useDebounce(v, ms)`
- callback debounce: `useDebouncedCallback(fn, ms)` → identical signature,
same `.cancel()`/`.isPending()` semantics we relied on, plus `.flush()`
and leading/trailing/maxWait options we now get for free.
Files touched (7 use-sites): data-table.tsx, input-search.tsx,
use-table-state.ts, use-table-query-filter.ts, knowledges-provider.tsx,
use-flow-files-search.ts, use-detail-navigation.ts.
pnpm + Vite gotcha: pnpm hoists into `.pnpm/<name>@<ver>/`, so any
transitive `import React from 'react'` from a third-party package can
resolve to its own physical copy. With one path through our code and
another through use-debounce, the runtime crashed at `useRef` with
"Cannot read properties of null (reading 'useRef')". Added
`resolve.dedupe: ['react', 'react-dom']` in vite.config.ts so the
bundle ships exactly one React.
Tests:
- new input-search.test.tsx (10 tests) covers collapsed/expanded
presentation, expand-on-click + rAF focus, burst-typing coalesce,
Esc-clear mid-debounce, two-stage Esc semantics, external-source-wins
race, Ctrl+F / Cmd+F global hotkey expansion.
- data-table.test.tsx +5 tests: deep-link initial filterValue,
6-keystroke burst coalesce, unmount-mid-debounce cleanup, two
independent DataTables on one page (settings/prompts shape),
back-button-style external clear, three-round type/clear stress.
- full suite: 541/541 passing, lint 0 errors.
Manual smoke via Chrome DevTools MCP on every page that mounts a
filter input — /flows, /templates, /settings/api-tokens,
/settings/prompts (both tables independent), /knowledges (filter
+ semantic search hitting the server through ?qs=). All scenarios
collapse to a single `[{v="", u=""}]` transition; no URL re-emit
after X, no leftover timer after Esc, no cross-table leakage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous iteration moved DataTableFilter / InputSearch off
`useDebouncedValue` and onto an inline `setTimeout` + `pendingTimerRef`
+ `cancelPendingEmit` dance. That fixed all four race scenarios but
left imperative scheduling living inside components — a code smell
flagged in review. After surveying the React 19.2 surface area
(`useDeferredValue` is for *render* priority, not side-effect throttling;
`useTransition` doesn't apply to controlled inputs; `useEffectEvent` is
restricted to handlers called from within Effects) and `use-debounce`'s
API, the right shape is a small, dependency-free hook.
`useDebouncedCallback(fn, delayMs)` returns a stable callable with
`.cancel()` / `.isPending()`. Timer state lives in a closure variable
inside a `useState` lazy initialiser, so:
- the returned identity never changes (safe in effect deps and memoised
children);
- there are no refs to read during render (no React Compiler complaints);
- `fn` and `delayMs` go through `useLatestRef` so the dispatched call
always sees the freshest closure — inline arrows in parents are free.
DataTableFilter / InputSearch shrink to one useState + one useRef
(`lastEmittedReference` distinguishes our own router round-trip from a
true external change) + one useEffect (external sync). All four
regression tests added in the previous commit still pass; five new unit
tests pin `useDebouncedCallback` semantics (burst coalescing, cancel,
isPending, latest-closure read, unmount cleanup). Browser repro on
/flows for both X-after-flush and X-before-flush scenarios: a single
transition `v="" u=""`, no resurrection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously DataTableFilter (and InputSearch) layered four cells holding
the same logical value — `localValue` (sync useState), `debouncedValue`
(async useState inside `useDebouncedValue`), `lastEmittedReference`
(ref), and the parent's `query` prop (async round-trip) — and stitched
them together with two opposing useEffect's. The async `debouncedValue`
cell could be read out of phase from any other effect, which leaked
through as four distinct races: X clear after debounce flush (URL/state
snap back ~50–80 ms), X clear *before* debounce flush (pending timer
fires after clear), external `query` change mid-debounce (pending
typed value clobbers external), and tail-end character loss on fast
typing across the round-trip boundary.
Drop the extra storage cell. The debounce is now an imperative
`pendingTimerReference` mutated only by `handleChange`,
`cancelPendingEmit`, and the external-sync effect. All outbound
emission goes through one `emit(next)` function that synchronously
cancels any pending timer before sending. `handleClear` → `emit('')`.
External `query` change → cancel pending + accept. Unmount → cancel.
`onQueryChange` lives behind `useLatestRef` so effect deps stay
stable across parent re-renders.
Four regression tests in data-table.test.tsx pin all four scenarios.
Verified by rolling back the fix temporarily — the X-clear race test
fails with the exact `emitted = ['', 'alpha', '']` from the original
browser repro. With the fix in place all four pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DataTableFilter / InputSearch listed `onQueryChange` (an inline arrow from
the parent — fresh reference per render) in the emit effect's deps. After
the X-clear handler set `lastEmitted=''` and propagated upstream, the
parent re-rendered and handed back a new handler reference, re-running
the effect while `debouncedValue` was still the pre-clear value
(`useState` inside `useDebouncedValue` hadn't yet been updated by its
trailing `setTimeout`). The effect saw `debouncedValue='alpha' !==
lastEmitted=''` and re-emitted `'alpha'`, snapping URL/state back for
~50–80 ms. JSDOM regression captured `emitted = ['', 'alpha', '']`.
Stash the handler in `useLatestRef` and drop it from deps so the effect
fires only when the observed value actually changes. Same fix in
InputSearch's Esc-clear path. New test in data-table.test.tsx pins the
behavior — fails with the previous code (`['', 'alpha', '']`), passes
now (`['', '']`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue #310 asks how to provide a Google Vertex AI API key in .env for
Anthropic Claude. PentAGI currently has no dedicated Vertex AI provider
path in code: backend/pkg/config and backend/cmd/installer do not read
VERTEX_API_KEY, GOOGLE_APPLICATION_CREDENTIALS, or any vertex_ai
variable. The supported routes for Claude today are direct Anthropic
(ANTHROPIC_API_KEY / ANTHROPIC_SERVER_URL) and AWS Bedrock (BEDROCK_*).
Document this explicitly so users do not assume a hidden Vertex AI
configuration path exists:
- README.md: add a NOTE callout inside the Anthropic Provider
Configuration section listing the supported routes and pointing
users who need Vertex AI today at the OpenAI-compatible custom LLM
provider path (LLM_SERVER_URL / LLM_SERVER_KEY / LLM_SERVER_MODEL)
fronted by a translating gateway, with a caveat that reliability
depends on the gateway.
- backend/docs/config.md: add a matching Note paragraph under the
Anthropic section that points at the AWS Bedrock and custom LLM
provider sections, and states that no VERTEX_API_KEY or
GOOGLE_APPLICATION_CREDENTIALS variable is wired into provider
initialization today.
Docs-only change. No runtime Go code, no installer behavior, no
generated files, no new environment variables. All env var names cited
in the new text already exist in the current PentAGI .env.example,
backend/pkg/config, and backend/cmd/installer.
DeepSeek V4 thinking mode defaults to enabled on both deepseek-v4-flash
and deepseek-v4-pro; non-thinking behavior requires an explicit toggle.
Per official docs, in thinking mode temperature/top_p/presence_penalty/
frequency_penalty are ignored, so the existing Flash role sampling knobs
would have been silently no-ops without the toggle.
Add extra_body.thinking.type=disabled to the five non-thinking Flash
roles so deepseek-v4-flash actually runs in non-thinking mode and
honors the role's temperature/top_p settings:
- simple, simple_json, adviser, searcher, enricher
Pro roles (primary_agent, assistant, generator, refiner, reflector,
coder, installer, pentester) intentionally keep thinking enabled (the
V4 default) for reasoning, tool-use, and security analysis.
PentAGI provider config already supports extra_body as a first-class
yaml field on AgentConfig and forwards it through openai.WithExtraBody,
which the vxcontrol langchaingo fork serializes at the top level of the
Chat Completions request - the same pattern Kimi uses for tool_choice.
No code, schema, or LiteLLM prefix changes required.
Touches:
- backend/pkg/providers/deepseek/config.yml (embedded production config)
- examples/configs/deepseek.provider.yml (user-facing example)
No change to role-to-model mapping, model metadata, pricing, README
wording, LiteLLM prefix, unrelated providers, lifecycle, queues, or
installer flow.
- Update model descriptions to reflect V4 1M context window (up to 384K output)
instead of legacy 128K wording in models.yml and README.
- Split Flash and Pro pricing per official DeepSeek API docs:
- deepseek-v4-flash: input 0.14 / output 0.28 / cache_hit 0.0028 per 1M tokens
- deepseek-v4-pro: input 0.435 / output 0.87 / cache_hit 0.003625 per 1M tokens
- Apply per-role price split across all 13 role configs in both the embedded
config.yml and the user-facing examples/configs/deepseek.provider.yml.
- Replace stale "cache pricing is 10% of input cost" claim in the README,
which no longer holds for either V4 model.
- No change to LiteLLM prefix behavior, role-to-model mapping, lifecycle,
queues, GraphQL schema, migrations, frontend, or installer flow.
- README: align supported-models intro with the local convention used
by every other provider section ("Models marked with `*` are used
in default configuration"), so the asterisk on each model ID has a
near-by explanation.
- Installer help (`LLMFormDeepSeekHelp`): swap the legacy
"DeepSeek-Chat" / "DeepSeek-Reasoner" bullets in "Default PentAGI
Models" for the current `deepseek-v4-flash` / `deepseek-v4-pro`
defaults so the wizard guidance matches the bundled config.
No code, schema, or LiteLLM prefix behavior changes.
The JSDoc blocks added in afb9b63 mostly paraphrased the 12 lines of
code below them. Reviewing line-by-line:
- "two axes" / "filter active vs no filter" / per-state copy — code
reads identically and is right next to the comment.
- "kept inline because tightly coupled to effectiveQuery" — every
helper in this file is inline, that's the existing convention.
- "colSpan={columns.length} parent" — the one call site is 10 lines
below in the same file.
- "for callers that have not migrated yet" — `empty?:` being optional
already says this.
- "so existing tests stay green" — the test itself documents the
contract; deleting the fallback fails it loudly.
Kept the one piece TypeScript can't express: a single-line hint that
`entityName` wants the plural lowercase form so the generated copy
reads naturally mid-sentence. That shows up in IDE autocomplete and
saves a "Flows" → "No Flows match" first-try mistake.
No behaviour change. 28/28 DataTable tests pass, 0 ESLint errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves QA report observation O9. Every list page used to render the
bare "No results." label when its DataTable had no rows — both for a
truly empty dataset and when a filter narrowed the result set to zero.
The Resources page already had a better pattern (shadcn <Empty> block
with a filter-aware copy that cited the query), so propagate it to the
shared DataTable.
API
- New optional `empty?: { entityName?: string }` prop on `DataTable`.
- When `entityName` is provided, the body cell renders an `<Empty>`
block:
filter active → Search icon + "No matches" + "No <entity> match
<code>{query}</code>. Try a different query."
no filter → Inbox icon + "No <entity> yet"
and the pagination footer mirrors the entity name ("No <entity>"
instead of "No results").
- When `entityName` is omitted, the existing bare "No results." cell
and "No results" footer are kept verbatim — preserves backward-compat
for call sites that have not migrated and for the existing test that
asserts the legacy copy.
Per-page wiring
Plural lowercase names match how a screen reader reads them mid-
sentence:
flows → "flows"
templates → "templates"
knowledges → "knowledge documents"
/settings/providers → "providers"
/settings/api-tokens → "API tokens"
/settings/prompts ×2 → "agent prompts" / "tool prompts"
Tests
- Existing `'No results.'` assertion stays valid: it renders a
DataTable without the `empty` prop, exercising the legacy fallback
path.
- Added 3 new cases under `DataTable — empty results`:
1. legacy fallback when `empty.entityName` is omitted
2. data-empty "No <entity> yet" when no filter is active
3. filter-empty "No <entity> match <query>. Try a different
query." with the query cited verbatim
Verified
- 0 ESLint errors, 44 pre-existing warnings.
- 511/511 vitest pass in sequential mode and when the affected files
run isolated. A pre-existing concurrency flake in
`detail-navigation-sheet.test.tsx` (`typing narrows the listbox
immediately when searchDebounceMs=0`) occasionally surfaces under
vitest's default `--file-parallelism` — unrelated to this change,
passes deterministically with `--no-file-parallelism` and when the
file is run in isolation.
- Browser sweep across 7 tables (`/flows`, `/templates`,
`/knowledges`, `/settings/{providers,api-tokens,prompts(×2)}`) all
render the new copy with `?q=ZZZZZZ`.
References
- shadcn `Empty` component (already vendored in
`components/ui/empty.tsx`)
- shadcn.io `tables-empty` block FAQ (best-practice: distinguish
filter-empty vs data-empty via `hasActiveFilters` boolean, ground
the empty cell in a Lucide icon, surface a "next-step" hint).
- Existing precedent in `pages/resources/resources.tsx` —
`No resources match <code>{q}</code>. Try a different query.`
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverts the O10 part of commit 04702c3. The original change renamed
the provider clone URL parameter from `?id=` to `?clone=`. On second
look the rename solved no real problem:
- No security issue: PentAGI is a single-tenant local admin tool, so
there is no cross-tenant info leak from a shared URL.
- No bug: the form behaviour was identical before and after.
- No UX impact: the URL is generated by the "Clone" action, not typed
by users; the bare `?id=` is also explicit enough in context
(settings-provider.tsx gates it on `isNew`, so the meaning is
"source provider to clone from").
What the rename did cost: it broke backward compatibility with any
bookmarks/links saved in `?id=` form. Restore `?id=` to remove the
needless churn.
The rest of commit 04702c3 (O7 dialog case, O8 "API Tokens" unify,
O11 button label + JSDoc, O12 italic placeholder) addressed real
inconsistencies and stays in place.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves QA report observations O7, O8, O10, O11, O12.
O7 — Confirm dialog title case
The Resources page rendered "Delete Directory" / "Delete Resource" in
Title Case while every other delete confirm (template, knowledge,
token) used Sentence case ("Delete template", "Delete token"). Aligned
the resources copy to "Delete directory" / "Delete resource".
O8 — /settings/api-tokens identity mismatch
Three names referred to the same page:
- sidebar link / h1: "PentAGI API"
- tab title (route registry): "API tokens"
- menuItems entry: "PentAGI API"
Unified everything to "API Tokens". Also removed the now-redundant
api-tokens special case in settings-layout's SettingsHeader (it always
returned the same value the menuItems lookup would have produced).
O10 — Provider Clone URL parameter
`/settings/providers/new?id=5` collided semantically with the rest of
the codebase, where `?id=` usually means "this is the record being
edited". Renamed to `?clone=5` so the intent is explicit when the URL
is shared or shows up in logs. Updated the reader in
settings-provider.tsx (`formQueryParams.id` → `formQueryParams.clone`)
to match.
O11 — "Create Provider" button accessibility
The button is a DropdownMenuTrigger that opens a provider-type chooser
before navigating to /new — not a submit button. Sighted users get
this from `<ChevronDown />` and Radix's `aria-haspopup="menu"`, but
the bare visible text "Create Provider" reads as a confirmation
action to screen readers. Added `aria-label="Create provider — choose
type"` and a block comment explaining the pattern so future
maintainers don't try to "fix" the dropdown into a direct submit.
O12 — `(unnamed)` placeholder rendering
Tokens with empty name rendered as literal `(unnamed)` with
text-muted-foreground only. Added `font-normal italic` so the
placeholder reads as missing data rather than a real name (the column
otherwise uses font-medium).
Verified: 0 ESLint errors, 508/508 tests pass.
Browser regression sweep across 9 pages (/dashboard, /flows,
/templates, /knowledges, /resources, /flows/:id, /settings/{providers,
api-tokens,prompts}) still returns 0 icon-only buttons without an
accessible name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The DeepSeek provider config still defaulted to the legacy
`deepseek-chat` and `deepseek-reasoner` model names, which the
upstream DeepSeek API has announced for deprecation on 2026-07-24.
A first-run install therefore breaks once the legacy names are
removed.
Swap the defaults to the current DeepSeek V4 family:
- non-thinking roles use `deepseek-v4-flash`
- reasoning-heavy roles use `deepseek-v4-pro`
The change is limited to the embedded `config.yml` / `models.yml`
inside `backend/pkg/providers/deepseek`, the matching example at
`examples/configs/deepseek.provider.yml`, the `DeepSeekAgentModel`
fallback constant in `deepseek.go`, and three doc references
(README.md, backend/docs/config.md, backend/docs/llms_how_to.md)
plus one installer help string in
`backend/cmd/installer/wizard/locale/locale.go`. LiteLLM prefix
behavior is untouched.
Resolves QA report BUG #16 and BUG #17.
BUG #16 — /settings/prompts/<invalid> tab title fallback
formatPromptId() blindly capitalized whatever the URL passed in, so a
nonexistent id like /settings/prompts/99999 produced the tab title
"99999 — PentAGI" (and an "Edit Prompt" header with a literal "99999"
in the error message). Valid prompt ids are camelCase identifiers
(e.g. "agentSelector"). Reject anything that does not match
/^[a-z][a-zA-Z]*$/ and fall back to a generic "Prompt", so the title
becomes "Prompt — PentAGI" for invalid ids; valid ids keep their
existing "Agent Selector — PentAGI" rendering.
BUG #17 — Chrome DevTools "form field needs id/name" + label-for
Two separate issues on /flows and /flows/🆔
1) Four hidden <input type="file"> elements driven programmatically
by use-resources-upload / use-flow-files-upload / flow-form lacked
`name`. Added descriptive names (`resource-upload`, `flow-file-upload`,
`flow-form-attachment`) plus `aria-hidden="true"` and `tabIndex={-1}`
— these inputs are never user-focused (the button triggers .click()
via a ref), so excluding them from the accessibility tree is correct.
2) <FormControl><Switch/></FormControl> + <FormLabel> for the "Use Agents"
field on the assistant flow form was rendered without a wrapping
<FormItem>, so useFormField() read the default empty context and
formItemId became "undefined-form-item". Chrome then flagged "label
for=undefined-form-item" as an orphan. Wrapped the cluster in
<FormItem className="flex flex-row items-center gap-0"> so useId()
generates a real id and the label binds to the Switch trigger
button correctly.
Verified: 0 ESLint errors, 508/508 tests pass.
On /flows the page now has 0 form fields without id/name.
On /flows/:id the only remaining unlabeled fields are third-party
internals we do not own (xterm.js helper textarea, Radix Switch
primitive checkbox, react-textarea-autosize ghost) — Chrome's count
dropped accordingly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Page-level row-action triggers were split between two conventions:
- 4 pages used <span className="sr-only">Open menu</span> inside <Button>
- 3 pages (touched in commit 9eee2b4) followed the same pattern for consistency
Project has no i18n pipeline that benefits from sr-only text-node
translation, and the rest of the codebase already uses aria-label for
icon-only buttons (pagination, forms, theme tabs, toggles). Unify all
6 page-level row-action triggers to aria-label="Open menu":
- pages/flows/flows.tsx
- pages/knowledges/knowledges.tsx
- pages/templates/templates.tsx
- pages/settings/settings-providers.tsx
- pages/settings/settings-prompts.tsx (×2 — agent + tool tables)
- pages/settings/settings-api-tokens.tsx
shadcn UI library components (sheet, dialog, sidebar, breadcrumb) keep
their sr-only spans untouched — that is upstream shadcn convention and
not in scope here.
Verified: 0 ESLint errors, 508/508 tests pass, browser sweep across
6 list pages confirms aria-label="Open menu" rendered correctly and
no icon-only buttons remain without an accessible name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The clamping effect compared the URL to a `safePageIndex` derived from the
internal mirror. After popstate the URL refreshes one render before the
mirror catches up; after a page-size change the mirror refreshes one
render before the URL — so each side echoed the stale source back over
the fresh one, oscillating `/flows` ↔ `/flows?page=2` indefinitely.
Now clamping only fires when both URL and mirror agree out-of-range, and
goes through a new `setPage(idx, { replace: true })` opt-in so a typed
`?page=999` no longer leaves a history entry the next back-press would
re-trigger.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Radix ScrollArea wraps children in a display:table viewport that grows
to intrinsic content, so renderItem rows with white-space:nowrap text
pushed each option past the sheet's right edge. Swap to plain
overflow-y-auto and add min-w-0 to the flex chain so truncate finally
takes effect on long flow / template / knowledge titles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strip section banners, "Helper function to X" labels above self-describing
identifiers, "GraphQL query/mutation/subscription" markers, and the
copy-pasted `// Create debounced function` / `// Filter by search` blocks
that lined every flow feature. Where a comment hid a non-obvious reason
(history blocker pattern, watch() vs getValues(), CJK font fallbacks),
rewrite it as a WHY comment instead of dropping it.
Net diff: -571/+78 across 57 files. Lint clean (pre-existing `any`
warnings unchanged), `tsc` errors unchanged, 506/506 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Terser strips the inner function-expression name in `apolloTitle()` during
the production build, leaving an anonymous `function({params})` whose
`fn.name === ''`. The old PascalCase heuristic in `DocumentTitle` then
misclassified the component as a `(params) => string` resolver, called it
with raw `match.params`, and crashed `variables({flowId})` with
"Cannot destructure property 'flowId' of undefined" on every /flows/:id
view in production.
Attach an explicit `Symbol.for('pentagi.apolloTitle')` marker via
`Object.assign` in the factory and expose `isApolloTitle()` as the only
public guard. Symbol-keyed properties survive Terser regardless of
`mangle.properties` settings, and `Symbol.for` keeps the marker stable
across accidental double-bundles (workers, SSR boundaries). The
`document-title` consumer now imports the guard, not the symbol, so the
marker stays an implementation detail of the apollo-title module.
Cover the regression with a dedicated `apollo-title.test.tsx` that
simulates name stripping via `Object.defineProperty(fn, 'name', { value: '' })`
and asserts the guard still recognizes the component.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HTML's default `type` for a `<button>` inside a `<form>` is `submit`,
so every Button without an explicit `type` silently posted the parent
form on click — repro on knowledge detail: clicking the
`DetailNavigationButtons` Prev/Position/Next cluster (or the Anonymize
button) fired the form's submit handler and kicked off an
`updateKnowledge` mutation that 502'd. Mirrors the existing convention
in `InputGroupButton`. `asChild` still defers to the consumer's
element; the one genuine submit caller (`FormSubmitButton`) is
explicit. Audit confirms no other Button uses default-submit
intentionally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`useDetailNavigation` now owns a controllable local search (debounced,
ANDed with the URL `?q=` filter) and `DetailNavigationSheet` surfaces it
as a header input by default (`hasSearch={true}`). Sheet auto-focus no
longer steals focus from the search input when `filteredItems` shrinks
on a debounce flush — gate the option-focus effect on `document.activeElement`
so typing past the boundary keeps the caret in place. ArrowDown from the
input synchronously promotes focus into the listbox.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>