feat(FN-1141): improve dashboard mobile adaptations

- Extend mobile CSS for SettingsModal, AgentsView, ExecutorStatusBar, ActiveAgentsPanel, toast placement, and background task popovers
- Rename BackgroundTasksIndicator list/item class hooks to align with the updated responsive styling contract
- Add focused component tests covering settings, agents, and utility mobile adaptations plus key CSS media-rule assertions
- Document mobile component adaptation behavior and testing guidance in the dashboard README
This commit is contained in:
gsxdsm
2026-04-08 08:48:21 -07:00
parent 69621e78dc
commit 4eef067baa
6 changed files with 598 additions and 8 deletions

View File

@@ -160,6 +160,27 @@ Mobile dropdown behavior follows a consistent viewport-aware anchoring pattern s
- For scrollable dropdown lists and modal content containers, apply `-webkit-overflow-scrolling: touch` to preserve iOS momentum scrolling. - For scrollable dropdown lists and modal content containers, apply `-webkit-overflow-scrolling: touch` to preserve iOS momentum scrolling.
- Use base selectors for reusable scroll lists (for example: `.dep-dropdown`, `.model-combobox-list`, `.quick-scripts-dropdown__list`, `.file-browser-list`) and reinforce modal surfaces in the main mobile media query (`@media (max-width: 768px)`). - Use base selectors for reusable scroll lists (for example: `.dep-dropdown`, `.model-combobox-list`, `.quick-scripts-dropdown__list`, `.file-browser-list`) and reinforce modal surfaces in the main mobile media query (`@media (max-width: 768px)`).
### Mobile Component Adaptations
In addition to the global mobile foundation, several power-user surfaces now include component-specific mobile behavior tuned for touch interaction and narrow viewports (`≤768px`).
- **SettingsModal**
- The settings layout collapses from sidebar/content columns into a stacked flow (`.settings-layout` uses `flex-direction: column`).
- The sidebar becomes a horizontally scrollable tab strip (`.settings-sidebar` switches to row layout with hidden scrollbars and touch momentum scrolling).
- Settings content remains independently scrollable (`.settings-content` keeps `flex: 1; min-height: 0; overflow-y: auto`) so tabs stay reachable while content scrolls.
- Settings form controls inside `.settings-content` enforce `font-size: 16px` on mobile to prevent iOS zoom-on-focus.
- **AgentsView**
- Board mode collapses to a single column (`.agent-board { grid-template-columns: 1fr; }`).
- Controls stack vertically (`.agent-controls` + `.agent-controls-actions`) with full-width action buttons and touch-friendly sizing.
- State filter stretches full width on mobile.
- Tree-view indentation is reduced (`.agent-tree__indent--1..4`) to prevent horizontal overflow at deeper hierarchy levels.
- **Utility components**
- **BackgroundTasksIndicator** popover switches to fixed viewport anchoring on mobile (`left/right: 8px`, `bottom: 40px`) so it is not clipped by parent containers.
- **ExecutorStatusBar** mobile layout includes tighter spacing and overflow guards (`min-width: 0`, hidden overflow in segments) for narrow screens.
- **ActiveAgentsPanel** grid stacks to one column on mobile (`.active-agents-grid { grid-template-columns: 1fr; }`).
- **ToastContainer** shifts above footer surfaces with safe-area awareness (`bottom: calc(44px + env(safe-area-inset-bottom, 0px))`, full-width toasts).
### Executor Status Bar ### Executor Status Bar
A persistent footer status bar at the bottom of the dashboard displays real-time executor statistics in project view. The status bar provides immediate visibility into the engine's state without opening modals or hovering over badges. A persistent footer status bar at the bottom of the dashboard displays real-time executor statistics in project view. The status bar provides immediate visibility into the engine's state without opening modals or hovering over badges.

View File

@@ -68,7 +68,7 @@ export function BackgroundTasksIndicator({
<div className="background-tasks-indicator__popover-header"> <div className="background-tasks-indicator__popover-header">
Background Tasks Background Tasks
</div> </div>
<div className="background-tasks-indicator__sessions"> <div className="background-tasks-indicator__popover-list">
{sessions.map((session) => { {sessions.map((session) => {
const Icon = TYPE_ICONS[session.type]; const Icon = TYPE_ICONS[session.type];
const isGenerating = session.status === "generating"; const isGenerating = session.status === "generating";
@@ -77,7 +77,7 @@ export function BackgroundTasksIndicator({
return ( return (
<div <div
key={session.id} key={session.id}
className="background-tasks-indicator__session" className="background-tasks-indicator__item"
onClick={() => { onClick={() => {
onOpenSession(session); onOpenSession(session);
setPopoverOpen(false); setPopoverOpen(false);
@@ -112,7 +112,7 @@ export function BackgroundTasksIndicator({
/> />
)} )}
<button <button
className="background-tasks-indicator__dismiss" className="background-tasks-indicator__item-dismiss"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onDismissSession(session.id); onDismissSession(session.id);

View File

@@ -0,0 +1,135 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { AgentsView } from "../AgentsView";
import type { Agent, AgentCapability, AgentState } from "../../api";
vi.mock("../../api", () => ({
fetchAgents: vi.fn(),
fetchAgentStats: vi.fn(),
createAgent: vi.fn(),
updateAgent: vi.fn(),
updateAgentState: vi.fn(),
deleteAgent: vi.fn(),
startAgentRun: vi.fn(),
fetchModels: vi.fn(() => Promise.resolve({ models: [] })),
}));
import {
fetchAgents,
fetchAgentStats,
updateAgent,
updateAgentState,
deleteAgent,
startAgentRun,
} from "../../api";
const mockAgents: Agent[] = [
{
id: "agent-001",
name: "Mobile Executor",
role: "executor" as AgentCapability,
state: "active" as AgentState,
taskId: "FN-101",
lastHeartbeatAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
},
{
id: "agent-002",
name: "Mobile Reviewer",
role: "reviewer" as AgentCapability,
state: "idle" as AgentState,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
},
];
const eventSourceFactory = vi.fn(() => ({
addEventListener: vi.fn(),
close: vi.fn(),
}));
describe("AgentsView mobile adaptations", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
vi.stubGlobal("EventSource", eventSourceFactory as unknown as typeof EventSource);
vi.mocked(fetchAgents).mockResolvedValue(mockAgents);
vi.mocked(fetchAgentStats).mockResolvedValue({
total: 2,
byState: { active: 1, idle: 1 },
byRole: { executor: 1, reviewer: 1 },
});
vi.mocked(updateAgent).mockResolvedValue(mockAgents[0]);
vi.mocked(updateAgentState).mockResolvedValue(mockAgents[0]);
vi.mocked(deleteAgent).mockResolvedValue(undefined);
vi.mocked(startAgentRun).mockResolvedValue({
id: "run-1",
agentId: "agent-001",
startedAt: new Date().toISOString(),
endedAt: null,
status: "active",
});
});
it("renders board view grid and board cards", async () => {
const { container } = render(<AgentsView addToast={vi.fn()} />);
await waitFor(() => expect(screen.getByText("Agents")).toBeTruthy());
fireEvent.click(screen.getByRole("button", { name: "Board view" }));
await waitFor(() => {
expect(container.querySelector(".agent-board")).toBeTruthy();
expect(container.querySelectorAll(".agent-board-card").length).toBeGreaterThan(0);
});
});
it("renders list view cards", async () => {
const { container } = render(<AgentsView addToast={vi.fn()} />);
await waitFor(() => expect(screen.getByText("Agents")).toBeTruthy());
fireEvent.click(screen.getByRole("button", { name: "List view" }));
await waitFor(() => {
expect(container.querySelector(".agent-list")).toBeTruthy();
expect(container.querySelectorAll(".agent-card").length).toBeGreaterThan(0);
});
});
it("renders agent controls, filter, and action buttons", async () => {
const { container } = render(<AgentsView addToast={vi.fn()} />);
await waitFor(() => expect(screen.getByText("Agents")).toBeTruthy());
expect(container.querySelector(".agent-controls")).toBeTruthy();
expect(container.querySelector(".agent-state-filter")).toBeTruthy();
expect(container.querySelector(".agent-controls-actions")).toBeTruthy();
});
it("switches between board, list, and tree views", async () => {
const { container } = render(<AgentsView addToast={vi.fn()} />);
await waitFor(() => expect(screen.getByText("Agents")).toBeTruthy());
fireEvent.click(screen.getByRole("button", { name: "Tree view" }));
await waitFor(() => expect(container.querySelector(".agent-tree__view")).toBeTruthy());
fireEvent.click(screen.getByRole("button", { name: "Board view" }));
await waitFor(() => expect(container.querySelector(".agent-board")).toBeTruthy());
fireEvent.click(screen.getByRole("button", { name: "List view" }));
await waitFor(() => expect(container.querySelector(".agent-list")).toBeTruthy());
});
it("renders state filter select with expected options", async () => {
render(<AgentsView addToast={vi.fn()} />);
await waitFor(() => expect(screen.getByLabelText("Filter agents by state")).toBeTruthy());
const select = screen.getByLabelText("Filter agents by state") as HTMLSelectElement;
expect(select).toBeTruthy();
const optionValues = Array.from(select.options).map((option) => option.value);
expect(optionValues).toEqual(["all", "idle", "active", "running", "paused", "error", "terminated"]);
});
});

View File

@@ -0,0 +1,121 @@
import fs from "node:fs";
import path from "node:path";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SettingsModal } from "../SettingsModal";
import type { Settings } from "@fusion/core";
const stylesPath = path.resolve(__dirname, "../../styles.css");
const defaultSettings = {
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 15_000,
groupOverlappingFiles: false,
autoMerge: true,
mergeStrategy: "direct",
recycleWorktrees: false,
worktreeInitCommand: "",
testCommand: "",
buildCommand: "",
autoResolveConflicts: true,
smartConflictResolution: true,
modelPresets: [],
autoSelectModelPreset: false,
defaultPresetBySize: {},
ntfyEnabled: false,
ntfyTopic: undefined,
ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval"],
taskStuckTimeoutMs: undefined,
maxStuckKills: 6,
runStepsInNewSessions: false,
maxParallelSteps: 2,
} as Settings;
vi.mock("../../api", () => ({
fetchSettings: vi.fn(() => Promise.resolve({ ...defaultSettings })),
updateSettings: vi.fn(() => Promise.resolve({ ...defaultSettings })),
updateGlobalSettings: vi.fn(() => Promise.resolve({ ...defaultSettings })),
fetchAuthStatus: vi.fn(() => Promise.resolve({ providers: [{ id: "anthropic", name: "Anthropic", authenticated: false }] })),
loginProvider: vi.fn(() => Promise.resolve({ url: "https://auth.example.com/login" })),
logoutProvider: vi.fn(() => Promise.resolve({ success: true })),
saveApiKey: vi.fn(() => Promise.resolve({ success: true })),
clearApiKey: vi.fn(() => Promise.resolve({ success: true })),
fetchModels: vi.fn(() => Promise.resolve({ models: [], favoriteProviders: [], favoriteModels: [] })),
testNtfyNotification: vi.fn(() => Promise.resolve({ success: true })),
fetchBackups: vi.fn(() => Promise.resolve({ count: 0, totalSize: 0, backups: [] })),
createBackup: vi.fn(() => Promise.resolve({ success: true })),
exportSettings: vi.fn(() => Promise.resolve({ success: true, data: {} })),
importSettings: vi.fn(() => Promise.resolve({ success: true })),
fetchMemory: vi.fn(() => Promise.resolve({ memory: "" })),
saveMemory: vi.fn(() => Promise.resolve({ success: true })),
}));
import { fetchSettings } from "../../api";
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function expectMobileRule(css: string, selector: string, declaration: string): void {
const pattern = new RegExp(
`@media\\s*\\(max-width:\\s*768px\\)\\s*\\{[\\s\\S]*?${escapeRegExp(selector)}\\s*\\{[\\s\\S]*?${escapeRegExp(declaration)}`,
);
expect(pattern.test(css)).toBe(true);
}
describe("SettingsModal mobile adaptations", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders mobile-targeted settings layout classes", async () => {
const { container } = render(<SettingsModal onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
expect(container.querySelector(".settings-layout")).toBeTruthy();
expect(container.querySelector(".settings-sidebar")).toBeTruthy();
expect(container.querySelector(".settings-content")).toBeTruthy();
});
it("renders settings nav items with active class for touch styling", async () => {
const { container } = render(<SettingsModal onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
const navItems = container.querySelectorAll(".settings-nav-item");
expect(navItems.length).toBeGreaterThan(0);
expect(container.querySelector(".settings-nav-item.active")).toBeTruthy();
});
it("renders form controls inside settings-content for 16px mobile targeting", async () => {
const { container } = render(<SettingsModal onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
const controls = container.querySelectorAll(".settings-content input, .settings-content select, .settings-content textarea");
expect(controls.length).toBeGreaterThan(0);
});
it("shows scope indicators and updates scope banner across sections", async () => {
const user = userEvent.setup();
const { container, getByText } = render(<SettingsModal onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
expect(container.querySelectorAll(".settings-scope-icon").length).toBeGreaterThan(0);
expect(getByText("These settings only affect this project.")).toBeTruthy();
await user.click(getByText("Appearance"));
expect(getByText("These settings are shared across all your kb projects.")).toBeTruthy();
});
it("contains required mobile settings CSS overrides", () => {
const css = fs.readFileSync(stylesPath, "utf-8");
expectMobileRule(css, ".settings-layout", "flex-direction: column;");
expectMobileRule(css, ".settings-sidebar", "flex-direction: row;");
expectMobileRule(css, ".settings-sidebar", "overflow-x: auto;");
expectMobileRule(css, ".settings-sidebar", "scrollbar-width: none;");
expectMobileRule(css, ".settings-sidebar::-webkit-scrollbar", "display: none;");
expectMobileRule(css, ".settings-content textarea", "font-size: 16px;");
});
});

View File

@@ -0,0 +1,183 @@
import fs from "node:fs";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import type { Agent, AiSessionSummary } from "../../api";
import type { Toast } from "../../hooks/useToast";
vi.mock("../../hooks/useExecutorStats", () => ({
useExecutorStats: vi.fn(),
}));
vi.mock("../../hooks/useLiveTranscript", () => ({
useLiveTranscript: vi.fn(() => ({
entries: [],
isConnected: false,
})),
}));
import { useExecutorStats } from "../../hooks/useExecutorStats";
import { BackgroundTasksIndicator } from "../BackgroundTasksIndicator";
import { ExecutorStatusBar } from "../ExecutorStatusBar";
import { ActiveAgentsPanel } from "../ActiveAgentsPanel";
import { ToastContainer } from "../ToastContainer";
const stylesPath = path.resolve(__dirname, "../../styles.css");
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function expectMobileRule(css: string, selector: string, declaration: string): void {
const pattern = new RegExp(
`@media\\s*\\(max-width:\\s*768px\\)\\s*\\{[\\s\\S]*?${escapeRegExp(selector)}\\s*\\{[\\s\\S]*?${escapeRegExp(declaration)}`,
);
expect(pattern.test(css)).toBe(true);
}
describe("Utility component mobile adaptations", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useExecutorStats).mockReturnValue({
stats: {
runningTaskCount: 1,
blockedTaskCount: 2,
stuckTaskCount: 0,
queuedTaskCount: 3,
inReviewCount: 4,
executorState: "running",
maxConcurrent: 5,
lastActivityAt: new Date().toISOString(),
},
loading: false,
error: null,
refresh: vi.fn(),
});
});
it("renders BackgroundTasksIndicator pill when sessions exist", () => {
const sessions: AiSessionSummary[] = [
{
id: "sess-1",
type: "planning",
status: "generating",
title: "Refine onboarding flow",
projectId: "proj-1",
updatedAt: new Date().toISOString(),
},
];
render(
<BackgroundTasksIndicator
sessions={sessions}
generating={1}
needsInput={0}
onOpenSession={vi.fn()}
onDismissSession={vi.fn()}
/>,
);
expect(screen.getByRole("button", { name: /AI 1/i })).toBeTruthy();
});
it("renders BackgroundTasksIndicator popover on pill click", () => {
const sessions: AiSessionSummary[] = [
{
id: "sess-2",
type: "subtask",
status: "awaiting_input",
title: "Break down API tasks",
projectId: "proj-1",
updatedAt: new Date().toISOString(),
},
];
render(
<BackgroundTasksIndicator
sessions={sessions}
generating={0}
needsInput={1}
onOpenSession={vi.fn()}
onDismissSession={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /AI 1/i }));
expect(screen.getByText("Background Tasks")).toBeTruthy();
expect(screen.getByText("Break down API tasks")).toBeTruthy();
});
it("returns null for BackgroundTasksIndicator with no sessions", () => {
const { container } = render(
<BackgroundTasksIndicator
sessions={[]}
generating={0}
needsInput={0}
onOpenSession={vi.fn()}
onDismissSession={vi.fn()}
/>,
);
expect(container.firstChild).toBeNull();
});
it("renders ExecutorStatusBar segments", () => {
render(<ExecutorStatusBar tasks={[]} />);
const bar = screen.getByRole("status");
expect(bar).toHaveTextContent("Running");
expect(bar).toHaveTextContent("Blocked");
expect(bar).toHaveTextContent("Queued");
expect(bar).toHaveTextContent("In Review");
});
it("renders ActiveAgentsPanel grid and cards when agents are provided", () => {
const agents: Agent[] = [
{
id: "agent-1",
name: "Live Agent",
role: "executor",
state: "active",
taskId: "FN-555",
lastHeartbeatAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
},
];
const { container } = render(<ActiveAgentsPanel agents={agents} />);
expect(container.querySelector(".active-agents-grid")).toBeTruthy();
expect(container.querySelectorAll(".live-agent-card").length).toBe(1);
});
it("returns null for ActiveAgentsPanel when no agents are active", () => {
const { container } = render(<ActiveAgentsPanel agents={[]} />);
expect(container.firstChild).toBeNull();
});
it("renders toasts in ToastContainer", () => {
const toasts: Toast[] = [
{ id: 1, message: "Saved", type: "success" },
{ id: 2, message: "Failed", type: "error" },
];
const { container } = render(<ToastContainer toasts={toasts} onRemove={vi.fn()} />);
expect(container.querySelector(".toast-container")).toBeTruthy();
expect(container.querySelector(".toast-success")).toBeTruthy();
expect(container.querySelector(".toast-error")).toBeTruthy();
});
it("contains mobile CSS overrides for adapted utility and layout components", () => {
const css = fs.readFileSync(stylesPath, "utf-8");
expectMobileRule(css, ".settings-layout", "flex-direction: column;");
expectMobileRule(css, ".agent-board", "grid-template-columns: 1fr;");
expectMobileRule(css, ".active-agents-grid", "grid-template-columns: 1fr;");
expectMobileRule(css, ".toast-container", "bottom: calc(44px + env(safe-area-inset-bottom, 0px));");
expectMobileRule(css, ".background-tasks-indicator__popover", "position: fixed;");
});
});

View File

@@ -4422,6 +4422,19 @@ body {
background: var(--color-info); background: var(--color-info);
} }
@media (max-width: 768px) {
.toast-container {
bottom: calc(44px + env(safe-area-inset-bottom, 0px));
right: 8px;
left: 8px;
}
.toast {
width: 100%;
font-size: 14px;
}
}
@keyframes toast-in { @keyframes toast-in {
from { from {
opacity: 0; opacity: 0;
@@ -5630,10 +5643,15 @@ body {
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
overflow-x: auto; overflow-x: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
scrollbar-width: none;
padding: 6px 8px; padding: 6px 8px;
gap: 2px; gap: 2px;
} }
.settings-sidebar::-webkit-scrollbar {
display: none;
}
.settings-nav-item { .settings-nav-item {
border-left: none; border-left: none;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
@@ -5656,6 +5674,13 @@ body {
.settings-content { .settings-content {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow-y: auto;
}
.settings-content input,
.settings-content select,
.settings-content textarea {
font-size: 16px;
} }
.settings-section-heading { .settings-section-heading {
@@ -5667,12 +5692,23 @@ body {
padding: 0 14px; padding: 0 14px;
} }
.settings-content .btn,
.auth-provider-row .btn {
min-height: 44px;
}
.auth-provider-row { .auth-provider-row {
flex-wrap: wrap; flex-wrap: wrap;
padding: 12px 14px; padding: 12px 14px;
gap: var(--space-sm); gap: var(--space-sm);
} }
.backup-list ul {
max-height: 220px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* Card edit button: always visible on mobile (no hover) */ /* Card edit button: always visible on mobile (no hover) */
.card-edit-btn { .card-edit-btn {
opacity: 1; opacity: 1;
@@ -18214,9 +18250,16 @@ html .column.drag-over * {
.executor-status-bar { .executor-status-bar {
padding: var(--space-xs) var(--space-md); padding: var(--space-xs) var(--space-md);
gap: var(--space-xs); gap: 4px;
font-size: 11px; font-size: 11px;
height: 32px; height: 32px;
overflow: hidden;
}
.executor-status-bar__segment {
min-width: 0;
gap: 4px;
overflow: hidden;
} }
.executor-status-bar__label { .executor-status-bar__label {
@@ -18226,6 +18269,10 @@ html .column.drag-over * {
.executor-status-bar__divider { .executor-status-bar__divider {
height: 12px; height: 12px;
} }
.background-tasks-indicator {
flex-shrink: 0;
}
} }
/* Light theme support */ /* Light theme support */
@@ -18300,12 +18347,12 @@ html .column.drag-over * {
color: var(--text-muted); color: var(--text-muted);
} }
.background-tasks-indicator__sessions { .background-tasks-indicator__popover-list {
max-height: 240px; max-height: 240px;
overflow-y: auto; overflow-y: auto;
} }
.background-tasks-indicator__session { .background-tasks-indicator__item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
@@ -18314,7 +18361,7 @@ html .column.drag-over * {
cursor: pointer; cursor: pointer;
} }
.background-tasks-indicator__session:last-child { .background-tasks-indicator__item:last-child {
border-bottom: none; border-bottom: none;
} }
@@ -18341,7 +18388,7 @@ html .column.drag-over * {
color: var(--text-muted); color: var(--text-muted);
} }
.background-tasks-indicator__dismiss { .background-tasks-indicator__item-dismiss {
flex-shrink: 0; flex-shrink: 0;
background: none; background: none;
border: none; border: none;
@@ -18351,6 +18398,21 @@ html .column.drag-over * {
border-radius: 4px; border-radius: 4px;
} }
@media (max-width: 768px) {
.background-tasks-indicator__popover {
position: fixed;
bottom: 40px;
left: 8px;
right: 8px;
min-width: auto;
max-width: none;
}
.background-tasks-indicator__pill {
min-height: 44px;
}
}
/* === DirectoryPicker === */ /* === DirectoryPicker === */
.directory-picker { .directory-picker {
display: flex; display: flex;
@@ -21631,6 +21693,63 @@ html .column.drag-over * {
} }
} }
@media (max-width: 768px) {
.agent-board {
grid-template-columns: 1fr;
}
.agent-board-actions .btn,
.agent-card-actions .btn {
min-height: 44px;
}
.agent-controls {
flex-direction: column;
align-items: stretch;
gap: var(--space-sm);
}
.agent-controls-actions {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.agent-controls-actions .btn {
width: 100%;
min-height: 44px;
}
.agent-state-filter {
width: 100%;
min-height: 44px;
}
.agent-state-filter-select {
width: 100%;
}
.agents-view-header {
padding: var(--space-sm) var(--space-md);
flex-wrap: wrap;
gap: var(--space-sm);
}
.agents-view-title h2 {
font-size: 16px;
}
.agent-tree__indent--1 { padding-left: 16px; }
.agent-tree__indent--2 { padding-left: 32px; }
.agent-tree__indent--3 { padding-left: 48px; }
.agent-tree__indent--4 { padding-left: 64px; }
.agent-tree__toggle {
min-width: 44px;
min-height: 44px;
}
}
/* === Active Agents Panel === */ /* === Active Agents Panel === */
.active-agents-panel { .active-agents-panel {
margin-bottom: var(--space-lg); margin-bottom: var(--space-lg);
@@ -21724,6 +21843,17 @@ html .column.drag-over * {
animation: pulse 1.5s infinite; animation: pulse 1.5s infinite;
} }
@media (max-width: 768px) {
.active-agents-grid {
grid-template-columns: 1fr;
}
.live-agent-card {
min-width: 0;
overflow: hidden;
}
}
/* === New Agent Dialog === */ /* === New Agent Dialog === */
.agent-dialog-overlay { .agent-dialog-overlay {
position: fixed; position: fixed;