Skip to content

feat(app): desktop composer UX — boxed surface, inline selectors, focus & scrollbar fixes - #59

Merged
leduckhc merged 14 commits into
mainfrom
feat/desktop-composer-ux
Jul 15, 2026
Merged

feat(app): desktop composer UX — boxed surface, inline selectors, focus & scrollbar fixes#59
leduckhc merged 14 commits into
mainfrom
feat/desktop-composer-ux

Conversation

@leduckhc

@leduckhc leduckhc commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Reopens the work formerly tracked in #57 (closed when the branch was renamed from `chore/snapshot-v004` to `feat/desktop-composer-ux`).

What changed

  • Multiline composer with inline model & thinking selectors, plus an ACP session-mode selector.
  • Unified, boxed composer surface: input + model/thinking/mode selectors + attach + send now share one rounded box with a single static background (darker than the transcript, no hover shift). Top divider removed; expanded field starts at 3 rows.
  • Focus fix: the desktop keymap scope no longer steals focus from the composer TextField, so the chat input is clickable/typeable again. Regression + macOS e2e tests added.
  • Scrollbars hidden app-wide on desktop (transcript, sidebar, pickers) via a global `MaterialScrollBehavior`; content still scrolls via trackpad/wheel.
  • Composer footer selector overflow fixes on narrow widths.
  • Server: re-emit `session.meta` from the native pi rpc adapter; extract `MetaModel` type alias.
  • Only use the merge icon for worktrees with an open PR.

Testing

  • `flutter analyze` clean; composer + keymap-scope unit/widget tests pass.
  • macOS e2e verifies the composer is hit-testable and accepts text.

Summary by CodeRabbit

  • New Features
    • Added composer footer controls for selecting model, thinking level, and session mode.
    • Composer supports an expanded multiline layout with footer actions (including persistent expanded mode on desktop).
    • Session mode metadata is exposed and can be updated from the composer.
    • Desktop worktree header icons now reflect pull request state.
    • Scrollbars are suppressed across the desktop app.
  • Bug Fixes
    • Improved composer focus handling and typing behavior.
    • Moved model/thinking controls from the session overflow menu to the composer footer.
  • Tests
    • Added end-to-end and widget coverage for composer selectors, menu content, focus behavior, and worktree icons, plus backend adapter mode handling.

leduckhc added 11 commits July 15, 2026 14:01
Redesign the chat composer into a taller multiline input (auto-grows to 10
lines) with a footer toolbar: model + thinking-effort selectors on the left,
add/send on the right. Desktop always shows the full form; mobile keeps its
one-line compact state when unfocused and expands to the desktop form on focus.

The selectors reuse the existing /model and /thinking flows. The model selector
renders nothing until session.meta arrives with a non-empty models list; the
thinking selector is hidden when the agent reports no thinking support.

Model/Thinking are removed from the desktop header overflow menu (now Rename /
Quit only) and the read-only model-thinking line is dropped from the mobile
session header, since both are now editable inline in the composer.
Wrap the footer selectors in an Expanded row of Flexible children so long
model/thinking labels ellipsize instead of triggering a RenderFlex overflow on
narrow (phone) widths. Add tests for the compact-hidden footer and the
narrow-width overflow regression (CodeRabbit review).
Replace the bare-Text narrow-width test with one using the actual
ComposerModelSelector/ComposerThinkingSelector and a long model name, asserting
the label is single-line ellipsized (not just that Flexible wrapping avoids
overflow). Moved to composer_selectors_test.dart where Riverpod is wired
(CodeRabbit review).
PR #56 switched every worktree row to the call_merge symbol. Restore the prior
fork_right (branch) icon for worktrees without an open PR, keeping call_merge
(accent) only where a PR is open.
session.meta (model + thinking + selectable models) stopped being emitted when
pi-mirroring was removed in #26, so the app's model/thinking selectors had no
data. Re-source it from pi's rpc protocol: query get_state + get_available_models
at boot and emit a merged session.meta; map the composer's model/thinking
actions to set_model/set_thinking_level and re-query on success.

Note: ACP (0.26) exposes only session *modes*, not a model list or thinking
level, so ACP agents (codex) can't feed these selectors — tracked separately.
ACP agents (e.g. codex) have no model/thinking concept — only session modes
(ask/code/architect). Surface them as a third composer selector:

- server (acp.ts): capture newSession modes, emit them via session.meta, keep
  them in sync on current_mode_update, and map the 'mode' action to ACP's
  session/set_session_mode.
- app: extend SessionMeta with a modes state; add ComposerModeSelector (hidden
  unless modes exist) wired into the desktop + mobile composer footers.

Native pi sessions carry no modes (selector hidden); ACP sessions carry no
model/thinking (those selectors hidden). Tests added on both sides.
Deduplicate the repeated {provider,id,name} shape into a shared MetaModel alias (CodeRabbit nitpick).
Wrap the mode bottom-sheet Column in a SingleChildScrollView so a long list of agent modes (or large text scaling) stays reachable (CodeRabbit review).
The idle-focus reclaim treated any node inside the scope as idle, so
focusing the composer TextField (a descendant of the scope) immediately
yanked focus back to the fallback node, making the chat input impossible
to type into. Restrict the descendant check to empty FocusScopeNodes so
real focusable leaves keep their focus.

Adds a headless regression test plus a macOS e2e proving the input is
hit-testable and accepts text.
Group the input, model/thinking/mode selectors, attach and send buttons
into a single rounded box with one static background (darker than the
transcript, no hover shift). Remove the top divider, hide the input
scrollbar, and start the expanded field at 3 rows.
Add a global MaterialScrollBehavior that suppresses scrollbars across
the desktop app (transcript, sidebar, pickers). Content still scrolls via
trackpad/wheel.
@cursor

cursor Bot commented Jul 15, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds model, thinking, and mode metadata across ACP and pi adapters, exposes selectors in composer footers, updates desktop composer and menu layouts, and adds coverage for composer behavior, metadata synchronization, focus handling, worktree icons, and desktop menus.

Changes

Composer metadata and desktop UI

Layer / File(s) Summary
Session metadata contracts and adapter synchronization
app/lib/store/models.dart, server/src/adapters/acp.*, server/src/adapters/pi.*
Session metadata now includes modes, while ACP and pi adapters emit metadata and map model, thinking, and mode actions to backend operations.
Composer expansion and selector controls
app/lib/ui/composer/*, app/lib/ui/session/session_screen.dart, app/test/composer*
Composer supports footer actions and persistent expansion; selectors render from metadata and dispatch selection actions.
Desktop composer and session menu integration
app/lib/desktop/chat/desktop_chat_pane.dart, app/integration_test/desktop/*, app/test/desktop/desktop_chat_pane*
Desktop composers receive selector footers, start-session composers remain expanded, and model/thinking actions are removed from the session menu.
Desktop supporting behavior and regression coverage
app/lib/desktop/chat/desktop_sidebar.dart, app/lib/desktop/desktop_app.dart, app/lib/desktop/chat/keymap_scope.dart, app/test/desktop/*
Worktree icons reflect pull-request state, scrollbars are disabled globally, and focus behavior is covered for desktop keymap scopes.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ComposerModeSelector
  participant StoreController
  participant AcpAdapter
  participant ACP
  ComposerModeSelector->>StoreController: Send mode action
  StoreController->>AcpAdapter: Dispatch mode selection
  AcpAdapter->>ACP: setSessionMode
  ACP-->>AcpAdapter: current_mode_update
  AcpAdapter-->>ComposerModeSelector: Emit updated session.meta
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main desktop composer UX changes: boxed surface, inline selectors, focus fixes, and scrollbar handling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/desktop-composer-ux

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
app/test/desktop/desktop_sidebar_test.dart (1)

206-242: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover non-open PRs in this regression test.

This test distinguishes state: 'OPEN' from pr == null, so it would still pass if a closed or merged PR rendered the merge icon. Add a closed/merged fixture and assert that it does not produce Symbols.call_merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/test/desktop/desktop_sidebar_test.dart` around lines 206 - 242, The
regression test around “worktree icon: merge symbol only when a PR is open”
currently covers only an open PR and a missing PR. Add a worktree fixture with a
closed or merged PullRequest, include its session in the test setup, and update
the icon assertions so only the open PR produces Symbols.call_merge while the
closed/merged PR uses the non-merge icon.
app/lib/ui/composer/composer.dart (1)

141-145: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider theme tokens instead of hardcoded hex for boxColor.

0xFF0E0E0E/0xFFEFEFEF are magic constants disconnected from the rest of the theme-driven styling in this file. If the app's color scheme changes (e.g. dynamic/Material You theming), this box won't track it.

Please check whether makitDarkTheme/makitLightTheme already define an equivalent surface-container token that could replace these literals.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/ui/composer/composer.dart` around lines 141 - 145, Update the
boxColor calculation to use the existing surface-container color token from
makitDarkTheme or makitLightTheme instead of hardcoded hex values, while
preserving Colors.transparent when widget.glass is true and selecting the
appropriate theme value for the current brightness.
server/src/adapters/acp.test.ts (1)

80-130: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider adding coverage for the agent-initiated current_mode_update path.

This test only covers captureModes (via newSession) and the client-initiated mode action. It doesn't exercise agentRef.update(...) sending a current_mode_update notification (the path handled in acp.ts's buildClient().sessionUpdate), which is exactly the branch with the currentModeId/modeId field-name bug flagged in acp.ts. A test that sends { sessionUpdate: "current_mode_update", modeId: "..." } and asserts the re-emitted session.meta.modes.current would have caught it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/adapters/acp.test.ts` around lines 80 - 130, Extend the test
around ScriptedAgent and session.meta to cover an agent-initiated
current_mode_update notification. Send an update through agentRef.update with
sessionUpdate set to current_mode_update and a modeId, then wait for the emitted
session.meta and assert modes.current matches that modeId, exercising
buildClient().sessionUpdate.
server/src/adapters/pi.ts (1)

441-450: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

set_model/set_thinking_level response handling is untested.

normalizeModel(evt.data) on line 444 assumes the set_model response payload is the model object directly (matching the docs/rpc.md comment), but pi.test.ts only exercises get_state/get_available_models responses, not these two branches. If the actual response shape differs, normalizeModel just returns null and the code falls through to requestMeta() — low risk since the following get_state re-query is the well-tested path — but a quick test would remove the ambiguity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/adapters/pi.ts` around lines 441 - 450, The response branches for
set_model and set_thinking_level in the pi adapter lack test coverage. Add
focused tests that simulate successful responses for both commands, verify
set_model normalizes and adopts the returned model via metaModel, and verify
both branches call requestMeta to re-query state; use the existing pi.test.ts
response fixtures and assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/lib/ui/composer/composer.dart`:
- Around line 141-145: Update the boxColor calculation to use the existing
surface-container color token from makitDarkTheme or makitLightTheme instead of
hardcoded hex values, while preserving Colors.transparent when widget.glass is
true and selecting the appropriate theme value for the current brightness.

In `@app/test/desktop/desktop_sidebar_test.dart`:
- Around line 206-242: The regression test around “worktree icon: merge symbol
only when a PR is open” currently covers only an open PR and a missing PR. Add a
worktree fixture with a closed or merged PullRequest, include its session in the
test setup, and update the icon assertions so only the open PR produces
Symbols.call_merge while the closed/merged PR uses the non-merge icon.

In `@server/src/adapters/acp.test.ts`:
- Around line 80-130: Extend the test around ScriptedAgent and session.meta to
cover an agent-initiated current_mode_update notification. Send an update
through agentRef.update with sessionUpdate set to current_mode_update and a
modeId, then wait for the emitted session.meta and assert modes.current matches
that modeId, exercising buildClient().sessionUpdate.

In `@server/src/adapters/pi.ts`:
- Around line 441-450: The response branches for set_model and
set_thinking_level in the pi adapter lack test coverage. Add focused tests that
simulate successful responses for both commands, verify set_model normalizes and
adopts the returned model via metaModel, and verify both branches call
requestMeta to re-query state; use the existing pi.test.ts response fixtures and
assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5f1f96a5-de4e-42fa-ad56-fcf1663d8e1c

📥 Commits

Reviewing files that changed from the base of the PR and between cf61642 and 6d2adc7.

📒 Files selected for processing (19)
  • app/integration_test/desktop/composer_e2e_test.dart
  • app/lib/desktop/chat/desktop_chat_pane.dart
  • app/lib/desktop/chat/desktop_sidebar.dart
  • app/lib/desktop/chat/keymap_scope.dart
  • app/lib/desktop/desktop_app.dart
  • app/lib/store/models.dart
  • app/lib/ui/composer/composer.dart
  • app/lib/ui/composer/composer_selectors.dart
  • app/lib/ui/session/session_screen.dart
  • app/test/composer_selectors_test.dart
  • app/test/composer_test.dart
  • app/test/desktop/desktop_chat_pane_menu_test.dart
  • app/test/desktop/desktop_chat_pane_test.dart
  • app/test/desktop/desktop_sidebar_test.dart
  • app/test/desktop/keymap_scope_test.dart
  • server/src/adapters/acp.test.ts
  • server/src/adapters/acp.ts
  • server/src/adapters/pi.test.ts
  • server/src/adapters/pi.ts

leduckhc added 3 commits July 15, 2026 17:28
The sidebar keyed the merge icon on pr != null, so a closed/merged PR
would still render call_merge despite the intent. Gate it on the PR
state being OPEN and cover a merged-PR fixture in the regression test.
Replace the hardcoded composer box hex with named kComposerBoxLight /
kComposerBoxDark constants in theme.dart, alongside the rest of the
palette.
…ches

Add coverage for the agent-initiated current_mode_update notification
(re-emits session.meta) and for pi's set_model/set_thinking_level
responses adopting the model and re-querying state.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/src/adapters/pi.test.ts (1)

51-60: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Wait for boot commands instead of sleeping.

The 150ms delay is scheduler-dependent. Delayed boot writes can make these tests flaky or contaminate assertions after writes.length = 0. Poll for both boot command types before asserting or resetting writes.

  • server/src/adapters/pi.test.ts#L51-L60: wait until get_state and get_available_models appear.
  • server/src/adapters/pi.test.ts#L116-L124: wait for the same boot commands before clearing writes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/adapters/pi.test.ts` around lines 51 - 60, Replace the
scheduler-dependent delay in the boot assertions with polling that waits until
both get_state and get_available_models commands appear before asserting. Apply
the same boot-command wait in server/src/adapters/pi.test.ts lines 51-60 and
lines 116-124, ensuring the second site waits before clearing writes so delayed
boot commands cannot contaminate later assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@server/src/adapters/pi.test.ts`:
- Around line 51-60: Replace the scheduler-dependent delay in the boot
assertions with polling that waits until both get_state and get_available_models
commands appear before asserting. Apply the same boot-command wait in
server/src/adapters/pi.test.ts lines 51-60 and lines 116-124, ensuring the
second site waits before clearing writes so delayed boot commands cannot
contaminate later assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 481237ba-3348-40d8-bef2-2302d3a2fa87

📥 Commits

Reviewing files that changed from the base of the PR and between 6d2adc7 and ea96480.

📒 Files selected for processing (6)
  • app/lib/app/theme.dart
  • app/lib/desktop/chat/desktop_sidebar.dart
  • app/lib/ui/composer/composer.dart
  • app/test/desktop/desktop_sidebar_test.dart
  • server/src/adapters/acp.test.ts
  • server/src/adapters/pi.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/lib/desktop/chat/desktop_sidebar.dart
  • app/test/desktop/desktop_sidebar_test.dart
  • app/lib/ui/composer/composer.dart

@leduckhc
leduckhc merged commit e77ce73 into main Jul 15, 2026
11 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 15, 2026
@leduckhc
leduckhc deleted the feat/desktop-composer-ux branch July 15, 2026 16:40
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant