Commit Graph
226 Commits
Author SHA1 Message Date
Sergey KozyrenkoandClaude Opus 4.7 35e077fd8b fix(frontend): always refetch knowledgeDocuments on provider remount
The Apollo client's global nextFetchPolicy is 'cache-first', so returning
to /knowledges via SPA navigation would serve potentially stale cache.
Subscriptions are now scoped to /knowledges* and detach on other pages,
so changes from AI agents during flow runs would be missed until a full
reload. Override nextFetchPolicy to keep cache-and-network for this query.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:26:44 +07:00
Sergey KozyrenkoandClaude Opus 4.7 258b677c01 fix(frontend): scope KnowledgesProvider to /knowledges routes
KnowledgesProvider sat in RootLayout, so knowledgeDocuments (with full
content) and its 3 subscriptions fired on every authenticated page —
a ~2.1 MB payload on /flows, /dashboard, etc. All consumers of
useKnowledges live under /knowledges*, so wrap those routes in a
dedicated layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:45:48 +07:00
Sergey KozyrenkoandClaude Opus 4.7 cc6b525f57 fix(frontend): cap DataTable filter input at 200 chars
A paste of multi-KB content into the filter would land in the URL
verbatim (`?q=` plus the raw blob), which exceeds the practical
reverse-proxy limit (~2–4 KB) and breaks the share-link experience.
The user-facing entry point is a single `<input>`, so a DOM-level
`maxLength` is sufficient — it truncates both typing and paste before
the value ever reaches React state, the URL, or localStorage. 200 chars
is well above any realistic search term while leaving plenty of
headroom for non-ASCII expansion under percent-encoding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 21:41:42 +07:00
Sergey KozyrenkoandClaude Opus 4.7 022e8f3df0 refactor(frontend): unify table URL state under useTableState
The split `useTableQueryFilter` + `usePagination` pair had a latent batching
race: react-router v6 feeds every functional `setSearchParams(updater)`
queued in a single React tick the same pre-batch snapshot, so a `setFilter`
+ `setPage` issued from the same event handler (e.g. a debounced filter
commit landing alongside a paging click) would collapse — the second
write erased the first and `q` disappeared from the URL. The earlier
window.location workaround papered over the symptom for the typical case
but didn't remove the underlying possibility.

Replace both hooks with `useTableState`, which owns filter + pageIndex +
storage roundtrip together and routes every URL write through a single
microtask-coalesced `update(patch)`. When multiple `update` calls fire in
the same tick — `setFilter` and `setPage`, two synchronous handlers, an
effect and a click — they merge into one navigation rather than racing.
Replace conflicts resolve in favour of push, so intentional history
entries (paging) survive coalescence with replace-only updates (filter
typing). The `MemoryRouter` fallback (latest snapshot via ref) is kept
only as a defensive read; the coalescence itself makes it redundant.

A regression test (`two top-level updaters firing in the same tick keep
both params`) locks this in: the previous behaviour failed it, the new
implementation passes it without touching `window.location`.

Read-only siblings stay on `useTableQueryFilterReader` — detail pages
don't write the URL, so no race surface to remove. `usePagination`
deleted entirely; its callers migrated to `useTableState`.

474/474 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 21:26:14 +07:00
Sergey KozyrenkoandClaude Opus 4.7 019ddcb1a7 fix(frontend): preserve URL params across batched setSearchParams updates
When a debounced filter commit and a paging button click land in the same
React tick, react-router v6 feeds the functional updater of every queued
`setSearchParams` call the same pre-batch snapshot. The second write
overwrites the first instead of stacking, so `setFilter('foo')` followed
by `setPage(5)` collapses to `?page=6` and `q` is lost. The QA pass
caught this on /flows: clicking >> right after typing in the filter
silently dropped `?q=` from the share-able URL.

Build the next `URLSearchParams` from `window.location.search` instead,
since that's the freshest source of truth under BrowserRouter and
sidesteps react-router's batching entirely. Fall back to the latest
react-router snapshot (stashed in a render-synced ref) under
MemoryRouter, which the hook tests use — `MemoryRouter` doesn't sync
its in-memory history to `window.location`.

478/478 existing tests still pass; the browser repro that previously
produced `?page=6` now correctly produces `?q=bypass&page=6`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 20:48:06 +07:00
Sergey Kozyrenko cd0fff34f3 fix 2026-05-16 20:30:38 +07:00
Sergey KozyrenkoandClaude Opus 4.7 ca32c199e8 refactor(frontend): compose DataTable storageKey via usePageStorageKeys
Hard-coding the full `table_4_/settings/prompts:agents` / `:tools` keys in
the page duplicated the route prefix that `usePageStorageKeys` already
computes — if the prefix ever bumps (e.g. `table_4_` → `table_5_` on a
storage migration), every call site silently goes stale. Read the route
base from the hook instead and only own the per-table suffix in the page.
Updated the `storageKey` JSDoc on DataTable to document this composition
pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 20:28:15 +07:00
Sergey KozyrenkoandClaude Opus 4.7 de695ec1c5 docs(frontend): DataTable QA scenarios and findings
Capture the QA pass we did against the multi-column search rollout:
INP baselines per page, the four bugs we hit (two fixed in the
follow-up commit, two left as backlog), the negative results from
the adversarial input pass (XSS / SQLi / regex meta / unicode /
prototype pollution via localStorage), and the regression scenarios
(S1–S11) so the next round of changes has a concrete checklist
instead of "run it and see if it feels OK".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 20:22:32 +07:00
Sergey KozyrenkoandClaude Opus 4.7 1d81974fa3 fix(frontend): unique DataTable input id and per-instance storageKey
Two issues uncovered by the QA pass over /settings/prompts, where one page
mounts two DataTable instances:

1. Both inputs shared a hard-coded `id="data-table-search"` and
   `name="search"`. `document.getElementById` only ever found the first
   one, screen readers couldn't disambiguate the two filter inputs, and
   any test selector keyed on the id silently picked the wrong field.
   Switch to `useId()` so the id and name are per-instance, non-empty,
   and stable across renders.

2. Both tables persisted sorting / column visibility / search-column
   narrowing into the same `table_4_/settings/prompts` slot. The last
   writer won the race; on reload the loser inherited the winner's
   state. Add a `storageKey?: string` prop to DataTable (mirroring the
   existing `useTableQueryFilter` API) and pass `…:agents` / `…:tools`
   for the two tables on /settings/prompts so they get distinct slots.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 20:22:15 +07:00
Sergey KozyrenkoandClaude Opus 4.7 f12b469199 feat(frontend): extend Settings/Prompts search across both tables
For both the Agent Prompts and Tool Prompts tables, mark every accessor
column with `meta.searchable: true` (name + status fields) and drop the
explicit `filterColumn="displayName"`. Typing "Custom" now surfaces
every prompt currently overridden — a much more useful starting point
than scanning the column visually. Added `columnMenuLabel` overrides
where the underlying accessorKey wasn't human-readable on its own.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:27:10 +07:00
Sergey KozyrenkoandClaude Opus 4.7 eedc6ece41 feat(frontend): extend Settings/API Tokens search to id and status
Mark `name`, `tokenId`, and `status` with `meta.searchable: true` and
drop the explicit `filterColumn="name"`. The bigger wins are tokenId
(operators often paste a fragment from logs or a request header to find
the owning token) and status (typing "revoked" surfaces every revoked
token without scrolling). Date columns stay excluded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:25:03 +07:00
Sergey KozyrenkoandClaude Opus 4.7 3a61d601e0 feat(frontend): extend Settings/Providers search to name and type
Mark `name` and `type` with `meta.searchable: true` and drop the
explicit `filterColumn="name"` so users can also narrow by provider
type — typing "anthropic" surfaces every provider of that type without
having to remember its display name. Updated the placeholder to match
the broader scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:23:31 +07:00
Sergey KozyrenkoandClaude Opus 4.7 8bdc10a41f feat(frontend): extend Templates search to title and text
Mark both accessor columns with `meta.searchable: true` and drop the
explicit `filterColumn="title"` so the picker offers both fields and
OR-matches across them. Searching the body text is the bigger win —
template titles are short, so the previous single-column behaviour
missed templates that referenced the same topic in their body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:22:02 +07:00
Sergey KozyrenkoandClaude Opus 4.7 b740c4769e feat(frontend): extend Knowledges search to type, question, and preview
Mark `docType`, `question`, and `content` with `meta.searchable: true` so
the multi-column picker offers all three as candidates and OR-matches
across them. Drop the explicit `filterColumn="question"` so the
zero-config path (search across all `meta.searchable` columns) takes
over. The `Flags` column has no accessor and would need a custom string
extractor to participate — skipped for now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:20:37 +07:00
Sergey KozyrenkoandClaude Opus 4.7 fd0edebf83 feat(frontend): extend Flows search to provider and terminals
Switch the `provider` and `terminals` columns from `accessorKey` to
`accessorFn` so the global filter receives plain strings — the provider
name and the joined list of terminal images — instead of the raw object
or array, which the predicate would just stringify to `[object Object]`.
The cell renderers keep reading `row.original` directly, so the visible
output is unchanged.

Dates stay excluded: substring matching against formatted timestamps
gives unpredictable hits (e.g. typing "3" suddenly matches every row
whose ISO contains 03) and date filtering belongs in a range picker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:18:54 +07:00
Sergey KozyrenkoandClaude Opus 4.7 63d059834f perf(frontend): debounce DataTable filter input commits
Typing into the Flows search felt sluggish — Chrome reported INP 271 ms with input delay 253 ms, meaning every keystroke had to wait for the previous keystroke's render to complete before the event handler could even run. The chain on every keypress was: `setSearchParams` → react-router rerenders the whole route subtree → TanStack re-derives the global filter → 91 rows × 3 predicate calls → reconcile ~70 cells (Tooltip + Badge + ProviderIcon). At ~250 ms per cycle a fast typist queues several keystrokes behind a running render, so the input appeared to lag visibly.

Split the input value from the upstream commit: `DataTableFilter` now owns a local string state that updates synchronously on every keystroke, while a 150 ms debounce mirrors the value into `onQueryChange`. The router / TanStack cascade now fires once per typing pause instead of once per keystroke, and the input never has to wait its turn behind an in-flight reconciliation. After the change Chrome reports INP 43 ms with input delay 2 ms — typing feels instant.

Externally visible behaviour is unchanged: the X button clears immediately (no debounce on explicit clears), and external `query` resets (back button, programmatic clear) sync down through a guarded effect that ignores the value we just emitted ourselves. Vitest tests that previously asserted against the synchronous commit now wait for the debounced row-model update via `findBy*` / `waitFor`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 16:50:20 +07:00
Sergey KozyrenkoandClaude Opus 4.7 38bc1deda9 refactor(frontend): inline getColumnId into data-table.tsx
`getColumnId` only has one consumer — `lib/` is for utilities shared across the codebase, not for a single-call helper. Move it next to `columnPickerLabel` as a module-level function inside `data-table.tsx`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 16:18:42 +07:00
Sergey KozyrenkoandClaude Opus 4.7 847c873f01 refactor(frontend): composite globalFilter for DataTable multi-column search
The previous implementation drove `searchColumns` narrowing through a regenerated `columns` array with per-column `enableGlobalFilter`. It worked, but every selection change spawned a fresh set of `ColumnDef` objects via spread, invalidating TanStack's column-instance cache and conflicting with the upstream "memoise columns" guidance. The column-id resolver was also duplicated across two memos.

Replace it with TanStack's canonical mechanism for dynamic filters: pack the query and the active column set into a single `state.globalFilter` value of shape `{ columns, query }`, and consult both fields from a custom `globalFilterFn`. Any change to the query or to the active column set produces a new object reference, which is exactly what TanStack watches to re-run the filter pipeline — no imperative `setGlobalFilter` pokes, no closure-only narrowing that silently goes stale, and the parent's `ColumnDef` references reach `useReactTable` untouched so the column cache stays warm. The id resolver moves into a single `getColumnId` helper in `lib/column-utils.ts`.

A regression test exercises the exact sequence that exposed the bug fixed in d4c1b13 (type a query that matches via one column, then uncheck that column — the row must disappear without retyping). A second test pre-seeds `localStorage.searchColumns` with an id that the current columns no longer expose and asserts the new rebase effect prunes it on mount.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 16:13:16 +07:00
Sergey KozyrenkoandClaude Opus 4.7 d4c1b1342e fix(frontend): refilter DataTable when search columns change
The first implementation routed the active-column set through TanStack's
`getColumnCanGlobalFilter` predicate and tried to nudge the pipeline with
`table.setGlobalFilter((current) => current)`. TanStack treats that as a
no-op because the resolved value equals the current state, so the filter
pipeline never re-ran after the user narrowed the picker — the table kept
matching against the previous column set.

Bake the active set into per-column `enableGlobalFilter` via a memoised
`tanstackColumns` instead. TanStack sees a new columns reference whenever
the user's selection changes and refilters automatically, which is the
standard mechanism for this kind of dynamic filter and verified end-to-end
in the browser on the Flows list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:15:11 +07:00
Sergey KozyrenkoandClaude Opus 4.7 8ccc2db3cb feat(frontend): enable multi-column search on Flows list
Mark `id`, `title`, and `status` as `meta.searchable: true` so the DataTable picks them up as candidates for the new global-filter search, and drop the explicit `filterColumn="title"` prop so users can search by uuid prefix or by typing a status keyword (e.g. "running"). `provider` and `terminals` stay excluded — both are non-string values that would only emit `[object Object]` matches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:58:03 +07:00
Sergey KozyrenkoandClaude Opus 4.7 939b3cf763 feat(frontend): multi-column search in DataTable
The single-column `filterColumn` prop forced every page to pick one searchable field, which is awkward when a row has several useful text fields (e.g. flows have both `title` and the original `task`). Switch the engine to TanStack's `globalFilter` so the input can match across an OR-set of columns, and add a "Search in" dropdown next to "Columns" that lets the user narrow that set at runtime.

Opt-in stays explicit: `filterColumn: string` keeps the legacy single-column behaviour (no picker), `string[]` activates the picker on the listed columns, and an undefined prop falls back to columns marked with `meta.searchable: true`. The chosen subset persists alongside other table preferences in `table_4_<path>`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:54:30 +07:00
Sergey Kozyrenko 3cc677712b fix: pnpm lock 2026-05-16 13:36:20 +07:00
Dmitry Ng c5a7df7438 feat(toolcalls): implement toolcall management features via REST API
- Added a new SQL migration to insert toolcall privileges into the privileges table.
- Introduced the `ToolcallService` for managing toolcall data, including retrieval of toolcalls and flow-specific toolcalls.
- Implemented API endpoints for fetching toolcalls and toolcall details, with appropriate permission checks.
- Enhanced Swagger documentation to include new toolcall endpoints and their specifications.
- Created a new model for toolcalls, defining their structure and validation rules.
- Added error handling for invalid toolcall requests and not found scenarios.
2026-05-16 13:24:29 +07:00
Dmitry Ng ec9d8eb129 feat(docker): add new provider configurations for Qwen 3.6 35B models
- Included two new provider YAML files for Qwen 3.6 35B models: `vllm-qwen3.6-35b-a3b-fp8-no-think.provider.yml` and `vllm-qwen3.6-35b-a3b-fp8.provider.yml`.
- Updated Dockerfile to copy the new configuration files into the appropriate directory.
2026-05-16 13:24:29 +07:00
Sergey KozyrenkoandClaude Opus 4.7 7dae4be61d refactor(frontend): headless controller for DetailNavigation
Detail pages duplicated DetailNavigationToolbar's internal navigation
state (prev/next, sheet open, position label) because mobile chrome lives
inside a DropdownMenuItem and could not reuse the toolbar component.
Promote useDetailNavigation to return a full DetailNavigationController,
have the leaf components (Buttons / Sheet / Toolbar) read from it, and
drop the ~40 LOC mobile mirror block on each of three pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:11:35 +07:00
Sergey KozyrenkoandClaude Opus 4.7 8dbee4fb64 style(frontend): use Badge for sheet header counters
Plain spans with `text-muted-foreground text-sm tabular-nums` rendered as
loose text floating in the header — visually weak and easy to miss. Wrap
each counter in a `<Badge variant="secondary">` so it reads as a discrete
pill (matches the rest of the design language, e.g. doc-type badges in
the knowledges list).

Touches three locations that all share the icon + title + counter
header pattern:
- `pages/templates/template.tsx`: Preset-templates mobile Sheet header
- `pages/templates/template.tsx`: Preset-templates desktop aside header
- `components/shared/detail-navigation/detail-navigation-sheet.tsx`:
  detail-navigation sheet header (Flows / Templates / Knowledges count)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:50:39 +07:00
Sergey KozyrenkoandClaude Opus 4.7 8fc8f63a75 refactor(frontend): unify Preset-templates panel with DetailNavigationSheet
The Preset-templates side panel on `/templates/:id` now mirrors the
header + scrollable body structure of `<DetailNavigationSheet>`, so the
two Sheet surfaces on this page (preset picker and Templates list) look
and behave the same: bordered header row with icon + title + total
counter on the right, scrollable body below.

- Mobile `<Sheet>` and desktop `<aside>` share an identical header
  (`border-b p-4` with `FileText`, "Preset templates", and a
  `tabular-nums` counter). The mobile variant uses `<SheetTitle>` so
  screen readers announce it; the desktop variant uses an `<h3>` styled
  to match. The previous `sr-only` SheetTitle is gone.
- Body switches from `<ScrollArea flex-1>` to a plain
  `<div className="min-w-0 flex-1 overflow-y-auto">`. Radix `ScrollArea`'s
  Viewport wraps children in a `display: table` div whose width grows to
  intrinsic content size, defeating `w-full min-w-0` on the inner card
  flex rows — the chevron buttons were rendering off-screen on a 390px
  viewport because of it. Plain `overflow-y-auto` respects the parent
  width and the chevrons land inside the Sheet.
- Each preset Card and its inner flex row pick up `w-full min-w-0`,
  the title `<span>` picks up `min-w-0`, so the
  `flex-1 min-w-0` title button actually shrinks and `truncate` kicks in
  on long preset names. Asides also gain `gap-2` between cards so they
  read as discrete items instead of a single bordered run.
- Drop the now-unused `ScrollArea` import.
- `components/ui/scroll-area.tsx`: collapse the `<Viewport>` JSX onto
  one line and replace `h-full w-full` with `size-full` — a prettier
  cleanup that landed alongside the rewrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:39:46 +07:00
Sergey KozyrenkoandClaude Opus 4.7 c2d368654b refactor(frontend): unify Preset-templates panel with DetailNavigationSheet
Both Sheets on the template detail page now follow the same skeleton:

  SheetContent (flex flex-col gap-0 p-0)
    SheetHeader (border-b p-4)
      SheetTitle (flex items-center gap-2 pr-8 text-base)
        <icon> <label> <counter on ml-auto>
    ScrollArea (flex-1)
      <list>

This mirrors `components/shared/detail-navigation/detail-navigation-sheet.tsx`,
so the navigation sheet (Templates 1/8 selector) and the Preset-templates
side panel look and behave the same way on every viewport — same width
cap (`max-w-sm`), same bordered header, same scroll behaviour via Radix
`ScrollArea` instead of plain `overflow-y-auto`.

The desktop `<aside>` variant gets the same skeleton too — wrapped in a
`flex flex-col` so the header strip stays pinned while the preset list
scrolls in the `ScrollArea` below. The pinned header swaps the muted
`<h3>` for the same icon + label + counter line as the mobile sheet, so
the platforms read the same.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 09:40:33 +07:00
Sergey KozyrenkoandClaude Opus 4.7 35adf05ab9 refactor(frontend): visible Preset-templates title per platform
Split the panel heading so each form factor gets the title styled for
its container:

- Mobile (`<Sheet>`): use a visible `<SheetTitle>` (no more `sr-only`)
  in the sheet's own area, styled at `text-base` to match other Radix
  dialog headers in the app.
- Desktop (inline `<aside>`): keep the existing muted `<h3>` outside
  the scroll area so it stays pinned while the preset list scrolls.

Lift the title out of `asideContent` so it isn't rendered twice on
desktop; trim the wrapper's `p-4` to `px-4 pb-4` since the title
provides the top spacing in both layouts. Drop `p-2` on `SheetContent`
in favour of `p-0` so the new title can own its own padding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:01:44 +07:00
Sergey KozyrenkoandClaude Opus 4.7 a6f386c5d7 fix(frontend): satisfy Radix dialog a11y requirements on sheets
Radix DialogContent (which `SheetContent` extends) warns when neither a
DialogTitle nor an explicit opt-out is provided, and again when no
Description / `aria-describedby={undefined}` is set. Two places hit
this:

- `pages/templates/template.tsx` preset-templates panel: the visible
  `<h3>` lives inside `asideContent` and is reused by the desktop
  non-Sheet variant, so add an `sr-only` `<SheetTitle>` inside
  `<SheetContent>` for screen readers. Pair it with an explicit
  `aria-describedby={undefined}` — the panel is a flat list of presets
  with no descriptive sub-text, so opting out is honest.

- `components/shared/detail-navigation/detail-navigation-sheet.tsx`:
  already had a `<SheetTitle>` (Flows / Templates / Knowledges), but the
  Description warning was still firing. Add the same
  `aria-describedby={undefined}` opt-out on `<SheetContent>` — the
  listbox of items is self-describing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:42:42 +07:00
Sergey KozyrenkoandClaude Opus 4.7 c196e55d5d feat(frontend): mobile-friendly headers across detail and list pages
Roll the truncate-chain + mobile-navigation pattern introduced for the
/flows/:id page out to every other detail and list page so a long title
never pushes the action buttons off-screen on a ~390px viewport.

Detail pages
- `templates/template.tsx` and `features/knowledges/knowledge-header.tsx`
  now mirror `pages/flows/flow.tsx`: left container becomes
  `flex min-w-0 flex-1` with `shrink-0` on the SidebarTrigger/Separator,
  Breadcrumb/BreadcrumbList/BreadcrumbItem get the `min-w-0` /
  `flex-nowrap` chain, BreadcrumbPage gets `truncate`, the right action
  area becomes `flex shrink-0`. InlineEditInput in both pages now uses
  `w-64 min-w-0 max-w-full flex-1` so the rename input shrinks with the
  parent instead of forcing it to ≥256px.
- On `isMobile`, the DetailNavigationToolbar is hidden and re-surfaced
  inside the existing actions dropdown as a single row matching
  `pages/flows/flow.tsx` (icon + label + Prev/Position/Next button
  group with shared borders; middle button doubles as the sheet
  trigger). DetailNavigationSheet is mounted separately, controlled by
  `isMobileNavSheetOpen` state.

List pages
- `pages/flows/flows.tsx`, `pages/knowledges/knowledges.tsx`,
  `pages/templates/templates.tsx`, `pages/resources/resources.tsx`,
  and `pages/flows/new-flow.tsx` get the same header truncate chain
  preventively — most page titles are short, but the structure stays
  consistent across pages and protects against future longer labels.
- `pages/templates/templates.tsx` Title cell gains `max-w-[380px]
  truncate` (matched to the existing Text-cell cap) so a long template
  title can't blow up the row.
- Settings pages (`settings-providers.tsx`, `settings-api-tokens.tsx`)
  don't render a breadcrumb header — their `SettingsLayout` already
  shows the sidebar trigger — but their inline header rows still leaked
  on narrow screens. Add `min-w-0 flex-1 truncate` to the description
  text and `shrink-0` to the Create button so the right-edge CTA stays
  visible while the left text degrades gracefully.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:21:25 +07:00
Sergey KozyrenkoandClaude Opus 4.7 7a2f882cea feat(frontend): mobile-friendly flow detail header and unified picker
Make the /flows/:id page usable on a 390px viewport and replace the two
parallel template/attachment dropdowns with one trigger that works the
same way on every screen.

Header
- Breadcrumb chain (`flex min-w-0 flex-1` on the left container, `flex
  shrink-0` on the right one, `min-w-0 flex-nowrap` on BreadcrumbList,
  `min-w-0 gap-2` on BreadcrumbItem, `truncate` on BreadcrumbPage) so the
  flow title can shrink past its intrinsic width instead of pushing the
  action buttons off-screen.
- InlineEditInput: `w-64 min-w-0 max-w-full flex-1` — keeps the 256px
  default on desktop but lets the rename input collapse to whatever space
  is left on narrow viewports.
- On `isMobile`, the desktop DetailNavigationToolbar and the favorite Star
  button are hidden and re-surfaced inside the Flow-actions dropdown:
  a single row matching the theme-menu pattern in `main-sidebar.tsx`
  (icon + label + Prev/Position/Next button group sharing borders), plus
  a separate "Add/Remove favorites" item. The position button doubles as
  the sheet trigger, so the mobile navigation has the same affordances as
  the desktop toolbar.

Flow form
- Templates and Resources (formerly "Attachments") share one Ellipsis
  trigger placed next to the Send button on every viewport — the two
  separate FileText/Paperclip dropdowns are gone.
- Inside the dropdown, Radix `<Tabs>` switches between picker panels
  rendered above the tab strip; the strip lives at the bottom so it
  lands next to the trigger. The dropdown opens upward (`side="top"`
  with `align="end"`) and has a fixed `w-72` so it stays inside the
  viewport on phone-width screens.
- Tab switch is deferred one tick (`setTimeout(..., 0)`) before
  mutating `pickerTab`. Radix DropdownMenuItem listens to `pointerup`
  directly, so a synchronous swap let the pointerup that ended the tab
  click land on the freshly mounted "Upload files" item in the Resources
  panel and fired its `onSelect`.
- Send button gets `shrink-0`; combined trigger picks up `ml-auto` so
  Send/trigger stay glued to the right edge without two `ml-auto` items
  fighting over leftover space.

Detail navigation primitives
- Expose `DetailNavigationSheet` and `useNavigation` from the package
  index so a page can compose its own mobile UI without re-implementing
  the filtered-subset / Prev-Next algorithm.
- `DetailNavigationSheet`: add `pr-8` to `SheetTitle` so the trailing
  total counter (e.g. "311") stops sitting underneath the absolutely
  positioned close button on the right edge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:03:26 +07:00
Sergey Kozyrenko 829271e7e4 refactor(frontend): inline useControllable into autocomplete.tsx
The hook had exactly one consumer (components/ui/autocomplete.tsx) and
was a tiny ~20-line Radix-style controlled/uncontrolled state mirror.
Move it inline as a private helper next to the component that uses it,
so a future reader doesn't have to chase a one-call hook through hooks/.
useLatestRef stays in @/hooks/ — it's genuinely cross-cutting.
2026-05-15 12:41:56 +07:00
Sergey Kozyrenko e041b11cef refactor(frontend): move table-filter into detail-navigation/ as text-filter
The createTextMatcher helper has only one production consumer
(detail-navigation/use-navigation.ts) and the original 'table-filter'
name implied a tie to <DataTable>'s column filter that the codebase
never actually wired (DataTable still uses TanStack's built-in
'includesString'). Move the file into detail-navigation/ as a private
text-filter module so the matcher lives next to its only caller and the
name reflects what it does (text matching) rather than aspirational
table-filter alignment.

If the table column filter is ever switched to use createTextMatcher
for diacritic folding, the import would simply move back up — but
that's a separate behaviour change, not a naming concern.
2026-05-15 12:34:42 +07:00
Sergey Kozyrenko 33dba9dbf4 refactor(frontend): inline cycleColumnSort into data-table.tsx
Move the lone cycleColumnSort function from lib/table-sort.ts straight
into components/ui/data-table.tsx — it was a one-consumer helper used
only by DataTableColumnHeader, so co-locating it removes the
cross-folder hop without changing behaviour. Exported alongside
DataTableColumnHeader for parity with the shadcn convention and so a
future custom header can reuse the cycle. The four unit tests move
into data-table.test.tsx as a sibling describe block; lib/table-sort
is gone.
2026-05-15 12:22:32 +07:00
Sergey Kozyrenko 7a6d50a1c1 refactor(frontend): relocate report module to lib/report/ folder
Move features/report/ → lib/report/ — the module is mostly pure
utilities (generateReport, generateFileName, copyToClipboard,
downloadTextFile) plus an internally lazy-loaded React-pdf component,
not a feature with its own domain/provider/queries. lib/ is the
honest home; the folder grouping addresses the original audit
complaint of a 658-line .tsx loose in lib/. Consumers swap
'@/features/report' for '@/lib/report'.
2026-05-15 12:06:55 +07:00
Sergey Kozyrenko 31c894ee65 refactor(frontend): move report module from lib/ to features/report/
The PDF generator (~658 lines, lazy-loaded) and its public-API wrapper
report.ts are functionally one module. They now live together in
features/report/ instead of being scattered in lib/, which was a poor
home for a 658-line React-pdf component anyway. The lazy-import path
inside report.ts stays relative (./report-pdf), and the four consumers
(flow.tsx, flow-files.tsx, flow-report.tsx, resources.tsx) just swap
'@/lib/report' for '@/features/report'.

Note: copyToClipboard and downloadTextFile come along for the ride —
they're generic helpers used in non-report contexts too. Splitting them
out cleanly into lib/ would be worth a follow-up if you want stricter
domain boundaries.
2026-05-15 11:52:02 +07:00
Sergey Kozyrenko f46de8d374 refactor(frontend): consolidate inline-edit module and drop the rename framing
Group inline-rename-input.tsx and use-inline-edit-title.ts into a new
components/shared/inline-edit/ folder, the same pattern we applied to
detail-navigation, overwrite, and unsaved-changes. Rename to drop the
misleading "rename" framing — the API has nothing rename-specific
(autoFocus, busy, defaultValue, onCancel, onSave) and the same component
is used for quick-create and inline-edit flows too:
  InlineRenameInput → InlineEditInput
  useInlineEditTitle → useInlineEdit
  INLINE_RENAME_MAX_LENGTH → inlined as default `maxLength = 200`
2026-05-15 11:41:52 +07:00
Sergey Kozyrenko c75335eac9 refactor(frontend): inline DataTableColumnHeader into data-table.tsx
Fold the standalone SortableColumnHeader into components/ui/data-table.tsx
and rename it to DataTableColumnHeader (the canonical name from
ui.shadcn.com/docs/components/data-table). The two pieces only ever
shipped together — every consumer paired SortableColumnHeader with
DataTable inside the TanStack column definition. Keeping data-table as
a single file matches the convention already used elsewhere in
components/ui/. Also rename the prop label → title to match the upstream
shadcn signature; touches 20 JSX usages across 5 list pages.
2026-05-15 10:46:46 +07:00
Sergey Kozyrenko d8e84d0df7 refactor(frontend): rename useOverwriteAction to useOverwrite
Inside `components/shared/overwrite/` the folder name already supplies
context, so the `-action` suffix is redundant — same principle that
turned overwrite-confirm-dialog into overwrite-dialog. Rename file
use-overwrite-action.ts → use-overwrite.ts, export useOverwriteAction
→ useOverwrite, and the internal UseOverwriteAction* prop/result types
to match. The shape lines up with the project's verb-noun action-hook
convention (useResourcesCopy, useResourcesMove).
2026-05-15 10:09:48 +07:00
Sergey Kozyrenko 90eba577b2 refactor(frontend): drop verbose suffixes from overwrite module names
Within `components/shared/overwrite/` the folder name already supplies
context, so the file/component names don't need to repeat it. Rename
overwrite-confirm-dialog → overwrite-dialog and overwrite-cta-buttons →
overwrite-buttons (and matching exports OverwriteConfirmDialog →
OverwriteDialog, OverwriteCtaButtons → OverwriteButtons) so the module
matches the file-and-export naming convention used by detail-navigation.
2026-05-15 10:03:49 +07:00
Sergey Kozyrenko 4d2e4ff5bf refactor(frontend): consolidate overwrite and unsaved-changes into module folders
Group the overwrite trio (OverwriteConfirmDialog, OverwriteCtaButtons,
useOverwriteAction) into components/shared/overwrite/ — they cross-import
the OverwriteConflict type and 12 consumers always pull from the same
logical module. Same pattern for the unsaved-changes pair: move the
dialog from components/shared/ and the UI-agnostic guard hook from
hooks/ into components/shared/unsaved-changes/. Each folder ships an
index.ts so consumers reach the module through one import path.
2026-05-15 09:50:08 +07:00
Sergey Kozyrenko ef0dbcf0e2 refactor(frontend): consolidate detail-navigation into a single module folder
Move the Prev/Position/Next toolbar and its supporting hooks from scattered
locations under components/shared/ and hooks/ into a single
components/shared/detail-navigation/ folder. Rename ListNavigation* →
DetailNavigation* (toolbar/buttons/sheet) so the names reflect what they
actually do — sibling navigation between detail pages, not navigation
within a list. The internal algorithmic hook drops its now-redundant
prefix (useFilteredListNavigation → useNavigation, computeListNavigation →
computeNavigation), and useDetailNavigation moves alongside the toolbar
since it's the public composition layer that produces toolbarProps.
2026-05-15 09:33:12 +07:00
Sergey KozyrenkoandClaude Opus 4.7 21d61b37ee perf(frontend): lazy-load @react-pdf/renderer and drop dead html2pdf.js
- @react-pdf/renderer (~1.5 MB) moved to an async chunk. lib/report.ts
  used to statically re-export PDF helpers from ./report-pdf, which
  pulled the entire PDF library into every page that imports
  lib/report (flow.tsx, flow-report.tsx). The re-exports are now
  async wrappers using `await import('./report-pdf')`, so the
  library loads only when the user actually triggers a PDF export.
  Initial JS for /flows/:id/report drops from ~2.0 MB to ~500 KB.
- Drop html2pdf.js: zero imports anywhere in the source, but it was
  declared as a dependency and reserved a (now-empty) manual chunk in
  vite.config.ts. Removing it also drops 22 transitive packages from
  the lockfile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:38:07 +07:00
Sergey KozyrenkoandClaude Opus 4.7 420944946e style(frontend): apply prettier formatting
Whitespace, line wrapping and Tailwind class ordering only — no code
changes. Brings 11 files in line with the prettier + tailwindcss
plugin config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 06:55:28 +07:00
Sergey KozyrenkoandClaude Opus 4.7 abe45e33c9 chore(frontend): rename Cyrillic clipboard.ts and remove dead code
- src/lib/сlipboard.ts → clipboard.ts: the filename used a Cyrillic 'с'
  (U+0441) instead of Latin 'c', which made the path silently
  unmatchable by anyone typing the import with a Latin letter. Update
  the 4 importers in features/flows.
- Delete unreachable settings-mcp-server(s).tsx (~1.3k lines): no
  routes registered in app.tsx and nothing imports them. Remove the
  matching dead branches and commented menu entry from
  settings-layout.tsx.
- Drop unused dev deps simple-git-hooks and lint-staged: no config in
  the repo, not used by CI or Docker, hooks were never wired up. Also
  remove simple-git-hooks from pnpm.onlyBuiltDependencies.
- Drop unused tsconfig path aliases: @/ui/*, @env (env.ts doesn't even
  exist), and @pkg. Only @/* remains.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 06:53:53 +07:00
Sergey KozyrenkoandClaude Opus 4.7 692d91cffa fix(frontend): sync URL on pageSize change, label knowledge columns
- DataTable: route handlePageSizeChange through handlePaginationChange so
  onPageChange fires when picking "All" on a high page drops pageIndex to 0.
  Without this, the URL kept a stale ?page=N while the display correctly
  clamped to "Page 1 of 1".
- DataTable: reconcile effect in controlled mode now compares
  externalPageIndex (the URL source of truth) against safePageIndex instead
  of the internal mirror, which can drop to 0 via handlePageSizeChange and
  hide the URL mismatch from the previous comparison.
- knowledges: add meta.columnMenuLabel to docType and question columns so
  the Columns dropdown reads "Type" / "Question" — same labels as the
  table headers — aligning with the Flags/Preview entries.
- data-table.test: cover both pathways — out-of-range controlled pageIndex
  on mount, and pageSize=All from page 2.
- vitest.setup: polyfill hasPointerCapture / setPointerCapture /
  releasePointerCapture so Radix Select interactions don't crash in jsdom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:46:17 +07:00
Sergey KozyrenkoandClaude Opus 4.7 a8a7431613 fix(frontend): clamp page index, sync settings filters, harden inline rename
- DataTable: derive a clamped `safePageIndex` from URL + `pageCount`; use it
  for display + reconcile URL via effect when raw differs. Fixes "Page 999
  of 31" and "Showing 9981–307 of 307" when a hand-typed URL, a filter
  narrowing the dataset, or a pageSize=All bump leaves the URL out of range.
- DataTable: only persist `pageSize` to storage when it differs from
  `initialPageSize` so a fresh mount no longer seeds `{ pageSize: 10 }` via
  StrictMode dev double-invoke.
- useTableQueryFilter: storage replay no longer drops `?page=` — that flag
  belongs to user-driven `setFilter`, not to restoration of prior state.
  Out-of-range pages after replay are now handled by the clamp above.
- Settings/Providers + Settings/API tokens: pass `filterValue` / `onFilterChange`
  from `useTableQueryFilter` so the filter input is URL- and storage-synced,
  matching the pattern used by flows, knowledges, templates, resources.
- InlineRenameInput: export `INLINE_RENAME_MAX_LENGTH = 200` and apply it as
  the native `maxLength` attribute. UX guard against paste-bombs that would
  break truncation; `defaultValue` is untouched so existing long records
  remain editable.
- knowledges: render a "Flags" header for the badges column instead of `null`,
  aligning with `meta.columnMenuLabel` and other column headers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:48:15 +07:00
Sergey KozyrenkoandCursor 5319d1cb65 feat(frontend): unify list tables with URL sync, navigation, and Vitest
- Add table-state, url-params, table-filter, view-options-storage utilities
- Add hooks: pagination, table query filter, filtered list/detail navigation, inline title edit, page storage keys
- Add shared list navigation toolbar/sheet/buttons, sortable headers, inline rename input
- Refactor DataTable; integrate flows/knowledges/templates/resources/settings pages
- Remove adaptive column visibility and legacy table-storage
- Configure Vitest with tests for hooks, lib, and components

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 21:36:14 +07:00
Sergey KozyrenkoandCursor fff79b3979 feat(knowledges): inline rename and delete actions in knowledge header and list
Adds an actions dropdown to the knowledge detail header with inline
rename (double-click or "Rename") and delete with a confirmation dialog.
The form keeps any unsaved edits when an external rename refreshes the
cache (`keepDirtyValues` + reactive `values`), and `onBeforeNavigateAway`
suppresses the unsaved-changes guard right after a successful delete.

The knowledges listing gets the same: a "Rename" item in the row
dropdown and context menu opens an in-row editor, and clicking the row
no longer navigates while a rename is in progress.

Drops the redundant `knowledgeName` prop that was threaded through
`KnowledgeForm`/`KnowledgeLayout`/`KnowledgeHeader` — the header now owns
the title and computes it locally from `knowledge.question`.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 20:52:51 +07:00