Skip to content

feat(mobile): readable Agents + Cluster views at 390px wide#251

Merged
jaylfc merged 4 commits intomasterfrom
feat/mobile-agents-cluster-polish
Apr 24, 2026
Merged

feat(mobile): readable Agents + Cluster views at 390px wide#251
jaylfc merged 4 commits intomasterfrom
feat/mobile-agents-cluster-polish

Conversation

@jaylfc
Copy link
Copy Markdown
Owner

@jaylfc jaylfc commented Apr 24, 2026

Summary

Two taOS mobile-PWA views were unreadable on narrow screens. Fixed without changing desktop layout.

Agents view

The agent row packed the ID, status chip, and four icon actions into a single flex line — at 390px the name collapsed to "0.2..." and the actions fought the chip. Gated the single-row layout behind useIsMobile; on mobile the row now stacks:

  1. dot + emoji + name + status chip
  2. host (with server icon) + vectors count
  3. paused/disk-warn chips (conditional)
  4. action buttons at 44×44 for touch targets

Toolbar: min-w-0 on the left group and shrink-0 on Deploy Agent so the CTA never gets squeezed out.

Cluster view

The fixed w-72 aside next to the worker card forced the right-hand Hardware panel into a ~30px column on a 390px viewport, wrapping "x86_64" and breaking the CPU model one character per line.

Replaced the split with MobileSplitView (already used by MessagesApp). Mobile gets iOS-style one-pane-at-a-time slide nav; desktop keeps side-by-side layout. Worker detail header also flex-wraps now so "Open worker UI" drops below the name when cramped, URL uses break-all, and the "Sort by" label is sr-only to reclaim width.

Changes

  • desktop/src/apps/AgentsApp.tsx — mobile stacked row + toolbar flex fix.
  • desktop/src/apps/ClusterApp.tsx — MobileSplitView, wrapping header, sr-only label.
  • desktop/src/apps/__tests__/AgentsApp.mobile.test.tsx — 2 mobile-layout invariants.

Style reference

Matched MessagesApp conventions: useIsMobile hook, MobileSplitView component, shrink-0 / min-w-0 flex discipline, text-shell-text-* tokens. No new design primitives.

Test plan

  • Frontend suite — 304 passed / 3 pre-existing snap-zones failures unchanged.
  • Mobile-layout assertions: name + chip visible at 390px, all action buttons have aria-label.
  • TypeScript clean on changed files.
  • Live visual check on device.

Summary by CodeRabbit

  • Style

    • Enhanced mobile layouts for Agents and Cluster apps with improved responsiveness
    • Improved touch targets and optimized text handling for better mobile user experience
    • Redesigned agent display with mobile-friendly card layout
  • Tests

    • Added mobile layout tests to verify responsive behavior on mobile devices

jaylfc added 2 commits April 24, 2026 01:32
At 390px the agent name, status chip, and four icon action buttons all
competed for a single flex row, so the name truncated to "0.2..." and the
actions collided with the chip. Gated the single-row layout behind
useIsMobile and dropped a four-row mobile layout when small:

  row 1: dot + emoji + name + status chip
  row 2: host (with server icon) + vectors count
  row 3: paused/disk-warn chips (conditional)
  row 4: action buttons at 44x44 for touch targets

Toolbar gets min-w-0 on the left group and shrink-0 on the Deploy Agent
button so the CTA never gets squeezed out.
The fixed w-72 aside next to the worker card forced the right-hand
Hardware panel into a 30px column on a 390px viewport, wrapping
"x86_64" across multiple lines and breaking "Intel(R) Core(TM)..." one
character per line.

Reuse MobileSplitView (already used by MessagesApp) so mobile shows the
worker list and detail one pane at a time with iOS-style slide nav,
while desktop keeps the side-by-side layout unchanged.

Also: flex-wrap the worker detail header so "Open worker UI" button
drops below the name when cramped; break-all on the URL; sr-only the
"Sort by" label to buy back ~50px of toolbar width.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Warning

Rate limit exceeded

@jaylfc has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 34 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 10 minutes and 34 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0fd13882-b093-4769-b7e8-938b669a7c22

📥 Commits

Reviewing files that changed from the base of the PR and between cd39685 and dc80b5c.

📒 Files selected for processing (2)
  • desktop/src/apps/ClusterApp.tsx
  • desktop/src/apps/__tests__/AgentsApp.mobile.test.tsx
📝 Walkthrough

Walkthrough

These changes implement mobile-responsive UI layouts for the Agents and Cluster applications by introducing mobile-specific card designs, state-driven split views, and adjusted toolbar layouts, accompanied by a new mobile test suite to verify mobile rendering behavior.

Changes

Cohort / File(s) Summary
Mobile Layout Refactoring
desktop/src/apps/AgentsApp.tsx, desktop/src/apps/ClusterApp.tsx
AgentsApp restructures agent row rendering with useIsMobile() hook to display a mobile card layout with centralized action buttons, while maintaining desktop flex row layout. ClusterApp introduces MobileSplitView for mobile master-detail navigation. Both components adjust toolbar layouts for narrow viewports, including gap-based flex behavior and truncation updates.
Mobile Testing
desktop/src/apps/__tests__/AgentsApp.mobile.test.tsx
New test suite verifying AgentsApp mobile layout at 390px viewport by mocking useIsMobile() and stubbing subcomponents, checking for agent display details and validating action button presence and accessibility names.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 Mobile layouts hop and spring,
Responsive cards do their thing!
Split views dance on narrow screens,
Buttons cluster, crisp and clean—
A rabbit's dream: each tap just right! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: responsive mobile layout improvements for Agents and Cluster views at 390px viewport width, matching the file changes and PR objectives.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mobile-agents-cluster-polish

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
desktop/src/apps/AgentsApp.tsx (1)

138-138: Avoid running useIsMobile() inside every AgentRow.

Each row instance adds its own media-query subscription. With many agents, listener count and resize work scale linearly. Compute once in AgentsApp and pass isMobile as a prop.

♻️ Suggested refactor
- function AgentRow({ ... }) {
-   const isMobile = useIsMobile();
+ function AgentRow({ isMobile, ... }: { isMobile: boolean; ... }) {
export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
+ const isMobile = useIsMobile();
  ...
  {agents.map((agent) => (
    <AgentRow
      key={agent.name}
+     isMobile={isMobile}
      agent={agent}
      ...
    />
  ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/AgentsApp.tsx` at line 138, The component currently calls
useIsMobile() inside every AgentRow which creates a media-query subscription per
row; instead compute isMobile once in AgentsApp using useIsMobile() (the
existing const isMobile = useIsMobile() in AgentsApp) and pass that boolean down
as a prop (e.g., <AgentRow isMobile={isMobile} ...>), then remove/replace any
useIsMobile() calls inside AgentRow and use the passed prop (isMobile) for
conditional rendering/behavior.
desktop/src/apps/__tests__/AgentsApp.mobile.test.tsx (1)

71-96: Prefer restoring/stubbing fetch to avoid test cross-contamination.

Directly assigning global.fetch in beforeEach works here, but it’s safer to restore mocks after each test.

🧪 Suggested hygiene update
-import { describe, it, expect, vi, beforeEach } from "vitest";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

 beforeEach(() => {
   ...
-  global.fetch = vi.fn().mockImplementation((url: string) => {
+  vi.stubGlobal("fetch", vi.fn().mockImplementation((url: string) => {
     ...
-  });
+  }));
 });
+
+afterEach(() => {
+  vi.unstubAllGlobals();
+  vi.restoreAllMocks();
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/__tests__/AgentsApp.mobile.test.tsx` around lines 71 - 96,
The test directly assigns global.fetch in beforeEach which can leak into other
tests; replace the assignment with vi.stubGlobal('fetch', vi.fn(...)) using the
same mock implementation, and add an afterEach(() => vi.unstubAllGlobals()) to
restore the global fetch; reference beforeEach, global.fetch, vi.fn,
vi.stubGlobal, and vi.unstubAllGlobals so you can locate and update the test
setup and teardown.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@desktop/src/apps/__tests__/AgentsApp.mobile.test.tsx`:
- Line 18: The mock availableKvQuantOptions is declared async, returning a
Promise, but production usage is synchronous and the app consumes it in a state
updater; change the mock for availableKvQuantOptions to be a synchronous
function that directly returns the options object (remove async and the implicit
Promise) so consumers receive the plain object instead of a Promise; update the
mock definition where availableKvQuantOptions is declared in the test to return
{ k: ["fp16"], v: ["fp16"], boundary: false, flat: ["fp16"] } synchronously.

In `@desktop/src/apps/ClusterApp.tsx`:
- Around line 672-677: The mobile Back handler clears selection via
setSelected(null) but the worker polling logic re-selects the first worker
whenever selected === null; fix by adding a short-lived user-driven suppression
flag (e.g., suppressAutoSelect state) and set it true in the MobileSplitView
onBack handler before calling setSelected(null), then update the polling/refresh
code that currently auto-selects the first worker (the routine that assigns
selected when selected === null) to skip auto-selection while suppressAutoSelect
is true; clear suppressAutoSelect when the user explicitly selects a worker or
after the next successful poll/timeout so normal auto-selection resumes.

---

Nitpick comments:
In `@desktop/src/apps/__tests__/AgentsApp.mobile.test.tsx`:
- Around line 71-96: The test directly assigns global.fetch in beforeEach which
can leak into other tests; replace the assignment with vi.stubGlobal('fetch',
vi.fn(...)) using the same mock implementation, and add an afterEach(() =>
vi.unstubAllGlobals()) to restore the global fetch; reference beforeEach,
global.fetch, vi.fn, vi.stubGlobal, and vi.unstubAllGlobals so you can locate
and update the test setup and teardown.

In `@desktop/src/apps/AgentsApp.tsx`:
- Line 138: The component currently calls useIsMobile() inside every AgentRow
which creates a media-query subscription per row; instead compute isMobile once
in AgentsApp using useIsMobile() (the existing const isMobile = useIsMobile() in
AgentsApp) and pass that boolean down as a prop (e.g., <AgentRow
isMobile={isMobile} ...>), then remove/replace any useIsMobile() calls inside
AgentRow and use the passed prop (isMobile) for conditional rendering/behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d3c3603d-0059-474b-8fb5-8db27b136c82

📥 Commits

Reviewing files that changed from the base of the PR and between c208d8a and cd39685.

📒 Files selected for processing (3)
  • desktop/src/apps/AgentsApp.tsx
  • desktop/src/apps/ClusterApp.tsx
  • desktop/src/apps/__tests__/AgentsApp.mobile.test.tsx

Comment thread desktop/src/apps/__tests__/AgentsApp.mobile.test.tsx Outdated
Comment thread desktop/src/apps/ClusterApp.tsx Outdated
Comment on lines +672 to +677
<MobileSplitView
listTitle="Cluster"
detailTitle={selectedWorker?.name ?? ""}
listWidth={288}
selectedId={selected}
onBack={() => setSelected(null)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Back navigation is overridden by the next worker refresh.

Line 677 clears selection, but the polling path re-selects the first worker whenever selected === null (Lines 548-551). On mobile, users can get pushed back into detail view ~10s after pressing Back.

💡 Suggested fix
+ const [userClearedSelection, setUserClearedSelection] = useState(false);

  const fetchWorkers = useCallback(async () => {
    try {
      const res = await fetch("/api/cluster/workers", { headers: { Accept: "application/json" } });
      if (res.ok) {
        const json = await res.json();
        if (Array.isArray(json)) {
          setWorkers(json as ClusterWorker[]);
          setSelected((cur) => {
            if (cur && json.some((w: ClusterWorker) => w.name === cur)) return cur;
+           if (cur === null && userClearedSelection) return null;
            return json.length > 0 ? (json[0] as ClusterWorker).name : null;
          });
        }
      }
    } catch {
      /* ignore */
    }
    setLoading(false);
- }, []);
+ }, [userClearedSelection]);

  ...
  <MobileSplitView
    ...
-   onBack={() => setSelected(null)}
+   onBack={() => {
+     setUserClearedSelection(true);
+     setSelected(null);
+   }}
    list={
      ...
      {sortedWorkers.map((w) => (
        <WorkerListCard
          key={w.name}
          worker={w}
          selected={selected === w.name}
-         onSelect={() => setSelected(w.name)}
+         onSelect={() => {
+           setUserClearedSelection(false);
+           setSelected(w.name);
+         }}
        />
      ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/ClusterApp.tsx` around lines 672 - 677, The mobile Back
handler clears selection via setSelected(null) but the worker polling logic
re-selects the first worker whenever selected === null; fix by adding a
short-lived user-driven suppression flag (e.g., suppressAutoSelect state) and
set it true in the MobileSplitView onBack handler before calling
setSelected(null), then update the polling/refresh code that currently
auto-selects the first worker (the routine that assigns selected when selected
=== null) to skip auto-selection while suppressAutoSelect is true; clear
suppressAutoSelect when the user explicitly selects a worker or after the next
successful poll/timeout so normal auto-selection resumes.

jaylfc added 2 commits April 24, 2026 02:34
Production availableKvQuantOptions() in lib/cluster.ts is synchronous;
the async mock did not match the real signature.
When the user hits 'back' in the mobile split-view the next 10s poll
re-selected json[0] and pushed them back into the detail pane.

Add a userNavigatedBack ref. Set it true in onBack, clear it false when
the user manually taps a WorkerListCard. fetchWorkers now skips
auto-selecting the first worker while the ref is set.
@jaylfc jaylfc merged commit 01ae49a into master Apr 24, 2026
8 checks passed
@jaylfc jaylfc deleted the feat/mobile-agents-cluster-polish branch April 24, 2026 09:47
jaylfc added a commit that referenced this pull request Apr 24, 2026
…257)

Previous PRs (#251 mobile polish, #254 desktop icons, #255 install-to-
worker picker) committed only the TypeScript sources under desktop/src/
without rebuilding the vite output under static/desktop/. The Pi serves
the compiled bundles directly, so the browser never loaded the new code
and the mobile Agents/Cluster views, service icons, and worker picker
all silently rendered the pre-polish implementation.

Rebuild catches all three up in a single hashed-asset turn.
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