Skip to content

Chat-first landing + threads-demo A2UI/OpenGenUI patterns#15

Draft
jerelvelarde wants to merge 4 commits into
mainfrom
jerel/chat-first-mode-and-genui
Draft

Chat-first landing + threads-demo A2UI/OpenGenUI patterns#15
jerelvelarde wants to merge 4 commits into
mainfrom
jerel/chat-first-mode-and-genui

Conversation

@jerelvelarde

Copy link
Copy Markdown
Owner

Summary

  • New / landing renders a centered chat (no canvas behind) so fresh threads start as a clean conversation; /leads keeps the full kanban + sidebar layout. Toggle button on each side switches.
  • Adds the canonical threads-demo shapes adapted to the lead-form domain:
    • A2UI shared statestate.followups with manage_followups (agent writes) and an inline editable list (user writes), so agent and user collaborate on the same array.
    • Open Generative UIToolFallbackCard restyled as the threads-demo collapsible reasoning card (native <details>, spinning wrench → green check, auto-opens while running).
  • Durability — agent state is mirrored to localStorage on every change and rehydrated on mount, so starting a new thread no longer loses the imported Notion data.
  • Single LeadCopilotShell component now owns every frontend tool registration; both routes mount it once so the agent surface stays identical across modes.

Test plan

  • npm run dev boots cleanly (UI 3010, BFF 4010, agent 8133)
  • / shows the centered chat-only landing; "Open canvas" link navigates to /leads
  • /leads shows the kanban + sidebar; "Chat only" link in the header navigates back to /
  • "Import the leads from Notion." populates state.leads and persists to localStorage
  • After import, opening a new thread via the threads drawer keeps leads visible (hydrated from localStorage)
  • "Add follow-up tasks for the top 3 hottest leads and show the list." mounts an inline FollowupList; toggling items in the UI updates state.followups; agent's next call sees the user-side edits
  • Calling notion_health_check (or any tool without a custom render slot) renders the new collapsible ToolFallbackCard

🤖 Generated with Claude Code

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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +216 to +223
window.localStorage.setItem(
LEADS_CACHE_KEY,
JSON.stringify({
leads: current.leads,
sync: current.sync,
header: current.header,
}),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To maintain consistency with the frontend TypeScript definition and the system prompt, leadId should be marked as optional or allow None if it's not always present.

Suggested change
leadId: str
leadId: Optional[str]

const { agent } = useAgent();
const state = mergeAgentState(agent?.state);
const setState = (updater: (prev: AgentState) => AgentState) => {
agent?.setState(updater(mergeAgentState(agent?.state)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
agent?.setState(updater(mergeAgentState(agent?.state)));
agent?.setState((prev) => updater(mergeAgentState(prev)));

Comment on lines +31 to +32
const leadName = (id?: string) =>
id ? leads.find((l) => l.id === id)?.name ?? "" : "";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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] ?? "" : "");

jerelvelarde and others added 3 commits May 9, 2026 01:24
- 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>
@jerelvelarde jerelvelarde marked this pull request as draft May 9, 2026 08:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant