feat: align native macOS chat with the web chat capability set#109712
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 302dd032e2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| func replaceMessages(_ messages: [OpenClawChatMessage]) { | ||
| guard self.messages != messages else { return } | ||
| self.messages = messages | ||
| self.seedInputHistory(from: messages) |
There was a problem hiding this comment.
Avoid seeding history from session-clear placeholders
When a user switches back to a session that has transcript history plus local-only entries such as /compact, clearSessionOwnedState() calls replaceMessages([]) after sessionKey already points at the destination session, so this new unconditional seedInputHistory(from: []) resets that session's transcript snapshot without clearing its entries. The next live/cache history load then treats the same transcript rows as new and moves them ahead of the local-only entries, so Up recalls an older transcript message before the more recent local command after a session switch or identity restart. Skip history seeding for the empty clear path, or make the seed operation aware that this is just a view reset rather than an authoritative empty transcript.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0f4718b2fd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var inputHistoriesBySession: [String: ChatInputHistory] = [:] | ||
| /// Unlike web persistence, native drafts stay in memory. Attachments are excluded because | ||
| /// the staging guard prevents session switches while they are being prepared. | ||
| @ObservationIgnored | ||
| var draftsBySession: [String: String] = [:] |
There was a problem hiding this comment.
Scope composer caches by routing identity
These new composer-history/draft caches are keyed only by sessionKey, but bare keys such as global/main aliases are mutable: syncDeliveryIdentity restarts the transcript when activeAgentId or the routing contract changes while leaving these dictionaries intact. In that case, returning to the same bare key under a different agent/contract can restore a saved draft or Up-arrow history from the previous owner, even though currentSessionSnapshot treats it as a different session. Key these caches by the same routing identity or clear them when the identity changes.
Useful? React with 👍 / 👎.
| .contextMenu { | ||
| self.replyMessageButton(for: msg) | ||
| Button { |
There was a problem hiding this comment.
Preserve copy in the speech context menu
When a speech controller is present, listenable assistant messages take this branch, and its context menu still omits copyMessageButton(for:) while the other macOS message branches include it. Since this change now wires speech into the mac chat surfaces, right-clicking an assistant response in the native mac full window/panel shows Reply/Listen but no longer offers Copy Message, regressing the documented copy action for exactly the messages users are most likely to copy.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
openclaw/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift
Lines 820 to 822 in 3b7cae6
When this snapshot path changes the default agent or routing contract for a bare main/global chat, syncDeliveryIdentity() restarts the transcript through clearSessionOwnedState(), but that session-owned clear path does not clear the new replyTarget. The reply chip can therefore survive after the same visible key is rebound to a different agent/contract, and the next non-slash send will quote a message from the previous routing owner into the new session.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b8f75f2e1b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| extension OpenClawChatViewModel { | ||
| func recallPreviousInput(caretOnFirstLine: Bool) -> Bool { | ||
| var history = self.inputHistoriesBySession[self.sessionKey] ?? ChatInputHistory() | ||
| guard history.isRecalling || caretOnFirstLine || self.input.isEmpty else { return false } |
There was a problem hiding this comment.
Gate history recall on the actual caret start
When a draft is long enough to soft-wrap (or the caret is in the middle of the first hard line) and there is any input history, this guard lets Up-arrow recall a previous message instead of moving the caret within the text view because caretOnFirstLine only checks for preceding newline characters. That can unexpectedly replace the in-progress draft during normal multi-line editing; require the caret to be at the start of the draft, or compute the actual visual first-line boundary, before entering history navigation.
Useful? React with 👍 / 👎.
|
Codex review: needs real behavior proof before merge. Reviewed July 17, 2026, 8:19 AM ET / 12:19 UTC. Summary PR surface: Docs +1, Other +4421. Total +4422 across 59 files. Reproducibility: not applicable. to the feature request itself; however, the four blocking regressions are source-reproducible from the routing-identity, transcript-replacement, outbox-enqueue, and key-routing paths on the current PR head. Review metrics: 2 noteworthy metrics.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review findings
Review detailsBest possible solution: Keep the shared host-wiring design, but merge only after composer state is scoped and invalidated with the routing identity, every accepted enqueue schedules delivery, history recall requires a true draft boundary, reply targets track transcript lifetime, the trace default is owner-approved, and the remaining paths have redacted live proof. Do we have a high-confidence way to reproduce the issue? Not applicable to the feature request itself; however, the four blocking regressions are source-reproducible from the routing-identity, transcript-replacement, outbox-enqueue, and key-routing paths on the current PR head. Is this the best way to solve the issue? No, not yet. Reusing shared chat capabilities through the native host is the right architectural direction, but the current lifecycle handling and default-on trace setting make this implementation unsafe to merge without repair and owner approval. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against db3213264a65. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Docs +1, Other +4421. Total +4422 across 59 files. View PR surface stats
Acceptance criteria:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (5 earlier review cycles)
|
Live test on macOS (signed dev build, real gateway)Exercised the new capabilities end-to-end on a Mac Studio against a live gateway (latest Empty session: starter prompts + intro, voice-note mic and Talk Mode button in the composer Toolbar menu: new "Sessions…" manager entry (⇧⌘S) and the persisted "Show reasoning & tool activity" toggle (default on) Live send/receive roundtrip with per-message usage metadata New per-message context-menu Reply action Reply chip in the composer (web-parity blockquote goes out on send) Verified live: gateway connect, session create (⌘N), send → streamed reply ("Writing" indicator) → terminal reply with usage line, reply target selection and cancel, starter prompt cards, voice-note and talk controls rendered (mic capture not exercised — needs interactive permission). |
b34c877 to
f7e72b7
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f7e72b7af8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| } | ||
|
|
||
| public internal(set) var replyTarget: OpenClawChatReplyTarget? |
There was a problem hiding this comment.
Clear reply target on routing identity changes
When a mutable session key such as global/main is rebound by syncDeliveryIdentity, the view model restarts via clearSessionOwnedState() without running prepareComposerForSessionSwitch, so this new replyTarget survives while the transcript is replaced under the new agent/contract. A reply chip selected for agent A can therefore remain visible and prepend A's quoted text to the next message sent to agent B; clear the reply target in the identity-restart path as well.
Useful? React with 👍 / 👎.
| // Bubble render and flush scheduling stay with the displayed session | ||
| // (shipped behavior predating this path); the stored command delivers | ||
| // on the next flush trigger, e.g. returning here or a health event. | ||
| guard self.isCurrentSession(session) else { return true } |
There was a problem hiding this comment.
Flush accepted stale-session enqueues
When a healthy text send is routed through the outbox, for example behind existing queued rows, and the user switches sessions while outbox.enqueueCommand is suspended, this success return makes the caller retire the submitted draft via finishAcceptedComposerSend but skips the present/flush block below. If the new session's restore already ran before the row was inserted, no unhealthy→healthy transition occurs to flush it, so the message can sit queued until the user returns to that session or another flush trigger happens; schedule flushOutboxIfNeeded() for accepted stale-session rows before returning.
Useful? React with 👍 / 👎.
f7e72b7 to
85c8695
Compare
…s, and Listen into native chat
…ts to native chat composer
…e restored input-history entries
…table under prepends
85c8695 to
53e9ed4
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 53e9ed4494
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| func replaceMessages(_ messages: [OpenClawChatMessage]) { | ||
| guard self.messages != messages else { return } | ||
| self.messages = messages | ||
| self.seedInputHistory(from: messages) |
There was a problem hiding this comment.
Clear stale reply targets when replacing messages
When a reply chip is selected and the target row is later removed by a transcript replacement, such as deleting a queued outbox bubble or compacting/reloading history, this path updates messages but leaves replyTarget pointing at a UUID that is no longer visible. The next send will still prepend that deleted or compacted text, so users can accidentally quote stale context into a later turn; clear the reply target when its messageID is absent from the replacement set.
Useful? React with 👍 / 👎.
|
Merged via squash.
|
…law#109712) * feat(macos): wire sessions manager, assistant trace, talk, voice notes, and Listen into native chat * feat(clients): add input history, reply quoting, and per-session drafts to native chat composer * fix(clients): capture TTS server lease before gateway check and dedupe restored input-history entries * docs(clients): document reply-chip consume parity with web accepted-send semantics * fix(clients): retire sent-text recall stash and keep recall markers stable under prepends * test(clients): wait for outbox restore before asserting live-send paths * docs(clients): note cross-session outbox flush scheduling contract * chore(clients): split oversized chat files to satisfy SwiftLint length caps * chore(i18n): sync native app i18n inventory for new chat strings * chore: retrigger ci * chore(i18n): translate new chat strings and regenerate Apple catalogs * chore(i18n): regenerate Android localization resources for new chat strings * test(clients): await lifecycle cleanup before modeling replacement gateway traffic * chore(clients): split invoke timeout helper to satisfy SwiftLint length cap * chore(clients): drop unused OpenClawProtocol import from invoke timeout split





What Problem This Solves
The native macOS chat lags the web Control UI chat in day-to-day capabilities, even though much of the machinery already exists. Several fully implemented shared surfaces were unreachable on macOS: the sessions manager sheet (
ChatSessionsSheet) was dead code with no trigger anywhere, assistant reasoning/tool traces were hard-disabled, talk mode / voice notes / per-message Listen were implemented inOpenClawChatUIand wired on iOS but never injected by the macOS host, and the mac transport silently dropped theverboseLevelsession setting. The composer also lacked three everyday web-chat features: input history recall, reply/quote, and per-session draft retention.Why This Change Was Made
Aligning the native macOS chat with the web chat's capability set, preferring host wiring of existing shared code over new surface area. Two staged commits:
1.
feat(macos)— host wiring (mac app + minimal shared passthroughs)showSessionsstate inOpenClawChatViewis deleted.openclaw.webchat.showAssistantTrace, default on; compact panel stays minimal).MacChatSurfaceobserving wrapper bridges live state into the once-constructedNSHostingControllerhierarchy: talk control (bridged to the existingTalkModeController/AppStateStorepaths without touching the menu-bar overlay), voice-note recording (blocked while Talk Mode owns mic capture), per-message Listen via gatewaytts.speakbound to the transport's server lease with local synthesis fallback (mirrors the iOS client), and starter prompts on empty sessions.MacGatewayChatTransportnow forwardsverboseLevelinsessions.patchinstead of dropping it.2.
feat(clients)— shared composer parity (macOS + iOS)ui/src/pages/chat/input-history.tssemantics. Slash-panel navigation keeps priority.ui/src/pages/chat/chat-send.ts(prependReplyQuote, same Markdown escape set, 500/120 UTF-16 truncation limits), so transcripts look identical across surfaces.All shared-package API changes are additive with defaulted parameters; iOS call sites compile unchanged.
User Impact
macOS chat gains: full session management (rename/archive/restore/search), visible reasoning and tool activity with a toggle, talk mode + voice notes + read-aloud in the chat surfaces, starter prompts,
/verbosethat actually sticks, Up/Down input history, reply-with-quote, and drafts that survive session switching. iOS inherits the composer features. Docs updated:docs/platforms/mac/webchat.md.Evidence
swift build --package-path apps/shared/OpenClawKit --target OpenClawChatUI— passswift test --package-path apps/shared/OpenClawKit --parallel— 48/48 XCTest; Swift Testing 853/856 with 3 pre-existing issues in untouched surfaces (see below)swift build --package-path apps/macos --configuration release— passswift test --package-path apps/macos --parallel— 1280/1280 passtts.speak(src/gateway/server-methods/tts.ts),sessions.patchverboseLevel(src/gateway/sessions-patch.ts:465)ui/src/pages/chat/chat-send.tsprependReplyQuote+ escape regexGatewayErrorsTests.swift:59, copy changed in 551c963), watch legacy payload decode (WatchCommandsTests.swift:74, compat added in ec65e10 and lost by a later watch commit), and a load-flaky attachment timeout (passes 9/10 isolated). CI does not run this suite; follow-up task filed to repair it and add coverage.