Parent epic: #1446 (Amicus — AI Study Partner v1)
Phase: 3 · Size: M · Depends on: #1462 (peek), #1455 (streaming chat)
A 2-3 turn mini-conversation experience that lives inside the FAB peek sheet. Reuses the streaming and citation infrastructure from #1455 but keeps thread state ephemeral (not persisted until user promotes to a full thread via #1464).
Files to create
app/src/components/amicus/PeekMiniConversation.tsx — the component
app/src/hooks/usePeekConversation.ts — ephemeral conversation state hook
app/src/components/amicus/__tests__/PeekMiniConversation.test.tsx
Files to modify
app/src/components/amicus/AmicusPeekSheet.tsx — mount PeekMiniConversation when a chip or input triggers it
Ephemeral state
Conversation in the peek is NOT persisted to user.db until user taps "Continue in Amicus tab →" (#1464). Before that, it lives in component/hook state only. If user dismisses the peek without promoting, the conversation is discarded.
This is a deliberate UX choice: the peek is quick-and-disposable; if a conversation is worth keeping, the user explicitly escalates it.
usePeekConversation hook
export interface PeekMessage {
role: 'user' | 'assistant';
content: string;
citations?: AmicusCitation[];
isStreaming?: boolean;
}
export interface UsePeekConversation {
messages: PeekMessage[];
isStreaming: boolean;
turnCount: number; // 0, 1, 2, 3
error: AmicusError | null;
send: (text: string, chapterRef?: ChapterRef) => Promise<void>;
reset: () => void; // clear conversation
snapshotForPromotion: () => PeekMessage[]; // called by #1464
}
Turn counting: a "turn" = user message + assistant response. At turn 3, the UI surfaces the handoff CTA. Send is not disabled at turn 3 — user can still continue, but handoff is strongly suggested.
Streaming: reuses streamChat from #1455 (same proxy endpoint). The difference is purely state location — messages live in hook memory, not user.db.
Component layout
Rendered inside the peek sheet below the chip area (chips hide once conversation starts).
┌─────────────────────────────────────┐
│ [drag handle] │
│ Amicus · Romans 9 │
│ ─────────────────────────────────── │
│ [user bubble] │
│ [assistant bubble streaming...] │
│ [follow-up chip] [chip] [chip] │
│ ─────────────────────────────────── │
│ At turn 3: │
│ ┌─ Continue in Amicus tab → ──────┐ │
│ └─────────────────────────────────┘ │
│ ─────────────────────────────────── │
│ [input field] [→] │
└─────────────────────────────────────┘
Reuses AssistantMessageBubble, UserMessageBubble, CitationPill, FollowUpChips from #1455. Do not duplicate rendering logic.
Differences from full thread
- Auto-collapses if peek is closed (lose ephemeral state)
- No thread title, no back button, no thread-level actions (pin/delete/rename)
- Scroll contained within peek — doesn't interfere with underlying screen scroll
Citation interactions
Citation pill taps in the peek do navigate to source (via #1456). This dismisses the peek and navigates the underlying stack — user can then reopen the FAB to return.
Error states
- Rate limit 429 → inline banner in peek: "You've used your monthly Amicus queries. Upgrade to Amicus+ or wait until [date]." + upgrade CTA
- Offline → inline banner: "Amicus needs a connection."
- Other errors → inline banner with retry
No error state collapses the peek — user can always dismiss manually.
Performance
- First token ≤ 1.5s (same as full tab, uses same proxy)
- Sheet height transitions smoothly as conversation grows
- Streaming does not cause peek to re-layout its container (use internal scroll)
Acceptance criteria
Out of scope
Parent epic: #1446 (Amicus — AI Study Partner v1)
Phase: 3 · Size: M · Depends on: #1462 (peek), #1455 (streaming chat)
A 2-3 turn mini-conversation experience that lives inside the FAB peek sheet. Reuses the streaming and citation infrastructure from #1455 but keeps thread state ephemeral (not persisted until user promotes to a full thread via #1464).
Files to create
app/src/components/amicus/PeekMiniConversation.tsx— the componentapp/src/hooks/usePeekConversation.ts— ephemeral conversation state hookapp/src/components/amicus/__tests__/PeekMiniConversation.test.tsxFiles to modify
app/src/components/amicus/AmicusPeekSheet.tsx— mount PeekMiniConversation when a chip or input triggers itEphemeral state
Conversation in the peek is NOT persisted to
user.dbuntil user taps "Continue in Amicus tab →" (#1464). Before that, it lives in component/hook state only. If user dismisses the peek without promoting, the conversation is discarded.This is a deliberate UX choice: the peek is quick-and-disposable; if a conversation is worth keeping, the user explicitly escalates it.
usePeekConversationhookTurn counting: a "turn" = user message + assistant response. At turn 3, the UI surfaces the handoff CTA. Send is not disabled at turn 3 — user can still continue, but handoff is strongly suggested.
Streaming: reuses
streamChatfrom #1455 (same proxy endpoint). The difference is purely state location — messages live in hook memory, not user.db.Component layout
Rendered inside the peek sheet below the chip area (chips hide once conversation starts).
Reuses
AssistantMessageBubble,UserMessageBubble,CitationPill,FollowUpChipsfrom #1455. Do not duplicate rendering logic.Differences from full thread
Citation interactions
Citation pill taps in the peek do navigate to source (via #1456). This dismisses the peek and navigates the underlying stack — user can then reopen the FAB to return.
Error states
No error state collapses the peek — user can always dismiss manually.
Performance
Acceptance criteria
anytypes; lint cleanOut of scope