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

12 KiB
Raw Blame History

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:

    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.

    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

    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:

    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:

    <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

    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:

    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:

    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:

    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:

    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

    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:

    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.