Skip to content

feat(desktop): add a Puppet host that drives the Chats screen headlessly - #1974

Draft
bmc08gt wants to merge 3 commits into
code/cashfrom
feat/reflex-desktop-cli
Draft

bmc08gt wants to merge 3 commits into
code/cashfrom
feat/reflex-desktop-cli

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

./gradlew :apps:desktop:puppet runs ChatsViewModel over seeded chats with no network, and takes one JSON request per line on stdin:

  • {"cmd":"ls"} returns the Chats node: a state snapshot plus its actions and each action's input schema.
  • {"cmd":"do","action":"FilterSelected","input":{"filter":"Unread"}} dispatches the action and returns the node again.
  • Failures come back as {"ok":false,"error":{"code":...}} with unknown_action (lists the valid actions), bad_input (includes the schema), bad_request, or fixture_violation.

./gradlew :apps:desktop:installPuppet writes apps/desktop/build/install/puppet-host/bin/puppet-host, which runs the same host without Gradle, so stdout carries only protocol lines. A session starts in about 1.4 s. The script points at the built classes and libraries instead of copying jars, because two modules produce a jar with the same name.

The host is meant to be driven by a shared puppet CLI that holds the protocol schemas and the behavior tests both platforms run. That repo isn't published yet.

Changes

  • libs/puppet: the host. Node and action types, a JSON schema derived from each action's serializer, and the request loop.
  • Chats events: the five events the screen sends (FilterSelected, ChipsRevealed, SelectionStarted, SelectionToggled, SelectionEnded) are @Serializable under short names and implement a new PublicAction marker in ui/navigation. ChatListFilter is @Serializable. A test checks the node lists exactly the PublicAction events.
  • Fixture (apps/desktop/.../puppet): fakes ChatCoordinator and builds the rest of the screen's dependencies with their real constructors. Repositories are proxies that record each call to a ledger and then throw. The host reports any recorded call as fixture_violation, because much of the app swallows the throw in runCatching.

The seam is ChatCoordinator rather than ChatRepository: the repository is the gRPC-facing layer, and faking above it keeps the coordinator's real list logic out of the test. The fixture is hand-wired instead of a second Metro graph, since ChatModule binds RealChatCoordinator with no way to override it.

Separate fix: desktop graph on code/cash

The first commit fixes a break already on code/cash. #1954 gave WalletViewModel a defaulted TransactionRouter parameter, and the desktop graph's REQUIRE_OPTIONAL_BINDING ignores Kotlin defaults, so Metro reports a missing binding. Desktop now binds TransactionRouter.DetailsOnly. It doesn't handle the wallet's OpenScreen routes yet, so Android's chat-aware router would have nothing to open.

#1954 gave WalletViewModel a defaulted TransactionRouter parameter. The
desktop graph sets REQUIRE_OPTIONAL_BINDING, so Metro ignores the default
and the graph fails with a missing binding for TransactionRouter.

Bind DetailsOnly rather than Android's chat-aware router: desktop doesn't
handle the wallet's OpenScreen routes yet, so a route to a DM would go
nowhere.
`./gradlew :apps:desktop:reflex` runs ChatsViewModel over seeded chats and
reads one JSON request per line: `{"cmd":"ls"}` returns the screen's node
(state plus its actions and their input schemas), and
`{"cmd":"do","action":...,"input":{...}}` dispatches an action and returns
the node again. Errors come back as unknown_action, bad_input,
bad_request or fixture_violation.

- libs/reflex holds the host: node and action types, a JSON schema built
  from each action's serializer, and the request loop.
- The five events the Chats screen sends are now @serializable under short
  names and marked PublicAction (ui/navigation). A test checks the node
  lists exactly the PublicAction events.
- The fixture fakes ChatCoordinator and builds the rest with real
  constructors. Repositories are proxies that record each call to a
  ledger and throw; the host turns any recorded call into
  fixture_violation, since much of the app swallows the throw in
  runCatching.
@bmc08gt bmc08gt self-assigned this Nov 6, 2026
@github-actions github-actions Bot added type: feature New functionality area: ui Compose UI, theme, components, resources area: build-system Gradle, convention plugins, build-logic and removed type: feature New functionality labels Nov 6, 2026
`./gradlew :apps:desktop:installPuppet` writes
build/install/puppet-host/bin/puppet-host, which runs the host on the
JDK 21 toolchain without Gradle. A session starts in about 1.5s and its
stdout holds only responses; through the Gradle task, every launch paid
Gradle's startup and the build script's `VersionCode` line came first.

The script points at the built classpath instead of copying jars into a
lib/ folder, because several modules build a jar with the same name
(core-jvm.jar) and the copies would collide.
@github-actions github-actions Bot added the type: feature New functionality label Nov 6, 2026
@bmc08gt bmc08gt changed the title feat(desktop): drive the Chats screen headlessly over stdin feat(desktop): add a Puppet host that drives the Chats screen headlessly Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: build-system Gradle, convention plugins, build-logic area: ui Compose UI, theme, components, resources type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant