Skip to content

Studio Code: add /remote-session slash command - #3315

Merged
gcsecsey merged 20 commits into
trunkfrom
gcsecsey/stu-1655
May 5, 2026
Merged

Studio Code: add /remote-session slash command#3315
gcsecsey merged 20 commits into
trunkfrom
gcsecsey/stu-1655

Conversation

@gcsecsey

@gcsecsey gcsecsey commented May 1, 2026

Copy link
Copy Markdown
Member

Related issues

Proposed Changes

Earlier iterations of /remote-session blocked 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 as ui.showError, then calls startDaemon() to spawn the detached child. Idempotent if a daemon is already running. Reports the PID and tells the user to ping Dolly (@wordpress_com_bot) on Telegram.
  • /remote-session stop — calls stopDaemon(), surfaces friendly messages for already-stopped / SIGKILL fallback / refused-to-die.

There is intentionally no /remote-session status slash command — the bottom-bar indicator already shows whether the daemon is running, and studio code remote-session status covers the out-of-REPL case.

Mechanics:

  • SlashCommandDef gains an optional getArgumentCompletions(prefix) hook so typing /remote-session lights up start/stop in the autocomplete dropdown via pi-tui's existing argument-completion path.
  • The REPL dispatcher now matches on the first whitespace token (/${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 in 96b7ab6cb — now justified by an actual subcommand surface.
  • New daemon-status-poll module 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 reads Remote session active in green and coexists with the existing login statusMessage (Remote session active · Logged in as Gergely).
  • AiOutputAdapter gains setDaemonStatus({ running, pid? }); JsonAdapter no-ops it.

Testing Instructions

  1. Build: npm run cli:build.
  2. With STUDIO_ENABLE_REMOTE_SESSION=true, launch studio code. Type /remote-session should appear in the autocomplete dropdown. Type /remote-session (trailing space) — start, stop should appear.
  3. Without a WP.com login, run /remote-session start. The REPL should show a red error about needing a bearer token.
  4. After /login (or an existing token), run /remote-session start — should print a success message and the bottom-right of the prompt should show a green Remote session active message.
  5. From a fresh terminal, run node apps/cli/dist/cli/main.mjs code remote-session status — should report running, same PID.
  6. Send a Telegram message to the bot — the daemon should reply.
  7. Back in the REPL, run /remote-session stop — should print Remote-session stopped (PID …) and the indicator should disappear.
  8. External-stop path: start the daemon via /remote-session start, then run studio code remote-session stop from another terminal. Within 5s, the bottom-bar indicator should clear on its own (5s poll).
  9. With the flag off (unset STUDIO_ENABLE_REMOTE_SESSION), confirm /remote-session is hidden from autocomplete and falls through to the AI agent.
  10. Tests: npm test -- --project cli apps/cli/ai/tests/slash-commands.test.ts apps/cli/ai/tests/daemon-status-poll.test.ts.
Scenario Trigger Screenshot
Autocomplete lists /remote-session (flag on) Type / in the REPL CleanShot 2026-05-01 at 14 14 54@2x
Subcommand autocomplete Type /remote-session CleanShot 2026-05-01 at 14 14 35@2x
Missing token → red error, no daemon spawned /remote-session start (no WP.com login) CleanShot 2026-05-01 at 14 15 47@2x
Daemon started + bottom-bar indicator /remote-session start (logged in) CleanShot 2026-05-01 at 14 16 17@2x
External status check matches studio code remote-session status (fresh terminal) CleanShot 2026-05-01 at 14 16 43@2x
Telegram round-trip Send a message to @wordpress_com_bot CleanShot 2026-05-01 at 14 18 18@2x
Daemon stopped + indicator cleared /remote-session stop CleanShot 2026-05-01 at 14 18 37@2x
External stop reflected within 5s studio code remote-session stop (fresh terminal, then watch the REPL) CleanShot 2026-05-01 at 14 19 06@2xCleanShot 2026-05-01 at 14 19 08@2x
Slash command hidden when flag is off Type / with STUDIO_ENABLE_REMOTE_SESSION unset CleanShot 2026-05-01 at 14 19 55@2x

Pre-merge Checklist

  • TypeScript clean, lint clean, full suite passes (1537 tests).
gcsecsey added 11 commits April 30, 2026 15:19
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).'
@gcsecsey gcsecsey changed the title apps/cli: /remote-session slash command (daemon-driven) May 1, 2026
@gcsecsey
gcsecsey marked this pull request as ready for review May 1, 2026 13:11
@gcsecsey

gcsecsey commented May 1, 2026

Copy link
Copy Markdown
Member Author

@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!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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|stop slash command (gated by STUDIO_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.

Comment thread apps/cli/commands/ai/index.ts Outdated
Comment thread apps/cli/ai/slash-commands.ts Outdated
Comment thread apps/cli/ai/slash-commands.ts Outdated
Comment thread specs/studio-code-remote-session-spec.md Outdated
Comment thread apps/cli/ai/slash-commands.ts
gcsecsey added 5 commits May 1, 2026 14:26
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.
Base automatically changed from gcsecsey/stu-1649 to trunk May 5, 2026 12:05
gcsecsey added 2 commits May 5, 2026 13:28
# 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 epeicher left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Image Image
{
name: 'remote-session',
description: __( 'Manage the Telegram remote-session daemon (start, stop)' ),
enabled: isRemoteSessionEnabled,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@gcsecsey

gcsecsey commented May 5, 2026

Copy link
Copy Markdown
Member Author

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!

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 /remote-session. The previous subcommands also continue to work:

CleanShot.2026-05-05.at.13.48.10.mp4
@gcsecsey
gcsecsey enabled auto-merge (squash) May 5, 2026 12:51
@wpmobilebot

wpmobilebot commented May 5, 2026

Copy link
Copy Markdown
Collaborator

📊 Performance Test Results

Comparing f8b4a4d vs trunk

app-size

Metric trunk f8b4a4d Diff Change
App Size (Mac) 1454.03 MB 1454.03 MB +0.01 MB ⚪ 0.0%

site-editor

Metric trunk f8b4a4d Diff Change
load 1502 ms 1535 ms +33 ms ⚪ 0.0%

site-startup

Metric trunk f8b4a4d Diff Change
siteCreation 8102 ms 8078 ms 24 ms ⚪ 0.0%
siteStartup 4949 ms 4931 ms 18 ms ⚪ 0.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

@epeicher

epeicher commented May 5, 2026

Copy link
Copy Markdown
Contributor

In the meantime, I just pushed a small improvement, to make the start/stop subcommands interactively selectable, if the user only enters /remote-session.

That's great! I was about to suggest that, great UX

@gcsecsey

gcsecsey commented May 5, 2026

Copy link
Copy Markdown
Member Author

E2E tests seems flaky, I've re-triggered these.

@gcsecsey
gcsecsey merged commit 3c9fa4f into trunk May 5, 2026
10 checks passed
@gcsecsey
gcsecsey deleted the gcsecsey/stu-1655 branch May 5, 2026 22:34
gcsecsey added a commit that referenced this pull request May 6, 2026
## 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?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

4 participants