Skip to content

web: chat, the wallet and chat screens, and a build that works - #14

Open
ryannzander wants to merge 6 commits into
mainfrom
b/chat-send
Open

ryannzander wants to merge 6 commits into
mainfrom
b/chat-send

Conversation

@ryannzander

@ryannzander ryannzander commented Sep 5, 2026 •

Copy link
Copy Markdown
Owner

chat.send and chat.history, the messages table, a mock layer that runs the whole thing with nothing real behind it, and two screens. Plus the discovery that next build has never worked in this repo.

Screens. Wallet is the main one, navbar leads to the chat, per Ryan. The wallet leads with a panel drawn as the Flipper's amber LCD, and when a proposal is waiting that panel becomes the request, so the screen and the device in your hand show the same words at the same time. Statuses are worded for a person: "Waiting on you", not PENDING_HUMAN.

AGENT_MODE=mock stands in for the chain, the device and the model at once, since all three are missing and none of them are our code. ScriptedResponses matches keywords and calls the real tools through the real loop, so only the choice of tool is fake. Opt-in, because an invented balance looks exactly like a real one, and both screens say so when it is on.

Design calls in the chat layer

  • The transcript and the model's history are one table. Split apart they could disagree, and a model that saw a different conversation from the one on screen produces symptoms that all read as the model being stupid.
  • The user's row is written before the turn runs. A turn can fail after the model has already written a proposal, and a proposal that nothing in the transcript asked for is worse than a dangling user row.

Two bugs found on the way

  • The scripted router read an amount of 0 out of the digits inside a recipient address, so "send some eth to 0x7099…" would have proposed sending nothing. Caught by a test.
  • next build has never succeeded, on any commit. Confirmed against the tree before these changes, not assumed. @tappy/protocol imports its own modules as ./types.js, which Node and tsc resolve and webpack does not. And lint had never run, so errors had piled up in loop.ts, approvals.ts and schema.ts. Turbopack serves dev and is more forgiving, which is why nobody hit either.

CI ran neither the web tests, nor lint, nor the build. 52 tests only ever ran on somebody's laptop. All three are in the workflow now. The build is the only check that resolves modules the way Vercel will.

Checked: 52 web tests, 8 protocol tests, typecheck, lint, and a production build all pass locally.

Not checked, and this is what to do before merging. Nobody has seen either screen, verify:chat has never run, and migration 0001 has never been applied. This machine had no Postgres, no Docker and no .env. All three need a database and nothing else:

pnpm --filter @tappy/web db:migrate
pnpm --filter @tappy/web verify:chat        # scripted model, no API key needed
AGENT_MODE=mock pnpm --filter @tappy/web dev

Still open: chat.send and chat.history are unauthenticated. See the comment at the top of chat.ts and the earlier comment on this PR. Blocks deploying, not merging.

@ryannzander

Copy link
Copy Markdown
Owner Author

A background security review flagged this branch, and it is right on all three counts. They are one root cause: chat.send and chat.history are publicProcedure and there is no middleware.ts anywhere in the app.

No money moves without the human's press. What does break on a public URL:

  • a stranger spends the OpenAI budget and makes the Flipper buzz mid-demo;
  • one stranger proposal sits at PENDING_HUMAN and insertProposal refuses a second, so the real one is locked out for ten minutes;
  • the transcript is one global conversation, so their text is in the history the model reads on the next turn.

Not covered by DECISIONS #10, which is about the agent's tools. trpc.ts already draws the same line for the bridge.

Documented in the router and in docs/HANDOFF.md rather than fixed. Picking a scheme decides how the UI talks to the server, which is not mine to choose. This needs an answer before the app is deployed anywhere, not before this PR merges.

@ryannzander ryannzander changed the title web: the messages table and chat.send web: chat, the wallet and chat screens, and a build that works Sep 5, 2026
The transcript and the model's history are one table. `text` is what the
human reads, `items` is the raw Responses output the turn produced, and a
row cannot half-commit, so the two cannot drift. A model that saw a
different conversation from the one on screen is the worst bug this app
could have, because every symptom of it reads as the model being stupid.

`sendChatTurn` writes the user's row before running the turn, not with
the reply afterwards. A turn can fail after the model has already written
a proposal, and a proposal no message asked for is worse than a dangling
user row. The loop prepends the same message to the items it returns, so
that copy is dropped, and it is asserted rather than assumed.

History goes to the model newest-N-rows-then-sorted. Sliced by row
because a row holds a whole turn, so a function_call can never be
separated from the output that answers it.

`agentToolsFromEnv` throws by name: ChainReader and ShopReader have no
implementation while nothing is deployed (issue #5), and a loop that
quietly runs with no tools would report sending money it never sent.

`verify:chat` drives the turn against a real Postgres with a scripted
model. Written, not run: no database on this machine.
Both procedures are publicProcedure and there is no middleware anywhere
in the app, so on a public URL a stranger can read the transcript and
write into it. No money moves without the human's press, but the demo is
grief-able: one stranger proposal at PENDING_HUMAN locks out the real one
for ten minutes, and the transcript is global, so their text is in the
history the model reads on the operator's next turn.

Not covered by DECISIONS #10, which is about the agent's tools. trpc.ts
already draws this line for the bridge. No scheme picked here: the answer
shapes how the UI talks to the server, and that is Ryan's call.
Three things are missing and none of them are our code: nothing is
deployed, the Flipper spike is unrun, and there is no OpenAI budget.
mock.ts stands in for all three behind the interfaces that already
existed, so the rest of the app can be built and shown.

ScriptedResponses matches keywords and calls the real tools through the
real loop, so everything except the choice of tool is exercised for real.
It is not a model and the file says so in capitals.

AGENT_MODE gates it and defaults to live. A machine meant to be talking
to Sepolia should stop rather than quietly serve an invented balance,
because an invented balance looks exactly like a real one. chat.mode
exists so the UI can say which it is.

The mock addresses are repeated digits so that one turning up in a
proposal on a live machine says what happened faster than an error would.

A test caught the routing reading an amount of 0 out of the digits inside
a recipient address, so "send some eth to 0x7099..." would have proposed
sending nothing. The address comes out before the amount goes looking.
The device is the hero, not the balance. The wallet screen leads with a
panel drawn as the Flipper's amber LCD, and when something is waiting
that panel becomes the request, so the screen and the device in your hand
show the same words at the same time. Everything else is quiet: no cards,
no shadows, no gradients. Statuses are worded for a person, so a proposal
reads "Waiting on you" rather than PENDING_HUMAN.

The chat opens empty with the pitch in two sentences, per Ryan's call on
the layout: wallet as the main screen, navbar to the chat.

`next build` had never succeeded in this repo, on any commit. Two causes,
both pre-existing and both confirmed against the tree before these
changes. `@tappy/protocol` imports its own modules as `./types.js`, which
Node and tsc resolve and webpack does not, so the resolver now maps `.js`
to `.ts`. And lint had never run, so a dozen errors had piled up in
loop.ts, approvals.ts and schema.ts. Turbopack serves `dev` and is more
forgiving, which is why nobody hit either one.

CI ran neither the web tests, nor lint, nor the build. Fifty-two tests
only ever ran on somebody's laptop. All three are in the workflow now,
and the build is the only check that resolves modules the way Vercel
will.
next lint loads next.config.js, which imports env.js and validates. The
build step already had it and the lint step did not, so CI failed on a
missing DATABASE_URL rather than on anything in the code.

This branch has not been deployed

No deployments
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