Skip to content

Add SortToggle element (Oldest First / Newest First) for the Logs, History, and Network lists #1371

@cliffhall

Description

@cliffhall

Summary

Add a reusable SortToggle element — a small Mantine Select with two options, "Sort: Oldest First" and "Sort: Newest First" — and wire it into the toolbars of the Logs, History, and Network screens so the user can flip chronological order. The control owns no list state itself; it just emits the selected direction.

None of those three screens currently exposes a chronological sort affordance, so this issue covers both creating the element and adopting it in the three places it belongs.

Proposed change

1. New element

clients/web/src/components/elements/SortToggle/SortToggle.tsx, mirroring the file layout of sibling elements (ListToggle/, ConnectionToggle/, etc.):

export type SortDirection = \"oldest-first\" | \"newest-first\";

export interface SortToggleProps {
  value: SortDirection;
  onChange: (next: SortDirection) => void;
  // Optional, for forms / tests
  \"aria-label\"?: string;
}

Implementation notes:

  • Use Mantine Select with data of two options whose label is the user-facing string (\"Sort: Oldest First\", \"Sort: Newest First\") and value is the SortDirection literal. Single-select, no clear button, no search.
  • Controlled component — value/onChange only, no internal state. Each screen owns its own direction.
  • Width should be intrinsic to the longest label so the control doesn't resize when the user flips direction. A fixed w (e.g. 180–200) is fine; pick whatever matches the existing ServerListControls / AppControls width conventions.
  • Theming: configure via Select.withProps() if a project-wide default applies (e.g. size=\"sm\", allowDeselect={false}). Per AGENTS.md, prefer props/theme variants over CSS classes.

2. Wire into the three screens

  • LoggingScreen (clients/web/src/components/screens/LoggingScreen/LoggingScreen.tsx) — add sortDirection and onSortChange props, render the toggle in the existing controls row, and sort the rendered entries based on direction (don't mutate the prop array — clone or useMemo the sorted view).
  • HistoryScreen (clients/web/src/components/screens/HistoryScreen/HistoryScreen.tsx) — same shape. The screen already does derived work via useMemo (see the entries.map(extractMethod) block at HistoryScreen.tsx:46); the sorted view fits the same pattern.
  • NetworkScreen (clients/web/src/components/screens/NetworkScreen/NetworkScreen.tsx) — same shape.

For each screen, the parent (App.tsx / InspectorView) holds the sortDirection state and passes it down. Default direction is \"newest-first\" for all three — that matches the typical "tail of a log" mental model and avoids the user having to scroll on first open. Confirm during implementation if any of the three has a stronger reason to default differently.

The underlying state sources (MessageLogState, FetchRequestLogState, the requestor-task history) should not have their internal ordering changed — sorting stays a presentation concern in the screen.

3. Persist the preference in localStorage

The user's chosen direction must survive page reloads. Use useLocalStorage from @mantine/hooks (already a dependency at clients/web/package.json:30) — it handles SSR-safe hydration, JSON serialization, and cross-tab sync via the storage event automatically, so we don't have to roll our own.

  • Three independent keys, one per screen, so toggling sort on the Logs screen doesn't change History or Network:
    • inspector.sortDirection.logs
    • inspector.sortDirection.history
    • inspector.sortDirection.network
  • Namespace with a shared inspector. prefix (matches what an mcp.json-flavored app would use as its localStorage namespace) so future preferences can co-exist without collisions and are easy to clear in bulk.
  • Default value passed to useLocalStorage is \"newest-first\". If the stored value isn't one of the two valid SortDirection literals (manual edit, schema drift, future option added then removed), fall back to the default rather than passing it through to the Select — invalid values would put the toggle in an unselectable state.
  • The hook lives in the parent that owns sortDirection (currently scoped to App.tsx / InspectorView). The SortToggle element itself stays purely controlled — it does not know about localStorage. This keeps the element trivially reusable in stories, tests, and any future non-persistent context.

Note that no current code in the repo uses localStorage directly (grep -rn localStorage clients/web/src returns nothing). This issue establishes the convention; later preferences (theme, compact-mode default, etc.) should follow the same inspector.<scope>.<key> pattern.

Acceptance criteria

  • SortToggle exists at clients/web/src/components/elements/SortToggle/ with .tsx, .test.tsx, and .stories.tsx.
  • Story coverage: both selected states (Oldest First, Newest First) and a play that flips the value and asserts onChange is called with the new direction.
  • LoggingScreen, HistoryScreen, and NetworkScreen each render the toggle in their toolbar and reorder their list in response. Screen-level tests cover the reorder.
  • Default direction is \"newest-first\" on all three screens unless implementation surfaces a reason to differ.
  • Each screen's selection persists to its own localStorage key under the inspector.sortDirection.* namespace and is restored on reload.
  • Invalid / unknown stored values fall back to \"newest-first\" rather than rendering an unselectable state.
  • Tests for the persistence behavior (round-trip set → reload → restored), including the fallback path for a corrupted stored value.
  • No flat CSS-in-JS or CSS classes for layout/styling that could be expressed as theme variants or .withProps() constants. No inline styles.
  • Per-file coverage gate (lines ≥ 90, statements ≥ 85, functions ≥ 80, branches ≥ 50) passes for the new file.

Out of scope

  • Multi-column sort, custom sort keys, or sort-by-anything-other-than-timestamp. Two options only.

Metadata

Metadata

Assignees

No one assigned

    Labels

    v2Issues and PRs for v2

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions