Commit 0dfe29b
authored
feat: editable messages and sibling navigation (#269)
* feat: editable messages and sibling navigation
Allow users to edit a previously sent message, which forks the
conversation into sibling branches at that point. Adds tree query
methods to ConversationRecord, an UpdateSiblingsAction, and
handle_edit/handle_navigate on HistoryController so the server can
fork history and switch between sibling branches. The client gets an
edit button and inline editor on user messages, sibling
prev/next navigation, and the siblings metadata/transport plumbing to
support it. Includes Playwright e2e coverage for editing and sibling
navigation.
* feat: rich-text edit box with editable attachments
Replaces the plain textarea used to edit a previously-sent message with
the same TiptapInput component the main composer uses, styled as a
boxed mini-composer, and extends it to support editing staged
attachments end-to-end (edit box -> ChatMessages/ChatContainer props ->
sendMessageEdit wire payload -> pkg-py handle_edit validation).
Also includes the UX fixes and cleanup that came out of testing this:
- Enter/Mod+Enter can submit an attachments-only edit (canSubmitEmpty).
- Edit-box chrome no longer lets the pill background paint over the
attach button; edit bubble takes the full row width.
- The pencil edit button is keyboard-focusable (opacity-based hiding
instead of visibility, revealed on :focus-visible/:focus-within).
- Stale attachment notices (downscale/size/gif-converted) are cleared
when a staged tray is replaced instead of resurfacing on reopen.
- useAttachmentStaging hook and AttachmentTray component extracted out
of ChatInput for reuse in the edit box.
- CHANGELOG entry for editable messages/sibling navigation; branch_from
test-fixture logic moved out of ConversationRecord into a pkg-py test
helper; pencil icon moved into the shared icons module.
Includes matching Playwright/unit test coverage and JS dist rebuilds.
* feat: reveal edit button via press-and-hold on touch devices
Touch devices had no way to reach the message-edit button, since it
was only revealed via :hover/:focus-within. A ~500ms press-and-hold
gesture on a user message now reveals it (gated to pointerType ===
"touch" and to editable, non-editing user messages), dismissed by
tapping elsewhere. Desktop mouse/keyboard behavior is unchanged.
* feat(pkg-r): editable messages and sibling branch navigation
Ports Python's message-editing and sibling (branch) navigation feature to
R's HistoryController, so chat_enable_history() understands the same
${id}_message_edit / ${id}_message_navigate wire protocol Python already
handles. The JS/TS client is fully shared between packages, so no
frontend changes were needed -- this is a server-only port.
Adds tree-traversal helpers on ConversationRecord, HistoryController
methods (send_sibling_metadata, handle_navigate, handle_edit), wiring
into chat_enable_history(), and extends the shared R/Python
behavior-matrix test harness to cover both new methods.
* fix(pkg-r): edit a message after the first without duplicating it
Editing any user message past the first left the pre-edit message on
screen alongside the freshly resubmitted one. The history save trigger
fired as soon as the server-side stream finished, before the browser
had echoed the completed assistant reply back over the websocket. That
one-message lag caused the reply to be attached to the wrong node,
which threw off every later edit's fork-point calculation by one.
Move the save trigger to fire off the client's message echo instead
(matching the Python implementation), so it only runs once the browser
has actually reported the finished reply.
* chore: rebuild web assets after rebasing onto main
* `air format` (GitHub Actions)
* fix(pkg-r): remove duplicated on_response save observer
The `message_response_effect` observer (and its label `history_on_response`)
was registered twice with identical bodies, so `on_response()` fired twice per
client echo, the first observer instance was orphaned (never destroyed), and
`cancel()` called `$destroy()` on the reassigned handle twice. Drop the
duplicate block and the extra `$destroy()`.
* fix(pkg-r): normalize edited-message attachments to match Python
Python's `handle_edit` reconstructs a canonical {mime, data_url, name, size}
record per attachment (dropping any extra client-sent fields) before echoing
them back through `update_input`; R passed the raw client list straight
through. Reconstruct the same shape in R for parity and to avoid trusting
client-supplied fields.
* fix: branch-navigation internals — remembered descendant, UI-count alignment, dead-flag cleanup
Groups four tightly-coupled history-internals changes (Python + R) that touch
the same functions and can't be cleanly separated:
- selected_child: navigating between sibling branches now returns to the
descendant last viewed inside that subtree instead of jumping to the newest
leaf. Adds an optional `selected_child` per node, recorded along the active
path by a new set_current_leaf() wherever the leaf moves, and followed by
subtree_leaf() (falling back to newest when unset). Round-trips through both
stores; backward-compatible (unset => today's behavior).
- ui_message_count(): node_id_for_message_index() and the sibling-metadata
builder now count a node with no `ui` as one message, matching replay_ui()'s
fabricated fallback, so client message indices and the server mapping agree.
- Drop the write-only `_suppress_next_save` flag in Python (never read; the
count-based guard already covers restore echoes; R still needs its flag).
- Comment why an empty sibling-metadata payload can't leave a stale badge.
* fix(js): seed edit editor only when entering edit mode
The seeding effect's deps include message.content/attachments, so a
re-reported message object arriving mid-edit would re-run setInputValue and
clobber the user's in-progress text (and re-steal focus). Guard on a
previous-state ref so seeding happens only on the false->true isEditing
transition.
* fix(js): key edit state by message id and stabilize edit callbacks
ChatMessages tracked the open edit by list index while the list is keyed by
id, so a reorder/shrink mid-edit could point at the wrong message; track it by
msg.id instead. Also pass stable useCallback handlers (onStartEdit/onCancelEdit)
instead of inline closures so ChatMessage's memo() can actually skip untouched
subtrees during streaming. onStartEdit now takes the message id.
* a11y(js): announce sibling position via aria-live
The prev/next buttons have aria-labels, but the "1 / 2" counter was a plain
span, so screen readers didn't announce version changes on navigation. Wrap it
in aria-live="polite".
* chore: rebuild web assets
Rebuilt js/dist and copied to both packages for the editable-message review
fixes (seed-on-open, id-keyed/memoized edit callbacks, aria-live counter).
---------
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>1 parent ad7d568 commit 0dfe29b
56 files changed
Lines changed: 5106 additions & 764 deletions
File tree
- js
- dist
- src
- chat
- transport
- utils
- tests
- chat
- helpers
- transport
- pkg-py
- src/shinychat
- www
- tests
- playwright/chat/history_edit
- pkg-r
- R
- inst/lib/shiny
- tests/testthat
- fixtures
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
274 | 274 | | |
275 | 275 | | |
276 | 276 | | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
277 | 291 | | |
278 | 292 | | |
279 | 293 | | |
| |||
310 | 324 | | |
311 | 325 | | |
312 | 326 | | |
| 327 | + | |
| 328 | + | |
313 | 329 | | |
314 | 330 | | |
315 | 331 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| 35 | + | |
35 | 36 | | |
36 | 37 | | |
37 | 38 | | |
| |||
66 | 67 | | |
67 | 68 | | |
68 | 69 | | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
69 | 76 | | |
70 | 77 | | |
71 | 78 | | |
| |||
97 | 104 | | |
98 | 105 | | |
99 | 106 | | |
| 107 | + | |
| 108 | + | |
100 | 109 | | |
101 | 110 | | |
102 | 111 | | |
| |||
450 | 459 | | |
451 | 460 | | |
452 | 461 | | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
453 | 474 | | |
454 | 475 | | |
455 | 476 | | |
456 | 477 | | |
457 | 478 | | |
| 479 | + | |
458 | 480 | | |
459 | 481 | | |
460 | 482 | | |
| |||
0 commit comments