taskchef 7.10.0 → 7.11.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.10.0",
3
+ "version": "7.11.0",
4
4
  "description": "Dispatch work from a data-only workspace to visible Codex project tasks.",
5
5
  "author": {
6
6
  "name": "Favo Yang",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.10.0",
3
+ "version": "7.11.0",
4
4
  "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
5
5
  "license": "MIT",
6
6
  "author": "Favo Yang",
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  clearNotifications,
3
3
  dismissNotification,
4
+ filterTasks,
4
5
  findCurrentTask,
5
- KNOWN_TASK_STATUSES,
6
6
  latestTurnPresentation,
7
7
  mergeProjectedTurns,
8
8
  nextDateFilterRefreshDelay,
@@ -10,8 +10,9 @@ import {
10
10
  notificationOpenLabel,
11
11
  notificationTitle,
12
12
  reconcileNotifications,
13
+ statusFilterCounts,
14
+ statusFilterText,
13
15
  taskStatusLabel,
14
- taskWithinDateFilter,
15
16
  turnPresentation,
16
17
  } from "./state.js";
17
18
  import { openTaskFromControl } from "./actions.js";
@@ -388,20 +389,43 @@ function taskCard(task) {
388
389
 
389
390
  function render() {
390
391
  const project = elements.projectFilter.value;
391
- const status = elements.statusFilter.value;
392
+ const status = elements.statusFilter.querySelector("input:checked")?.value ?? "";
392
393
  const date = elements.dateFilter.value;
393
- const visible = state.tasks.filter((task) =>
394
- (!project || task.project.name === project)
395
- && (!status || taskStatusLabel(task) === status)
396
- && taskWithinDateFilter(task, date));
394
+ const now = Date.now();
395
+ const contextualTasks = filterTasks(state.tasks, { project, date, now });
396
+ const visible = filterTasks(contextualTasks, { status, now });
397
+ const counts = statusFilterCounts(state.tasks, { project, date, now });
398
+ for (const label of elements.statusFilter.querySelectorAll("[data-status-label]")) {
399
+ const value = label.dataset.statusLabel;
400
+ label.textContent = statusFilterText(value, counts[value]);
401
+ }
397
402
  elements.taskList.replaceChildren(...visible.map(taskCard));
398
403
  elements.emptyState.hidden = visible.length > 0;
399
404
  elements.taskCount.textContent = `${visible.length} of ${state.tasks.length} task${state.tasks.length === 1 ? "" : "s"}`;
400
405
  clearTimeout(dateRefreshTimer);
401
- const refreshDelay = nextDateFilterRefreshDelay(visible, date);
406
+ const refreshDelay = nextDateFilterRefreshDelay(contextualTasks, date, now);
402
407
  dateRefreshTimer = refreshDelay === null ? null : setTimeout(render, refreshDelay);
403
408
  }
404
409
 
410
+ function selectStatusFromKeyboard(event) {
411
+ if (event.target?.name !== "status-filter") return;
412
+ const inputs = [...elements.statusFilter.querySelectorAll('input[name="status-filter"]')];
413
+ const currentIndex = inputs.indexOf(event.target);
414
+ if (currentIndex < 0) return;
415
+ let nextIndex = currentIndex;
416
+ if (["ArrowRight", "ArrowDown"].includes(event.key)) nextIndex = (currentIndex + 1) % inputs.length;
417
+ else if (["ArrowLeft", "ArrowUp"].includes(event.key)) {
418
+ nextIndex = (currentIndex - 1 + inputs.length) % inputs.length;
419
+ } else if (event.key === "Home") nextIndex = 0;
420
+ else if (event.key === "End") nextIndex = inputs.length - 1;
421
+ else if (![" ", "Enter"].includes(event.key)) return;
422
+ event.preventDefault();
423
+ for (const input of inputs) input.checked = false;
424
+ inputs[nextIndex].checked = true;
425
+ inputs[nextIndex].focus();
426
+ render();
427
+ }
428
+
405
429
  function applySnapshot(snapshot) {
406
430
  const reconciled = reconcileNotifications({
407
431
  initialized: state.initialized,
@@ -424,11 +448,6 @@ function applySnapshot(snapshot) {
424
448
  [...new Set(state.tasks.map((task) => task.project.name))].sort(),
425
449
  "All projects",
426
450
  );
427
- replaceOptions(
428
- elements.statusFilter,
429
- [...new Set([...KNOWN_TASK_STATUSES, ...state.tasks.map(taskStatusLabel)])],
430
- "All statuses",
431
- );
432
451
  if (state.selectedTask) {
433
452
  const updated = state.tasks.find((task) => task.id === state.selectedTask.id);
434
453
  if (updated && elements.dialog.open) openDialog(updated);
@@ -457,6 +476,7 @@ events.addEventListener("dashboard-error", (event) => {
457
476
 
458
477
  elements.projectFilter.addEventListener("change", render);
459
478
  elements.statusFilter.addEventListener("change", render);
479
+ elements.statusFilter.addEventListener("keydown", selectStatusFromKeyboard);
460
480
  elements.dateFilter.addEventListener("change", render);
461
481
  elements.dismissDashboardMessage.addEventListener("click", () => {
462
482
  elements.dashboardMessage.hidden = true;
@@ -35,12 +35,31 @@
35
35
  <option value="">All projects</option>
36
36
  </select>
37
37
  </label>
38
- <label>
39
- Status
40
- <select id="status-filter">
41
- <option value="">All statuses</option>
42
- </select>
43
- </label>
38
+ <fieldset class="status-filter-fieldset">
39
+ <legend>Status</legend>
40
+ <div id="status-filter" class="status-filter-options">
41
+ <label class="status-filter-option">
42
+ <input type="radio" name="status-filter" value="" checked>
43
+ <span data-status-label="">All</span>
44
+ </label>
45
+ <label class="status-filter-option">
46
+ <input type="radio" name="status-filter" value="working">
47
+ <span data-status-label="working">Working</span>
48
+ </label>
49
+ <label class="status-filter-option">
50
+ <input type="radio" name="status-filter" value="needs input">
51
+ <span data-status-label="needs input">Needs input</span>
52
+ </label>
53
+ <label class="status-filter-option">
54
+ <input type="radio" name="status-filter" value="completed">
55
+ <span data-status-label="completed">Completed</span>
56
+ </label>
57
+ <label class="status-filter-option">
58
+ <input type="radio" name="status-filter" value="failed">
59
+ <span data-status-label="failed">Failed</span>
60
+ </label>
61
+ </div>
62
+ </fieldset>
44
63
  <label>
45
64
  Updated
46
65
  <select id="date-filter">
@@ -5,7 +5,13 @@ export const KNOWN_TASK_STATUSES = [
5
5
  "needs input",
6
6
  "completed",
7
7
  "failed",
8
- "unresolved",
8
+ ];
9
+ export const STATUS_FILTERS = [
10
+ { value: "", label: "All" },
11
+ { value: "working", label: "Working" },
12
+ { value: "needs input", label: "Needs input" },
13
+ { value: "completed", label: "Completed" },
14
+ { value: "failed", label: "Failed" },
9
15
  ];
10
16
 
11
17
  const NOTIFICATION_TITLES = new Map([
@@ -98,6 +104,35 @@ export function taskWithinDateFilter(task, filter, now = Date.now()) {
98
104
  return Number.isFinite(meaningfulTime) && meaningfulTime >= now - windowMs;
99
105
  }
100
106
 
107
+ export function filterTasks(
108
+ tasks,
109
+ { project = "", status = "", date = "all", now = Date.now() } = {},
110
+ ) {
111
+ return tasks.filter((task) =>
112
+ (!project || task.project.name === project)
113
+ && (!status || taskStatusLabel(task) === status)
114
+ && taskWithinDateFilter(task, date, now));
115
+ }
116
+
117
+ export function statusFilterCounts(
118
+ tasks,
119
+ { project = "", date = "all", now = Date.now() } = {},
120
+ ) {
121
+ const contextualTasks = filterTasks(tasks, { project, date, now });
122
+ return Object.fromEntries(STATUS_FILTERS.map(({ value }) => [
123
+ value,
124
+ value
125
+ ? contextualTasks.filter((task) => taskStatusLabel(task) === value).length
126
+ : contextualTasks.length,
127
+ ]));
128
+ }
129
+
130
+ export function statusFilterText(value, count) {
131
+ const option = STATUS_FILTERS.find((filter) => filter.value === value);
132
+ if (!option) return "";
133
+ return count > 0 ? `${option.label} (${count})` : option.label;
134
+ }
135
+
101
136
  export function nextDateFilterRefreshDelay(tasks, filter, now = Date.now()) {
102
137
  const windowMs = DATE_WINDOWS_MS.get(filter);
103
138
  if (windowMs === null || windowMs === undefined) return null;
@@ -70,6 +70,15 @@ main { padding-block: 28px 80px; }
70
70
 
71
71
  label { display: grid; gap: 6px; color: var(--muted); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
72
72
  select { min-width: 180px; padding: 9px 34px 9px 11px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface); color: var(--text); }
73
+ .status-filter-fieldset { min-width: 0; margin: 0; padding: 0; border: 0; }
74
+ .status-filter-fieldset legend { margin-bottom: 6px; padding: 0; color: var(--muted); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
75
+ .status-filter-options { display: flex; flex-wrap: wrap; gap: 6px; }
76
+ .status-filter-option { position: relative; display: block; color: var(--text); font-size: 0.82rem; letter-spacing: 0; text-transform: none; cursor: pointer; }
77
+ .status-filter-option input { position: absolute; width: 1px; height: 1px; opacity: 0; }
78
+ .status-filter-option span { display: block; padding: 7px 11px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); white-space: nowrap; }
79
+ .status-filter-option:hover span { border-color: var(--accent); }
80
+ .status-filter-option:has(input:checked) span { border-color: var(--accent); background: var(--accent); color: white; box-shadow: 0 1px 2px rgb(24 33 29 / 16%); }
81
+ .status-filter-option:has(input:focus-visible) span { outline: 3px solid var(--accent); outline-offset: 3px; }
73
82
  .task-count { margin: 0 0 9px auto; color: var(--muted); font-size: 0.9rem; }
74
83
 
75
84
  .task-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
@@ -152,6 +161,7 @@ pre { max-height: 280px; margin: 0; padding: 14px; overflow: auto; border-radius
152
161
  .title-row { align-items: flex-start; }
153
162
  .toolbar { align-items: stretch; flex-direction: column; }
154
163
  .toolbar label, .toolbar select { width: 100%; }
164
+ .toolbar .status-filter-option { width: auto; }
155
165
  .task-count { margin: 0; }
156
166
  .task-heading { align-items: flex-start; }
157
167
  .task-footer { align-items: flex-start; flex-direction: column; }
@@ -176,6 +186,7 @@ pre { max-height: 280px; margin: 0; padding: 14px; overflow: auto; border-radius
176
186
  --warning: #e4bd77;
177
187
  }
178
188
  .dashboard-message { border-color: #66592e; background: #38321d; color: #f1d98c; }
189
+ .status-filter-option:has(input:checked) span { color: var(--background); }
179
190
  .status-failed { background: #4e2927; }
180
191
  .status-needs_input { background: #4c3b20; }
181
192
  .status-interrupted { background: #37332f; color: #d6d0c7; }