diff --git a/AGENTS.md b/AGENTS.md index c4f7d53ac..394da6322 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -627,6 +627,8 @@ If Claude behavior is unclear, check the official Claude Agent SDK docs, the ins ## Frontend Lifecycle Rules +- When the session rail hides `ComposerTaskProgress`, clear its transient open state and hover/press mode. Hiding the rendered control does not unmount the component's state. After undocking, the first click or keyboard activation must open the popup rather than close a retained pinned preview; keep the persisted rail preference separate from this temporary interaction state. + - Before any provider snapshots have hydrated, model resolution must preserve an explicit saved thread or draft slug using that provider's normal alias normalization. An empty registry is not an authoritative model-removal signal and must not replace a user's choice with a newly shipped default such as Astra. Once a real catalog is available, keep the existing instance-scoped availability and hidden-model checks; changing a bundled default must affect only genuinely missing selections. - The backend-authoritative `interfaceScalePercent` client setting is bounded to 80-130%, defaults to 100%, and is applied as the root HTML `font-size` percentage. This intentionally scales Cafe's rem-based typography, controls, icons, and spacing together while leaving browser zoom and stylesheet defaults intact; 100% must remove the inline override rather than pinning a redundant value. Keep the control in Settings -> Appearance, validate persisted values at the shared contract boundary, and verify compact title bars, fixed-format controls, composer typography/bounds, and overflow behavior at both bounds when changing shared layout. Desktop composer text and inline file/skill chips must remain rem-based so they scale with the rest of the interface. The composer may retain an absolute 16px text override only for coarse-pointer touch devices because iOS zooms focused editable text below that size; keep the editor's desktop min/max heights and padding rem-based. - The renderer should display backend/provider truth and should not synthesize terminal, running, or active-turn state that can conflict with orchestration projections. diff --git a/apps/web/src/components/chat/ComposerTaskProgress.browser.tsx b/apps/web/src/components/chat/ComposerTaskProgress.browser.tsx index 0ae8e592e..808c87e36 100644 --- a/apps/web/src/components/chat/ComposerTaskProgress.browser.tsx +++ b/apps/web/src/components/chat/ComposerTaskProgress.browser.tsx @@ -54,6 +54,35 @@ describe("ComposerTaskProgress", () => { document.body.innerHTML = ""; }); + it("returns from the session rail with a closed popup and accepts the first press", async () => { + const plan: ComposerTaskProgressPlan = { + steps: [{ step: "Review the change", status: "inProgress" }], + }; + const mounted = await mountProgress(plan); + try { + const trigger = page.getByRole("button", { + name: "Task progress: step 1 of 1. Show task list", + }); + trigger.element().focus(); + await userEvent.keyboard("{Enter}"); + await vi.waitFor(() => expect(progressPopup()).not.toBeNull()); + await mounted.screen.rerender(); + expect(progressTrigger()).toBeNull(); + await mounted.screen.rerender( + , + ); + await vi.waitFor(() => + expect(progressTrigger()?.getAttribute("aria-expanded")).toBe("false"), + ); + expect(progressPopup()).toBeNull(); + trigger.element().focus(); + await userEvent.keyboard("{Enter}"); + await vi.waitFor(() => expect(progressPopup()).not.toBeNull()); + } finally { + await mounted.cleanup(); + } + }); + it("stays hidden when there is no task plan", async () => { const withoutPlan = await mountProgress(null); try { diff --git a/apps/web/src/components/chat/ComposerTaskProgress.tsx b/apps/web/src/components/chat/ComposerTaskProgress.tsx index 0b42fe132..7bba8b9bd 100644 --- a/apps/web/src/components/chat/ComposerTaskProgress.tsx +++ b/apps/web/src/components/chat/ComposerTaskProgress.tsx @@ -1,5 +1,5 @@ import { BotIcon } from "lucide-react"; -import { memo, useRef, useState } from "react"; +import { memo, useEffect, useRef, useState } from "react"; import type { WorkLogEntry } from "../../session-logic"; import { type SubagentRosterEntry } from "../subagents/SubagentRosterRow"; @@ -39,6 +39,14 @@ export const ComposerTaskProgress = memo(function ComposerTaskProgress(props: { const [open, setOpen] = useState(false); const openModeRef = useRef<"hover" | "press" | null>(null); const triggerRef = useRef(null); + useEffect(() => { + if (props.sessionRailVisible) { + // Docking hides this component without unmounting its state. Clear the + // pinned preview so the first press after undocking opens it normally. + openModeRef.current = null; + setOpen(false); + } + }, [props.sessionRailVisible]); const subagents = (props.subagents ?? []).filter( (entry): entry is SubagentRosterEntry => entry.subagent !== undefined, ); diff --git a/docs/pr-assets/task-progress-reset/README.md b/docs/pr-assets/task-progress-reset/README.md new file mode 100644 index 000000000..a9058a2ff --- /dev/null +++ b/docs/pr-assets/task-progress-reset/README.md @@ -0,0 +1,7 @@ +These images and the recording show the real `ComposerTaskProgress` component with a synthetic one-step plan. No live account or project data is used. + +- `before.png` and `after.png`: after opening with Enter, docking to the session rail, and returning to the composer. +- `before-first-press.png` and `after-first-press.png`: after pressing Enter once on the returned control. The old component closes its retained popup; the repaired component opens it. +- `interaction.webm`: the repaired open, dock, return, and first-press sequence. + +The before capture uses the unchanged component from dev commit `99fbaec89da429924171c89d66a8f3455e42d9b0`. Temporary capture harnesses were removed after recording. The permanent browser regression verifies the same lifecycle without recording delays. diff --git a/docs/pr-assets/task-progress-reset/after-first-press.png b/docs/pr-assets/task-progress-reset/after-first-press.png new file mode 100644 index 000000000..60cc306b2 Binary files /dev/null and b/docs/pr-assets/task-progress-reset/after-first-press.png differ diff --git a/docs/pr-assets/task-progress-reset/after.png b/docs/pr-assets/task-progress-reset/after.png new file mode 100644 index 000000000..e3b98ba64 Binary files /dev/null and b/docs/pr-assets/task-progress-reset/after.png differ diff --git a/docs/pr-assets/task-progress-reset/before-first-press.png b/docs/pr-assets/task-progress-reset/before-first-press.png new file mode 100644 index 000000000..83d0c5f0c Binary files /dev/null and b/docs/pr-assets/task-progress-reset/before-first-press.png differ diff --git a/docs/pr-assets/task-progress-reset/before.png b/docs/pr-assets/task-progress-reset/before.png new file mode 100644 index 000000000..68fe0ff45 Binary files /dev/null and b/docs/pr-assets/task-progress-reset/before.png differ diff --git a/docs/pr-assets/task-progress-reset/interaction.webm b/docs/pr-assets/task-progress-reset/interaction.webm new file mode 100644 index 000000000..b0023270b Binary files /dev/null and b/docs/pr-assets/task-progress-reset/interaction.webm differ