web: Add Tasks to the monitoring sidebar (#1681)

* web: add Tasks to the monitoring sidebar (#1680)

Make Tasks a pinnable panel in the monitoring sidebar alongside
Logs / Protocol / Network / Console, so a live task list can be watched
beside any screen.

- InspectorView: add TASKS_TAB, extend the MonitorTab union / MONITOR_TABS
  / isMonitorTab, and add a monitorScreens[TASKS_TAB] entry built from a
  shared tasksScreenProps object (so the embedded and full-size instances
  can't drift), with the shared column search injected into its ui.search.
  The full-size Tasks screen now uses the same shared props.
- TasksScreen / TaskListPanel: add an embedded prop mirroring
  LoggingScreen/LogStreamPanel — when embedded the screen fills its column
  height and drops the controls sidebar, and TaskListPanel scrolls via
  EmbeddableScrollArea (whose full-size cap matches the old viewport calc,
  so the standalone screen is unchanged).

Tasks is capability-gated: it only appears in the monitor group when the
server advertises the tasks capability (via availableTabs), so no change
is needed to the failed-connect subset.

Closes #1680

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* web: embedded task list ignores the status filter (review #1681)

Mirror the sibling monitor panels: the embedded column exposes only the
shared search box (its status filter lives in the full-size sidebar,
which the embedded view drops), so a stale status filter must not
silently hide tasks with no visible control to clear it. Thread an
`ignoreStatus` flag into TaskListPanel.matchesFilters, set from the
`embedded` prop — matching LogStreamPanel's `ignoreLevels`,
NetworkStreamPanel's `ignoreCategories`, and ProtocolListPanel's
`ignoreDirectionAndMethod` (#1616). Add tests for the ignored-status /
still-applied-search embedded behavior, plus a direct assertion that the
Tasks radio appears in the monitor column.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-07-13 19:11:06 -04:00 • committed by GitHub
commit 590359abc5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 123 additions and 45 deletions

View file

@ -80,6 +80,36 @@ describe("TaskListPanel", () => {
expect(screen.queryByText(/^Completed/)).not.toBeInTheDocument();
});
it("ignores the status filter when embedded (only the search box is exposed there)", () => {
// A stale status filter from the full-size screen must not silently hide
// tasks in the embedded column, which has no control to see or clear it.
renderWithMantine(
<TaskListPanel
{...baseProps}
tasks={sampleTasks}
statusFilter="working"
embedded
/>,
);
expect(screen.getByText("Active (2)")).toBeInTheDocument();
expect(screen.getByText("Completed (2)")).toBeInTheDocument();
});
it("still applies the search text when embedded", () => {
renderWithMantine(
<TaskListPanel
{...baseProps}
tasks={sampleTasks}
statusFilter="working"
searchText="Connection"
embedded
/>,
);
// Status filter ignored, but the text filter narrows to the failed task.
expect(screen.getByText("Completed (1)")).toBeInTheDocument();
expect(screen.queryByText(/^Active/)).not.toBeInTheDocument();
});
it("filters tasks by search text matching statusMessage", () => {
renderWithMantine(
<TaskListPanel

View file

@ -1,16 +1,9 @@
import { useMemo, useState } from "react";
import {
Button,
Group,
Paper,
ScrollArea,
Stack,
Text,
Title,
} from "@mantine/core";
import { Button, Group, Paper, Stack, Text, Title } from "@mantine/core";
import type { Task, TaskStatus } from "@modelcontextprotocol/sdk/types.js";
import { TaskCard } from "../TaskCard/TaskCard";
import type { TaskProgress } from "../TaskCard/TaskCard";
import { EmbeddableScrollArea } from "../../elements/EmbeddableScrollArea/EmbeddableScrollArea";
import { ListToggle } from "../../elements/ListToggle/ListToggle";
import { useScrollMemory } from "../../../hooks/useScrollMemory";
@ -21,6 +14,13 @@ export interface TaskListPanelProps {
statusFilter?: TaskStatus;
onCancel: (taskId: string) => void;
onClearCompleted: () => void;
/**
* True when rendered inside the monitoring sidebar. Switches the scroll region
* from the viewport-height calc to filling its flex parent (via
* `EmbeddableScrollArea`), so it fits below the column's controls without
* viewport math (#1616).
*/
embedded?: boolean;
}
const PanelContainer = Paper.withProps({
@ -56,9 +56,17 @@ function isActiveStatus(status: TaskStatus): boolean {
function matchesFilters(
task: Task,
searchText: string,
statusFilter?: TaskStatus,
statusFilter: TaskStatus | undefined,
// The embedded column exposes only the shared search box (its status filter
// lives in the full-size sidebar, which the embedded view drops), so it
// applies the text filter but skips the status filter — mirrors
// LogStreamPanel's `ignoreLevels` etc. so a stale status filter can't
// silently hide tasks with no visible control to clear it (#1616).
ignoreStatus: boolean,
): boolean {
if (statusFilter && task.status !== statusFilter) return false;
if (!ignoreStatus && statusFilter && task.status !== statusFilter) {
return false;
}
if (searchText) {
const term = searchText.toLowerCase();
const searchable =
@ -75,13 +83,17 @@ export function TaskListPanel({
statusFilter,
onCancel,
onClearCompleted,
embedded = false,
}: TaskListPanelProps) {
const viewportRef = useScrollMemory("tasks-list");
const [compact, setCompact] = useState(false);
const filteredTasks = useMemo(
() => tasks.filter((t) => matchesFilters(t, searchText, statusFilter)),
[tasks, searchText, statusFilter],
() =>
tasks.filter((t) =>
matchesFilters(t, searchText, statusFilter, embedded),
),
[tasks, searchText, statusFilter, embedded],
);
const activeTasks = useMemo(
@ -111,10 +123,7 @@ export function TaskListPanel({
{!hasResults ? (
<EmptyState>No tasks</EmptyState>
) : (
<ScrollArea.Autosize
viewportRef={viewportRef}
mah="calc(100vh - var(--app-shell-header-height, 0px) - 150px)"
>
<EmbeddableScrollArea embedded={embedded} viewportRef={viewportRef}>
<Stack gap="md">
{activeTasks.length > 0 && (
<>
@ -153,7 +162,7 @@ export function TaskListPanel({
</>
)}
</Stack>
</ScrollArea.Autosize>
</EmbeddableScrollArea>
)}
</PanelContainer>
);