feat: /restart slash command + Claude adapter session reset#374
Merged
zomux merged 13 commits intoMay 12, 2026
Conversation
Native Swift composer rebuild that addresses a stack of macOS/iOS issues surfaced during dogfooding. Single round of work covering input, send, and attachment paths. Composer (ComposerTextView) - Replace SwiftUI TextEditor with NSViewRepresentable / UIViewRepresentable wrapping NSTextView / UITextView so we can intercept Return-to-send around IME composition (hasMarkedText / markedTextRange) — fixes false sends mid-Pinyin / Kana composition. - macOS: extend readablePasteboardTypes + acceptableDragTypes to include PNG / JPEG / TIFF / public.image / fileURL so Cmd+V validates and our paste(_:) override actually runs (was previously rejected at the menu validator → system bonk). - macOS: paste(_:) handles PNG → JPEG → TIFF → NSImage fallback; file URL paste from Finder. - iOS: subclass UITextView; canPerformAction / paste(_:) read UIPasteboard.images. Hardware Cmd+V works; soft Return inserts newline to match iMessage. Bubble layout (Issue: empty padding on user bubbles) - MessageBubble drops maxWidth: .infinity on the prose-only path so short messages hug their text; code/table content still claims full row. Toolbar titles - macOS: ChatView sets navigationTitle = current session title (replaces default "OpenAgents Go" in the window toolbar) with agent names as subtitle. - ThreadListView gets a visible workspace name + slug header at the top on macOS (split-view sidebar doesn't surface .navigationTitle). Stop button - Add WorkspaceAPI.sendAgentControl mirroring the React app's pattern. - WorkspaceStore.stopAllAgents fans out workspace.agent.control events with optimistic "Stopping..." status; retries once after 3s; clears the flag on terminal status from the message poll. - ChatGPT-style: send arrow swaps to a red stop circle while an agent is working; grays out while a stop is in flight. Attachment UX - Dedup optimistic vs real attachment messages: rewrite optimistic content to finalContent in-place after api.sendMessage returns so the poll's content-equality dedup matches. Fallback matcher compares the set of attached filenames if exact-equality misses (mid-upload race). Fixes the "two bubbles per upload" symptom. - ImageDownsampler (ImageIO-based): any pasted/picked/imported image whose longest side is > 2000px gets resized to 2000 and re-encoded as PNG. Targets the exact Anthropic many-image-request limit so a Retina screenshot can't poison the conversation. - AttachmentChip: image variant shows a 48x48 thumbnail with X overlay (decoded via ImageIO, cached on the chip). Non-image keeps the Slack-style filename + size row. - Long text paste >= 5000 chars converts to a Pasted-text-<ts>.txt attachment chip instead of dumping into the input field (matches ChatGPT's published threshold). Bumps MARKETING_VERSION 0.2.1 -> 0.2.2.
Adds a typed /restart command in the OpenAgents Go (Swift) composer that
clears the per-channel Claude CLI session, so the next user message
starts a fresh conversation. Used to recover from Anthropic's
many-image-conversation rejection (>2000px-poisoned threads) without
abandoning the channel.
Claude adapter (sdk/src/openagents/adapters/claude.py)
- _on_control_action now recognizes action="restart" alongside "stop".
When invoked with payload.channel=<id>:
* Kills the in-flight Claude subprocess for that channel only (no
spurious "Execution stopped by user" status — we'll post a clearer
one below).
* Drops the per-channel entry from _channel_sessions and persists.
Next message → no --resume → fresh Claude CLI session, empty context.
* Posts a "Session restarted — next message starts fresh." status
message so the user sees what happened.
- Falls back to clearing all sessions if no channel param is provided.
- Other adapters (Hermes, OpenClaw, Codex) ignore the action this round.
Swift composer (packages/go/OpenAgents/Views/ComposerTextView.swift)
- New onSlashKey: ((Selector) -> Bool)? callback threaded through the
NSTextView coordinator. textView(_:doCommandBy:) consults it before
the existing Return-to-send logic, so the slash-command popup gets
first crack at ↑/↓/Tab/Return/Esc when it's open.
- iOS path takes the parameter for source-call uniformity but ignores it
(popup keyboard nav is macOS-only this round).
Swift chat view (packages/go/OpenAgents/Views/ChatView.swift)
- Slash-command interception in send(): trimmed text starting with "/"
routes to handleSlashCommand instead of sendMessage. /restart fires
store.restartSession; unknown commands surface via the existing
lastError banner.
- Slash-command autocomplete popup mirrors the React @mention pattern.
The popup floats above the input bar while the user types a one-line
/-prefixed token, listing matching commands with description and icon.
Tab inserts the highlighted command; Enter on an exact match falls
through to the composer's send path; Esc dismisses.
- Architecture supports more commands by appending to availableCommands.
Swift store + model
- Message.localStatus(channel:content:idPrefix:) generalizes
localStoppingStatus so /restart can reuse the optimistic-status
placeholder pattern.
- WorkspaceStore.restartSession(sessionId:) fans out the restart
control event to every agent in the session, mirroring stopAllAgents.
- pollNewMessages drops local-restart- (and any local-status-)
placeholders alongside local-stopping- when real agent status arrives.
|
@baryhuang is attempting to deploy a commit to the Raphael's projects Team on Vercel. A member of the Team first needs to authorize it. |
Adds /status as a second slash command alongside /restart. Users can type /status (or pick it from the autocomplete popup) to see the remote daemon's uptime, openagents version, agent type, and network. Daemon (Python — base adapter) - BaseAdapter.__init__ tracks self._started_at = time.time(). - BaseAdapter._on_control_action now handles action="status" — posts back a "**Agent status**" chat message with name, type, openagents package version (via importlib.metadata), uptime (humanized: "3h 12m" / "45m 7s" / etc.), and network. - Putting it in the base means all adapter types report status uniformly without per-adapter implementations. Daemon (Python — Claude adapter) - _on_control_action now calls await super()._on_control_action(...) for actions it doesn't recognize, so the base-class status handler runs after stop/restart fall-through. Swift app - /status entry in ChatView.availableCommands (autocomplete popup). - /status case in handleSlashCommand → store.requestSessionStatus. - WorkspaceStore.requestSessionStatus(sessionId:) fans out the status control event to every agent in the session (mirroring restartSession), with an optimistic "Checking status…" placeholder. - Unknown-command error message updated: "Available: /restart, /status". Architecture supports more commands by appending to availableCommands and adding a case in handleSlashCommand. /status validates the fall-through-to-super pattern in adapter overrides.
As the user types after the leading slash, each suggestion's name renders the matched prefix in bold and the unmatched tail in muted gray, so it's visually clear what's narrowing the list. The leading slash itself is also muted so the bolded match stands out. Standard pattern from Slack / VSCode command palette / Discord — kicks in once we have more than one slash command (now /restart and /status).
Mirrors the existing 'stopped'/'stopping failed' handling for stop. Without this, the typing/busy indicator stayed pinned forever after /restart because the 'Session restarted - next message starts fresh' status message didn't match the terminal regex, so isAgentWorking still returned true.
After typing /restart, the user sees: 1. 'Restarting session…' (optimistic) 2. 'Session restarted - next message starts fresh.' (terminal status, busy indicator clears thanks to the regex update in the previous commit) 3. 'Checking status…' (optimistic, from the chained /status) 4. '**Agent status**' block with Uptime: 0s, version, network — proves the agent actually restarted Control events are FIFO per agent, so order is preserved at the daemon.
When /restart is invoked, the per-channel Claude session-id is cleared but the agent process keeps running. Without this change the auto-followup /status reported the old process uptime, making it look like the restart didn't take effect — a user reported seeing 'Uptime: 11m 52s' immediately after a successful /restart. Resetting _started_at on restart shifts the semantic from 'process uptime' to 'uptime since last restart', which is what 'restart' suggests anyway and matches the standard server-uptime convention.
Previously /restart did an in-process session-clear: kill the in-flight Claude subprocess, drop the channel's session UUID, reset _started_at. The agent process kept running, so 'uptime' lied about what 'restart' had done. Now /restart writes `restart:<agentName>` to ~/.openagents/daemon.cmd, the same IPC the `agn restart <name>` CLI uses. The daemon's command-file poller picks it up within ~1s, calls restartAgent, the current adapter's run() loop exits cleanly via the checkStop interval, and the daemon spawns a fresh adapter instance — new _started_at, new client connection, sessions reloaded from the on-disk file (which we already cleared the channel from). Sibling agents on the same daemon (opeagents-bot, company-os-bary, etc.) are untouched. The Node daemon process itself stays up. The pre-bounce work — kill subprocess, persist cleared session, post the 'Session restarted' status — happens BEFORE the IPC write so it lands while we're still online. If the IPC write fails (daemon crashed, fs error), we fall back to resetting _started_at in-process so the next /status still shows uptime reset.
The deployed openagents-org/agent-launcher runs the JavaScript adapters under sdk/.openagents/nodejs/.../src/adapters/. The Python files in sdk/src/openagents/adapters/ are not used by the daemon and were only being maintained for parity. Per user direction, drop the parity work and let the JS adapter be the source of truth. Restores base.py and claude.py to their develop state. The /restart and /status features remain fully implemented in the JS adapter (deployed out-of-tree) and in the Swift app (in-tree).
…side
These are the daemon-side changes that pair with the Swift slash-command
work in packages/go. Tested against the deployed copy at
`agents-api.caremojo.app` (network 0048fff6, agent lovecareliving).
base.js:
- _startedAt timestamp set in constructor; used by status reporter.
- _onControlAction handles 'status' → posts an 'Agent status' chat
message with name, type, agent-launcher version, uptime, network.
- _skipExistingControlEvents advances _lastControlId past existing
events on startup so a fresh adapter (after a restart bounce) doesn't
re-process the same restart event and trigger a loop.
- run() reorder: _skipExistingControlEvents + heartbeat + control poll
start IMMEDIATELY after join, before the message-cursor advance.
Slash commands are no longer gated on _skipExistingEvents.
claude.js:
- _onControlAction handles 'restart': kills the in-flight Claude
subprocess for the channel, clears the channel's session UUID,
posts the 'Session restarted' status, then writes 'restart:<name>'
to ~/.openagents/daemon.cmd. Daemon's command-file poller picks it
up and bounces just THIS agent — sibling agents on the same daemon
are untouched.
- Falls through to super._onControlAction(...) for unknown actions
so the base 'status' handler still runs.
workspace-client.js:
- pollControl now requests sort=desc + limit=10. With the default ASC
sort, a busy workspace's limit window filled with ancient stop
events for unrelated agents and nothing matched. After fetching, we
re-sort filtered results ascending so callers process oldest-first.
daemon.js:
- restartAgent now waits up to 20s for _adapters[name] to actually
disappear before calling _launchAgent. stopAgent's existing 5s wait
was too short for graceful shutdown (control-poller cleanup,
disconnect, in-flight kill), so the launch hit the duplicate-launch
guard and the agent stayed stuck in 'stopped' with no relaunch.
docs/:
- slash-test-{1,2,3}.png: screenshots from the Swift app showing
/status autocomplete, /restart autocomplete, and the /restart →
bounce → /status confirmation chain (uptime resets to 4s).
When the user taps the input on iPhone, the keyboard popped up and immediately dismissed before they could type a character. Cause: the iOS updateUIView path was calling resignFirstResponder() whenever the FocusState binding read 'false', and SwiftUI re-renders during normal state propagation can briefly produce that stale 'false' read right after textViewDidBeginEditing fires. Drop the auto-resign branch — UIKit already dismisses the keyboard naturally (tap outside, Done key, etc.). The auto-PROMOTE on wantsFocus=true stays. Now matches the macOS path which never had this branch.
zomux
approved these changes
May 12, 2026
Contributor
zomux
left a comment
There was a problem hiding this comment.
Session reset logic is correct — sessions are cleared and persisted before daemon bounce, and _skipExistingControlEvents prevents restart loops. Stacked on #373 which is now merged. Minor suggestions (non-blocking):
- Empty
catch {}blocks in_skipExistingControlEventsandclaude.jsshould log warnings — silent failures in the control-event cursor path could mask issues - Consider a client-side cooldown on
/restartto prevent rapid daemon bounces daemon.cmdwrite uses string interpolation withagentName— low risk since it's from config, but sanitizing at registration time would be defensive
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.



Summary
Adds a typed
/restartcommand in the OpenAgents Go (Swift) composer that clears the per-channel Claude CLI session so the next user message starts a fresh conversation. Used to recover from Anthropic's many-image-conversation rejection (>2000px-poisoned threads) without abandoning the channel — title, members, scrollback, and files all persist; only the agent's "remember the last N turns" is wiped.Three pieces
Claude adapter (Python)
sdk/src/openagents/adapters/claude.py—_on_control_actionlearnsaction="restart"alongside"stop". When invoked withpayload.channel=<id>:_channel_sessionsand persists. Next message arrives with no--resume→ fresh Claude CLI session."Session restarted — next message starts fresh."status message.Falls back to clearing all sessions if no channel param. Other adapters (Hermes, OpenClaw, Codex) ignore
restartthis round — follow-up PR.Swift composer / chat view
ChatView.send()— trimmed text starting with/routes tohandleSlashCommandinstead ofsendMessage./restartfiresstore.restartSession; unknown commands surface via the existinglastErrorbanner.@mentionpattern, lives only in the native composer. Floats above the input bar while the user types a one-line/-prefixed token. Tab inserts the highlighted command; Enter on an exact match falls through to send; Esc dismisses.onSlashKey: ((Selector) -> Bool)?callback threaded throughComposerTextViewand the NSTextView coordinator. Popup gets first crack at ↑/↓/Tab/Return/Esc before the composer's Return-to-send logic.Architecture supports more commands by appending to
availableCommands— only/restartships today.Swift store + model
Message.localStatus(channel:content:idPrefix:)generalizes the existinglocalStoppingStatushelper so the optimistic-status pattern serves both Stop and/restart.WorkspaceStore.restartSession(sessionId:)fans out the restart control event to every agent in the session, mirroringstopAllAgentsshape.pollNewMessagesdropslocal-restart-placeholders alongsidelocal-stopping-when real agent status arrives.Test plan
xcodebuild -scheme OpenAgentsGo_macOS Debug + ReleaseandOpenAgentsGo_iOS Debugbuild clean./→ popup appears with/restart. Typer→ still matches. Typex→ popup empty / hidden. Type space → popup hides./restart. Esc → popup closes. Click row → input fills./restartin the input, press Enter → optimisticRestarting session…appears in the chat.Restart: cleared session for channel=…, kills any in-flight subprocess, and postsSession restarted — next message starts fresh.status./restart, agent recalls a fact from earlier in the thread. Run/restart. Ask again — agent reports no recall./garbageand send →lastErrorbanner showsUnknown command: /garbage. Available: /restart. No backend call.Deferred
restarthandlers — follow-up.