feat: harden runtime readiness and session workflow

This commit is contained in:
User
2026-07-14 10:27:25 +02:00
parent 6d7738d538
commit 6dbf93fff9
52 changed files with 1464 additions and 169 deletions
@@ -0,0 +1,274 @@
# 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.