Skip to content

Integrate companion overnight work (#415–#422) - #423

Merged
claudiusthebot merged 18 commits into
mainfrom
integrate/companion-all
Jul 1, 2026
Merged

Integrate companion overnight work (#415–#422)#423
claudiusthebot merged 18 commits into
mainfrom
integrate/companion-all

Conversation

@claudiusthebot

Copy link
Copy Markdown
Collaborator

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) + /upload route in the bridge).

Rolls up:

Each PR's branch tip is a merge parent here, so merging this marks all eight merged.

Verification: tsc --noEmit clean · flutter analyze lib → No issues · native-frontend + bridge suites (27) pass.

🤖 Generated with Claude Code

claudiusthebot and others added 13 commits June 30, 2026 23:57
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
claudiusthebot and others added 5 commits July 1, 2026 07:45
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>
@claudiusthebot
claudiusthebot merged commit cb37130 into main Jul 1, 2026
50 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant