From cb9b9d72d65cfe794ff8bc9ffdcf826ecf2bf4ae Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 00:14:03 +0000 Subject: [PATCH 1/4] feat(chat): add the Chat surface at /chat beside RFQ, Mailbox and Counterparties Chat reads the mailbox's own device-local records one counterparty at a time: Sent copies, imported payment links, and the OTC, payment and escrow state Mailbox persisted while it read the chain, grouped by contact. Nothing is fetched or posted from the page, and it is not settlement authority. - The header gains a Chat tab; RFQ, Mailbox, Counterparties and Pay are unchanged. - Left rail: contact name, latest record, unread count, search, and a needs-action filter over open offers, unpaid requests and fillable escrows. - Centre: chronological incoming and outgoing entries with compact record cards (offer, invoice, escrow, payment memo) that disclose the full Mailbox card, plus a composer that sends a body-only encrypted letter through the same policy, helper funding, key lookup, encryption and submit path as Compose. Value actions stay in Mailbox. - Right rail: wallet identity with the RFQ and Mail handoffs, open RFQs (bilateral offers, and workspace RFQs naming the contact as maker), pending payments and escrows; selecting an entry shows its detail in place and can locate it in the conversation. The eager application shell stays inside its recorded 450 kB budget by deferring the proposal-only market view and the dry cross-chain review, two secondary read-only tools; Chat itself loads with its sibling surfaces. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01DL5RB43eSUCvsor8uVPfUa --- README.md | 4 + docs/APP20_PRIVATE_DESK.md | 5 + src/app/chat/ChatComposer.tsx | 150 +++ src/app/chat/ChatContextPanel.tsx | 312 ++++++ src/app/chat/ChatConversationRail.tsx | 237 +++++ src/app/chat/ChatRecordCard.tsx | 118 +++ src/app/chat/ChatTimeline.tsx | 132 +++ src/app/chat/chat-model.test.ts | 353 +++++++ src/app/chat/chat-model.ts | 1026 +++++++++++++++++++ src/app/chat/chat-send.test.ts | 135 +++ src/app/chat/chat-send.ts | 365 +++++++ src/app/chat/chat.module.css | 1340 +++++++++++++++++++++++++ src/app/chat/page.tsx | 702 +++++++++++++ src/app/components/AppShell.tsx | 20 +- src/app/globals.css | 12 + src/app/routes.test.ts | 2 + src/app/routes.ts | 1 + src/main.tsx | 54 +- 18 files changed, 4956 insertions(+), 12 deletions(-) create mode 100644 src/app/chat/ChatComposer.tsx create mode 100644 src/app/chat/ChatContextPanel.tsx create mode 100644 src/app/chat/ChatConversationRail.tsx create mode 100644 src/app/chat/ChatRecordCard.tsx create mode 100644 src/app/chat/ChatTimeline.tsx create mode 100644 src/app/chat/chat-model.test.ts create mode 100644 src/app/chat/chat-model.ts create mode 100644 src/app/chat/chat-send.test.ts create mode 100644 src/app/chat/chat-send.ts create mode 100644 src/app/chat/chat.module.css create mode 100644 src/app/chat/page.tsx diff --git a/README.md b/README.md index 32ac2ac..50d8aa5 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,9 @@ Three surfaces form one workflow: 2. **Mailbox** — encrypted correspondence, structured evidence, invoice completion, and backup recovery 3. **Counterparties** — a device-encrypted directory with RFQ and Mail handoffs +**Chat** (`/chat`) complements them: it reads the same device-local records one +counterparty at a time. Multi-maker requests stay in the RFQ workspace. + Users connect once in the header. The Ready Wallet API path does not expose a viewing key to APP20; the optional Privy browser-owned SDK derives/holds its viewing key on the user's device. Mailbox keys also stay on-device. The STRK20 @@ -36,6 +39,7 @@ origin is live. This is not a Mainnet value-moving release. | Private RFQ | `/rfq` | Two localnet fixture makers, collateralized quotes, browser selection, and atomic Take. No production maker network. | | Mailbox | `/mail/inbox` | Encrypted correspondence, payment handoffs, and self-backups. Evidence and recovery transport, not settlement authority. | | Counterparties | `/contacts` | Device-encrypted labels and addresses with RFQ/Mail handoffs and optional encrypted backup. | +| Chat | `/chat` | One counterparty at a time: device-local letters, offers, invoices, and escrows with that contact, plus their open RFQs, pending payments, and escrows. Reads Mailbox's records; value actions stay in Mailbox. | | Separate tools | `/funding`, `/send`, `/cross-chain-review`, `/recovery/privy`, `/pay` | Funding readiness, unavailable public send, dry cross-chain review, Privy recovery, and unsigned payment links. | | Read-only views | `/rfq/operations`, `/rfq/markets/:tokenA/:tokenB/proposal`, `/swap/:tokenA/:tokenB` | Operations status, proposal-only market planning, and non-executable pair handoffs. | diff --git a/docs/APP20_PRIVATE_DESK.md b/docs/APP20_PRIVATE_DESK.md index 463881c..52a9593 100644 --- a/docs/APP20_PRIVATE_DESK.md +++ b/docs/APP20_PRIVATE_DESK.md @@ -6,6 +6,11 @@ APP20 joins three surfaces into one workflow: 2. **Mailbox** — encrypted correspondence, authenticated backup, and non-authoritative evidence. 3. **Counterparties** — a device-encrypted address book with RFQ and Mail handoffs. +**Chat** (`/chat`) reads the same device-local Mailbox records one counterparty at a +time: letters, offers, invoices, and escrows with that contact beside their open RFQs, +pending payments, and escrows. It complements the multi-maker RFQ workspace and takes +no value action of its own. + APP20 is a bookless invited-maker venue using the existing STRK20 privacy pool. It does not create a pool, AMM, public order book, or automatic public fallback. See [`GAPS.md`](GAPS.md) for open engineering work. ## Localnet-only boundary diff --git a/src/app/chat/ChatComposer.tsx b/src/app/chat/ChatComposer.tsx new file mode 100644 index 0000000..0aaa123 --- /dev/null +++ b/src/app/chat/ChatComposer.tsx @@ -0,0 +1,150 @@ +import { Link } from "@tanstack/react-router"; +import { useState, type FormEvent, type KeyboardEvent } from "react"; +import { ProvingProgress } from "@/components/mail/OperationProgress"; +import { + CHAT_LETTER_MAX_CHARS, + type ChatLetterBudget, + type ChatSendBlocker, +} from "./chat-send"; +import styles from "./chat.module.css"; + +export type ChatKeyState = + | { kind: "ready" } + | { kind: "missing" } + | { kind: "locked"; busy: boolean; error?: string }; + +export type ChatComposerStatus = Readonly<{ + kind: "sending" | "ok" | "error"; + message: string; + startedAt?: number; +}> | null; + +type ChatComposerProps = { + contactName: string; + value: string; + onChange: (value: string) => void; + blocker: ChatSendBlocker | null; + keyState: ChatKeyState; + onUnlock: (passphrase: string) => void; + sending: boolean; + status: ChatComposerStatus; + budget: ChatLetterBudget; + onSend: () => void; +}; + +export default function ChatComposer({ + contactName, + value, + onChange, + blocker, + keyState, + onUnlock, + sending, + status, + budget, + onSend, +}: ChatComposerProps) { + const [passphrase, setPassphrase] = useState(""); + const disabled = Boolean(blocker) || sending; + const canSend = !disabled && value.trim().length > 0 && budget.fits; + + function submit(event: FormEvent) { + event.preventDefault(); + if (canSend) onSend(); + } + + function onKeyDown(event: KeyboardEvent) { + if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + if (canSend) onSend(); + } + } + + return ( +
+ +