Files
PaperClipAI/scripts/check-task-chat-motion.mjs
T
scotttongandClaude Fable 5 474c4138ac feat(ui): task-chat redesign phase 2 — v5–v7 reconciliation + live-thread completeness
Reconcile the baseline with the accepted v5–v7 prototype decisions and finish
the live-thread work items (, plan §5), all behind enableTaskChatRedesign:

- Composer: v7 comp-bar (attach + far left, rectangle status-chip mode trigger
  via --tc-mode-* hues incl. ask-blue #3b82f6, assignee combobox left of send,
  all controls 32px, no shortcut hints), per-message mode with ⇧⇥ cycling,
  attachments via button/paste/drag-drop, reassign-on-send with implicit reopen.
- Scroll pill: stock shadcn MessageScrollerButton (32px round, arrow-only) with
  tokenized entry/exit motion and easing-aware re-follow (smooth glide cannot
  unpin the follow state; wheel/touch cancels).
- Turn fold: run activity grouped into TaskChatTurn — live turns stream
  expanded, settling folds to a one-line '✓ Worked · 38s · 3 tools · +34 −3'
  summary (--motion-turn-fold 380ms, instant for settled history, reduced-motion
  honored), settled turns interleave before their run's reply comment.
- Bubbles: agent header gains the textless brand-gradient capsule avatar +
  name · mode chip (deterministic agent→gradient hash).
- Properties pane: tab label Plans→Plan; drag-resizable (clamp ≥260px,
  dbl-click reset 322px, width remembered) + maximizable (320ms glide, spacer
  holds flex slot, 840px content cap) — flag-off DOM untouched.
- Motion: new tokens (turn-fold, scroll-pill enter/exit + easings, pane-glide)
  in index.css :root + reduced-motion zeroing + tweak-panel catalog entries.

Verified: workspace tsc clean; check-task-chat-motion + token gates clean;
53 task-chat/PropertiesPanel/gate tests, 29 experimental-settings tests and
21 server instance-settings tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 16:55:27 -07:00

107 lines
3.7 KiB
JavaScript

#!/usr/bin/env node
/**
* Finish-line gate for the Task Chat Redesign (flag: enableTaskChatRedesign).
*
* 1. No hardcoded timing in the redesign surface: fails if any redesigned
* component/page contains a raw `<digits>ms` duration or a `cubic-bezier(`
* literal. All motion must reference the --motion-* tokens in
* ui/src/index.css (the single motion source), so the dev tweak panel can
* retune them live.
*
* Documented allowlist: ui/src/components/task-chat/motion-tokens.ts is the
* easing-preset catalog the tweak panel offers — those cubic-bezier values
* are data for the tool, analogous to the token layer, not component motion.
* Test files are excluded.
*
* 2. Flag-off isolation seams are single conditionals (grep-provable): asserts
* the IssueDetail chat-tab seam and the IssueProperties tab-shell seam exist.
*
* Exit non-zero on any violation.
*/
import { readFileSync, readdirSync, statSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join, relative } from "node:path";
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const SCAN_DIRS = ["ui/src/components/task-chat"];
const SCAN_FILES = [
"ui/src/components/TaskChatThread.tsx",
"ui/src/components/PropertiesPanel.tsx",
"ui/src/components/TaskChatRedesignGate.tsx",
"ui/src/pages/TaskChatLab.tsx",
"ui/src/components/issue-properties/IssuePropertiesPlansTab.tsx",
"ui/src/components/issue-properties/IssuePropertiesArtifactsTab.tsx",
];
const ALLOWLIST = new Set(["ui/src/components/task-chat/motion-tokens.ts"]);
const MS_RE = /\d+ms\b/;
const CUBIC_RE = /cubic-bezier\(/;
function isCheckable(path) {
if (!/\.(ts|tsx)$/.test(path)) return false;
if (/\.test\.(ts|tsx)$/.test(path)) return false;
return true;
}
function walk(dir, out) {
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) walk(full, out);
else out.push(full);
}
}
const files = [];
for (const d of SCAN_DIRS) walk(join(repoRoot, d), files);
for (const f of SCAN_FILES) files.push(join(repoRoot, f));
const violations = [];
for (const file of files) {
const rel = relative(repoRoot, file).split("\\").join("/");
if (!isCheckable(rel) || ALLOWLIST.has(rel)) continue;
const lines = readFileSync(file, "utf8").split("\n");
lines.forEach((line, i) => {
if (MS_RE.test(line) || CUBIC_RE.test(line)) {
violations.push(`${rel}:${i + 1}: ${line.trim()}`);
}
});
}
// Seam assertions (clause B: single grep-provable conditional in each file).
const seams = [
{
file: "ui/src/pages/IssueDetail.tsx",
needle: "? TaskChatThread : IssueChatThread",
label: "chat-tab seam",
},
{
file: "ui/src/components/issue-properties/IssueProperties.tsx",
needle: "if (!taskChatRedesignEnabled) return propertiesBody;",
label: "properties-pane seam",
},
];
const missingSeams = [];
for (const seam of seams) {
const text = readFileSync(join(repoRoot, seam.file), "utf8");
if (!text.includes(seam.needle)) missingSeams.push(`${seam.label} (${seam.file})`);
}
let failed = false;
if (violations.length > 0) {
failed = true;
console.error("Hardcoded timing found in the task-chat redesign surface:");
for (const v of violations) console.error(` ${v}`);
console.error("Use a --motion-* token from ui/src/index.css instead.");
}
if (missingSeams.length > 0) {
failed = true;
console.error("Missing flag-off isolation seam(s):");
for (const s of missingSeams) console.error(` ${s}`);
}
if (failed) process.exit(1);
console.log(
`check-task-chat-motion: OK (${files.filter((f) => isCheckable(relative(repoRoot, f).split("\\").join("/"))).length} files scanned, seams present).`,
);