[feat] Rebuild the mobile sessions list - #6735
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 SummarySummary by CodeRabbit
WalkthroughThe mobile sessions page now uses local filter and table components. It supports persisted grouping, activity windows, archive-only and status filters, debounced search, row actions, grouped rendering, sticky headers, empty states, and a loading skeleton. ChangesSessions list redesign
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant SessionListScreen
participant SessionFilterMenu
participant useSessionsList
participant SessionListTable
SessionListScreen->>SessionFilterMenu: render filters and persisted view
SessionFilterMenu->>SessionListScreen: change filter or grouping
SessionListScreen->>useSessionsList: request filtered sessions
useSessionsList->>SessionListTable: provide rows and query state
SessionListTable->>SessionListScreen: invoke row and reset actions
Merge Risk: 🔵 Low · up to The mobile sessions list can show a false empty state, retain sessions outside an activity window, expose overlapping row actions, and repeat status labels. These are localized and straightforward to address. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
web/mobile/src/features/sessions/states/SessionsPageSkeleton.tsx (1)
62-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReplace the inline grid style with a Tailwind utility. Use
grid-cols-[minmax(160px,2fr)_minmax(120px,1fr)_96px]inclassNameand removeSKELETON_GRID. This follows the checked-in frontend styling rule forweb/mobile.web/mobile/src/features/sessions/SessionListScreen.tsx (1)
31-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReduce the sessions comments to one short line.
web/AGENTS.mdapplies this rule toweb/mobile/**/*.{ts,tsx}. Keep only concise comments for non-obvious constraints in the sessions feature; move design rationale to the PR description or an ADR.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Advanced
Run ID: d6ce6d29-35d3-444d-84d3-d251a9c425da
📒 Files selected for processing (21)
web/mobile/src/features/sessions/SessionFilterMenu.tsxweb/mobile/src/features/sessions/SessionListScreen.tsxweb/mobile/src/features/sessions/SessionListTable.tsxweb/mobile/src/features/sessions/SessionRowCells.tsxweb/mobile/src/features/sessions/SessionRowMenu.tsxweb/mobile/src/features/sessions/pageFrame.tsweb/mobile/src/features/sessions/sessionListView.tsweb/mobile/src/features/sessions/states/SessionsEmpty.tsxweb/mobile/src/features/sessions/states/SessionsError.tsxweb/mobile/src/features/sessions/states/SessionsNoMatch.tsxweb/mobile/src/features/sessions/states/SessionsPageSkeleton.tsxweb/mobile/src/pages/w/[workspace_id]/p/[project_id]/sessions/index.tsxweb/mobile/tests/unit/sessionListView.test.tsweb/packages/agenta-entities/src/session/state/listOptions.tsweb/packages/agenta-sessions/src/state/filters.tsweb/packages/agenta-sessions/src/state/index.tsweb/packages/agenta-sessions/src/state/useSessionList.tsweb/packages/agenta-sessions/src/state/useSessionsList.tsweb/packages/agenta-shared/src/hooks/useDebouncedAtomSearch.tsweb/packages/agenta-ui/src/list-table/ListTable.tsxweb/packages/agenta-ui/src/list-table/types.ts
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
web/mobile/scripts/generate-shadcn-tokens.ts (1)
287-290: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winApply the one-line source-comment rule to both changed comment blocks.
web/mobile/scripts/generate-shadcn-tokens.ts#L287-L290: replace the four-line comment with one short line.web/packages/agenta-sessions/src/row/sessionRowStatus.ts#L12-L17: replace the multi-line JSDoc with one short line.Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Advanced
Run ID: 6a5bdbfd-a88b-4ab6-a0ac-5f08fc1a7198
⛔ Files ignored due to path filters (1)
web/mobile/src/styles/theme.generated.cssis excluded by!**/*.generated.*
📒 Files selected for processing (3)
web/mobile/scripts/generate-shadcn-tokens.tsweb/mobile/src/features/sessions/SessionFilterMenu.tsxweb/packages/agenta-sessions/src/row/sessionRowStatus.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- web/mobile/src/features/sessions/SessionFilterMenu.tsx
Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Advanced
Run ID: f37b6cd7-d8a5-4967-b14c-46a3e4e17934
📒 Files selected for processing (6)
web/mobile/src/features/sessions/SessionListScreen.tsxweb/mobile/src/features/sessions/SessionListTable.tsxweb/mobile/src/features/sessions/SessionRowCells.tsxweb/mobile/src/features/sessions/states/SessionsPageSkeleton.tsxweb/packages/agenta-sessions/src/state/useSessionsList.tsweb/packages/agenta-shared/src/hooks/useDebouncedAtomSearch.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- web/mobile/src/features/sessions/SessionRowCells.tsx
- web/mobile/src/features/sessions/states/SessionsPageSkeleton.tsx
- web/mobile/src/features/sessions/SessionListTable.tsx
- web/mobile/src/features/sessions/SessionListScreen.tsx
- web/packages/agenta-sessions/src/state/useSessionsList.ts
Included review availability: Your plan provides up to 8 included reviews per hour; 4 remain after this review.
| // surface. It cancels a pending write of ours: the reader's half-typed term lost the race the | ||
| // moment the value changed underneath it. | ||
| useEffect(() => { | ||
| if (initial === lastApplied.current) return |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Cancel pending drafts on scope reset; an unchanged initial leaves a pending search to overwrite the reset.
There was a problem hiding this comment.
Real, fixed in 2a865df.
The hook cannot see this one: applySessionScopeAtom clears the search atom, but when the atom is already empty its value never changes, so the effect that watches initial never fires and the pending write survives to land 300ms later. An unchanged value is not an event.
So the screen does it instead, since it is what knows a scope was applied: the ?mode= effect now calls clearSearch() alongside applyScope(), which cancels the pending timer and the draft with it.
Railway Preview Environment
|
46f5ed2 to
8ec8c56
Compare
useDebouncedAtomSearch started its draft at "" and never read the atom back. Search atoms are module-level, so a reader who leaves a narrowed list and returns finds an empty box over filtered rows, which reads as broken rather than as filtered. An optional third argument, defaulted, so the three existing two-argument callers are unchanged.
…able The sessions page and the automations page next to it configured their lists two different ways: a toolbar of a search field, an agent select, a status segmented control and two switches on one, a single filter popover on the other. This puts sessions on the automations shape, so the app has one way to narrow a list rather than one per page. The list is now ListTable + ListTableToolbar + FilterMenu, the same components the automations screen mounts. The menu holds Type (automation runs and archived, multi-select because one picks WHICH sessions and the other WIDENS the set), Status, Agent, and Group by, which defaults to Agent. No sort row: the server orders sessions by recency, and a client-side sort would only re-order the pages that happen to have loaded. Filtering still runs on the shared atoms in @agenta/sessions/state, so this page and the desktop one narrow the same set the same way; only the controls differ. @agenta/sessions-ui is untouched and the desktop page renders as before. Rows drop to one line — status dot, title, pin, rename, agent, updated, kebab — losing the message preview and the waiting chip. Pins keep their own group above whatever the grouping is: a pin is an explicit request, not a property to sort by. Grouping is client-side over the rows already loaded, so a group grows as the reader pages. That is the same bargain the automations list makes.
…ve a phone the title The pair sat on every row at rest, which made a list of titles read as a list of controls. They now appear on the row's hover, as a pair rather than two independently fading glyphs, and each is a real 20px button with a hover fill instead of a bare glyph — a pinned row keeps its pin, since the filled glyph IS how the row says it is pinned. The pin is local rather than SessionPinButton: that one carries a tooltip, and one firing off a row you are only passing over is noise. The aria-label still names it. Below sm the row drops the Agent column and both inline buttons. Four columns in 375px left the title a dozen characters and pushed Updated off the edge; the agent is the default grouping, so the heading above the run says it once instead of every row repeating it, and the kebab still carries pin and rename. Updated is right-aligned so the timestamp sits against the kebab rather than floating in a 96px track — ListTableColumn gains an optional headerClassName so the header follows its cells. Rename gets this app's own field: preflight is off here, so the border and the font have to be stated, and the ring is the one every other input on this surface draws.
The Status filter offered All, Live and Waiting on you. "Live" is a fact about the infrastructure — the sandbox is up — not the question a reader is asking, and there was no way to ask for the two states they actually look for. Status now reads All, Waiting, Running, Idle. All three narrow on the SERVER, so this is not a pass over a fetched page: the backend's liveness flags nest (is_alive superset of is_running), which makes running is_running:true and idle the complement, is_alive:false. Verified against the API directly — is_alive:false returns rows where is_alive:true returns none. live stays in SessionStatusFilter because the desktop controls still offer it; this only adds two members and their flag mappings. Desktop's option lists are untouched. One overlap worth knowing: a session whose gate is open but whose sandbox has died is both waiting and idle, because the status derives waiting from the pending count before it looks at liveness.
…tates Sessions is the long list in this app, so the column names and the group heading now stay put while the rows scroll under them. Sticky and the frame's own horizontal scroller are mutually exclusive, and not by choice: overflow-x:auto computes overflow-y to auto too, which makes that box a scrollport with no vertical range — a sticky header inside it has nothing to stick to. So stickyHeader hands the overflow up to the page scroller, which already scrolls both axes. Affordable here because the column minima fit every width this page is read at, so that scroller was never doing anything. Group headings needed a box each. sticky is bounded by the containing block, so headings sharing one flat parent stick for the rest of the TABLE — they piled up at the same offset, painted over each other, and left a hole where each was pulled out of the flow. A div per group makes each hand off to the next as its own run ends. Layout is unchanged: every row is its own grid and the parent is a plain block either way. The empty states are the automations ones: a distinct "No sessions yet" that says what a session is, and a "nothing matches" that names the search or the filters and carries the way out. The shared SessionListEmpty said both with one line and one link.
Type was a multi-select over two toggles, so "automation runs" and "archived" widened the list rather than replacing it — Archived showed archived sessions ALONGSIDE the active ones, which is not what a reader picking Archived is asking for. It is now single-select over three sets, the way the sidebar's Type facet works: Chat (the default), Automation, Archived. Picking one shows THOSE. Archived rides on the API's archived_only, which wins over include_archived server-side and was already plumbed as far as querySessionsPage; this carries it the last hop through the list query and its key. Last activity joins it with the sidebar's own options — All, Today, Last 7 days, Last 30 days — defaulting to seven days. It maps to windowing.oldest, which the DAO reads as coalesce(updated_at, created_at) >= oldest, so it is a server predicate like the rest. A descending cursor pages on newest and never sets oldest, so the floor survives every page after the first. The floor is rounded down to the hour, or a value recomputed from Date.now() would mint a fresh query key on every render. It is a hook argument rather than a shared atom because the two surfaces want different defaults for it. Archived rows render at 60% — archived is a state the reader chose to put a session out of the way, and a full-strength page of them reads as the live list. The kebab keeps its weight, since unarchiving is the point of being there.
…ge's shape while it loads Pinning the first session threw the whole table back to its skeleton, and every pin after that dimmed it. Three causes, all in useSessionsList: - excludeSessionIds: pinnedIds put the pin set in the MAIN list's query key, so every toggle re-keyed and refetched it. It is not needed: recentRows already drops pinned ids from that query's rows, on the same frame as the toggle. A page can now render one row short when it contains a pinned one, which the existing top-up handles. - isPending folded in the pinned query, whose first fetch starts the moment you pin your first session — exactly when the skeleton appeared. - isPlaceholder folded it in too, and the pinned query re-keys on every pin because its ids ARE its key. Both flags now read the main list only. The pins group arrives a beat later, above rows that never move. The route returned null until router.query populated, so a hard load opened on a blank screen and snapped into a full list. It renders the page's real geometry instead — title, toolbar, column rule, rows. Grouping by agent no longer labels every run "Unknown agent" while the roster is still loading. That fallback is for an agent this client cannot name; until the query answers, it cannot name ANY of them, so the rows stay in one unlabelled run rather than making a confident claim that is about to change. A pinned row now hides its pin like every other row. The Pinned heading already says it, and one row wearing a glyph the others reveal on hover read as a different kind of row. The date-bucket test asserted in UTC while the implementation uses the local calendar day, so it failed anywhere east of Greenwich. It derives the instant from local midnight now.
Pins no longer answer to the surface's own narrowing. pinnedSessionListArgs spread the shared filters, so the activity window and the archive-only view were applied to the pin query too — and because the mobile list defaults that window to seven days, pinning anything older than a week dropped it out of the Pinned group while it was still pinned. Both are neutralised there now, for the same reason the function already overrides the origin policy: a pin outranks the filters. An empty list caused by the Last activity facet said "No sessions yet". filtersActive only reads the shared atoms, and that window is a hook argument, so it fell through to the state that claims the account has none — over a project full of them, on the default path. The table counts the window itself now. "Clear search" cleared every filter. The no-match state took one handler for both of its labels, so undoing a search that matched nothing also threw away the agent, status and type the reader had chosen. Two callbacks now: the search alone, or the whole view. A phone painted the four-column table for a frame. useMediaQuery server-defaults to false, so the first render always chose the wide column set; this app is served at /m, so narrow is the honest default and the swap is gone. Also: the sticky header height and the group heading's offset are one named constant instead of 36px written three times; the page frame lives in its own module, so the screen and its skeleton cannot drift and the route goes back to being a thin shell; and STATUS_FLAGS no longer sits between useSessionList and the doc comment that describes it.
…t has none The empty state was guessing from the filters, and either way round it got one case wrong. Counting the activity window told a brand-new project that filters were hiding sessions it does not have; not counting it told a project whose work is all older than a week that it has none. Neither is knowable from what the page already holds — the list is filtered by definition, and the window is on by default — so it asks. One unfiltered row, run only while the list is actually empty, and the state renders nothing until that answers rather than flashing the wrong claim. That also drops the narrowed flag: if the project has sessions and this list has none, something on the page narrowed them away, whichever control it was.
The waiting dot used the semantic colorWarning, which is a TEXT amber (#8a6400). At 7px it read as brown, so the one row that costs something to miss was the one you could not pick out. The nav rail has painted a blocked session with --ag-run-status-warning (#F79009) all along, so the two disagreed about the same session. sessionRowStatus now takes its waiting and running fills from those run-status hues, which puts every surface that lists sessions on the rail's palette. They were not bridged into web/mobile, so the token script gains them. Generated CSS regenerated and committed, per the theme rules.
The search field ignored writes it did not make. applySessionScopeAtom clears sessionSearchAtom when you land on a ?mode= route, and useDebouncedAtomSearch seeds its draft once and never looks again, so the box kept a term the rows no longer answered. It follows the atom now, but only for outside writes: every keystroke eventually sets the atom too, and adopting each new value blindly would overwrite the draft the reader has already typed past. A lastApplied ref tells the two apart, the same guard SessionSearchControl uses on the desktop. Archived pins were still dropped. Clearing archivedOnly in the pinned arguments was not enough, because includeArchived defaults to false: you cannot pin an archived session, but you can archive a pinned one, and the row then left the group instead of staying put. Pins take includeArchived: true, for the same reason they ignore the origin policy. The activity floor comment claimed it refreshes on an hour rollover. It does not: useMemo depends only on the facet. The drift is a boundary moving under a long-lived tab, not a wrong answer, and a timer to correct it would refetch the whole list to move a line nobody is looking at. The comment says what the code does now. Also from the review: the skeleton's grid is a Tailwind utility rather than an inline style, and the longest comment blocks in the sessions feature are cut to a line or two, per the repo's comment-density rule.
13px and a fainter rule, so the head of the table reads as a label for what is under it rather than as a row in its own right.
…s room The dot carried the whole status, so reading it meant hovering for the tooltip or grouping by Status. From sm up the table now has a Status column: the dot and the word beside it, which is the shape the automations table next door already uses. A dot and a coloured word, never a pill, because the column is read down and a stack of pills reads as a stack of buttons. A phone keeps the dot beside the title and no column. A fourth reading column at 375px would cost the title the width it needs to be a title, and the word is still in the tooltip and the grouping. SessionRowStatusMeta gains textClassName alongside dotClassName. The two are not the same colour and should not be: the dot is a mark, so it takes the vivid run-status hue, and #F79009 as 13px type on white is a smear. The word takes the readable semantic text token. Waiting and Running carry colour; the rest are states rather than calls to action, so they read as muted as the timestamp beside them.
8ec8c56 to
e90abf2
Compare
Three ways they had drifted, all in the sessions copy.
The glyph size was a `size={16}` prop, and the button's own `[&_svg:not([class*='size-'])]:size-3` beat the attribute it produces. The prop did nothing and the icon rendered at 12px, two pixels smaller than the automations kebab rather than the two bigger it was meant to be. It is `className="size-3.5"` now, the way the automations button already writes it: three dots read smaller than a glyph that fills its box, so matching by measurement mismatches by eye.
The hover fill was the button default rather than the `foreground/10` the automations kebab uses, and the glyph was missing `aria-hidden` beside a button that already carries a label.
Moving the status into its own column left the name flush against the column edge, and with it went the one mark that separated a chat you started from a run an automation started. Two lists in one column read as one long list. From sm up the cell leads with the kind: a chat bubble or a bolt, the same two marks the Type facet offers, so the filter and the row agree. A phone keeps the status dot there instead, since it has no Status column to carry it and status is the thing worth knowing at that width.
A keystroke already on the debounce survived a scope reset. applySessionScopeAtom clears the search atom, but if the atom was already empty its value never changed, so nothing told the field — and 300ms later the pending write landed and re-narrowed the list the route had just scoped. The hook cannot see this: an unchanged value is not an event. The screen knows a scope was applied, so it clears the draft alongside it, which cancels the pending write.
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/mobile/src/features/sessions/SessionListScreen.tsx (1)
67-77: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAdvance
activityFloorat the next hour boundary while a bounded activity filter is active.SessionListScreenmemoizes it only byview.activity.useSessionListsends it as the server-side lower bound and includes it in the query key. Focus refetches reuse the stale boundary, so a long-lived tab can retain non-pinned sessions beyond the selected 24-hour, 7-day, or 30-day window until remount or facet change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Advanced
Run ID: 0352d58a-315f-4bc7-a859-7a553631ec60
📒 Files selected for processing (4)
web/mobile/src/features/sessions/SessionListTable.tsxweb/mobile/src/features/sessions/SessionRowCells.tsxweb/packages/agenta-sessions/src/row/sessionRowStatus.tsweb/packages/agenta-ui/src/list-table/ListTable.tsx
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.
| // previous query's, or the probe may not have said yet whether this | ||
| // project has sessions. Either way both states would be a guess, and one | ||
| // of them tells a reader with 43 sessions that they have none. | ||
| list.isPlaceholder || probe.isPending ? null : projectHasSessions ? ( |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Handle probe failures before showing SessionsEmpty. When the enabled probe query fails, probe.isError is true and probe.data can be empty. projectHasSessions then becomes false, and line 211 renders SessionsEmpty. Render the probe error with a retry action instead.
| const FADED = "opacity-60" | ||
|
|
||
| /** A row's inline verb. Local rather than `SessionPinButton`, which carries a tooltip. */ | ||
| const RowActionButton = ({ |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Move RowActionButton and StatusDot to matching files. As per coding guidelines: “One component per file, exported with a name matching the file name.”
Also applies to: 45-45
Source: Coding guidelines
| onClick={onClick} | ||
| // The transparent ::after is the hit extender: 20px is under the touch guideline, and | ||
| // growing the box itself would grow the row. | ||
| className="relative flex size-5 shrink-0 cursor-pointer items-center justify-center rounded border-0 bg-transparent p-0 text-colorTextTertiary transition-colors after:absolute after:inset-[-10px] after:content-[''] hover:bg-colorFillSecondary hover:text-colorText [@media(hover:hover)]:after:inset-[-4px]" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Separate adjacent RowActionButton hit areas. Their 20px boxes have a 4px gap, but the coarse-pointer after:inset-[-10px] extenders overlap by 16px and can route a tap to the wrong action; reduce the inset or increase the gap.
| role="img" | ||
| aria-label={status.label} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Hide the wide status dot from assistive technology. The dot and adjacent text both announce vm.status.label, so each wide row repeats its status.
Also applies to: 155-157
| /** | ||
| * The sticky stack, under `stickyHeader`: the column header pins at the top and a group heading | ||
| * pins directly beneath it. | ||
| * | ||
| * `height` and `groupTop` are ONE measurement written twice, because Tailwind cannot derive the | ||
| * second from the first. Change either and you must change the other — a heading pinned at the | ||
| * wrong offset leaves a sliver of rows showing through the gap above it. | ||
| */ |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use one short line per code comment.
web/packages/agenta-ui/src/list-table/ListTable.tsx#L24-L31: reduce theSTICKYcomment to one short line.web/packages/agenta-ui/src/list-table/ListTable.tsx#L67-L71: reduce the scrollport comment to one short line.web/packages/agenta-ui/src/list-table/ListTable.tsx#L78-L81: reduce the sticky-header comment to one short line.web/packages/agenta-ui/src/list-table/ListTable.tsx#L119-L121: reduce the skeleton-height comment to one short line.web/packages/agenta-ui/src/list-table/ListTable.tsx#L138-L144: reduce the group-wrapper comment to one short line.web/packages/agenta-ui/src/list-table/ListTable.tsx#L155-L156: reduce the group-heading comment to one short line.web/packages/agenta-ui/src/list-table/ListTable.tsx#L207-L210: reduce the row-hover comment to one short line.
📍 Affects 1 file
web/packages/agenta-ui/src/list-table/ListTable.tsx#L24-L31(this comment)web/packages/agenta-ui/src/list-table/ListTable.tsx#L67-L71web/packages/agenta-ui/src/list-table/ListTable.tsx#L78-L81web/packages/agenta-ui/src/list-table/ListTable.tsx#L119-L121web/packages/agenta-ui/src/list-table/ListTable.tsx#L138-L144web/packages/agenta-ui/src/list-table/ListTable.tsx#L155-L156web/packages/agenta-ui/src/list-table/ListTable.tsx#L207-L210
Source: Coding guidelines
Context
The mobile sessions page and the automations page next to it configured their lists two different ways. Sessions had a toolbar of a search field, an agent select, a status segmented control and two switches. Automations had a single filter popover. Same app, two answers to the same question.
This puts sessions on the automations shape, so
/mhas one way to narrow a list rather than one per page.Stacked on
feat/mobile-automations, which is whereListTable,ListTableToolbarandFilterMenulive. Base it there so this PR shows only its own diff.Changes
The list is now
ListTable+ListTableToolbar+FilterMenu, the same components the automations screen mounts. Filtering still runs on the shared atoms in@agenta/sessions/state, so this page and the desktop one narrow the same set the same way. Only the controls differ.The filter menu holds five rows:
No sort row. The server orders sessions by recency everywhere in this app, and a client-side sort would only re-order the pages that happen to have loaded.
Every filter is a server predicate, which needed three additions to the shared layer:
SessionStatusFiltergainsrunningandidle. The backend's liveness flags nest (is_alive ⊇ is_running), so running isis_running: trueand idle is the complement,is_alive: false.livestays for the desktop controls.archivedOnlycarries the API'sarchived_onlythe last hop through the list query. It was already plumbed as far asquerySessionsPage.activityFloormaps towindowing.oldest, which the DAO reads ascoalesce(updated_at, created_at) >= oldest. A descending cursor pages onnewestand never setsoldest, so the floor survives past the first page.Rows drop to one line: status dot, title, pin, rename, agent, updated, kebab. The message preview and the waiting chip are gone. Below
smthe Agent column and the two inline buttons go too, because four columns in 375px left the title a dozen characters and pushed Updated off the edge. The kebab still carries pin and rename there.The column header and the group heading stay put while the rows scroll under them. That needed a
stickyHeaderprop rather than a class, because the frame's ownoverflow-x: autocomputesoverflow-ytoautoand makes the box a scrollport with no vertical range, so a sticky header inside it never moves. UnderstickyHeaderthe page scroller absorbs the overflow instead.Fixes found while building and reviewing this
excludeSessionIds: pinnedIdsput the pin set in the main query's key, so every toggle re-keyed and refetched it, andisPending/isPlaceholderboth folded in the pinned query, whose first fetch starts the moment you pin your first session. Nothing is listed twice without the pushdown:recentRowsalready drops pinned ids on the same frame as the toggle.pinnedSessionListArgsspread the shared filters, so a pin older than seven days dropped out of the Pinned group while still pinned. BothactivityFloorandarchivedOnlyare neutralised there now, for the same reason the function already overrides the origin policy.useMediaQueryserver-defaults tofalse, so the first render always chose the wide column set.nulluntilrouter.querypopulated. It renders the page's real geometry now.useDebouncedAtomSearchgains an optionalinitial, so returning to a narrowed list no longer shows an empty box over filtered rows.Tests
sessionListView.test.ts: 11 cases over the four groupings, the date buckets and the activity floor. The date test derives its instant from local midnight, so it does not assume the runner's timezone.pnpm --filter @agenta/mobile lintandtypes:checkclean.@agenta/oss types:checkclean, so the shared-package changes do not break desktop./m: every facet, reset, grouping, sticky scroll, pin and unpin, inline rename, dark mode, and 375px.tests/unit/fileOnlyRefusal.render.test.tsxfails on this branch with 3 errors. It is a chat test, untouched here, and its source is under@agenta/chat, whichfeat/mobile-automationsandfeat/mobile-home-focusare both editing.What to QA
/m. The list groups by agent and shows the last seven days. The toolbar is a search field and one filter button.smup. Pin and rename appear beside the title. Rename opens an input in place; Enter saves, Escape abandons./w/.../sessionsis unchanged. Same toolbar, same switches, same rows.