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.
- package/.codex-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/src/dashboard/dist/app.js +16 -16
- package/src/dashboard/dist/app.js.map +1 -1
- package/src/dashboard/dist/styles.css +1 -1
- package/src/dashboard/react/App.test.tsx +90 -0
- package/src/dashboard/react/App.tsx +22 -8
- package/src/dashboard/react/components/OpenChatButton.test.tsx +22 -0
- package/src/dashboard/react/components/OpenChatButton.tsx +25 -0
- package/src/dashboard/react/components/TaskCard.tsx +3 -11
- package/src/dashboard/react/components/TaskDetail.tsx +2 -9
- package/src/dashboard/react/styles.css +1 -3
|
@@ -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="
|
|
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,
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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-
|
|
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; }
|