Integrate companion overnight work (#415–#422) - #423
Merged
Conversation
New chats sat on the "New chat" placeholder for far too long. The session name was only derived (a free trim of the first user message — no model call) at the *end* of the first turn inside the backend handler, written straight to storage. The native chat registry's in-memory title was never updated and no chat_updated event fired, so connected clients kept showing "New chat" until a restart re-hydrated the persisted name. Derive the title the moment the first user message lands, in emitUser: update the in-memory entry and persist via the existing rename path, then the chat_updated broadcast that already follows propagates it live to every client. Guarded so it only fires while the chat still carries the placeholder, so a user's manual rename is never clobbered. Still zero model usage. - chats.ts: export DEFAULT_CHAT_TITLE so the guard shares one constant - index.ts: add maybeAutoTitle(), call it from emitUser() - tests: cover the placeholder-only guard and the no-usable-text case Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…names
The tool-call chip laid out the monospace tool name and the elapsed-time
readout as two non-flexible children of a MainAxisSize.min Row. A long
name like `mcp__email-tools__search_emails` consumed the whole row and
pushed the timer ("41s", "3m54s") off the right edge, clipping it.
Restructure so the name + arg share a flexible middle section that
ellipsizes when long, while the elapsed-time text is pinned on the right
and never clips. Chips now fill the available width uniformly, which also
tightens the padding consistency across the activity list.
Also de-noise MCP tool names for display: `mcp__email-tools__search_emails`
renders as `email · search_emails` instead of the full dunder-mangled id,
cutting the visual spam Dylan flagged. Non-MCP names (Bash, Read, …) are
untouched.
Applies to both the live turn (activity_card LiveTurn) and the collapsed
tool history in finished messages, since both reuse ToolChip.
flutter analyze: no issues.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In the companion app the bot's reactions landed on the wrong bubble —
Dylan: "react isn't working properly ... does a reaction to the bot's
message." Root cause: the native frontend ran each turn with
execute({ prompt }) but never passed a messageId, so formatUserPrompt
emitted no [msg_id:N] marker. With no id for the user's message, the
model had nothing to target and reacted to an id it did know — its own
sent message.
Thread the id through: emitUser now returns the numeric id it mints and
broadcasts for the user message, and the send handler passes it to
runTurn → execute as messageId. formatUserPrompt then includes
[msg_id:N], so react / reply_to / edit resolve to the user's message,
which the client already renders under that same id (so _addReaction
attaches it to the correct bubble). Also fixes reply_to targeting for
the same reason.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dylan has both the Claude and Codex backends enabled on the gateway but
the companion app only ever showed Claude — the native bridge never
exposed the backend layer. The multi-backend machinery already exists
(per-chat override, listAvailableBackends, rebindChat, the Telegram
/model backend submenu); this wires it through the bridge and adds a
picker to the app.
Bridge/engine:
- protocol: ClientChat gains `backend`; new BackendOption type.
- server: GET /backends (chatId) and POST /backend (chatId, backend).
/backend always returns 200 with {ok,error} — the client's decoder
drops >=400 bodies, and this is an app result, not an HTTP error.
- index: toClientChat reports the chat's backend; listBackends() returns
the enabled backends + the chat's active one; setBackend() mirrors the
Telegram flow — verify enabled, rebindChat, pin override, reset the
session/history/pulse (sessions aren't portable across backends),
broadcast chat_updated + status. listModels() now takes an optional
chatId so the active-model hint tracks the chat's backend.
Client:
- BackendOption model; ClientChat.backend; BridgeClient.backends()/
setBackend(); models() takes an optional chatId; AppState.backends()/
setBackend()/refreshModels(chatId).
- model sheet: a BACKEND chip row (shown when >1 backend is enabled) with
a switching spinner, a "starts a fresh conversation" note, and a
SnackBar on failure. Backends are fully gateway-driven — nothing is
hardcoded, so whatever the gateway reports as enabled is what shows.
tsc + flutter analyze clean; native-frontend + bridge suites (25) pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A polish pass on the app's motion, tuned to feel calm and premium rather than busy, and responsive across phone/tablet/desktop. - theme: add TalonMotion — a shared motion vocabulary (fast/base/slow durations + emphasized/standard curves) so every surface animates with the same rhythm and the whole app can be retuned in one place. - message rows: a one-shot entrance (gentle rise + fade + a whisper of scale; user rows drift in from the right, assistant fades in place). Gated so it plays ONCE per message and only for genuinely fresh messages — never history and never on scroll recycle (tracked via a seen-set + a recency check). Honours the platform "reduce motion" setting (MediaQuery.disableAnimations). - composer send button: springs to full size + colour when there's something to send, dips under the finger on press, and softens to a flat idle state when empty, with an accent glow. - sidebar chat tiles: press-scale, a slim accent bar that slides in on the selected tile, and a fading (not popping) delete affordance. All motion uses fixed, form-factor-safe geometry and proportional scale, and the readable max-width column (already present) keeps it consistent on wide screens. flutter analyze: no issues. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dylan asked for the settings/info surface to be "expanded more including
doctor and more expansive settings." Two new cards, both built entirely
from data the app already holds (no bridge/protocol changes):
- Diagnostics ("doctor"): pass/warn/fail check rows for Connection,
Bridge protocol (app vs daemon version match), Daemon health,
Transport (endpoint + TLS), and Authentication — plus a Reconnect
button and an always-available "Copy diagnostics" that dumps the live
status + recent log to the clipboard. Previously copy-diagnostics only
existed on the error path; now it's always reachable for support.
- About: assistant name, backend, default model, bridge protocol
version, active chats, uptime, and start time — selectable text so any
field can be copied.
flutter analyze: no issues.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First half of image support: the bot can now send images to the companion app. Previously `send(type=photo)` posted a `send_photo` gateway action the native bridge didn't handle, so nothing rendered. Bridge/engine: - protocol: ClientMessage gains `imagePath` (a relative `/media?id=…` the client resolves against its own base URL + token). - server: GET /media streams an attached file by id (auth-gated like the rest of the bridge; token accepted as a query param so <img>/Image.network works), with a small extension→content-type map. - index: an in-process media registry (id → absolute path) + emitPhoto(), which registers the file, emits an assistant message carrying imagePath, and persists a "[photo] caption" placeholder to history (history can't carry the live bytes, so images render in-session, mirroring the chat frontends). New mediaPath() handler resolves ids for the server. - actions: handle the `send_photo` action → emitPhoto. Client: - ClientMessage.imagePath; ConnectionConfig.mediaUrl() builds the full, token-authed URL. - message_bubble: an inline, rounded, width-capped image with quiet loading/error states and tap-to-zoom (full-screen InteractiveViewer). Image-only messages suppress the "…" text placeholder. tsc + flutter analyze clean; native-frontend + bridge suites (25) pass, incl. new send_photo coverage. Follow-up (separate PR, needs on-device testing): user → bot image upload (image_picker + a multipart /upload endpoint + feeding the saved path to the model). This PR is the render/serve foundation both halves share. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes image support (the send direction). The composer gains an
attach button; the picked image uploads to the bridge, renders inline in
the thread, and its saved path is handed to the model so it can read it.
Bridge/engine:
- server: POST /upload (raw binary body, 25 MB cap, binary-safe reader)
saves the file and returns { imagePath, path }. POST /send gains
optional imagePath + attachmentPath (SendOptions); text may be empty
when an image is attached.
- index: saveUpload() writes to the workspace uploads dir under a safe,
unique name; the upload handler registers it in the media map so it
serves back via /media. emitUser() can carry an imagePath (renders in
the thread; history keeps a "[photo]" placeholder). runTurn() appends
"[Attached image: <path>]" to the prompt so the model reads the file.
Client:
- file_picker dependency (chosen over image_picker for full desktop +
Android support, matching this app's targets). macOS entitlements gain
files.user-selected.read-only so the sandboxed build can read the pick.
- BridgeClient.uploadImage() + send() attachment args; AppState.uploadImage()
and sendMessage() attachment args.
- composer: attach button, a removable image preview chip, and a
pick → upload (spinner) → send flow. Send is enabled with text OR an
image; upload failures surface as a system note.
tsc + flutter analyze clean; native-frontend + bridge suites (25) pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # apps/companion/lib/src/models/bridge_models.dart # apps/companion/lib/src/ui/chat_view.dart # apps/companion/lib/src/ui/composer.dart # apps/companion/lib/src/ui/message_bubble.dart # src/frontend/native/index.ts # src/frontend/native/server.ts
file_picker's transitive flutter_plugin_android_lifecycle requires compiling against API 36+; the app was on flutter.compileSdkVersion (34), failing the release build. Pin compileSdk = 36 (minSdk/targetSdk unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The app-module compileSdk bump didn't propagate to plugin subprojects, so file_picker still compiled against android-34 and failed :file_picker:checkReleaseAarMetadata. Force compileSdk=36 on every Android subproject via a root subprojects afterEvaluate block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aluate
afterEvaluate fails ('project already evaluated') because Flutter's
evaluationDependsOn(':app') forces early evaluation. Configure each
Android library subproject's compileSdk=36 as the plugin is applied.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename namespace + applicationId from the com.example.talon_companion placeholder to org.talon.companion, and move MainActivity to the matching package path (manifest uses the relative .MainActivity). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause of the Android release failure: file_picker 8.3.7 resolves flutter_plugin_android_lifecycle to 2.0.35, whose AAR requires compiling against API 36, while file_picker itself compiles against 34 — an inconsistency that fails :file_picker:checkReleaseAarMetadata. Every Gradle-side compileSdk override hit AGP timing limits. Pin the dep to 2.0.24 (satisfies file_picker's ^2.0.22, builds against 34) so the toolchain is consistent. Reverts the gradle subproject override. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 1, 2026
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.
Integration of the eight companion-app PRs from the overnight polish pass, with all cross-PR merge conflicts resolved so every feature coexists (react message-id + image attachment in the native turn; motion send-button + attach button + busy spinner in the composer; entrance animation + inline image in the message bubble; backend
listModels(chatId)+/uploadroute in the bridge).Rolls up:
Each PR's branch tip is a merge parent here, so merging this marks all eight merged.
Verification:
tsc --noEmitclean ·flutter analyze lib→ No issues · native-frontend + bridge suites (27) pass.🤖 Generated with Claude Code