taskchef 7.23.2 → 7.23.3

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.23.2",
3
+ "version": "7.23.3",
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.23.2",
3
+ "version": "7.23.3",
4
4
  "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
5
5
  "license": "MIT",
6
6
  "author": "Favo Yang",
@@ -440,15 +440,94 @@ function usageBreakdownText(usage) {
440
440
  ].join(" · ");
441
441
  }
442
442
 
443
+ const SHIMMER_STEP_MS = 420;
444
+ const SHIMMER_PAUSE_MS = 1_800;
445
+ const SHIMMER_PULSE_MS = 420;
446
+ const SHIMMER_ANIMATION_ID = "taskchef-text-shimmer";
447
+ const shimmerMotionQuery = globalThis.matchMedia?.("(prefers-reduced-motion: reduce)") ?? null;
448
+ const shimmerRecords = new Set();
449
+
450
+ function animateShimmerRecord(record, reducedMotion) {
451
+ for (const animation of record.animations) animation.cancel();
452
+ record.animations = [];
453
+ if (reducedMotion) return;
454
+ for (const [shimmerIndex, glyph] of record.glyphs.entries()) {
455
+ if (typeof glyph.animate !== "function") continue;
456
+ const pulseEnd = SHIMMER_PULSE_MS / record.cycleMs;
457
+ const animation = glyph.animate([
458
+ { color: "var(--muted)", textShadow: "none", offset: 0 },
459
+ {
460
+ color: "var(--text)",
461
+ textShadow: "0 0 0.28em color-mix(in srgb, var(--text) 22%, transparent)",
462
+ offset: pulseEnd / 2,
463
+ },
464
+ { color: "var(--muted)", textShadow: "none", offset: pulseEnd },
465
+ { color: "var(--muted)", textShadow: "none", offset: 1 },
466
+ ], {
467
+ delay: shimmerIndex * SHIMMER_STEP_MS,
468
+ duration: record.cycleMs,
469
+ easing: "linear",
470
+ iterations: Infinity,
471
+ });
472
+ animation.id = SHIMMER_ANIMATION_ID;
473
+ record.animations.push(animation);
474
+ }
475
+ }
476
+
477
+ shimmerMotionQuery?.addEventListener?.("change", ({ matches }) => {
478
+ for (const record of shimmerRecords) animateShimmerRecord(record, matches);
479
+ });
480
+
481
+ export function cancelTextShimmers(root) {
482
+ for (const record of shimmerRecords) {
483
+ if (root !== record.element && !root?.contains?.(record.element)) continue;
484
+ for (const animation of record.animations) animation.cancel();
485
+ shimmerRecords.delete(record);
486
+ }
487
+ }
488
+
489
+ export function renderTextShimmer(element, value) {
490
+ const text = String(value ?? "");
491
+ const characters = Array.from(text);
492
+ const highlightedCharacters = characters.filter((character) => !/\s/u.test(character));
493
+ const cycleMs = Math.max(
494
+ SHIMMER_PULSE_MS + SHIMMER_PAUSE_MS,
495
+ (highlightedCharacters.length - 1) * SHIMMER_STEP_MS
496
+ + SHIMMER_PULSE_MS
497
+ + SHIMMER_PAUSE_MS,
498
+ );
499
+ const glyphs = [];
500
+ const nodes = characters.map((character) => {
501
+ if (/\s/u.test(character)) return document.createTextNode(character);
502
+ const glyph = document.createElement("span");
503
+ glyph.className = "text-shimmer-glyph";
504
+ glyph.textContent = character;
505
+ glyphs.push(glyph);
506
+ return glyph;
507
+ });
508
+ const accessibleText = document.createElement("span");
509
+ accessibleText.className = "visually-hidden";
510
+ accessibleText.textContent = text;
511
+ const animatedText = document.createElement("span");
512
+ animatedText.className = "text-shimmer-visual";
513
+ animatedText.setAttribute("aria-hidden", "true");
514
+ animatedText.append(...nodes);
515
+ element.replaceChildren(accessibleText, animatedText);
516
+ const record = { animations: [], cycleMs, element, glyphs };
517
+ shimmerRecords.add(record);
518
+ animateShimmerRecord(record, shimmerMotionQuery?.matches ?? false);
519
+ return element;
520
+ }
521
+
443
522
  export function usagePresentation(usage, { wholeTask = false } = {}) {
444
523
  const container = document.createElement("div");
445
524
  container.className = `usage-summary usage-${usage?.status ?? "unavailable"}`;
446
525
  if (usage?.status === "pending" || usage?.status === "calculating") {
447
526
  const text = document.createElement("span");
448
527
  text.className = "text-shimmer";
449
- text.textContent = usage.status === "pending"
528
+ renderTextShimmer(text, usage.status === "pending"
450
529
  ? "Tokens pending · available when turn finishes"
451
- : "Calculating token usage…";
530
+ : "Calculating token usage…");
452
531
  container.replaceChildren(text);
453
532
  return container;
454
533
  }
@@ -537,7 +616,8 @@ function turnTimeline(task, { highlightTurnRef = null } = {}) {
537
616
  resultLabel.textContent = "Result";
538
617
  const result = document.createElement("p");
539
618
  result.className = `preserve-lines${turnStatus === "working" ? " text-shimmer" : ""}`;
540
- appendLinkedText(result, presentation.summary, task);
619
+ if (turnStatus === "working") renderTextShimmer(result, presentation.summary);
620
+ else appendLinkedText(result, presentation.summary, task);
541
621
  const turnMetadata = document.createElement("p");
542
622
  turnMetadata.className = "result-history-turn";
543
623
  turnMetadata.textContent = `Turn ref ${turn.turnRef ?? "not recorded"}; Codex turn ${turn.turnId ?? "unavailable"}`;
@@ -771,6 +851,7 @@ function renderDialog(task, { highlightTurnRef = null } = {}) {
771
851
  (task.relatedGitHubLinks?.length ?? 0) === 0 && !task.relatedGitHubLinksTruncated
772
852
  );
773
853
  const timeline = turnTimeline(detailedTask, { highlightTurnRef });
854
+ cancelTextShimmers(elements.dialogResults);
774
855
  elements.dialogResults.replaceChildren(...timeline);
775
856
  const highlightedTurn = timeline.find((item) => item.dataset.turnRef === highlightTurnRef) ?? null;
776
857
  const replacedFocusedTurn = timeline.find((item) => item.dataset.turnRef === focusedTurnRef) ?? null;
@@ -810,6 +891,7 @@ function renderDialog(task, { highlightTurnRef = null } = {}) {
810
891
  ? "calculating"
811
892
  : "unavailable",
812
893
  };
894
+ cancelTextShimmers(elements.dialogUsage);
813
895
  elements.dialogUsage.replaceChildren(usagePresentation(
814
896
  taskUsage,
815
897
  { wholeTask: true },
@@ -954,7 +1036,8 @@ function taskCard(task) {
954
1036
  resultLabel.textContent = "Result";
955
1037
  const result = document.createElement("span");
956
1038
  result.className = `preserve-lines${latest.resultStatus === "working" ? " text-shimmer" : ""}`;
957
- appendLinkedText(result, latest.resultSummary, task);
1039
+ if (latest.resultStatus === "working") renderTextShimmer(result, latest.resultSummary);
1040
+ else appendLinkedText(result, latest.resultSummary, task);
958
1041
  summary.replaceChildren(requestLabel, request, resultLabel, result);
959
1042
  const relatedLinks = relatedGitHubLinks(task, { compact: true });
960
1043
  const time = timestampControl(
@@ -991,7 +1074,9 @@ function render() {
991
1074
  const value = label.dataset.statusLabel;
992
1075
  label.textContent = statusFilterText(value, counts[value]);
993
1076
  }
994
- elements.taskList.replaceChildren(...visible.map(taskCard));
1077
+ const cards = visible.map(taskCard);
1078
+ cancelTextShimmers(elements.taskList);
1079
+ elements.taskList.replaceChildren(...cards);
995
1080
  elements.emptyState.hidden = visible.length > 0;
996
1081
  elements.taskCount.textContent = `${visible.length} of ${state.tasks.length} task${state.tasks.length === 1 ? "" : "s"}`;
997
1082
  clearTimeout(dateRefreshTimer);
@@ -1096,6 +1181,8 @@ elements.dialog.addEventListener("cancel", (event) => {
1096
1181
  if (!manualTransitionPending()) resetManualTransition({ focus: true });
1097
1182
  });
1098
1183
  elements.dialog.addEventListener("close", () => {
1184
+ cancelTextShimmers(elements.dialogResults);
1185
+ cancelTextShimmers(elements.dialogUsage);
1099
1186
  if (!manualTransitionPending()) state.manualTransition = null;
1100
1187
  state.detailNavigation = null;
1101
1188
  renderNotifications();
@@ -185,20 +185,12 @@ dialog section + section { margin-top: 24px; }
185
185
  .usage-pending, .usage-calculating { display: flex; align-items: center; }
186
186
  .text-shimmer {
187
187
  color: var(--muted);
188
- background: linear-gradient(100deg, var(--muted) 46%, var(--text) 50%, var(--muted) 54%);
189
- background-size: 250% 100%;
190
- background-position: 100% 0;
191
- background-clip: text;
192
- -webkit-background-clip: text;
193
- -webkit-text-fill-color: transparent;
194
- animation: text-shimmer 6.4s ease-in-out infinite;
195
188
  }
196
- @keyframes text-shimmer {
197
- 0% { background-position: 100% 0; }
198
- 72%, 100% { background-position: -100% 0; }
189
+ .text-shimmer-glyph {
190
+ color: var(--muted);
199
191
  }
200
192
  @media (prefers-reduced-motion: reduce) {
201
- .text-shimmer { background: none; animation: none; -webkit-text-fill-color: currentcolor; }
193
+ .text-shimmer-glyph { color: currentcolor; text-shadow: none; }
202
194
  }
203
195
  pre { max-height: 280px; margin: 0; padding: 14px; overflow: auto; border-radius: 7px; background: var(--surface-muted); white-space: pre-wrap; overflow-wrap: anywhere; font: 0.86rem/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
204
196
  .metadata { display: grid; grid-template-columns: minmax(100px, 150px) 1fr; margin: 0; font-size: 0.88rem; }