Studio Code: add /remote-session slash command - #3315
Conversation
Adds `studio code remote-session start|stop|status` subcommands so the Telegram bridge can keep running after the user closes their terminal. The existing `studio code --remote-session` flag still works as a foreground entry point. Whole surface stays gated by STUDIO_ENABLE_REMOTE_SESSION.
- writePidFile() now mkdirs the config directory so the daemon doesn't rely on migration/logger init order having created it. - Fix the doc comment on DAEMON_CHILD_ENV_VAR to match the actual isDaemonChild() check (must be exactly "1"). - stopDaemon() leaves the PID file in place and returns stopped:false when the process is still alive after the SIGKILL grace, so status keeps reporting accurately. The CLI surfaces this as a stderr message and a non-zero exit. - Replace hardcoded PID 999999 in the test fixtures with a spawn-then-await-exit helper so the dead-PID assertions don't depend on the PID never being reused on Linux CI.
# Conflicts: # apps/cli/remote-session/index.ts
…TU-1655) Earlier iterations of /remote-session blocked the REPL while the poll loop ran. Now that the remote-session bridge can run as a background daemon (PR #3300 / STU-1649), the slash command becomes a thin REPL-side controller for it. The REPL never blocks. Subcommands (gated behind STUDIO_ENABLE_REMOTE_SESSION): - /remote-session start: validates config in-process so a missing token surfaces as ui.showError, then calls startDaemon() to spawn the detached child. Reports the PID via showSuccess and updates the bottom-bar daemon indicator. Idempotent: if the daemon is already running, reports the existing PID without re-spawning. - /remote-session stop: calls stopDaemon(), clears the indicator, and surfaces friendly messages for "already stopped" / "needed SIGKILL" / "process refused to die". - /remote-session status: synchronous getDaemonStatus() probe, updates the indicator, and prints the current state. Mechanics: - SlashCommandDef gains an optional getArgumentCompletions(prefix) hook so typing `/remote-session ` shows start/stop/status in the autocomplete dropdown via pi-tui's existing argument-completion path. - The REPL dispatcher matches on the first whitespace token rather than the full prompt, so `/remote-session <sub>` resolves to the remote-session command and the handler parses the subcommand itself. - A new daemon-status-poll module surfaces external start/stop and crashes in the bottom-bar indicator. It runs only when the feature flag is on and pulls daemon status from a synchronous fs probe every 5s. The PromptEditor renders `daemonStatusMessage` next to the existing `statusMessage` so login info and daemon state coexist. - AiOutputAdapter gains setDaemonStatus({ running, pid? }); JsonAdapter no-ops it. Tests: 14 unit tests for the slash command handler (start/stop/status subcommand routing, success and error paths, missing-token, already running, SIGKILL fallback, refused-to-die, stale PID-file cleanup), 3 unit tests for the polling module (gated off, ticking, error swallowing).
- Drop the redundant PID from the bottom-bar indicator. The reply line
('Remote-session daemon started (PID 67504).') already shows it; the
statusline just needs to flag presence, not duplicate the number.
- Append 'Message the Dolly bot on Telegram.' to both the freshly-started
and already-running success lines so the user knows where to drive the
agent from once the daemon is up.
The bottom-bar indicator already shows whether the daemon is running, and `studio code remote-session status` covers the out-of-REPL case, so a `/remote-session status` slash command is just noise. Drop it from the handler, the autocomplete suggestions, the usage hint, and the spec.
Tighten the wording and name the bot explicitly so users know exactly where to message: - 'Remote-session daemon started (PID N).' -> 'Remote-session started (PID N).' - 'Message the Dolly bot on Telegram.' -> 'Message Dolly (@wordpress_com_bot) on Telegram to continue this session remotely.' Same change applied to the already-running variant.
'Remote-session daemon (PID N) stopped.' -> 'Remote-session stopped (PID N).'
|
@youknowriad I added you as a reviewer following our previous chat on the slash commands in p1777372569384689-slack-C0AULRXLJQL. Now these are not blocking the REPL, let me know what you think! |
There was a problem hiding this comment.
Pull request overview
Adds a feature-flagged /remote-session REPL slash command that starts/stops the Telegram remote-session daemon without blocking the interactive UI, plus a bottom-bar indicator that mirrors daemon liveness (including when started/stopped externally).
Changes:
- Add
/remote-session start|stopslash command (gated bySTUDIO_ENABLE_REMOTE_SESSION) and add argument-completion support for subcommands. - Update the REPL dispatcher to resolve slash commands by the first whitespace token (to allow subcommands).
- Add a lightweight 5s daemon status poll that updates a new bottom-bar “remote session active” indicator; extend the output adapter interface accordingly.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| specs/studio-code-remote-session-spec.md | Updates spec/status notes to reflect the daemon-based slash command design. |
| apps/cli/remote-session/index.ts | Updates module docs to include the REPL slash command as a PID-file consumer. |
| apps/cli/commands/ai/index.ts | Changes slash-command dispatch to match on first token; starts/stops daemon-status polling in interactive mode. |
| apps/cli/ai/slash-commands.ts | Adds feature gating, argument completions hook, and `/remote-session start |
| apps/cli/ai/daemon-status-poll.ts | New module polling getDaemonStatus() and calling ui.setDaemonStatus(). |
| apps/cli/ai/ui.ts | Adds bottom-bar daemon status segment and wires autocomplete to active slash commands. |
| apps/cli/ai/output-adapter.ts | Extends AiOutputAdapter with setDaemonStatus; JSON adapter no-ops. |
| apps/cli/commands/ai/tests/ai.test.ts | Updates UI mock to include setDaemonStatus. |
| apps/cli/ai/tests/slash-commands.test.ts | New tests for feature gating, argument completions, and remote-session handler behavior. |
| apps/cli/ai/tests/daemon-status-poll.test.ts | New tests for polling behavior, gating, and error swallowing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
stopDaemon rethrows non-ESRCH errors from process.kill (e.g. EPERM when the PID was reused by another user, or any unexpected fs error while removing the PID file). The REPL dispatcher does not wrap slash-command handlers in a try/catch, so an uncaught throw would terminate the interactive session. Surface those errors via ui.showError and keep the REPL alive. Addresses copilot review on PR #3315.
…copy The 2026-05-01 status note previously described the bottom-bar indicator as 'Remote session: running (PID …)', but the implementation renders a green 'Remote session active' (no PID — that's already in the start success line). Addresses copilot review on PR #3315.
Previously the dispatcher matched on the first whitespace token for every slash command, which silently changed legacy behavior: `/clear foo` would now run `/clear` instead of falling through to the AI agent, and skill commands (no handler) would discard any user-supplied trailing text. Restrict first-token matching to commands that opt in by declaring `getArgumentCompletions` (i.e. commands that actually take subcommand args, like `/remote-session`). Everything else stays exact-match, so `/clear foo` and skill invocations behave exactly as they did pre-PR. Addresses copilot review on PR #3315.
The previous comment claimed feature-flag flips take effect 'without a restart', but AiChatUI builds the autocomplete provider once at startup (`new CombinedAutocompleteProvider(getActiveSlashCommands())`), so a runtime flag flip is only reflected by the dispatcher (which calls the helper on every input), not by autocomplete. Reword to describe the actual contract: the helper evaluates flags against current state, but cached consumers must be refreshed separately. Addresses copilot review on PR #3315.
`loadRemoteSessionConfig()` can throw non-RemoteSessionConfigError exceptions (fs permissions, JSON parse errors), and `startDaemon()` can throw beyond the typed AlreadyRunning/Timeout cases (spawn EAGAIN, unexpected fs write failures). The dispatcher does not wrap handler throws, so these previously terminated the REPL. Funnel both unknown branches into `ui.showError` so the interactive session stays alive. Typed errors keep their friendly per-case messages. Addresses copilot review on PR #3315.
# Conflicts: # apps/cli/ai/tests/slash-commands.test.ts # apps/cli/ai/ui.ts # apps/cli/remote-session/daemon.ts # apps/cli/remote-session/index.ts # specs/studio-code-remote-session-spec.md
…mmand Typing `/remote-session` (no args) used to print a one-line "Usage:" hint; that wasted a keystroke for the common case of "I want to toggle the daemon but don't remember which subcommand applies right now." Replace it with a two-option picker (Start / Stop) backed by the existing `ui.askUser` primitive — same interactive widget the agent SDK's `AskUserQuestion` tool drives, already used by /model and /provider, so the UX matches. `/remote-session bogus` (any unknown subcommand) still prints the usage hint rather than silently popping a picker that ignores the bad input. Esc / abort during the picker reports a one-line "selection canceled" notice and leaves the REPL alone. Tests cover all four paths: picker → start, picker → stop, picker → abort, and the `/remote-session bogus` usage fallback.
epeicher
left a comment
There was a problem hiding this comment.
Thanks @gcsecsey! I have tested this, and it works as expected, I can see my messages to the local agent when running the /remote-session slash command, I can see the new status bar change 💯 I can see the session status from another terminal.
I have also tested disabling the feature flag and I don't see the /remote-session flag as expected.
This LGTM!
| Terminal | Telegram |
|---|---|
![]() |
![]() |
| { | ||
| name: 'remote-session', | ||
| description: __( 'Manage the Telegram remote-session daemon (start, stop)' ), | ||
| enabled: isRemoteSessionEnabled, |
Thanks for testing this @epeicher! 🎉 In the meantime, I just pushed a small improvement, to make the start/stop subcommands interactively selectable, if the user only enters CleanShot.2026-05-05.at.13.48.10.mp4 |
📊 Performance Test ResultsComparing f8b4a4d vs trunk app-size
site-editor
site-startup
Results are median values from multiple test runs. Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff) |
That's great! I was about to suggest that, great UX |
|
E2E tests seems flaky, I've re-triggered these. |
## Related issues - Fixes STU-1682 - Follow-up to #3300 and #3315 ## How AI was used in this PR Implementation drafted with Claude Code; I reviewed the diff, ran the test suite, and smoke-tested the help output and the surviving entry points locally. ## Proposed Changes The experimental Telegram bridge originally shipped with a single autostart flag (`studio code --remote-session`). Since then, the daemonizable subcommand tree (`studio code remote-session start|stop|status`, #3300) and the in-REPL `/remote-session` slash command (#3315) have made the flag redundant. This PR removes it, so the surface has a single obvious way in. - Drops the `--remote-session`, `--remote-chat-id`, and `--remote-bot` options from the `studio code` parser, along with the short-circuit dispatcher that handed off to `runRemoteSession()`. The chat/bot pinning flags still live on `studio code remote-session start`. - Keeps the hidden `--message-from-stdin` option (still gated on `STUDIO_ENABLE_REMOTE_SESSION`) because the daemon's turn runner spawns `studio code --json --message-from-stdin` for each polled Telegram message. - Updates `specs/studio-code-remote-session-spec.md` and `RELEASE-NOTES.txt` so the documented entry points are the subcommand tree and slash command, with a STU-1682 status note explaining the cleanup. ## Testing Instructions - `npm run cli:build` - `node apps/cli/dist/cli/main.mjs code --help` should NOT list `--remote-session` (with or without `STUDIO_ENABLE_REMOTE_SESSION=true`). - `STUDIO_ENABLE_REMOTE_SESSION=true node apps/cli/dist/cli/main.mjs code remote-session --help` should still show `start`, `stop`, and `status`. - `STUDIO_ENABLE_REMOTE_SESSION=true node apps/cli/dist/cli/main.mjs code remote-session start --help` should still list `--detach`, `--remote-chat-id`, `--remote-bot`. - With `STUDIO_ENABLE_REMOTE_SESSION=true` and a configured token, run `studio code remote-session start --detach`, send a Telegram message, confirm the local agent replies, then `studio code remote-session stop`. - Inside an interactive `studio code` REPL, run `/remote-session start` and `/remote-session stop` and confirm the daemon indicator updates as before. - `npm test -- apps/cli/ai apps/cli/remote-session apps/cli/commands/ai` and `npm run typecheck` should pass. | Scenario | Command | Output | | --- | --- | --- | | Flag removed (feature flag on) | `STUDIO_ENABLE_REMOTE_SESSION=true studio code --help` | <img width="1314" height="688" alt="CleanShot 2026-05-06 at 10 37 30@2x" src="https://github.com/user-attachments/assets/0dd40f34-1fcd-4fce-839b-5444a14af3f8" /> | | Subcommand tree intact | `STUDIO_ENABLE_REMOTE_SESSION=true studio code remote-session --help` | <img width="1302" height="530" alt="CleanShot 2026-05-06 at 10 40 07@2x" src="https://github.com/user-attachments/assets/686fcf40-c7ec-4c6a-a441-0ba8dc143b63" /> | ## Pre-merge Checklist - [ ] Have you checked for TypeScript, React or other console errors?


Related issues
Proposed Changes
Earlier iterations of
/remote-sessionblocked the REPL while the poll loop ran. With the daemonization landed in #3300, the slash command becomes a thin REPL-side controller for the background daemon and the REPL never blocks.Subcommands, gated behind
STUDIO_ENABLE_REMOTE_SESSION:/remote-session start— validates config in-process so a missing token surfaces asui.showError, then callsstartDaemon()to spawn the detached child. Idempotent if a daemon is already running. Reports the PID and tells the user to pingDolly (@wordpress_com_bot)on Telegram./remote-session stop— callsstopDaemon(), surfaces friendly messages for already-stopped / SIGKILL fallback / refused-to-die.There is intentionally no
/remote-session statusslash command — the bottom-bar indicator already shows whether the daemon is running, andstudio code remote-session statuscovers the out-of-REPL case.Mechanics:
SlashCommandDefgains an optionalgetArgumentCompletions(prefix)hook so typing/remote-sessionlights upstart/stopin the autocomplete dropdown via pi-tui's existing argument-completion path./${name} <args>) so/remote-session <sub>resolves to the remote-session command and the handler parses the subcommand. This is the dispatcher change that was previously dropped in96b7ab6cb— now justified by an actual subcommand surface.daemon-status-pollmodule mirrors the on-disk PID file into a new bottom-bar indicator (PromptEditor.daemonStatusMessage) every 5s, so external start/stop and unexpected daemon death keep the REPL state honest. Indicator readsRemote session activein green and coexists with the existing loginstatusMessage(Remote session active · Logged in as Gergely).AiOutputAdaptergainssetDaemonStatus({ running, pid? });JsonAdapterno-ops it.Testing Instructions
npm run cli:build.STUDIO_ENABLE_REMOTE_SESSION=true, launchstudio code. Type/—remote-sessionshould appear in the autocomplete dropdown. Type/remote-session(trailing space) —start,stopshould appear./remote-session start. The REPL should show a red error about needing a bearer token./login(or an existing token), run/remote-session start— should print a success message and the bottom-right of the prompt should show a greenRemote session activemessage.node apps/cli/dist/cli/main.mjs code remote-session status— should report running, same PID./remote-session stop— should printRemote-session stopped (PID …)and the indicator should disappear./remote-session start, then runstudio code remote-session stopfrom another terminal. Within 5s, the bottom-bar indicator should clear on its own (5s poll).unset STUDIO_ENABLE_REMOTE_SESSION), confirm/remote-sessionis hidden from autocomplete and falls through to the AI agent.npm test -- --project cli apps/cli/ai/tests/slash-commands.test.ts apps/cli/ai/tests/daemon-status-poll.test.ts./remote-session(flag on)/in the REPL/remote-session/remote-session start(no WP.com login)/remote-session start(logged in)studio code remote-session status(fresh terminal)/remote-session stopstudio code remote-session stop(fresh terminal, then watch the REPL)/withSTUDIO_ENABLE_REMOTE_SESSIONunsetPre-merge Checklist