Skip to content

[feat] Rebuild the mobile sessions list - #6735

Open
ashrafchowdury wants to merge 16 commits into
feat/mobile-automationsfrom
feat/mobile-sessions-redesign
Open

[feat] Rebuild the mobile sessions list#6735
ashrafchowdury wants to merge 16 commits into
feat/mobile-automationsfrom
feat/mobile-sessions-redesign

Conversation

@ashrafchowdury

Copy link
Copy Markdown
Contributor

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 /m has one way to narrow a list rather than one per page.

Stacked on feat/mobile-automations, which is where ListTable, ListTableToolbar and FilterMenu live. 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:

Row Options
Type Chat (default), Automation, Archived. Single-select, like the sidebar's Type facet: picking one shows those instead of the rest.
Status All, Waiting, Running, Idle
Agent All agents plus the project roster
Last activity All, Today, Last 7 days (default), Last 30 days
Group by Agent (default), Date, Status, None

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:

  • SessionStatusFilter gains running and idle. The backend's liveness flags nest (is_alive ⊇ is_running), so running is is_running: true and idle is the complement, is_alive: false. live stays for the desktop controls.
  • archivedOnly carries the API's archived_only the last hop through the list query. It was already plumbed as far as querySessionsPage.
  • activityFloor maps to windowing.oldest, which the DAO reads as coalesce(updated_at, created_at) >= oldest. A descending cursor pages on newest and never sets oldest, 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 sm the 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 stickyHeader prop rather than a class, because the frame's own overflow-x: auto computes overflow-y to auto and makes the box a scrollport with no vertical range, so a sticky header inside it never moves. Under stickyHeader the page scroller absorbs the overflow instead.

Fixes found while building and reviewing this

  • Pinning reloaded the whole list. excludeSessionIds: pinnedIds put the pin set in the main query's key, so every toggle re-keyed and refetched it, and isPending/isPlaceholder both folded in the pinned query, whose first fetch starts the moment you pin your first session. Nothing is listed twice without the pushdown: recentRows already drops pinned ids on the same frame as the toggle.
  • Pinned sessions vanished under the activity window. pinnedSessionListArgs spread the shared filters, so a pin older than seven days dropped out of the Pinned group while still pinned. Both activityFloor and archivedOnly are neutralised there now, for the same reason the function already overrides the origin policy.
  • The empty state guessed. It cannot tell "you have none yet" from "your filters hid them" out of what the page holds, and guessing from the filters gets one case wrong either way. It asks now: one unfiltered row, run only while the list is empty.
  • "Clear search" cleared every filter. The no-match state took one handler for both of its labels. Two callbacks now.
  • 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.
  • Group headings read "Unknown agent" while the roster loaded. 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.
  • A hard load opened on a blank screen. The route returned null until router.query populated. It renders the page's real geometry now.
  • useDebouncedAtomSearch gains an optional initial, 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 lint and types:check clean. @agenta/oss types:check clean, so the shared-package changes do not break desktop.
  • Verified against a local stack on /m: every facet, reset, grouping, sticky scroll, pin and unpin, inline rename, dark mode, and 375px.

tests/unit/fileOnlyRefusal.render.test.tsx fails on this branch with 3 errors. It is a chat test, untouched here, and its source is under @agenta/chat, which feat/mobile-automations and feat/mobile-home-focus are both editing.

What to QA

  • Open Sessions on /m. The list groups by agent and shows the last seven days. The toolbar is a search field and one filter button.
  • Filter menu, Type, pick Automation. The list swaps to automation runs. Pick Archived. Only archived sessions, rendered faded.
  • Status, pick Running, then Idle. Both narrow on the server, so paging and counts stay honest.
  • Last activity, pick Today. Older rows go. Reset to defaults puts all five rows back and greys the reset row out.
  • Pin a session older than a week, then set Last activity to Today. The pin stays in the Pinned group.
  • Pin your first session. The list must not flash its skeleton or dim.
  • Hover a row from sm up. Pin and rename appear beside the title. Rename opens an input in place; Enter saves, Escape abandons.
  • At 375px: no Agent column, no inline buttons, and the table does not scroll sideways.
  • Scroll a long list. The column header and the current group heading stay pinned, and headings hand off to each other rather than piling up.
  • Search something that matches nothing. "Clear search" returns the rows without discarding the agent or status you had set.
  • Regression: the desktop sessions page at /w/.../sessions is unchanged. Same toolbar, same switches, same rows.

@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agenta-documentation Ready Ready Preview Sep 10, 2026 12:46pm UTC

Request Review

@ashrafchowdury
ashrafchowdury added this pull request to stack #6736 September 9, 2026 20:25
@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • release/.*

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Advanced

Run ID: 48d909ea-46b0-4bb0-853a-8ce616957fcb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Summary

Summary by CodeRabbit

  • New Features
    • Added a redesigned mobile sessions list with responsive tables, grouping, sticky headers, pagination, pinning, renaming, and row actions.
    • Added filters for session type, status, agent, activity period, grouping, and archived-only sessions.
    • Added search restoration, filter reset controls, and activity-based filtering.
    • Added clearer empty, no-match, error, retry, and loading states.
    • Added Running and Idle session status filters.
  • Bug Fixes
    • Replaced the blank initial loading screen with a sessions-page skeleton.

Walkthrough

The 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.

Changes

Sessions list redesign

Layer / File(s) Summary
Session query and filter contracts
web/packages/agenta-entities/..., web/packages/agenta-sessions/..., web/packages/agenta-shared/...
Session queries support archive-only results and activity bounds. Status filters include running and idle states. Pinned sessions bypass these bounds. Debounced atom search can use an initial value.
Session view model and grouping
web/mobile/src/features/sessions/sessionListView.ts, web/mobile/tests/unit/sessionListView.test.ts
The view model defines grouping modes, activity windows, date and status ordering, agent fallbacks, stable activity floors, and unit tests.
Mobile filter and session table composition
web/mobile/src/features/sessions/SessionListScreen.tsx, web/mobile/src/features/sessions/SessionFilterMenu.tsx, web/mobile/src/features/sessions/SessionListTable.tsx
The screen composes persisted view state, debounced search, filter controls, grouped sessions, pagination, and empty-list handling.
Session row actions and states
web/mobile/src/features/sessions/SessionRowCells.tsx, web/mobile/src/features/sessions/SessionRowMenu.tsx, web/mobile/src/features/sessions/states/*
Rows support status, rename, pin, agent, activity, and menu actions. Empty, error, and no-match states are provided.
Sticky ListTable support
web/packages/agenta-ui/src/list-table/*
ListTable adds optional sticky headers and group headings, header classes, grouped row hover behavior, and adjusted skeleton sizing.
Sessions loading surface
web/mobile/src/features/sessions/pageFrame.ts, web/mobile/src/features/sessions/states/SessionsPageSkeleton.tsx, web/mobile/src/pages/w/[workspace_id]/p/[project_id]/sessions/index.tsx
The route renders a table-aligned loading skeleton with shared page spacing while project identifiers load.
Shared session status styling
web/mobile/scripts/generate-shadcn-tokens.ts, web/packages/agenta-sessions/src/row/sessionRowStatus.ts
Run-status tokens and session row status dots use shared hues for waiting, running, and idle states.

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
Loading

Merge Risk: 🔵 Low · up to e90ab

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)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: rebuilding the mobile sessions list.
Description check ✅ Passed The description is directly related to the changeset and explains the mobile sessions redesign, filtering behavior, shared-layer updates, fixes, tests, and QA coverage.
Docstring Coverage ✅ Passed Docstring coverage is 60.00% which is sufficient. The required threshold is 60.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 23 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mobile-sessions-redesign

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ashrafchowdury ashrafchowdury changed the title [feat] Rebuild the mobile sessions list on the automations table [feat] Rebuild the mobile sessions list Sep 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
web/mobile/src/features/sessions/states/SessionsPageSkeleton.tsx (1)

62-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the inline grid style with a Tailwind utility. Use grid-cols-[minmax(160px,2fr)_minmax(120px,1fr)_96px] in className and remove SKELETON_GRID. This follows the checked-in frontend styling rule for web/mobile.

web/mobile/src/features/sessions/SessionListScreen.tsx (1)

31-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reduce the sessions comments to one short line. web/AGENTS.md applies this rule to web/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

📥 Commits

Reviewing files that changed from the base of the PR and between f6dac65 and 9c21639.

📒 Files selected for processing (21)
  • web/mobile/src/features/sessions/SessionFilterMenu.tsx
  • web/mobile/src/features/sessions/SessionListScreen.tsx
  • web/mobile/src/features/sessions/SessionListTable.tsx
  • web/mobile/src/features/sessions/SessionRowCells.tsx
  • web/mobile/src/features/sessions/SessionRowMenu.tsx
  • web/mobile/src/features/sessions/pageFrame.ts
  • web/mobile/src/features/sessions/sessionListView.ts
  • web/mobile/src/features/sessions/states/SessionsEmpty.tsx
  • web/mobile/src/features/sessions/states/SessionsError.tsx
  • web/mobile/src/features/sessions/states/SessionsNoMatch.tsx
  • web/mobile/src/features/sessions/states/SessionsPageSkeleton.tsx
  • web/mobile/src/pages/w/[workspace_id]/p/[project_id]/sessions/index.tsx
  • web/mobile/tests/unit/sessionListView.test.ts
  • web/packages/agenta-entities/src/session/state/listOptions.ts
  • web/packages/agenta-sessions/src/state/filters.ts
  • web/packages/agenta-sessions/src/state/index.ts
  • web/packages/agenta-sessions/src/state/useSessionList.ts
  • web/packages/agenta-sessions/src/state/useSessionsList.ts
  • web/packages/agenta-shared/src/hooks/useDebouncedAtomSearch.ts
  • web/packages/agenta-ui/src/list-table/ListTable.tsx
  • web/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.

Comment thread web/mobile/src/features/sessions/SessionListScreen.tsx Outdated
Comment thread web/packages/agenta-sessions/src/state/useSessionsList.ts
Comment thread web/packages/agenta-shared/src/hooks/useDebouncedAtomSearch.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
web/mobile/scripts/generate-shadcn-tokens.ts (1)

287-290: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Apply 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9c21639 and 8276e03.

⛔ Files ignored due to path filters (1)
  • web/mobile/src/styles/theme.generated.css is excluded by !**/*.generated.*
📒 Files selected for processing (3)
  • web/mobile/scripts/generate-shadcn-tokens.ts
  • web/mobile/src/features/sessions/SessionFilterMenu.tsx
  • web/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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8276e03 and 46f5ed2.

📒 Files selected for processing (6)
  • web/mobile/src/features/sessions/SessionListScreen.tsx
  • web/mobile/src/features/sessions/SessionListTable.tsx
  • web/mobile/src/features/sessions/SessionRowCells.tsx
  • web/mobile/src/features/sessions/states/SessionsPageSkeleton.tsx
  • web/packages/agenta-sessions/src/state/useSessionsList.ts
  • web/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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cancel pending drafts on scope reset; an unchanged initial leaves a pending search to overwrite the reset.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-pr-6735.up.railway.app/w
Project agenta-oss-clone-spike
Image tag pr-6735-78e07ce
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-09-10T12:58:19.115Z

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.
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Advance activityFloor at the next hour boundary while a bounded activity filter is active. SessionListScreen memoizes it only by view.activity. useSessionList sends 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8ec8c56 and e90abf2.

📒 Files selected for processing (4)
  • web/mobile/src/features/sessions/SessionListTable.tsx
  • web/mobile/src/features/sessions/SessionRowCells.tsx
  • web/packages/agenta-sessions/src/row/sessionRowStatus.ts
  • web/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 ? (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 = ({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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]"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment on lines +49 to +50
role="img"
aria-label={status.label}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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

Comment on lines +24 to +31
/**
* 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.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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 the STICKY comment 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-L71
  • web/packages/agenta-ui/src/list-table/ListTable.tsx#L78-L81
  • web/packages/agenta-ui/src/list-table/ListTable.tsx#L119-L121
  • web/packages/agenta-ui/src/list-table/ListTable.tsx#L138-L144
  • web/packages/agenta-ui/src/list-table/ListTable.tsx#L155-L156
  • web/packages/agenta-ui/src/list-table/ListTable.tsx#L207-L210

Source: Coding guidelines

@ashrafchowdury
ashrafchowdury removed this pull request from stack #6736 September 10, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant