Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
29 changes: 29 additions & 0 deletions apps/web/src/components/chat/ComposerTaskProgress.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<ComposerTaskProgress plan={plan} sessionRailVisible />);
expect(progressTrigger()).toBeNull();
await mounted.screen.rerender(
<ComposerTaskProgress plan={plan} sessionRailVisible={false} />,
);
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 {
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/components/chat/ComposerTaskProgress.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<HTMLButtonElement | null>(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,
);
Expand Down
7 changes: 7 additions & 0 deletions docs/pr-assets/task-progress-reset/README.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-assets/task-progress-reset/after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-assets/task-progress-reset/before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading