Files
ThothII/docs/superpowers/plans/2026-07-14-model-activity-layout-and-composer-state.md
T

275 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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(<ModelActivityPanel onClose={vi.fn()} />);
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
<div
data-testid="app-shell"
data-activity-layout={showActivity ? "open" : "closed"}
className="flex h-screen bg-background text-foreground"
>
{showActivity && <ModelActivityPanel onClose={() => setShowActivity(false)} />}
<div className={[
"flex min-w-0 flex-col",
showActivity ? "w-3/5 shrink-0" : "flex-1",
].join(" ")}>
```
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(<SteerInput sessionId={null} />);
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(<SteerInput sessionId={null} awaitingInput />);
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 <corrected-files>
git commit -m "test(frontend): verify activity layout and composer states"
```
If no corrective change was required, do not create an empty commit.