[00139] Implement Rust Widget APIs for Chat, Audio, Camera, and Signature Inputs - #135
Open
rorychatt wants to merge 5 commits into
Open
Conversation
Seven new widget types: chat, chat_message, chat_loading, chat_status, audio_input, camera_input and signature_input. All seven already have Ivy React counterparts under src/frontend/src/widgets, so the Ivy mapping and node translation are updated in the same commit rather than left to drift. - rusty/src/widgets/chat.rs, rusty/src/widgets/media_inputs.rs: the widgets themselves, via #[derive(Widget)]. - event_registry: five new EventName variants (send, cancel, capture, sign, clear). test_event_name_round_trip restates the variant list, so it grows too. - widget_names: seven mechanical Ivy.* mappings; the mechanical count goes 25 -> 32 and the widget total 38 -> 45, which the scan floor and the constructed-widget list both assert on. - ivy_node: OnSend/OnCancel join IVY_EVENT_NAMES because ChatWidget.tsx reads them; OnCapture/OnSign/OnClear stay out because no Ivy widget does. `sender` joins ENUM_PROPS so it reaches Ivy as "User"/"Assistant", while `facingMode` deliberately does not -- getUserMedia needs it lowercase. Chat::quick_replies, AudioInput::show_waveform and CameraInput::capture_mode have no Ivy counterpart and are documented as Rust-side only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four new widget_harness kinds (chat, audio_input, camera_input, signature_input) and seven renderWidget arms, plus specs covering both. The renderer stays plain DOM: no getUserMedia and no MediaRecorder, just a fixed stub data URL per capture. What the specs are for is proving the serialized props arrive and the events round-trip, and a real device would make them flaky without testing anything more. ChatApp holds its conversation in use_state so `send` is observable end to end -- a new chat_message bubble is the server's acknowledgement. AudioInputApp counts focus and blur rather than storing one boolean, because the re-render a focus triggers replaces the button and fires a blur that would reset a flag. The seven arms go into rusty-desktop/assets/index.html identically. That file is a copy of e2e/app/index.html with four deliberate divergences and no automated parity check, so omitting them would leave the desktop shell rendering "[Unknown widget: chat]". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New pages 34_chat.md and 35_media_inputs.md, and the widget counts in 02_concepts/02_widgets.md go 38 -> 45 types and 25 -> 32 mechanical mappings. The Rust-only list is unchanged: all seven new widgets have Ivy counterparts. Both pages spell out the divergences the code comments record -- quick replies and show_waveform/capture_mode being Rust-side only, upload_url versus on_capture, and why facingMode stays lowercase. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`EventName::normalize` stripped only a lowercase `on` prefix, so
`from_str("OnSend")` returned None, `canonicalize` passed the raw string
through, and `dispatch` looked the handler up under "OnSend" while the
derive had registered it under "send" — the handler never fired.
That is the casing the vendored React widgets use: `ChatWidget.tsx` calls
`eventHandler("OnSend", ..)` / `("OnCancel", ..)` and `ButtonWidget.tsx`
calls `eventHandler("OnClick", ..)`, and `ivy_node`'s module doc already
claimed canonicalize "accepts `OnClick`, `onClick` and `click` alike".
Now it does. The uppercase-remainder filter is unchanged, so `online`
and `Online` are still left alone rather than read as `on` + `line`.
The plan's test list asks for `from_str("OnCapture")`; this is what makes
it true. Also pins that all three spellings of the five new event names
canonicalize to the registered lowercase form.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #127
00139 — Implement Rust Widget APIs for Chat, Audio, Camera, and Signature Inputs
Seven Rust widgets for React components that already shipped in the vendored
frontend but had no Rust producer (issue #127). No file under
src/frontendchanged.
Branch
tendril/00139-ImplementRustWidgetAPIsForChatAudioCameraAndSignatureInputs,four commits off
origin/mainatc528820:f95602d9242799f9d8dd97c0f43eChanges
Four chat widgets (
rusty/src/widgets/chat.rs, new).Chatis the threadplus composer, holding
ChatMessagechildren, a placeholder, astreamingflag, quick replies, size and density, and the
on_send(String)/on_cancel()events.
ChatMessagecarries aChatSender(User/Assistant) andarbitrary children, with
ChatMessage::user(..)/::assistant(..)shorthands.
ChatLoadingis a typing indicator with no properties at all;ChatStatusis one line of text. All four use#[derive(Widget)]— nohand-written
to_jsonwas needed.Three media inputs (
rusty/src/widgets/media_inputs.rs, new).AudioInput(microphone),
CameraInput(stills or clips) andSignatureInput(pointer-drawnsignature) each deliver their result as a base64 data URL. Two transports coexist
deliberately:
on_capture/on_signsend the data URL over the event socket(the Rust-native path, and the one the harness exercises), while
upload_urlmatches what the React widgets do — POST the blob themselves and fire no capture
event. Rusty has no upload endpoint today, so
upload_urlis a pass-through propfor a future adapter; the module doc says so.
Five new event names (
rusty/src/core/event_registry.rs).Send,Cancel,Capture,Sign,ClearjoinedEventName, socanonicalizeresolves thebrowser-shaped spelling to the lowercase name the derive registers under.
A canonicalization fix in the same file.
EventName::normalizestripped onlya lowercase
onprefix, sofrom_str("OnSend")returnedNoneanddispatchlooked the handler up under
"OnSend"while it was registered as"send"— thehandler never fired.
Onis exactly the casing the real client sends(
ChatWidget.tsxcallseventHandler("OnSend", ..),ButtonWidget.tsx("OnClick", ..)), andivy_node.rs's module doc already claimed canonicalize"accepts
OnClick,onClickandclickalike". It now does. This is wider thanthis plan's own widgets — it repairs inbound Ivy events for every pre-existing
widget too. See
recommendations.md.Ivy mapping. Seven mechanical
snake_case → Ivy.PascalCasearms inwidget_names.rs(chat→Ivy.Chat, and so on), with the inventory counts andthe
every_widget_type_is_mappedscan floor raised 38 → 45 so the guard staystight. In
ivy_node.rs,IVY_EVENT_NAMESgained"OnCancel"and"OnSend"(the two
ChatWidget.tsxreads) andENUM_PROPSgained"sender"so"user"/"assistant"title-case to whatChatMessageWidgetPropsdeclares.facingModeis deliberately not inENUM_PROPS: that value goes straight togetUserMedia, which accepts lowercase only.Harness coverage. Four apps in
rusty-server/src/bin/widget_harness.rsandseven
casearms in the client renderer, added identically to both copies of it(
e2e/app/index.htmlandrusty-desktop/assets/index.html) — there is noautomated parity check between them, so the desktop shell would otherwise render
[Unknown widget: chat]. The arms are plain DOM withdata-*hooks and fixedstub payloads: no
getUserMedia, noMediaRecorder, no device permissionsanywhere.
Docs. New pages
03_widgets/34_chat.mdand03_widgets/35_media_inputs.md,plus the counts in
02_concepts/02_widgets.md(38 → 45 types, 25 → 32mechanical mappings).
API Changes
New, all re-exported from
rusty::widgetsand therefore fromrusty::prelude:ChatchatIvy.ChatChatMessagechat_messageIvy.ChatMessageChatLoadingchat_loadingIvy.ChatLoadingChatStatuschat_statusIvy.ChatStatusAudioInputaudio_inputIvy.AudioInputCameraInputcamera_inputIvy.CameraInputSignatureInputsignature_inputIvy.SignatureInputSupporting enums:
ChatSender { User, Assistant },FacingMode { User, Environment },CaptureMode { Image, Video }— allDefault, all serializingcamelCase.
EventNamegained five variants — it is a public enum, so a downstreamexhaustive
matchon it would need the new arms.Behaviour change:
EventName::from_str/canonicalizenow accept theOnprefix (
"OnClick","OnSend"). Strictly wider acceptance; nothing that parsedbefore parses differently, and
"online"/"Online"are still not read ason+line.Nothing was removed or renamed. No
rusty-ivymlchange: its element table is adeliberate allowlist, not an inventory.
Files Modified
New:
rusty/src/widgets/chat.rs(+518)rusty/src/widgets/media_inputs.rs(+930)e2e/tests/widgets/chat.spec.ts(+82)e2e/tests/widgets/media-inputs.spec.ts(+130)rusty-docs/docs/03_widgets/34_chat.md(+98)rusty-docs/docs/03_widgets/35_media_inputs.md(+128)Changed:
rusty/src/widgets/mod.rs(+4) — modules and re-exportsrusty/src/core/event_registry.rs(+57/−1) — five event names,Onprefixrusty/src/shared/widget_names.rs(+41/−6) — seven Ivy arms, counts, scan floorrusty/src/shared/ivy_node.rs(+77/−3) —IVY_EVENT_NAMES,ENUM_PROPS, docsrusty-server/src/bin/widget_harness.rs(+179) — four harness appse2e/app/index.html(+169) — seven renderer armsrusty-desktop/assets/index.html(+169) — the same seven, byte-identicalrusty-docs/docs/02_concepts/02_widgets.md(+2/−2) — countsManual Testing
Gates, all from
AGENTS.md, all re-run after the final commit:cargo fmt --all -- --checkcargo clippy --workspace --all-targets --no-default-features -- -D warningscargo clippy -p rusty-desktop --all-targets -- -D warningscargo build --workspace --no-default-featurescargo build -p rusty-desktopcargo test --workspace --no-default-featuresnode scripts/check-harness-script.js<script>parses (1067 lines)"npx playwright test(frome2e/)pnpm install --frozen-lockfile && pnpm run build(fromsrc/frontend)The 15 new Playwright tests drive the real harness binary end to end: a chat
sendround trip appends a bubble the server owns, a quick reply arrives as thesame event, cancel increments a server-side counter, and each media input's stub
capture arrives as a data URL.
cargo fmtneeds a priorcargo buildin a freshworktree, because
rusty-docs/src/generated/is build-script output andgitignored.
Two things worth knowing for anyone re-running this:
AudioInputAppstored focus as abool; setting it triggers a rebuild that replaces the focused button, whoseblurreset the flag before the assertion could see it. Fixed by countingfocuses and blurs instead, with a comment in the harness explaining why.
npm ci/pnpm installfail on this machine withUNABLE_TO_GET_ISSUER_CERT_LOCALLY. Worked around outside the repo withNODE_EXTRA_CA_CERTSpointing at a dump of the macOS system trust roots; norepo file was changed for it.
Not tested: no browser ever touched a microphone or camera, and no signature was
drawn with a pointer — the renderer arms send fixed stub payloads. The Ivy React
components themselves are exercised by nothing here; what is proven is the wire
shape Rusty emits and that events come back.
cargo run -p rusty-desktopwasnot opened by hand.
Commits
7c0f43e[00139] Canonicalize the PascalCase event names Ivy actually sendsf9d8dd9[00139] Document the chat and media capture widgets9242799[00139] Exercise the seven new widgets through the E2E harnessf95602d[00139] Add Chat and media capture widgets to the rusty crateCreated using Ivy Tendril.