Skip to content

LGC-2026-08-11T094425

@malmstein malmstein tagged this 10 Aug 21:34
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1217151440349294
Tech Design URL (if applicable):
https://app.asana.com/1/137249556945/project/481882893211075/task/1216352541975427
API Proposals URL(s) (if applicable): N/A — no `-api` module change

**Stacked on #9428 — review and merge that one first. This PR targets
its branch, so the diff
here is only the second step.**

### Description

Second PR of the native message-editing series. Adds an edit mode to
`NativeInputModeWidget`
that reduces it to text field + attachments + submit — nothing calls it
yet, so this PR has no
behaviour change either; a later PR wires the launch path.

- Edit mode is a **widget-local flag**, not a field on the shared
`NativeInputState` — that type
is per-tab state read by the app module and every `NativeInputPlugin`,
and a second widget
instance publishing to it would clobber the real tab. The five pure
visibility helpers
(`shouldShowPluginControls`, `shouldShowLeadingFireButton`,
`shouldShowTrailingFireButton`,
`shouldShowToggleRowBack`, `shouldShowCardRowBack`) take `isEditing` as
a parameter instead.
- The edit widget publishes under a **synthetic `"edit:$sessionId"`
state key**, never the real
tabId, so it can share a `ViewModelStoreOwner` with the real widget
without corrupting its
published state (state only re-publishes on change — a clobber would be
unrepairable).
- Model picker / reasoning picker / options / start-chat /
stop-streaming / voice / both back
arrows / the mode switch / the chat-suggestions overlay are all
suppressed in edit mode. The
  attachment strip and new-line button are kept.
- `AttachmentViewModel.adopt(images, files)` ingests already-encoded
base64 attachments (the FE
hands over an already-sent message's attachments) straight into state —
no `Uri` decode, no
format/size validation, no limit accounting, since an edit can only
remove attachments.

### Steps to test this PR

_No user-facing entry point yet — unit tests only at this stage_
- [x] `./gradlew :duckchat-impl:testDebugUnitTest` green
- [x] Confirm no behaviour change on the existing native input (browser
omnibar, Duck.ai tab,
contextual sheet) — `isEditing`/`isEditMode` default to `false`
everywhere

### UI changes
None — the edit mode isn't reachable from any UI yet.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches shared native input state publishing, attachment lifecycle,
and many visibility paths, but edit mode is unreachable from UI and
defaults preserve current behavior until a follow-up wires the launch
path.
> 
> **Overview**
> Adds an **edit mode** to `NativeInputModeWidget` so a second composer
can show **text, existing attachments, and submit** only. Nothing
launches it yet, so existing omnibar/Duck.ai/contextual behavior stays
the same when `isEditMode`/`isEditing` default to false.
> 
> Edit is a **widget-local flag**, not a field on shared
`NativeInputState`. Visibility helpers (`shouldShowPluginControls`,
fire/back buttons, etc.) take `isEditing` so model/reasoning/options
pickers, attach button, voice, mode switch, and streaming-driven UI are
suppressed in edit while the attachment strip and new-line affordance
remain.
> 
> The edit instance publishes per-tab state under a **synthetic key**
`edit:$sessionId` via `configureForEdit`, so it does not overwrite the
real tab’s store. **Global `chatState` streaming** is ignored for that
key so unrelated chats do not block submit or hide the edit surface.
> 
> **`AttachmentViewModel.adopt`** loads FE-provided base64
`SubmittedImage`/`SubmittedFile` without picker validation or limit
accounting (edits only remove attachments); edit mode also hides limit
errors and blocks `limitExceeded` from blocking submit.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
37d3a15a80f1937cbbc1c80f832aa2f34454f3ba. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Assets 2
Loading