* Thin global context for chat identifiers. The active conversation * id and the drawer's open/closed state both live in the URL (`?chat`) * so conversations are linkable and survive navigation. Everything * else transient — the selected model, streaming title, pending * message for the sidebar
| 30 | */ |
| 31 | |
| 32 | interface ChatStateValue { |
| 33 | /** |
| 34 | * Active conversation id. Mirrors `?chat` in the URL. When the |
| 35 | * drawer is closed this is still populated (the next open uses it), |
| 36 | * but `isOpen` will be false. |
| 37 | */ |
| 38 | conversationId: string; |
| 39 | /** True when the `?chat` param is present in the URL. */ |
| 40 | isOpen: boolean; |
| 41 | |
| 42 | /** |
| 43 | * Open the drawer with a fresh conversation. This is the default |
| 44 | * "Ask AI" / ⌘J behavior — previously this resumed a per-page |
| 45 | * conversation from localStorage, but that added a second source of |
| 46 | * truth next to the URL and was removed. The header's conversation |
| 47 | * dropdown is the way to resume a prior conversation. |
| 48 | */ |
| 49 | openChatForContext: () => void; |
| 50 | /** |
| 51 | * Start a brand-new conversation in the open drawer. Alias for |
| 52 | * openChatForContext — kept because several call sites use the |
| 53 | * name "newChat" for clarity. |
| 54 | */ |
| 55 | openNewChat: () => void; |
| 56 | /** Open the drawer on a specific existing conversation id. */ |
| 57 | openChat: (id: string) => void; |
| 58 | /** Close the drawer (clears `?chat`). */ |
| 59 | closeChat: () => void; |
| 60 | /** Switch the active conversation (leaves the drawer open). */ |
| 61 | switchConversation: (id: string) => void; |
| 62 | /** |
| 63 | * Start a fresh conversation inside the open drawer. Alias for |
| 64 | * openNewChat — kept for clarity at call sites. |
| 65 | */ |
| 66 | newConversation: () => void; |
| 67 | |
| 68 | /** Active agent name = model id from the whitelist. */ |
| 69 | agentName: string; |
| 70 | models: readonly ChatModelOption[]; |
| 71 | setAgent: (agentName: string) => void; |
| 72 | /** |
| 73 | * Tri-state derived from the `chat.models` query: |
| 74 | * - `null` — query hasn't resolved yet (show the neutral loading state) |
| 75 | * - `true` — at least one provider is configured on the API |
| 76 | * - `false` — no providers configured → show setup instructions |
| 77 | */ |
| 78 | isAiEnabled: boolean | null; |
| 79 | |
| 80 | /** |
| 81 | * Title being streamed for the active conversation, if any. Flips |
| 82 | * from null → accumulating deltas → null when either the stream |
| 83 | * finishes (persisted title takes over) or the conversation |
| 84 | * switches. The header renders this when non-null, falling back |
| 85 | * to the persisted title otherwise. |
| 86 | */ |
| 87 | streamingTitle: string | null; |
| 88 | setStreamingTitle: (value: string | null) => void; |
| 89 | /** |
nothing calls this directly
no outgoing calls
no test coverage detected