diff --git a/docs/superpowers/plans/2026-07-17-chat-interaction-state-polish.md b/docs/superpowers/plans/2026-07-17-chat-interaction-state-polish.md new file mode 100644 index 00000000..c26cc8b8 --- /dev/null +++ b/docs/superpowers/plans/2026-07-17-chat-interaction-state-polish.md @@ -0,0 +1,63 @@ +# Chat Interaction State Polish 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:** Keep suggestions stable until submission, allow read-only session switching during a request, and tighten two compact header/composer layouts. + +**Architecture:** Preserve request, billing, and session persistence logic. Change only JSX visibility/disabled guards and token-driven CSS, with source contract tests that fail on regression. + +**Tech Stack:** Next.js 16, React 19, TypeScript, Node test runner, CSS design tokens. + +## Global Constraints + +- Do not enable concurrent sends. +- Do not alter billing, cancellation, or model routing. +- Do not push the branch. +- Preserve unrelated working-tree changes. + +--- + +### Task 1: Suggestion visibility + +**Files:** +- Modify: `frontend/tests/starter-questions.test.ts` +- Modify: `frontend/src/app/page.tsx` +- Modify: `frontend/DESIGN.md` + +- [ ] Add failing contracts proving neither initial nor follow-up visibility guards reference `draft`. +- [ ] Run `node --test tests/starter-questions.test.ts` and confirm the follow-up contract fails. +- [ ] Remove the draft guard from follow-up suggestions while retaining loading and cancellation guards. +- [ ] Re-run the focused test and confirm it passes. + +### Task 2: Session navigation during requests + +**Files:** +- Modify: `frontend/tests/starter-questions.test.ts` +- Modify: `frontend/src/app/page.tsx` + +- [ ] Add a failing contract proving session-history buttons do not use `pendingSessionId` or `cancellationPending` as `disabled` state. +- [ ] Remove the disabled prop from existing-session buttons only; keep new-chat and send locks unchanged. +- [ ] Re-run the focused test and confirm it passes. + +### Task 3: Compact model selector and aligned credits + +**Files:** +- Modify: `frontend/tests/starter-questions.test.ts` +- Modify: `frontend/src/app/globals.css` +- Modify: `frontend/DESIGN.md` + +- [ ] Add failing CSS contracts for a 180px model popup and a one-line centered credit value. +- [ ] Change the popup width to `min(180px, calc(100vw - var(--space-6)))`. +- [ ] Make the credit value an inline flex box with `line-height: 1` and retain the existing 16px icon. +- [ ] Re-run the focused test and confirm it passes. + +### Task 4: Verification and commit + +**Files:** +- Verify all modified frontend files. + +- [ ] Run `npm test`, `npx tsc --noEmit`, and `npm run lint`. +- [ ] Run `npm run build` with the local environment loaded. +- [ ] Verify the empty-session preview keeps cards during typing and removes them after submit; verify follow-up suggestions follow the same rule. +- [ ] Inspect the final diff for unrelated files and debug artifacts. +- [ ] Commit only this feature's files; do not push. diff --git a/docs/superpowers/specs/2026-07-17-chat-interaction-state-polish-design.md b/docs/superpowers/specs/2026-07-17-chat-interaction-state-polish-design.md new file mode 100644 index 00000000..d53b6d22 --- /dev/null +++ b/docs/superpowers/specs/2026-07-17-chat-interaction-state-polish-design.md @@ -0,0 +1,26 @@ +# Chat Interaction State Polish + +## Goal + +Make question suggestions, session navigation, the model selector, and the credit indicator behave predictably without changing chat billing or allowing concurrent sends. + +## Approved behavior + +- Initial question cards remain visible while a user types or selects a question. They disappear only when the first user message is submitted. +- Follow-up suggestions remain visible while a user types or selects one. The current set disappears when the next user message is submitted; the next assistant answer may provide a new set. +- Existing sessions remain selectable while another session has a pending request. The pending request continues against its original session, while all new sends remain globally locked until it settles. +- The model popover width is reduced from 360px to 180px, with viewport collision protection retained. +- The credit icon and number share the same visual center using the existing flex layout and a compact line box. + +## Architecture + +Keep the current session-scoped request routing and billing locks. Correct only the UI guards: suggestion visibility follows submitted message state rather than draft state, session navigation no longer inherits the send lock, and CSS uses existing design tokens for the two layout adjustments. + +## Error and cancellation behavior + +Cancellation remains unchanged. If a send is withdrawn before output and the optimistic user message is removed, the previous suggestion set can reappear with the restored draft. + +## Verification + +- Contract tests cover draft-independent initial and follow-up suggestions, unlocked session navigation, the 180px popover, and credit alignment. +- Run the full frontend test suite, TypeScript, ESLint, production build, and a local preview interaction check. diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index d65d3897..6389d3b9 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -96,6 +96,7 @@ The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3: ### Model selector - **Structure:** a compact text trigger sits below the composer and opens an upward popover aligned to its left edge. The trigger shows only the active model name; each option shows only its model name and radio selection state. +- **Width:** the popover is capped at 180px with viewport collision protection. - **Surface:** canvas trigger with no card treatment; the popover uses the elevated canvas recipe, warm hairlines, and one selected-surface row. The action color is reserved for the selected indicator and focus ring. - **States:** closed, open, hover, focus-visible, selected, disabled, and unavailable catalog. Selecting a model closes the popover and only affects later messages in the current conversation. - **Accessibility:** the trigger and every option meet the 44px touch target; options are a native radio group, with a small roving-focus fallback so Tab, arrow keys, Space, and screen readers consistently expose the selected model inside the popover. @@ -105,6 +106,7 @@ The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3: - **Structure:** title, optional metadata, current-state marker. - **States:** default, hover, current, focus, disabled. +- **Request behavior:** existing sessions remain selectable for reading while a request is active; creating or sending another request stays locked until the active request settles. - **Surface:** translucent warm-gray sidebar; current uses a white glass surface and deep-brown marker. ### Message @@ -112,7 +114,7 @@ The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3: - **Variants:** assistant editorial text on canvas; user text on warm card surface; streaming; error. - **Identity:** every assistant message carries the 32px Jyotisha logo avatar; user messages stay visually lighter and avatar-free. - **Typography:** assistant body 16px with serif subheadings; user body 14px. -- **Suggestions:** keep follow-up actions compact, with 8px internal horizontal padding and a narrower 680px group width. +- **Suggestions:** keep follow-up actions compact, with 8px internal horizontal padding and a narrower 680px group width. The current set remains visible while the user types or selects a suggestion and leaves only when that question is submitted. - **Motion:** new messages enter with a short opacity/translate transition only. ### Suggestion card @@ -120,6 +122,7 @@ The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3: - **Structure:** topic label, question, directional icon. Categories are not numbered because they have no required order. At tablet widths, the cards stack into one column so Chinese questions keep natural phrase boundaries beside the persistent sidebar. - **Surface:** warm light cards; the lead card uses the pale brown emphasis surface and border instead of a dark block. - **States:** default, hover, active, focus, disabled, loading, fallback notice. +- **Visibility:** the three initial cards remain visible while the user types or chooses a question. They leave only after the question is submitted and the session receives its first user message. ### Start greeting diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 1f59aa1e..59977a9a 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -108,7 +108,7 @@ button:disabled { cursor: default; opacity: .45; } .session-nav { min-height: 0; flex: 1; display: flex; flex-direction: column; } .chevron { width: 18px; height: 18px; color: var(--color-ink-tertiary); } .chat-header strong, .chat-header span { display: block; } -.credit-button span { display: block; margin: 0; color: inherit; font-size: inherit; } +.credit-button span { display: inline-flex; align-items: center; margin: 0; color: inherit; font-size: inherit; line-height: 1; } .credit-button .credit-icon { width: 16px; height: 16px; flex: 0 0 auto; } .welcome-mark { margin-bottom: 20px; } .welcome h1, .auth-panel h1, .admin-header h1 { margin: 0; font-size: clamp(30px, 4vw, 42px); font-weight: 600; letter-spacing: -.035em; line-height: 1.1; } @@ -348,7 +348,7 @@ button:disabled { cursor: default; opacity: .45; } .model-selector-trigger svg { width: 16px; height: 16px; flex: 0 0 auto; color: var(--color-ink-tertiary); transition: transform 120ms ease-out; } .model-selector-trigger[data-popup-open] svg { transform: rotate(180deg); } .model-selector-positioner { z-index: 30; outline: 0; } -.model-selector-popup { width: min(360px, calc(100vw - var(--space-6))); border: 1px solid var(--color-border); padding: var(--space-3); border-radius: var(--radius-lg); background: var(--color-canvas); box-shadow: var(--shadow-elevated); transform-origin: var(--transform-origin); transition: opacity 120ms ease-out, transform 120ms var(--ease-out); } +.model-selector-popup { width: min(180px, calc(100vw - var(--space-6))); border: 1px solid var(--color-border); padding: var(--space-3); border-radius: var(--radius-lg); background: var(--color-canvas); box-shadow: var(--shadow-elevated); transform-origin: var(--transform-origin); transition: opacity 120ms ease-out, transform 120ms var(--ease-out); } .model-selector-popup[data-starting-style], .model-selector-popup[data-ending-style] { opacity: 0; transform: translateY(var(--space-1)); } .model-selector-title { margin: 0; color: var(--color-ink); font-family: var(--font-display); font-size: var(--type-title-sm); font-weight: 400; } .model-selector-options { display: grid; gap: var(--space-1); margin: var(--space-2) 0 0; padding: 0; border: 0; } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index ce01f57f..38dc4466 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1507,7 +1507,6 @@ export default function Home() { setComposerNotice(""); setMobileSidebarOpen(false); }} - disabled={Boolean(pendingSessionId) || cancellationPending} aria-current={session.id === activeSession?.id ? "page" : undefined} > {session.title} @@ -1586,7 +1585,7 @@ export default function Home() { {!profileComplete && onboardingStep === "name" && accountError &&
{accountError}
} - {profileComplete && presetMessageFinished && !draft.trim() && (onboardingPending ? ( + {profileComplete && presetMessageFinished && (onboardingPending ? (