taskchef 7.25.1 → 7.25.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ import { act, cleanup, render, screen } from "@testing-library/react";
2
+ import { afterEach, beforeEach, expect, test, vi } from "vitest";
3
+ import { DashboardApp } from "./App";
4
+ import { fixtureTask } from "./fixtures";
5
+ import type { DashboardSnapshot } from "./types";
6
+
7
+ const apiMocks = vi.hoisted(() => ({
8
+ connectDashboard: vi.fn(),
9
+ dashboardVersion: vi.fn(),
10
+ manualTransition: vi.fn(),
11
+ openInCodex: vi.fn(),
12
+ taskDetail: vi.fn(),
13
+ }));
14
+
15
+ vi.mock("./api", () => apiMocks);
16
+ vi.mock("./components/NotificationCenter", () => ({ NotificationCenter: () => null }));
17
+ vi.mock("./components/TaskCard", () => ({ TaskCard: () => null }));
18
+ vi.mock("./components/TaskDetail", () => ({ TaskDetail: () => null }));
19
+
20
+ afterEach(cleanup);
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+ apiMocks.dashboardVersion.mockResolvedValue("7.25.1");
24
+ });
25
+
26
+ test("places the filtered task count below the toolbar and updates it from live snapshots", () => {
27
+ const recent = new Date().toISOString();
28
+ const tasks = [
29
+ fixtureTask({
30
+ id: "11111111-1111-4111-8111-111111111111",
31
+ project: { name: "Another project", path: "/tmp/another", githubRepos: [] },
32
+ status: "completed",
33
+ meaningfulUpdatedAt: recent,
34
+ updatedAt: recent,
35
+ }),
36
+ fixtureTask({
37
+ id: "22222222-2222-4222-8222-222222222222",
38
+ project: { name: "Another project", path: "/tmp/another", githubRepos: [] },
39
+ meaningfulUpdatedAt: recent,
40
+ updatedAt: recent,
41
+ }),
42
+ fixtureTask({
43
+ id: "33333333-3333-4333-8333-333333333333",
44
+ status: "completed",
45
+ }),
46
+ ];
47
+ let onSnapshot: ((snapshot: DashboardSnapshot) => void) | undefined;
48
+ apiMocks.connectDashboard.mockImplementation((handlers) => {
49
+ onSnapshot = handlers.onSnapshot;
50
+ return { close: vi.fn() };
51
+ });
52
+
53
+ render(
54
+ <DashboardApp
55
+ initialFilters={{ date: "24h", project: "Another project", status: "completed" }}
56
+ initialTasks={tasks}
57
+ />,
58
+ );
59
+ const projectFilter = screen.getByRole("combobox", { name: "Project" });
60
+ const dateFilter = screen.getByRole("combobox", { name: "Updated" });
61
+ const toolbar = document.querySelector<HTMLElement>(".taskchef-toolbar");
62
+ const summary = document.querySelector<HTMLElement>(".taskchef-results-summary");
63
+ const taskList = screen.getByRole("region", { name: "Tasks" });
64
+
65
+ expect(projectFilter).toHaveValue("Another project");
66
+ expect(dateFilter).toHaveValue("Latest 24 hours");
67
+ expect(screen.getByRole("radio", { name: /Completed/ })).toBeChecked();
68
+ expect(summary).toHaveTextContent("Tasks: 1 of 3");
69
+ expect(toolbar).not.toContainElement(summary);
70
+ expect(toolbar?.nextElementSibling).toBe(summary);
71
+ expect(taskList).toHaveAttribute("aria-describedby", summary?.id);
72
+
73
+ act(() => {
74
+ onSnapshot?.({
75
+ tasks: [
76
+ tasks[0],
77
+ fixtureTask({
78
+ id: "44444444-4444-4444-8444-444444444444",
79
+ project: { name: "Another project", path: "/tmp/another", githubRepos: [] },
80
+ status: "completed",
81
+ meaningfulUpdatedAt: recent,
82
+ updatedAt: recent,
83
+ }),
84
+ ],
85
+ });
86
+ });
87
+
88
+ expect(summary).toHaveTextContent("Tasks: 2 of 2");
89
+ expect(summary).toHaveAttribute("aria-live", "polite");
90
+ }, 60_000);
@@ -77,20 +77,36 @@ interface NotificationState {
77
77
  signatures: Map<string, string>;
78
78
  }
79
79
 
80
+ export function TaskResultsSummary({
81
+ totalCount,
82
+ visibleCount,
83
+ }: {
84
+ totalCount: number;
85
+ visibleCount: number;
86
+ }) {
87
+ return (
88
+ <Text aria-live="polite" className="taskchef-results-summary" id="task-results-summary">
89
+ Tasks: {visibleCount} of {totalCount}
90
+ </Text>
91
+ );
92
+ }
93
+
80
94
  export function DashboardApp({
81
95
  connect = true,
96
+ initialFilters = {},
82
97
  initialTasks = [],
83
98
  }: {
84
99
  connect?: boolean;
100
+ initialFilters?: { date?: string; project?: string; status?: string };
85
101
  initialTasks?: Task[];
86
102
  }) {
87
103
  const [tasks, setTasks] = useState<Task[]>(initialTasks);
88
104
  const [connected, setConnected] = useState(false);
89
105
  const [message, setMessage] = useState<string | null>(null);
90
106
  const [version, setVersion] = useState<string | null>(null);
91
- const [project, setProject] = useState("");
92
- const [status, setStatus] = useState("");
93
- const [date, setDate] = useState("all");
107
+ const [project, setProject] = useState(initialFilters.project ?? "");
108
+ const [status, setStatus] = useState(initialFilters.status ?? "");
109
+ const [date, setDate] = useState(initialFilters.date ?? "all");
94
110
  const [now, setNow] = useState(() => Date.now());
95
111
  const [selectedTask, setSelectedTask] = useState<Task | null>(null);
96
112
  const [detailOpened, setDetailOpened] = useState(false);
@@ -310,10 +326,6 @@ export function DashboardApp({
310
326
  size="sm"
311
327
  value={date}
312
328
  />
313
- <Box className="taskchef-task-count-field">
314
- <Text className="taskchef-filter-label" mb={3}>Tasks</Text>
315
- <Text className="taskchef-count-value" size="sm">{visible.length} of {tasks.length}</Text>
316
- </Box>
317
329
  </Group>
318
330
  <Box>
319
331
  <Text className="taskchef-filter-label" mb={5}>Status</Text>
@@ -322,6 +334,8 @@ export function DashboardApp({
322
334
  </Stack>
323
335
  </Paper>
324
336
 
337
+ <TaskResultsSummary totalCount={tasks.length} visibleCount={visible.length} />
338
+
325
339
  {message && (
326
340
  <Alert color="yellow" icon={<IconAlertTriangle aria-hidden size={17} />} mt="md" role="status">
327
341
  <Group gap="sm" justify="space-between" wrap="nowrap">
@@ -331,7 +345,7 @@ export function DashboardApp({
331
345
  </Alert>
332
346
  )}
333
347
 
334
- <Stack aria-label="Tasks" component="section" gap="sm" mt="md">
348
+ <Stack aria-describedby="task-results-summary" aria-label="Tasks" component="section" gap="sm" mt="xs">
335
349
  {visible.map((task: Task) => (
336
350
  <TaskCard key={task.id} onOpenCodex={handleOpenCodex} onOpenDetail={(value) => void loadDetail(value)} task={task} />
337
351
  ))}
@@ -0,0 +1,22 @@
1
+ import { MantineProvider } from "@mantine/core";
2
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3
+ import { afterEach, expect, test, vi } from "vitest";
4
+ import { OpenChatButton } from "./OpenChatButton";
5
+
6
+ afterEach(cleanup);
7
+
8
+ test("uses the shared open-chat presentation and accessible task label", () => {
9
+ const onClick = vi.fn();
10
+ render(
11
+ <MantineProvider>
12
+ <OpenChatButton onClick={onClick} taskTitle="Review checkout reconciliation" />
13
+ </MantineProvider>,
14
+ );
15
+
16
+ const button = screen.getByRole("button", { name: "Open chat for Review checkout reconciliation" });
17
+ expect(button).toHaveTextContent("Open chat");
18
+ expect(button).toHaveAttribute("data-variant", "default");
19
+ expect(button.querySelector("svg")).toHaveAttribute("aria-hidden", "true");
20
+ fireEvent.click(button);
21
+ expect(onClick).toHaveBeenCalledOnce();
22
+ });
@@ -0,0 +1,25 @@
1
+ import { Button } from "@mantine/core";
2
+ import { IconExternalLink } from "@tabler/icons-react";
3
+
4
+ export function OpenChatButton({
5
+ loading,
6
+ onClick,
7
+ taskTitle,
8
+ }: {
9
+ loading?: boolean;
10
+ onClick: () => void;
11
+ taskTitle: string;
12
+ }) {
13
+ return (
14
+ <Button
15
+ aria-label={`Open chat for ${taskTitle}`}
16
+ leftSection={<IconExternalLink aria-hidden size={15} />}
17
+ loading={loading}
18
+ onClick={onClick}
19
+ size="compact-sm"
20
+ variant="default"
21
+ >
22
+ Open chat
23
+ </Button>
24
+ );
25
+ }
@@ -1,9 +1,9 @@
1
- import { Box, Button, Group, Paper, Stack, Text, Title } from "@mantine/core";
2
- import { IconExternalLink } from "@tabler/icons-react";
1
+ import { Box, Group, Paper, Stack, Text, Title } from "@mantine/core";
3
2
  import { latestTurnPresentation } from "../../state.js";
4
3
  import type { Task } from "../types";
5
4
  import { GitHubLinks } from "./GitHubLinks";
6
5
  import { LinkedText } from "./LinkedText";
6
+ import { OpenChatButton } from "./OpenChatButton";
7
7
  import { RelativeTime } from "./RelativeTime";
8
8
  import { ShimmerText } from "./ShimmerText";
9
9
  import { StatusBadge } from "./StatusBadge";
@@ -53,15 +53,7 @@ export function TaskCard({
53
53
  <GitHubLinks task={task} />
54
54
  <Group className="taskchef-card-footer" gap="md" justify="space-between">
55
55
  <RelativeTime label={`Updated time for ${task.title}`} value={task.meaningfulUpdatedAt ?? task.updatedAt} />
56
- <Button
57
- aria-label={`Open chat for ${task.title}`}
58
- leftSection={<IconExternalLink aria-hidden size={15} />}
59
- onClick={() => onOpenCodex(task)}
60
- size="compact-sm"
61
- variant="default"
62
- >
63
- Open chat
64
- </Button>
56
+ <OpenChatButton onClick={() => onOpenCodex(task)} taskTitle={task.title} />
65
57
  </Group>
66
58
  </Stack>
67
59
  </Paper>
@@ -19,13 +19,13 @@ import {
19
19
  IconCheck,
20
20
  IconClipboard,
21
21
  IconDots,
22
- IconExternalLink,
23
22
  IconX,
24
23
  } from "@tabler/icons-react";
25
24
  import { useEffect, useRef, useState, type ReactNode } from "react";
26
25
  import type { Task } from "../types";
27
26
  import { ActivityTimeline } from "./ActivityTimeline";
28
27
  import { GitHubLinks } from "./GitHubLinks";
28
+ import { OpenChatButton } from "./OpenChatButton";
29
29
  import { RelativeTime } from "./RelativeTime";
30
30
  import { StatusBadge } from "./StatusBadge";
31
31
  import { UsagePanel } from "./UsagePanel";
@@ -93,14 +93,7 @@ export function TaskDetail({
93
93
  <StatusBadge status={task.status} />
94
94
  </Group>
95
95
  <Group gap="xs" mt="md">
96
- <Button
97
- leftSection={<IconExternalLink aria-hidden size={15} />}
98
- loading={busy}
99
- onClick={onOpenCodex}
100
- size="compact-sm"
101
- >
102
- Open chat
103
- </Button>
96
+ <OpenChatButton loading={busy} onClick={onOpenCodex} taskTitle={task.title} />
104
97
  <Menu position="bottom-start" shadow="md" withinPortal zIndex={360}>
105
98
  <Menu.Target>
106
99
  <Tooltip label="More task actions">
@@ -28,8 +28,7 @@ body {
28
28
  .taskchef-toolbar .mantine-InputWrapper-root { flex: 1 1 190px; max-width: 240px; }
29
29
  .taskchef-toolbar .mantine-InputWrapper-label, .taskchef-filter-label { font-size: var(--mantine-font-size-sm); font-weight: 600; line-height: var(--mantine-line-height-sm); }
30
30
  .taskchef-toolbar .mantine-SegmentedControl-root { max-width: 650px; }
31
- .taskchef-task-count-field { align-self: flex-end; min-width: 90px; }
32
- .taskchef-count-value { display: flex; align-items: center; height: 36px; white-space: nowrap; }
31
+ .taskchef-results-summary { margin-top: 12px; color: var(--taskchef-muted); font-size: var(--mantine-font-size-xs); line-height: 1.35; white-space: nowrap; }
33
32
  .taskchef-task-row { background: light-dark(#fff, #202421); box-shadow: 0 1px 0 light-dark(rgb(22 31 25 / 3%), transparent); }
34
33
  .taskchef-task-row:hover { border-color: light-dark(var(--mantine-color-teal-4), var(--mantine-color-teal-8)); }
35
34
  .taskchef-task-main { min-width: 0; flex: 1; }
@@ -117,7 +116,6 @@ body {
117
116
  @media (max-width: 48em) {
118
117
  .taskchef-toolbar .mantine-InputWrapper-root { max-width: none; }
119
118
  .taskchef-toolbar .mantine-Group-root { align-items: stretch; }
120
- .taskchef-task-count-field { min-width: 76px; }
121
119
  .taskchef-toolbar .mantine-SegmentedControl-root { overflow-x: auto; }
122
120
  .taskchef-notifications { right: 16px; bottom: 16px; width: min(330px, calc(100vw - 32px)); }
123
121
  .taskchef-metadata { grid-template-columns: 1fr; }