Chat-first landing + threads-demo A2UI/OpenGenUI patterns#15
Chat-first landing + threads-demo A2UI/OpenGenUI patterns#15jerelvelarde wants to merge 4 commits into
Conversation
Refactors the lead-form starter to ship the canonical threads-demo shapes (shared-state A2UI + open generative UI) on top of the existing Notion + canvas surface, and introduces a chat-first entry point. - New `/` landing renders a centered CopilotChat (no canvas behind it) so a fresh thread starts as a clean conversation. `/leads` keeps the full canvas + sidebar layout. Toggle button on each side switches. - Extracts a single `LeadCopilotShell` component that registers every frontend tool exactly once per page, so chat-only and app modes share the same agent surface. - A2UI shared-state demo: `state.followups: Followup[]` plus a `manage_followups` frontend tool and `renderFollowups` slot that mounts a live editable list inline in chat. Both the agent and the user write to the same field — the list pattern from threads-demo, ported to the lead-form domain. - Open generative UI fallback (`ToolFallbackCard`) restyled to the threads-demo collapsible reasoning card: native `<details>`, spinning wrench while running, green check on complete, auto-open during execution. - Durability: the shell mirrors `agent.state.leads` to localStorage on every change and hydrates fresh threads from the cache on mount, so starting a new conversation no longer loses imported Notion data. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a "Chat-only" mode and a centralized LeadCopilotShell component to manage agent logic and state across different layouts. It implements a shared follow-up task system, allowing users and the agent to collaboratively manage lead-related actions. Key technical changes include localStorage persistence for lead data, enhanced tool execution visibility in the UI, and updated system prompts. Feedback addresses security concerns regarding PII storage in localStorage, type consistency between the frontend and backend, and performance optimizations for list rendering and state updates.
| window.localStorage.setItem( | ||
| LEADS_CACHE_KEY, | ||
| JSON.stringify({ | ||
| leads: current.leads, | ||
| sync: current.sync, | ||
| header: current.header, | ||
| }), | ||
| ); |
There was a problem hiding this comment.
Storing lead data (including names, emails, and potentially phone numbers) in localStorage in plain text poses a security and privacy risk, as localStorage is not encrypted and is accessible to any script running on the same origin (XSS risk). For a production application, consider using a more secure storage mechanism or ensuring that sensitive PII (Personally Identifiable Information) is not persisted this way.
| id: str | ||
| text: str | ||
| status: str # "pending" | "done" | ||
| leadId: str |
| const { agent } = useAgent(); | ||
| const state = mergeAgentState(agent?.state); | ||
| const setState = (updater: (prev: AgentState) => AgentState) => { | ||
| agent?.setState(updater(mergeAgentState(agent?.state))); |
There was a problem hiding this comment.
Using a functional update with agent.setState ensures that the state update is based on the most recent state, preventing potential race conditions when multiple updates occur in rapid succession.
| agent?.setState(updater(mergeAgentState(agent?.state))); | |
| agent?.setState((prev) => updater(mergeAgentState(prev))); |
| const leadName = (id?: string) => | ||
| id ? leads.find((l) => l.id === id)?.name ?? "" : ""; |
There was a problem hiding this comment.
The leadName function performs an O(N) search on the leads array for every followup item rendered. For better performance, especially with larger lead lists, consider creating a lookup map.
| const leadName = (id?: string) => | |
| id ? leads.find((l) => l.id === id)?.name ?? "" : ""; | |
| const leadMap = Object.fromEntries(leads.map((l) => [l.id, l.name])); | |
| const leadName = (id?: string) => (id ? leadMap[id] ?? "" : ""); |
- Chat-only landing now renders fully white (was lavender flanking the centered chat). Header and main panel both pinned to white. - The first time leads arrive during a session (after `Import the leads from Notion.`), chat-only auto-navigates to /leads. Returning users with cached leads stay on / — the navigation only fires when count transitions from 0 to >0 *after* localStorage rehydration completes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`openGenerativeUI={{}}` enables the A2UI subsystem to render its own
messages (`render_a2ui` / `generate_a2ui` / `log_a2ui_event`). Without
this filter, the wildcard ToolFallbackCard mounts the same tool call a
second time — React then raises duplicate-key warnings on the chat
message list. Matches the threads-demo's ignore list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LeadStateMiddleware.before_agent hydrates the agent's state from the local lead store on every fresh thread's first turn. From the frontend's perspective that's indistinguishable from a user-initiated import — leads.length transitions 0→N either way. The auto-nav was firing on any unrelated prompt (e.g. "generate a calculator"). The chat header's "Open canvas (N leads)" link is the explicit signal. Users open the canvas when they want to. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
/landing renders a centered chat (no canvas behind) so fresh threads start as a clean conversation;/leadskeeps the full kanban + sidebar layout. Toggle button on each side switches.state.followupswithmanage_followups(agent writes) and an inline editable list (user writes), so agent and user collaborate on the same array.ToolFallbackCardrestyled as the threads-demo collapsible reasoning card (native<details>, spinning wrench → green check, auto-opens while running).LeadCopilotShellcomponent now owns every frontend tool registration; both routes mount it once so the agent surface stays identical across modes.Test plan
npm run devboots cleanly (UI 3010, BFF 4010, agent 8133)/shows the centered chat-only landing; "Open canvas" link navigates to/leads/leadsshows the kanban + sidebar; "Chat only" link in the header navigates back to/state.leadsand persists to localStoragestate.followups; agent's next call sees the user-side editsnotion_health_check(or any tool without a custom render slot) renders the new collapsible ToolFallbackCard🤖 Generated with Claude Code