taskchef 8.7.0 → 9.0.0

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.
Files changed (149) hide show
  1. package/.codex-plugin/plugin.json +7 -10
  2. package/.mcp.json +3 -2
  3. package/README.md +31 -158
  4. package/docs/mcp-app.md +21 -4
  5. package/docs/spec.md +38 -746
  6. package/docs/taskchef-next-storage.md +2 -2
  7. package/index.js +2 -99
  8. package/package.json +12 -49
  9. package/skills/delegate/SKILL.md +44 -0
  10. package/skills/delegate/agents/openai.yaml +4 -0
  11. package/src/codex-app.js +3 -152
  12. package/src/codex-session-scanner.js +2 -2
  13. package/src/file-lock.js +25 -0
  14. package/src/mcp-app/react/NextTaskDetail.test.tsx +1 -1
  15. package/src/mcp-app/react/NextTaskDetail.tsx +8 -8
  16. package/src/mcp-app/react/main.test.tsx +3 -3
  17. package/src/mcp-app/react/main.tsx +7 -7
  18. package/src/mcp-app.js +4 -4
  19. package/src/mcp.js +4 -542
  20. package/src/next-cache.js +2 -2
  21. package/src/next-github.js +3 -3
  22. package/src/next-notifications.js +2 -2
  23. package/src/{dashboard/react → ui}/components/CardStatusIcons.tsx +1 -1
  24. package/src/{dashboard/react → ui}/components/GitHubLinks.tsx +1 -1
  25. package/src/{dashboard/react → ui}/components/LinkedText.tsx +1 -1
  26. package/src/{dashboard/react → ui}/components/RelativeTime.test.tsx +1 -1
  27. package/src/{dashboard/react → ui}/components/RelativeTime.tsx +1 -1
  28. package/src/{dashboard/react → ui}/components/ReplyImagePreview.test.tsx +1 -1
  29. package/src/{dashboard/react → ui}/components/TaskBoard.tsx +1 -1
  30. package/src/{dashboard/react → ui}/components/TaskCard.tsx +1 -1
  31. package/src/{dashboard/react → ui}/components/TurnContent.tsx +1 -1
  32. package/src/{dashboard/react → ui}/presentation.ts +1 -1
  33. package/src/version.js +2 -1
  34. package/BACKLOG.md +0 -48
  35. package/assets/e2e-benchmark-example.json +0 -50
  36. package/assets/taskchef-dispatcher-instructions.md +0 -44
  37. package/bin/taskchef.js +0 -20
  38. package/docs/advanced-guide.md +0 -497
  39. package/docs/dashboard-lifecycle.md +0 -149
  40. package/docs/dashboard-ui.md +0 -40
  41. package/docs/firstmate-taskchef-comparison.md +0 -283
  42. package/docs/images/ccusage-token-consumption.png +0 -0
  43. package/docs/images/dashboard-identity.jpg +0 -0
  44. package/docs/images/dashboard-token-pending.jpg +0 -0
  45. package/docs/images/interrupted-turn-recovery.jpg +0 -0
  46. package/docs/images/notification-event-snapshots.jpg +0 -0
  47. package/docs/images/readme-board.png +0 -0
  48. package/docs/images/readme-completed.png +0 -0
  49. package/docs/images/readme-list.png +0 -0
  50. package/docs/images/readme-needs-input.png +0 -0
  51. package/docs/images/readme-roles.png +0 -0
  52. package/docs/images/reported-work-duration-desktop.jpg +0 -0
  53. package/docs/images/reported-work-duration-mobile.jpg +0 -0
  54. package/docs/images/result-history-dashboard.jpg +0 -0
  55. package/docs/images/taskchef-pr-icon-comparison.jpg +0 -0
  56. package/docs/images/taskchef-unified-status-popup.jpg +0 -0
  57. package/docs/workflows.md +0 -476
  58. package/mcp/dashboard-session.js +0 -51
  59. package/scripts/benchmark-dispatch-prepare.js +0 -90
  60. package/scripts/e2e-benchmark.js +0 -206
  61. package/scripts/roles/resolve_roles.py +0 -293
  62. package/skills/taskchef-bootstrap/SKILL.md +0 -96
  63. package/skills/taskchef-bootstrap/agents/openai.yaml +0 -4
  64. package/skills/taskchef-bootstrap/references/project-index.md +0 -154
  65. package/skills/taskchef-copilot/SKILL.md +0 -75
  66. package/skills/taskchef-copilot/agents/openai.yaml +0 -4
  67. package/skills/taskchef-dashboard/SKILL.md +0 -24
  68. package/skills/taskchef-dashboard/agents/openai.yaml +0 -4
  69. package/skills/taskchef-delegate/SKILL.md +0 -102
  70. package/skills/taskchef-delegate/agents/openai.yaml +0 -4
  71. package/skills/taskchef-delegate/references/model-roles.md +0 -29
  72. package/skills/taskchef-executor/SKILL.md +0 -154
  73. package/skills/taskchef-executor/agents/openai.yaml +0 -4
  74. package/skills/taskchef-executor/references/compatibility.md +0 -22
  75. package/skills/taskchef-executor/references/ending-actions.md +0 -26
  76. package/src/backups.js +0 -359
  77. package/src/cli.js +0 -733
  78. package/src/config-audit.js +0 -99
  79. package/src/config-mutations.js +0 -106
  80. package/src/dashboard/actions.js +0 -136
  81. package/src/dashboard/app.js +0 -1172
  82. package/src/dashboard/dist/app.js +0 -269
  83. package/src/dashboard/dist/app.js.map +0 -1
  84. package/src/dashboard/dist/index.html +0 -16
  85. package/src/dashboard/dist/styles.css +0 -1
  86. package/src/dashboard/index.html +0 -148
  87. package/src/dashboard/react/App.test.tsx +0 -346
  88. package/src/dashboard/react/App.tsx +0 -515
  89. package/src/dashboard/react/api.test.ts +0 -40
  90. package/src/dashboard/react/api.ts +0 -103
  91. package/src/dashboard/react/components/ActivityTimeline.tsx +0 -118
  92. package/src/dashboard/react/components/ModelSettings.test.tsx +0 -180
  93. package/src/dashboard/react/components/ModelSettings.tsx +0 -106
  94. package/src/dashboard/react/components/NotificationCenter.test.tsx +0 -76
  95. package/src/dashboard/react/components/NotificationCenter.tsx +0 -111
  96. package/src/dashboard/react/components/PullRequestBadges.tsx +0 -18
  97. package/src/dashboard/react/components/TaskDetail.test.tsx +0 -255
  98. package/src/dashboard/react/components/TaskDetail.tsx +0 -271
  99. package/src/dashboard/react/components/UsagePanel.tsx +0 -96
  100. package/src/dashboard/react/index.html +0 -15
  101. package/src/dashboard/react/main.tsx +0 -12
  102. package/src/dashboard/react/overlay-state.test.ts +0 -110
  103. package/src/dashboard/react/overlay-state.ts +0 -60
  104. package/src/dashboard/react/presentation.test.tsx +0 -571
  105. package/src/dashboard/styles.css +0 -261
  106. package/src/dashboard-control.js +0 -34
  107. package/src/dashboard-manager.js +0 -339
  108. package/src/dashboard-session-process.js +0 -88
  109. package/src/dashboard-session.js +0 -86
  110. package/src/dashboard.js +0 -1303
  111. package/src/delegation.js +0 -277
  112. package/src/github.js +0 -107
  113. package/src/model-roles.js +0 -70
  114. package/src/recorded-usage-tracker.js +0 -92
  115. package/src/usage-tracker.js +0 -650
  116. package/src/usage.js +0 -367
  117. package/src/workspace-path.js +0 -48
  118. package/src/workspace.js +0 -3629
  119. /package/src/{dashboard/react → ui}/components/AnimatedUsage.test.tsx +0 -0
  120. /package/src/{dashboard/react → ui}/components/CardStatusIcons.test.tsx +0 -0
  121. /package/src/{dashboard/react → ui}/components/ImageGallery.tsx +0 -0
  122. /package/src/{dashboard/react → ui}/components/OpenChatButton.test.tsx +0 -0
  123. /package/src/{dashboard/react → ui}/components/OpenChatButton.tsx +0 -0
  124. /package/src/{dashboard/react → ui}/components/PrGlyph.tsx +0 -0
  125. /package/src/{dashboard/react → ui}/components/QueuedPrompts.test.tsx +0 -0
  126. /package/src/{dashboard/react → ui}/components/QueuedPrompts.tsx +0 -0
  127. /package/src/{dashboard/react → ui}/components/ReplyCover.test.tsx +0 -0
  128. /package/src/{dashboard/react → ui}/components/ReplyCover.tsx +0 -0
  129. /package/src/{dashboard/react → ui}/components/ReplyImagePreview.tsx +0 -0
  130. /package/src/{dashboard/react → ui}/components/ReplyMarkdown.test.tsx +0 -0
  131. /package/src/{dashboard/react → ui}/components/ReplyMarkdown.tsx +0 -0
  132. /package/src/{dashboard/react → ui}/components/SavedUsageFormat.test.tsx +0 -0
  133. /package/src/{dashboard/react → ui}/components/SavedUsageFormat.tsx +0 -0
  134. /package/src/{dashboard/react → ui}/components/ShimmerText.tsx +0 -0
  135. /package/src/{dashboard/react → ui}/components/StatsGrid.tsx +0 -0
  136. /package/src/{dashboard/react → ui}/components/StatusBadge.tsx +0 -0
  137. /package/src/{dashboard/react → ui}/components/TaskBoard.test.tsx +0 -0
  138. /package/src/{dashboard/react → ui}/components/TaskCardStats.test.tsx +0 -0
  139. /package/src/{dashboard/react → ui}/components/TaskCardStats.tsx +0 -0
  140. /package/src/{dashboard/react → ui}/components/TurnContent.test.tsx +0 -0
  141. /package/src/{dashboard/react → ui}/fixtures.ts +0 -0
  142. /package/src/{dashboard → ui}/github-links.js +0 -0
  143. /package/src/{dashboard/react → ui}/image-cache.test.ts +0 -0
  144. /package/src/{dashboard/react → ui}/image-cache.ts +0 -0
  145. /package/src/{dashboard → ui}/state.js +0 -0
  146. /package/src/{dashboard/react → ui}/styles.css +0 -0
  147. /package/src/{dashboard/react → ui}/test-setup.ts +0 -0
  148. /package/src/{dashboard → ui}/time.js +0 -0
  149. /package/src/{dashboard/react → ui}/types.ts +0 -0
@@ -1,148 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <meta name="color-scheme" content="light dark">
7
- <title>TaskChef Dashboard</title>
8
- <link rel="stylesheet" href="/styles.css">
9
- <script src="/app.js" type="module"></script>
10
- </head>
11
- <body>
12
- <header class="site-header">
13
- <div>
14
- <p class="eyebrow">TaskChef <span id="taskchef-version" class="dashboard-version"></span></p>
15
- <div class="title-row">
16
- <picture class="dashboard-icon" aria-hidden="true">
17
- <source srcset="/assets/taskchef-dark.svg" media="(prefers-color-scheme: dark)">
18
- <img src="/assets/taskchef.svg" alt="" width="48" height="48">
19
- </picture>
20
- <h1>TaskChef Dashboard</h1>
21
- </div>
22
- <p class="subtitle">Latest delegated work, updated as TaskChef reports change.</p>
23
- </div>
24
- <div class="connection" role="status" aria-live="polite">
25
- <span id="connection-dot" class="connection-dot"></span>
26
- <span id="connection-label">Connecting…</span>
27
- </div>
28
- </header>
29
-
30
- <main>
31
- <section class="toolbar" aria-label="Task filters">
32
- <div class="toolbar-primary">
33
- <label>
34
- Project
35
- <select id="project-filter">
36
- <option value="">All projects</option>
37
- </select>
38
- </label>
39
- <label>
40
- Updated
41
- <select id="date-filter">
42
- <option value="24h">Latest 24 hours</option>
43
- <option value="7d">Latest 7 days</option>
44
- <option value="all" selected>All time</option>
45
- </select>
46
- </label>
47
- </div>
48
- <fieldset class="status-filter-fieldset">
49
- <legend>Status</legend>
50
- <div id="status-filter" class="status-filter-options">
51
- <label class="status-filter-option">
52
- <input type="radio" name="status-filter" value="" checked>
53
- <span data-status-label="">All</span>
54
- </label>
55
- <label class="status-filter-option">
56
- <input type="radio" name="status-filter" value="working">
57
- <span data-status-label="working">Working</span>
58
- </label>
59
- <label class="status-filter-option">
60
- <input type="radio" name="status-filter" value="needs input">
61
- <span data-status-label="needs input">Needs input</span>
62
- </label>
63
- <label class="status-filter-option">
64
- <input type="radio" name="status-filter" value="completed">
65
- <span data-status-label="completed">Completed</span>
66
- </label>
67
- <label class="status-filter-option">
68
- <input type="radio" name="status-filter" value="failed">
69
- <span data-status-label="failed">Failed</span>
70
- </label>
71
- </div>
72
- </fieldset>
73
- <p id="task-count" class="task-count" aria-live="polite"></p>
74
- </section>
75
-
76
- <div id="dashboard-message" class="dashboard-message" hidden>
77
- <span id="dashboard-message-text" role="status" aria-live="polite"></span>
78
- <button id="dismiss-dashboard-message" class="icon-button" type="button" aria-label="Dismiss dashboard message">×</button>
79
- </div>
80
- <section aria-labelledby="tasks-heading">
81
- <h2 id="tasks-heading" class="visually-hidden">Tasks</h2>
82
- <div id="task-list" class="task-list"></div>
83
- <div id="empty-state" class="empty-state" hidden>
84
- <h2>No tasks match these filters</h2>
85
- <p>Choose a different project or status to see more work.</p>
86
- </div>
87
- </section>
88
- </main>
89
-
90
- <div id="notification-host">
91
- <aside id="notifications" class="notifications" aria-label="Task notifications" popover="manual">
92
- <div class="notifications-heading">
93
- <strong>Updates</strong>
94
- <button id="clear-notifications" class="text-button" type="button">Clear all</button>
95
- </div>
96
- <p id="notification-announcer" class="visually-hidden" role="status" aria-live="polite"></p>
97
- <div id="toast-list" class="toast-list"></div>
98
- </aside>
99
- </div>
100
-
101
- <dialog id="task-dialog" aria-labelledby="dialog-title">
102
- <div class="dialog-header">
103
- <div>
104
- <p id="dialog-project" class="eyebrow"></p>
105
- <h2 id="dialog-title" tabindex="-1"></h2>
106
- </div>
107
- <button id="close-dialog" class="icon-button" type="button" aria-label="Close task details">×</button>
108
- </div>
109
- <div class="dialog-actions" role="group" aria-label="Task actions">
110
- <button id="open-codex" class="primary-button task-action" type="button" aria-label="Open this task in Codex">
111
- <picture class="codex-icon" aria-hidden="true">
112
- <source srcset="/assets/codex-app-dark.png" media="(prefers-color-scheme: dark)">
113
- <img src="/assets/codex-app-light.png" alt="" width="18" height="18">
114
- </picture>
115
- <span>Open task</span>
116
- </button>
117
- <button id="more-task-actions" class="secondary-button more-task-actions" type="button" aria-label="More task actions" aria-expanded="false" aria-controls="manual-transition-panel">…</button>
118
- <div id="manual-transition-panel" class="manual-transition-panel" aria-busy="false" hidden>
119
- <div class="manual-transition-controls">
120
- <button id="copy-task-id" class="secondary-button" type="button" aria-label="Copy Task ID" data-manual-focus="copy">Copy Task ID</button>
121
- <button id="mark-task-completed" class="secondary-button" type="button" data-manual-focus="target-completed">Mark completed</button>
122
- <button id="mark-task-failed" class="danger-button" type="button" data-manual-focus="target-failed">Mark failed</button>
123
- <button id="archive-codex" class="danger-button" type="button" aria-label="Archive this chat in Codex" hidden>Archive chat</button>
124
- </div>
125
- <p id="manual-transition-status" class="manual-transition-pending" role="status" aria-live="polite" tabindex="-1" data-manual-focus="pending" hidden></p>
126
- <p id="manual-transition-error" class="manual-transition-error" role="alert" tabindex="-1" data-manual-focus="error" hidden></p>
127
- </div>
128
- </div>
129
- <nav id="dialog-related-links" class="github-links" aria-label="Related GitHub links" hidden></nav>
130
- <section>
131
- <h3>Usage</h3>
132
- <div id="dialog-usage" class="usage-panel" aria-live="polite"></div>
133
- </section>
134
- <section>
135
- <h3>Activity timeline</h3>
136
- <div id="dialog-results" class="result-history"></div>
137
- </section>
138
- <section>
139
- <h3>Original instruction</h3>
140
- <pre id="dialog-instruction"></pre>
141
- </section>
142
- <section>
143
- <h3>Metadata</h3>
144
- <dl id="dialog-metadata" class="metadata"></dl>
145
- </section>
146
- </dialog>
147
- </body>
148
- </html>
@@ -1,346 +0,0 @@
1
- import { act, cleanup, fireEvent, render, screen, within } from "@testing-library/react";
2
- import type { ElementType, ReactNode } from "react";
3
- import { afterEach, beforeEach, expect, test, vi } from "vitest";
4
- import { DashboardApp } from "./App";
5
- import { fixtureTask } from "./fixtures";
6
- import type { DashboardSnapshot } from "./types";
7
-
8
- const apiMocks = vi.hoisted(() => ({
9
- connectDashboard: vi.fn(),
10
- dashboardVersion: vi.fn(),
11
- manualTransition: vi.fn(),
12
- openInCodex: vi.fn(),
13
- taskDetail: vi.fn(),
14
- }));
15
-
16
- vi.mock("./api", () => apiMocks);
17
- vi.mock("./components/NotificationCenter", () => ({ NotificationCenter: () => null }));
18
- vi.mock("./components/TaskCard", () => ({ TaskCard: () => null }));
19
- vi.mock("./components/TaskBoard", () => ({ TaskBoard: ({ tasks, completedLimit, onMoreCompleted }: { tasks: ReturnType<typeof fixtureTask>[]; completedLimit: number; onMoreCompleted: () => void }) => (
20
- <section aria-label="Task board" data-completed-limit={completedLimit}>
21
- {tasks.map((task) => <span key={task.id}>{task.title}</span>)}
22
- <button onClick={onMoreCompleted}>Show more</button>
23
- </section>
24
- ) }));
25
- vi.mock("./components/TaskDetail", () => ({ TaskDetail: () => null }));
26
- vi.mock("@tabler/icons-react", async () => {
27
- const { createElement } = await vi.importActual<typeof import("react")>("react");
28
- const Icon = (props: Record<string, unknown>) => createElement("svg", props);
29
- return {
30
- IconArrowLeft: Icon,
31
- IconSettings: Icon,
32
- IconAlertTriangle: Icon,
33
- IconCircleFilled: Icon,
34
- IconMoon: Icon,
35
- IconRefresh: Icon,
36
- IconSun: Icon,
37
- };
38
- });
39
- vi.mock("@mantine/core", async () => {
40
- const { createElement } = await vi.importActual<typeof import("react")>("react");
41
- const primitive = (tag: string) => ({
42
- align: _align,
43
- c: _color,
44
- children,
45
- component,
46
- fw: _fontWeight,
47
- gap: _gap,
48
- justify: _justify,
49
- leftSection: _leftSection,
50
- mb: _marginBottom,
51
- mt: _marginTop,
52
- p: _padding,
53
- pb: _paddingBottom,
54
- radius: _radius,
55
- size: _size,
56
- ta: _textAlign,
57
- tt: _textTransform,
58
- withBorder: _withBorder,
59
- wrap: _wrap,
60
- ...props
61
- }: Record<string, unknown> & { children?: ReactNode; component?: ElementType }) => (
62
- createElement(component ?? tag, props, children)
63
- );
64
- const AppShell = Object.assign(primitive("div"), { Main: primitive("main") });
65
- const Select = ({
66
- "aria-label": ariaLabel,
67
- data,
68
- label,
69
- onChange,
70
- value,
71
- }: {
72
- "aria-label": string;
73
- data: Array<string | { label: string; value: string }>;
74
- label: string;
75
- onChange: (value: string | null) => void;
76
- value: string;
77
- }) => {
78
- const options = data.map((item) => typeof item === "string"
79
- ? { label: item, value: item }
80
- : item);
81
- const selected = options.find((item) => item.value === value)?.label ?? value;
82
- return createElement("label", null, label, createElement(
83
- "select",
84
- {
85
- "aria-label": ariaLabel,
86
- onChange: (event: { currentTarget: { value: string } }) => {
87
- const next = options.find((item) => item.label === event.currentTarget.value);
88
- onChange(next?.value ?? null);
89
- },
90
- value: selected,
91
- },
92
- options.map((item) => createElement("option", { key: item.value, value: item.label }, item.label)),
93
- ));
94
- };
95
- const SegmentedControl = ({
96
- "aria-label": ariaLabel,
97
- data,
98
- onChange,
99
- value,
100
- }: {
101
- "aria-label": string;
102
- data: Array<{ label: string; value: string }>;
103
- onChange: (value: string) => void;
104
- value: string;
105
- }) => createElement(
106
- "div",
107
- { "aria-label": ariaLabel, role: "radiogroup" },
108
- data.map((item) => createElement(
109
- "label",
110
- { key: item.value },
111
- createElement("input", {
112
- checked: item.value === value,
113
- name: ariaLabel,
114
- onChange: () => onChange(item.value),
115
- type: "radio",
116
- }),
117
- item.label,
118
- )),
119
- );
120
- return {
121
- ActionIcon: primitive("button"),
122
- Alert: primitive("div"),
123
- AppShell,
124
- Box: primitive("div"),
125
- Button: primitive("button"),
126
- CloseButton: primitive("button"),
127
- Container: primitive("div"),
128
- Group: primitive("div"),
129
- MantineProvider: ({ children }: { children?: ReactNode }) => createElement("div", null, children),
130
- Paper: primitive("div"),
131
- SegmentedControl,
132
- Select,
133
- Stack: primitive("div"),
134
- Text: primitive("p"),
135
- Title: primitive("h1"),
136
- Tooltip: primitive("div"),
137
- createTheme: (theme: unknown) => theme,
138
- useComputedColorScheme: () => "light",
139
- useMantineColorScheme: () => ({ setColorScheme: vi.fn() }),
140
- };
141
- });
142
-
143
- afterEach(cleanup);
144
- beforeEach(() => {
145
- vi.clearAllMocks();
146
- window.localStorage.clear();
147
- window.innerWidth = 1024;
148
- apiMocks.dashboardVersion.mockResolvedValue("7.25.1");
149
- });
150
-
151
- test("Settings uses the same capped content layout as List", () => {
152
- window.location.hash = "#settings";
153
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, json: async () => ({ profiles: [] }) }));
154
- const { container } = render(<DashboardApp connect={false} />);
155
- const settingsPage = container.querySelector(".taskchef-settings-page");
156
- expect(settingsPage).toHaveClass("taskchef-content");
157
- expect(container.querySelector(".taskchef-header")).not.toHaveClass("taskchef-list-layout");
158
- expect(container.querySelector(".taskchef-main")).not.toHaveClass("taskchef-list-layout");
159
- expect(within(settingsPage as HTMLElement).getByRole("link", { name: "Back to tasks" })).toBeInTheDocument();
160
- window.location.hash = "";
161
- });
162
-
163
- test("places the filtered task count below the toolbar and updates it from live snapshots", async () => {
164
- const recent = new Date().toISOString();
165
- const tasks = [
166
- fixtureTask({
167
- id: "11111111-1111-4111-8111-111111111111",
168
- project: { name: "Another project", path: "/tmp/another", githubRepos: [] },
169
- status: "completed",
170
- meaningfulUpdatedAt: recent,
171
- updatedAt: recent,
172
- }),
173
- fixtureTask({
174
- id: "22222222-2222-4222-8222-222222222222",
175
- project: { name: "Another project", path: "/tmp/another", githubRepos: [] },
176
- meaningfulUpdatedAt: recent,
177
- updatedAt: recent,
178
- }),
179
- fixtureTask({
180
- id: "33333333-3333-4333-8333-333333333333",
181
- status: "completed",
182
- }),
183
- ];
184
- let onSnapshot: ((snapshot: DashboardSnapshot) => void) | undefined;
185
- apiMocks.connectDashboard.mockImplementation((handlers) => {
186
- onSnapshot = handlers.onSnapshot;
187
- return { close: vi.fn() };
188
- });
189
-
190
- render(
191
- <DashboardApp
192
- initialFilters={{ project: "Another project", status: "completed" }}
193
- initialTasks={tasks}
194
- />,
195
- );
196
- const header = screen.getByRole("banner");
197
- const title = within(header).getByRole("heading", { level: 1 });
198
- const headerActions = document.querySelector<HTMLElement>(".taskchef-header-actions")!;
199
- expect(title).toHaveTextContent("TaskChef Dashboard");
200
- expect(await within(title).findByText("v7.25.1")).toHaveClass("taskchef-version");
201
- expect(header.querySelector(".taskchef-brand-icon")).toHaveAttribute("width", "28");
202
- expect(headerActions.children[0]).toContainElement(within(headerActions).getByRole("link", { name: "Settings" }));
203
- expect(headerActions.children[1]).toContainElement(within(headerActions).getByRole("button", { name: "Use dark theme" }));
204
- expect(headerActions.lastElementChild).toBe(within(headerActions).getByRole("status"));
205
- expect(within(headerActions).getByRole("link", { name: "Settings" })).toHaveClass("taskchef-icon-button");
206
- expect(within(headerActions).getByRole("button", { name: "Use dark theme" })).toHaveClass("taskchef-icon-button");
207
- const projectFilter = screen.getByRole("combobox", { name: "Project" });
208
- const viewSwitch = screen.getByRole("radiogroup", { name: "View" });
209
- const toolbar = document.querySelector<HTMLElement>(".taskchef-toolbar");
210
- const summary = document.querySelector<HTMLElement>(".taskchef-results-summary");
211
- const taskList = screen.getByRole("region", { name: "Tasks" });
212
-
213
- expect(header.querySelector(".taskchef-header")).toHaveClass("taskchef-list-layout");
214
- expect(document.querySelector(".taskchef-main")).toHaveClass("taskchef-list-layout");
215
- expect(projectFilter).toHaveValue("Another project");
216
- expect(screen.getByRole("combobox", { name: "Updated" })).toHaveValue("All time");
217
- expect(toolbar?.firstElementChild?.firstElementChild).toContainElement(viewSwitch);
218
- expect(viewSwitch.compareDocumentPosition(projectFilter) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
219
- expect(screen.getByRole("radio", { name: /Completed/ })).toBeChecked();
220
- expect(summary).toHaveTextContent("Tasks: 1 of 3");
221
- expect(toolbar).not.toContainElement(summary);
222
- expect(toolbar?.nextElementSibling).toBe(summary);
223
- expect(taskList).toHaveAttribute("aria-describedby", summary?.id);
224
- expect(taskList).toHaveClass("taskchef-content");
225
- expect(summary).toHaveClass("taskchef-content");
226
-
227
- act(() => {
228
- onSnapshot?.({
229
- tasks: [
230
- tasks[0],
231
- fixtureTask({
232
- id: "44444444-4444-4444-8444-444444444444",
233
- project: { name: "Another project", path: "/tmp/another", githubRepos: [] },
234
- status: "completed",
235
- meaningfulUpdatedAt: recent,
236
- updatedAt: recent,
237
- }),
238
- ],
239
- });
240
- });
241
-
242
- expect(summary).toHaveTextContent("Tasks: 2 of 2");
243
- expect(summary).toHaveAttribute("aria-live", "polite");
244
- }, 60_000);
245
-
246
- test("Board ignores the List status filter and restores it on return", () => {
247
- window.innerWidth = 1280;
248
- const tasks = [
249
- fixtureTask({ id: "one", title: "Working task" }),
250
- fixtureTask({ id: "two", title: "Completed task", status: "completed" }),
251
- ];
252
- render(<DashboardApp connect={false} initialFilters={{ status: "completed" }} initialTasks={tasks} />);
253
- expect(screen.getByRole("radio", { name: /Completed/ })).toBeChecked();
254
- fireEvent.click(screen.getByRole("radio", { name: "Board" }));
255
- expect(document.querySelector(".taskchef-header")).not.toHaveClass("taskchef-list-layout");
256
- expect(document.querySelector(".taskchef-main")).not.toHaveClass("taskchef-list-layout");
257
- const toolbar = document.querySelector<HTMLElement>(".taskchef-toolbar");
258
- expect(toolbar?.firstElementChild?.firstElementChild).toContainElement(screen.getByRole("radiogroup", { name: "View" }));
259
- expect(screen.getByRole("radiogroup", { name: "View" }).compareDocumentPosition(screen.getByRole("combobox", { name: "Project" })) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
260
- expect(screen.queryByRole("radiogroup", { name: "Status" })).not.toBeInTheDocument();
261
- expect(screen.queryByText("Tasks: 1 of 2")).not.toBeInTheDocument();
262
- expect(screen.getByRole("region", { name: "Task board" })).toHaveTextContent("Working task");
263
- expect(screen.getByRole("region", { name: "Task board" })).toHaveTextContent("Completed task");
264
- fireEvent.click(screen.getByRole("radio", { name: "List" }));
265
- expect(document.querySelector(".taskchef-header")).toHaveClass("taskchef-list-layout");
266
- expect(document.querySelector(".taskchef-main")).toHaveClass("taskchef-list-layout");
267
- expect(screen.getByRole("radio", { name: /Completed/ })).toBeChecked();
268
- expect(screen.getByText("Tasks: 1 of 2")).toBeInTheDocument();
269
- });
270
-
271
- test("Board preference and view switch remain available across screen widths", () => {
272
- window.innerWidth = 1280;
273
- const { unmount } = render(<DashboardApp connect={false} />);
274
- fireEvent.click(screen.getByRole("radio", { name: "Board" }));
275
- expect(window.localStorage.getItem("taskchef.dashboard.view")).toBe("board");
276
- expect(screen.getByRole("region", { name: "Task board" })).toBeInTheDocument();
277
- act(() => { window.innerWidth = 375; window.dispatchEvent(new Event("resize")); });
278
- expect(screen.getByRole("radiogroup", { name: "View" })).toBeInTheDocument();
279
- expect(screen.getByRole("region", { name: "Task board" })).toBeInTheDocument();
280
- fireEvent.click(screen.getByRole("radio", { name: "List" }));
281
- expect(screen.getByRole("region", { name: "Tasks" })).toBeInTheDocument();
282
- fireEvent.click(screen.getByRole("radio", { name: "Board" }));
283
- act(() => { window.innerWidth = 1200; window.dispatchEvent(new Event("resize")); });
284
- expect(screen.getByRole("region", { name: "Task board" })).toBeInTheDocument();
285
- unmount();
286
- window.innerWidth = 375;
287
- render(<DashboardApp connect={false} />);
288
- expect(screen.getByRole("region", { name: "Task board" })).toBeInTheDocument();
289
- });
290
-
291
- test("storage failure keeps the selected view in memory", () => {
292
- window.innerWidth = 1280;
293
- const setItem = vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => { throw new Error("blocked"); });
294
- render(<DashboardApp connect={false} />);
295
- fireEvent.click(screen.getByRole("radio", { name: "Board" }));
296
- expect(screen.getByRole("region", { name: "Task board" })).toBeInTheDocument();
297
- setItem.mockRestore();
298
- });
299
-
300
- test("completed expansion survives view changes and resets for project", () => {
301
- window.innerWidth = 1280;
302
- render(<DashboardApp connect={false} initialTasks={[fixtureTask({ project: { name: "Project A", path: "/tmp/a", githubRepos: [] } })]} />);
303
- fireEvent.click(screen.getByRole("radio", { name: "Board" }));
304
- const board = screen.getByRole("region", { name: "Task board" });
305
- fireEvent.click(screen.getByRole("button", { name: "Show more" }));
306
- expect(board).toHaveAttribute("data-completed-limit", "10");
307
- fireEvent.click(screen.getByRole("radio", { name: "List" }));
308
- fireEvent.click(screen.getByRole("radio", { name: "Board" }));
309
- expect(screen.getByRole("region", { name: "Task board" })).toHaveAttribute("data-completed-limit", "10");
310
- fireEvent.change(screen.getByRole("combobox", { name: "Project" }), { target: { value: "Project A" } });
311
- expect(screen.getByRole("region", { name: "Task board" })).toHaveAttribute("data-completed-limit", "5");
312
- });
313
-
314
- test("Updated filters both views and List counts, and resets completed expansion", () => {
315
- const recent = new Date().toISOString();
316
- const tasks = [
317
- fixtureTask({ id: "recent", title: "Recent task", status: "completed", updatedAt: recent, meaningfulUpdatedAt: recent }),
318
- fixtureTask({ id: "old", title: "Older task", status: "completed", updatedAt: "2020-01-01T00:00:00.000Z", meaningfulUpdatedAt: "2020-01-01T00:00:00.000Z" }),
319
- ];
320
- render(<DashboardApp connect={false} initialTasks={tasks} />);
321
- const updated = screen.getByRole("combobox", { name: "Updated" });
322
- expect(updated).toHaveValue("All time");
323
- expect(screen.getByText("Tasks: 2 of 2")).toBeInTheDocument();
324
- fireEvent.change(updated, { target: { value: "Latest 24 hours" } });
325
- expect(screen.getByText("Tasks: 1 of 2")).toBeInTheDocument();
326
- expect(screen.getByRole("radio", { name: "Completed 1" })).toBeInTheDocument();
327
- fireEvent.click(screen.getByRole("radio", { name: "Board" }));
328
- expect(screen.getByRole("region", { name: "Task board" })).toHaveTextContent("Recent task");
329
- expect(screen.getByRole("region", { name: "Task board" })).not.toHaveTextContent("Older task");
330
- fireEvent.click(screen.getByRole("button", { name: "Show more" }));
331
- expect(screen.getByRole("region", { name: "Task board" })).toHaveAttribute("data-completed-limit", "10");
332
- fireEvent.change(updated, { target: { value: "Latest 7 days" } });
333
- expect(screen.getByRole("region", { name: "Task board" })).toHaveAttribute("data-completed-limit", "5");
334
- fireEvent.change(updated, { target: { value: "All time" } });
335
- expect(screen.getByRole("region", { name: "Task board" })).toHaveTextContent("Older task");
336
- fireEvent.click(screen.getByRole("radio", { name: "List" }));
337
- expect(screen.getByText("Tasks: 2 of 2")).toBeInTheDocument();
338
- });
339
-
340
- test("old tasks remain available in List and Board", () => {
341
- const oldTask = fixtureTask({ id: "old", title: "Older task", updatedAt: "2020-01-01T00:00:00.000Z", meaningfulUpdatedAt: "2020-01-01T00:00:00.000Z" });
342
- render(<DashboardApp connect={false} initialTasks={[oldTask]} />);
343
- expect(screen.getByText("Tasks: 1 of 1")).toBeInTheDocument();
344
- fireEvent.click(screen.getByRole("radio", { name: "Board" }));
345
- expect(screen.getByRole("region", { name: "Task board" })).toHaveTextContent("Older task");
346
- });