diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..89b12bf7b8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,19 @@ +# Enforce LF line endings for all text files so Windows/macOS/Linux +# contributors see a clean `git status` without line-ending noise. +* text=auto eol=lf + +# Keep Windows-native files as CRLF if any are ever added. +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf + +# Binary files that should never be touched. +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.woff binary +*.woff2 binary +*.ttf binary +*.eot binary diff --git a/AGENTS.md b/AGENTS.md index 517962c707..759f68d395 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,10 @@ - Keep provider event logging opt-in. Raw provider prompts, tool payloads, approval answers, and runtime output must not be persisted by default; use `CUT3_ENABLE_PROVIDER_EVENT_LOGS=1` only for deliberate local debugging. - Keep provider exit failures visible end-to-end. If a runtime emits `session.exited` with a non-graceful reason, orchestration must preserve that reason in `thread.session.lastError` so OpenCode/Copilot/Kimi/Codex crashes do not look like silent clean stops. - When testing hot orchestration streams backed by PubSub, avoid `fork + sleep` subscription races. Start the collector with an explicit readiness handshake (for example `Effect.forkScoped` plus `Effect.yieldNow`, or another deterministic subscription barrier) before dispatching commands. +- Keep interactive controls properly disabled during in-flight async operations (e.g. export, share, revoke): users must not be able to trigger conflicting actions while a prior action is still completing. Guard format toggles, download buttons, and secondary actions behind the relevant `isSaving`/`isRevoking` flags. +- Keep ARIA semantics aligned with visual affordances: disclosure/expand buttons need `aria-expanded`, toggle-style buttons need `aria-pressed` or `role="radio"` with `aria-checked`, icon-only buttons need explicit `aria-label`, tree-like file lists need `role="tree"`, and controls revealed only on hover (e.g. terminal close buttons) must also be revealed on `focus-visible` so keyboard users can reach them. +- Keep `aria-label` values on interactive groups and their trigger buttons accurate and descriptive of the actual feature. Do not leave placeholder labels from copy-paste (e.g. "Subscription actions" for an editor picker, "Copy options" for an editor menu). +- When a button visually looks disabled (opacity, cursor-not-allowed), make it actually `disabled` so it is removed from tab order and does not fire click handlers. CSS-only faux-disabled states are a keyboard trap. ## Project Snapshot diff --git a/apps/web/src/components/DiffWorkerPoolProvider.tsx b/apps/web/src/components/DiffWorkerPoolProvider.tsx index 0e0bbe81b5..3ee1175704 100644 --- a/apps/web/src/components/DiffWorkerPoolProvider.tsx +++ b/apps/web/src/components/DiffWorkerPoolProvider.tsx @@ -4,6 +4,32 @@ import { useEffect, useMemo, type ReactNode } from "react"; import { useTheme } from "../hooks/useTheme"; import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffRendering"; +/** + * Catch the fire-and-forget `initialize()` promise on the worker pool singleton. + * + * `@pierre/diffs` calls `workerPoolManager.initialize()` inside + * `getOrCreateWorkerPoolSingleton` without attaching a `.catch()`. When the + * React tree unmounts quickly (e.g. browser-test teardown), the pool is + * terminated while init is still in flight, which causes an unhandled + * rejection ("WorkerPoolManager: workers failed to initialize"). Attaching a + * no-op catch here prevents that from surfacing as a test/runtime error. + */ +function DiffWorkerPoolInitGuard() { + const workerPool = useWorkerPool(); + + useEffect(() => { + if (!workerPool) return; + // `initialized` is `false | Promise | true` on WorkerPoolManager. + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- accessing internal library field + const pending = (workerPool as unknown as Record).initialized; + if (pending && typeof (pending as Promise).catch === "function") { + (pending as Promise).catch(() => undefined); + } + }, [workerPool]); + + return null; +} + function DiffWorkerThemeSync({ themeName }: { themeName: DiffThemeName }) { const workerPool = useWorkerPool(); @@ -49,6 +75,7 @@ export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) { tokenizeMaxLineLength: 1_000, }} > + {children} diff --git a/apps/web/src/components/PlanSidebar.tsx b/apps/web/src/components/PlanSidebar.tsx index 47bee930cc..16b51862f9 100644 --- a/apps/web/src/components/PlanSidebar.tsx +++ b/apps/web/src/components/PlanSidebar.tsx @@ -224,6 +224,7 @@ const PlanSidebar = memo(function PlanSidebar({
diff --git a/apps/web/src/components/chat/ThreadShareDialog.tsx b/apps/web/src/components/chat/ThreadShareDialog.tsx index 7f8c3fd231..bc5557b1bc 100644 --- a/apps/web/src/components/chat/ThreadShareDialog.tsx +++ b/apps/web/src/components/chat/ThreadShareDialog.tsx @@ -92,11 +92,15 @@ export const ThreadShareDialog = memo(function ThreadShareDialog(props: { variant="outline" size="sm" onClick={props.onOpenSharedView} - disabled={!hasShare || !props.shareUrl} + disabled={!hasShare || !props.shareUrl || props.isRevokingShare} > Open shared view -
diff --git a/apps/web/src/components/settings/PermissionPoliciesSection.tsx b/apps/web/src/components/settings/PermissionPoliciesSection.tsx index 2eee859362..42bb94e3fd 100644 --- a/apps/web/src/components/settings/PermissionPoliciesSection.tsx +++ b/apps/web/src/components/settings/PermissionPoliciesSection.tsx @@ -184,6 +184,7 @@ export function PermissionPoliciesSection({ size="icon-sm" variant="ghost" disabled={isFirst} + aria-label="Move rule up" onClick={() => onChangeRules(moveRule(rules, rule.id, -1))} > @@ -192,6 +193,7 @@ export function PermissionPoliciesSection({ size="icon-sm" variant="ghost" disabled={isLast} + aria-label="Move rule down" onClick={() => onChangeRules(moveRule(rules, rule.id, 1))} > @@ -199,6 +201,7 @@ export function PermissionPoliciesSection({