# Model activity layout and composer state Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make activity updates readable, use the full application width as a 40/60 Model-activity/chat layout while activity is open, and reserve green input highlighting for genuine user-input states. **Architecture:** Keep `AppShell` as the owner of the transient activity-panel and composer state. `ModelActivityPanel` converts the known punctuation-boundary stream-concatenation case into Markdown paragraphs before rendering. The composer receives an explicit boolean from `AppShell`; widgets keep their existing independently highlighted textareas. **Tech Stack:** React 18, TypeScript, Tailwind CSS, Zustand, Vitest, Testing Library, MSW. ## Global Constraints - Preserve the existing SSE and persisted-session contracts; this is frontend-only. - UI chrome and test names stay English; Italian stream content is rendered unchanged except for paragraph separation. - When Model activity is open, it occupies 40% and the conversation 60% of the app area; the session rail is not rendered. - The normal composer is white. It is green only after **New session** begins question entry and while a pending `freetext` widget awaits a response. - Existing widget textareas with `data-awaiting-input="true"` remain green whenever rendered. - Run `npx vitest run` and `npx tsc -b` from `frontend/` before claiming completion. --- ## File structure - `frontend/src/shell/ModelActivityPanel.tsx` owns activity Markdown normalization and the left panel's width. - `frontend/src/shell/ModelActivityPanel.test.tsx` verifies activity rendering and the sentence-boundary regression. - `frontend/src/shell/AppShell.tsx` owns conditional 40/60 shell layout, hides the right rail, and passes the composer state. - `frontend/src/shell/AppShell.new-session.test.tsx` verifies shell layout switches and question-entry highlighting. - `frontend/src/shell/SteerInput.tsx` renders the composer according to an explicit `awaitingInput` prop. - `frontend/src/shell/SteerInput.test.tsx` verifies white default and explicit green composer states. ### Task 1: Preserve readable activity message boundaries **Files:** - Modify: `frontend/src/shell/ModelActivityPanel.tsx:10-17` - Modify: `frontend/src/shell/ModelActivityPanel.test.tsx:1-62` **Interfaces:** - Consumes: `useSessionStore((s) => s.transcript)`, whose entries provide `text: string`. - Produces: `formatModelActivity(text: string): string`, returning normalized Markdown with blank lines between independent sentences concatenated without whitespace. - [ ] **Step 1: Write the failing regression test** Add this test after `paragraphs stay separated as distinct blocks`: ```tsx test("separates activity updates concatenated after sentence punctuation", () => { useSessionStore.getState().applyEvent({ type: "text_delta", text: "Ambiguità principale risolta.Finestra temporale risolta.Terza ambiguità risolta.", }); render(); const first = screen.getByText("Ambiguità principale risolta."); const second = screen.getByText("Finestra temporale risolta."); const third = screen.getByText("Terza ambiguità risolta."); expect(first.tagName).toBe("P"); expect(second.tagName).toBe("P"); expect(third.tagName).toBe("P"); }); ``` - [ ] **Step 2: Run the focused test to verify it fails** Run: `npx vitest run src/shell/ModelActivityPanel.test.tsx -t "concatenated after sentence punctuation"` Expected: FAIL because the three strings are rendered as one paragraph and exact individual text matches cannot be found. - [ ] **Step 3: Add the minimal Markdown normalization** In `formatModelActivity`, immediately after newline normalization, add the punctuation rule below. It targets only a sentence-ending `.`/`!`/`?` immediately followed by an uppercase Italian/Latin letter, which is the malformed streamed-update signature; it does not affect ordinary spaces, lowercase continuations, or Markdown lists. ```ts return text .replace(/\r\n?/g, "\n") .replace(/([.!?])(?=[A-ZÀ-ÖØ-Þ])/g, "$1\n\n") .replace(/^[\t ]*[•‣–]\s+/gm, "- ") ``` - [ ] **Step 4: Run the focused panel suite** Run: `npx vitest run src/shell/ModelActivityPanel.test.tsx` Expected: PASS, including existing Markdown and collapsed-tail behavior. - [ ] **Step 5: Commit the activity formatting task** ```bash git add frontend/src/shell/ModelActivityPanel.tsx frontend/src/shell/ModelActivityPanel.test.tsx git commit -m "fix(frontend): separate concatenated model activity updates" ``` ### Task 2: Switch the shell between normal and 40/60 activity layout **Files:** - Modify: `frontend/src/shell/AppShell.tsx:280-365` - Modify: `frontend/src/shell/AppShell.new-session.test.tsx:1-90` **Interfaces:** - Consumes: local `showActivity: boolean` and `toggleActivity()` in `AppShell`. - Produces: `data-activity-layout="open" | "closed"` on the shell root; the activity `aside` has `w-2/5`, the conversation column has `w-3/5`, and the right sessions `aside` renders only when `showActivity` is false. - [ ] **Step 1: Write the failing layout behavior test** Add this test to `AppShell.new-session.test.tsx`: ```tsx test("opening Model activity replaces the session rail with a 40/60 activity and chat layout", async () => { renderShell(); expect(screen.getByText("Sessions")).toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: /show model activity/i })); const shell = screen.getByTestId("app-shell"); expect(shell).toHaveAttribute("data-activity-layout", "open"); expect(screen.getByRole("heading", { name: "Model activity" }).closest("aside")).toHaveClass("w-2/5"); expect(screen.queryByText("Sessions")).not.toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: /hide model activity/i })); expect(shell).toHaveAttribute("data-activity-layout", "closed"); expect(screen.getByText("Sessions")).toBeInTheDocument(); }); ``` - [ ] **Step 2: Run the focused test to verify it fails** Run: `npx vitest run src/shell/AppShell.new-session.test.tsx -t "40/60 activity and chat layout"` Expected: FAIL because the root has no test id/layout marker, Model activity is not reachable without an active session, and the right rail remains mounted. - [ ] **Step 3: Make Model activity available and conditionally lay out the shell** Update `AppShell` as follows: ```tsx
{showActivity && setShowActivity(false)} />}
``` Remove the `activeSessionId &&` condition around the header toggle so it is available in the landing view. Change the panel root in `ModelActivityPanel.tsx` from `w-[30vw] max-w-[30vw]` to `w-2/5 shrink-0`; the panel and conversation widths then exactly fill the shell. Finally, wrap the existing right-session-rail `aside` in `!showActivity && (...)` so it is unmounted while the activity panel is open. - [ ] **Step 4: Run the focused shell suite** Run: `npx vitest run src/shell/AppShell.new-session.test.tsx` Expected: PASS, including composer focus and provisional session creation tests. - [ ] **Step 5: Commit the layout task** ```bash git add frontend/src/shell/AppShell.tsx frontend/src/shell/ModelActivityPanel.tsx frontend/src/shell/AppShell.new-session.test.tsx git commit -m "feat(frontend): use full width for open model activity" ``` ### Task 3: Make composer highlighting explicit and input-driven **Files:** - Modify: `frontend/src/shell/SteerInput.tsx:10-26,112-121` - Modify: `frontend/src/shell/AppShell.tsx:34-45,242-267,344-354` - Modify: `frontend/src/shell/SteerInput.test.tsx:1-74` - Modify: `frontend/src/shell/AppShell.new-session.test.tsx:28-44` **Interfaces:** - Consumes: `AppShell` local `awaitingQuestion: boolean` and `pendingWidget?.widget` from Zustand. - Produces: optional `SteerInput` prop `awaitingInput?: boolean`; its textarea has `data-awaiting-input="true"` and `thot-awaiting-input` exactly when that prop is true. - [ ] **Step 1: Write the failing component tests** Add these tests to `SteerInput.test.tsx`: ```tsx test("keeps the composer white by default", () => { render(); expect(screen.getByRole("textbox", { name: /new question/i })).not.toHaveAttribute("data-awaiting-input"); expect(screen.getByRole("textbox", { name: /new question/i })).not.toHaveClass("thot-awaiting-input"); }); test("marks the composer as awaiting input only when requested", () => { render(); expect(screen.getByRole("textbox", { name: /new question/i })).toHaveAttribute("data-awaiting-input", "true"); expect(screen.getByRole("textbox", { name: /new question/i })).toHaveClass("thot-awaiting-input"); }); ``` Extend the existing New-session focus test with: ```tsx expect(composer).toHaveAttribute("data-awaiting-input", "true"); expect(composer).toHaveClass("thot-awaiting-input"); ``` - [ ] **Step 2: Run the focused tests to verify they fail** Run: `npx vitest run src/shell/SteerInput.test.tsx src/shell/AppShell.new-session.test.tsx -t "composer|New session starts"` Expected: FAIL because a landing composer currently always has `data-awaiting-input="true"` and `SteerInput` has no `awaitingInput` prop. - [ ] **Step 3: Add the explicit question-entry state and prop** Add `awaitingInput?: boolean` to `SteerInput` props and replace the textarea attributes/classes with: ```tsx data-awaiting-input={awaitingInput ? "true" : undefined} className={[ "max-h-40 flex-1 resize-none rounded-lg bg-card px-1 py-1 text-sm leading-relaxed outline-none placeholder:text-muted-foreground", awaitingInput && "thot-awaiting-input", ].filter(Boolean).join(" ")} ``` In `AppShell`, initialize `const [awaitingQuestion, setAwaitingQuestion] = useState(false)`. Set it to true in `startNewSession`; set it to false in `beginSessionCreation`, `finishSessionCreation`, `stopSession`, `doResume`, and the `session_exit` effect. Keep it true on `failSessionCreation` so the retained question remains visibly ready to retry. Pass the prop to the composer: ```tsx awaitingInput={awaitingQuestion || pendingWidget?.widget === "freetext"} ``` Do not change `FreetextWidget.tsx` or `ReservedControls.tsx`: their rendered textareas already accurately signal a required user response. - [ ] **Step 4: Run focused input and shell tests** Run: `npx vitest run src/shell/SteerInput.test.tsx src/shell/AppShell.new-session.test.tsx` Expected: PASS. The initial landing composer and active-session steering input are white; New session and a pending freetext gate are green. - [ ] **Step 5: Commit the composer-state task** ```bash git add frontend/src/shell/AppShell.tsx frontend/src/shell/SteerInput.tsx frontend/src/shell/SteerInput.test.tsx frontend/src/shell/AppShell.new-session.test.tsx git commit -m "fix(frontend): highlight composer only when input is needed" ``` ### Task 4: Verify the integrated frontend change **Files:** - Modify only if verification exposes a TypeScript or test issue in the files listed above. **Interfaces:** - Consumes: the completed shell, activity-panel, and composer contracts from Tasks 1–3. - Produces: validated frontend behavior with no API or persistence changes. - [ ] **Step 1: Run the entire frontend test suite** Run: `npx vitest run` Expected: PASS with no failed test files. - [ ] **Step 2: Run the frontend typecheck** Run: `npx tsc -b` Expected: exit code 0 and no TypeScript diagnostics. - [ ] **Step 3: Inspect the final working-tree diff** Run: `git diff --check && git status --short` Expected: no whitespace errors. Confirm that only the planned frontend files and this plan/spec are present among this task's changes; preserve all unrelated pre-existing modifications. - [ ] **Step 4: Commit verification-only follow-up, if needed** If Steps 1–3 required a corrective code or test change, stage only that correction and commit it with: ```bash git add git commit -m "test(frontend): verify activity layout and composer states" ``` If no corrective change was required, do not create an empty commit.