Bumps zod 3.25.76 → 4.4.3 and @hookform/resolvers 3.10.0 → 5.2.2
(the resolvers v5 requires zod v4).
zod is used in 25 source files (schemas, form validators, table state
parsers). Build passes without changes — the project relies on the
shared subset of the v3/v4 API (z.object/z.string/z.number/z.enum,
z.infer, .min/.max/.optional, refinements). Runtime form validation
keeps disabling submit until required fields are filled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps react-resizable-panels from 3.0.6 to 4.11.1. The wrapper in
src/components/ui/resizable.tsx only re-exports PanelGroup, Panel and
PanelResizeHandle; their props remain compatible across the major.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps react-day-picker from 9.14.0 to 10.0.1. The Calendar wrapper in
src/components/ui/calendar.tsx uses the v9 classNames keys
(button_next/previous, day_button, range_*, week, weekday, weekdays)
and the components.Chevron API; all of these are preserved in v10.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps marked from 17.0.6 to 18.0.3. Used only in
src/lib/report/report-pdf.tsx for PDF generation via marked.lexer();
that API is compatible across the major.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps lucide-react from 0.553.0 to 1.16.0. The 0.x → 1.x jump is a
versioning policy change, not a breaking API change — all icon imports
keep working as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pin React/React-DOM to 19.2.6 and @types/react* to 19.2.0 in
package.json — lockfile already resolved the latest 19.x; this just
makes the minimum explicit so future installs don't regress.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Introduced `EMBEDDING_MAX_TEXT_BYTES` to limit the maximum byte size of text sent to the embedding model.
- Renamed database connection pool settings: `DATABASE_MAX_OPEN_CONNS`, `DATABASE_MAX_IDLE_CONNS`, and `DATABASE_VECTOR_MAX_CONNS` for improved PostgreSQL connection management.
- Updated relevant documentation to reflect these new configuration options and their usage.
- Adjusted various components to utilize the new settings for enhanced performance and resource management.
- Added ToolCallLogProvider interface with methods for logging tool calls, updating success and failure statuses.
- Introduced proxyToolCallLogProvider to handle ToolCall logging operations.
- Updated flow execution components to integrate ToolCall logging, including flow workers and controllers.
- Enhanced GraphQL schema to support ToolCall logs, including queries and subscriptions for real-time updates.
- Updated documentation to reflect the new ToolCall logging features and their usage.
- Introduced shared connection pooling for PostgreSQL using `*sql.DB` for sqlc and GORM, optimizing resource usage.
- Added new environment variables: `DB_MAX_OPEN_CONNS`, `DB_MAX_IDLE_CONNS`, and `DB_VECTOR_MAX_CONNS` for configurable connection limits.
- Updated documentation to reflect new connection pooling strategy and provide operational commands for monitoring.
- Implemented shared `pgxpool` for pgvector stores to reduce connection overhead and improve performance.
- Adjusted various components to utilize the new connection pooling setup, ensuring efficient database interactions.
- Introduced WaitTaskCompletion method in FlowWorker interface to block until the current task completes or the context expires.
- Implemented signalTaskComplete to manage task completion signaling across goroutines.
- Added waitFlowCompletion tool to handle waiting for task completion with configurable timeout.
- Updated assistant provider to include wait functionality for flow completion.
- Enhanced templates and tool registry to support new wait functionality.
Gated behind the `anonymize.call` privilege from `useUser()`; runs the
current `content` through the `anonymizeText` GraphQL mutation and writes
the result back. Desktop renders the button before Save; mobile collapses
it into the first item of the actions dropdown (the dropdown now appears
for new documents too when the privilege is present).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
`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>
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>
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>
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>
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>
- 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.
- 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.
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>
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>
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>
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>
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>
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>
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>
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>
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.
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.
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.
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'.