Implemented the next missing parity slice around remote-control slash commands.

This commit is contained in:
Abdelrahman Abdallah
2026-04-20 00:19:40 +02:00
parent a6a67d4c55
commit 266e1ac563
3 changed files with 222 additions and 2 deletions
+2 -2
View File
@@ -323,7 +323,7 @@ Missing npm slash commands (from `src/commands/` — 80+ commands total):
- [x] `/add-dir` — Add a new working directory
- [x] `/agents` — Inspect, create, update, and delete local agent definitions
- [x] `/branch` — Create a branch of the current conversation
- [ ] `/bridge` — Connect for remote-control sessions
- [x] `/bridge` — Connect for remote-control sessions (read-only status in this runtime)
- [x] `/btw` — Quick side question without interrupting main conversation
- [x] `/chrome` — Chrome extension settings
- [x] `/color` — Set the prompt bar color for this session
@@ -354,7 +354,7 @@ Missing npm slash commands (from `src/commands/` — 80+ commands total):
- [x] `/release-notes` — View release notes
- [x] `/reload-plugins` — Activate pending plugin changes
- [x] `/remote-env` — Configure default remote environment
- [ ] `/remote-setup` — Remote setup configuration
- [x] `/remote-setup` — Remote setup configuration (gh auth status + Claude.ai/code link)
- [x] `/rename` — Rename current conversation
- [x] `/resume`, `/continue` — Resume a previous conversation
- [x] `/rewind`, `/checkpoint` — Restore code/conversation to a previous point
+111
View File
@@ -539,6 +539,16 @@ def get_slash_command_specs() -> tuple[SlashCommandSpec, ...]:
description='List remote environments or set the default profile.',
handler=_handle_remote_env,
),
SlashCommandSpec(
names=('bridge', 'remote-control', 'rc'),
description='Report remote-control bridge status (read-only in this runtime).',
handler=_handle_bridge,
),
SlashCommandSpec(
names=('remote-setup', 'web-setup'),
description='Report Claude Code on the web setup readiness (gh + sign-in checks).',
handler=_handle_remote_setup,
),
)
@@ -2451,6 +2461,107 @@ def _handle_remote_env(
)
def _handle_bridge(
agent: 'LocalCodingAgent',
args: str,
input_text: str,
) -> SlashCommandResult:
runtime = agent.remote_runtime
requested_name = args.strip() or None
lines = ['Remote-control bridge: not implemented in the Python runtime.']
lines.append(
' The npm CLI hosts an interactive bridge against claude.ai; this '
'runtime only inspects the local remote-runtime state.'
)
if runtime is None:
lines.append(' (Remote runtime is unavailable.)')
return _local_result(input_text, '\n'.join(lines))
connection = runtime.active_connection
if connection is not None:
lines.append('')
lines.append('Active local remote connection:')
lines.append(f' - mode: {connection.mode}')
lines.append(f' - target: {connection.target}')
if connection.profile_name:
lines.append(f' - profile: {connection.profile_name}')
if connection.session_url:
lines.append(f' - session URL: {connection.session_url}')
if connection.workspace_cwd:
lines.append(f' - workspace: {connection.workspace_cwd}')
else:
lines.append('')
lines.append('No active local remote connection.')
if requested_name:
profile = runtime.get_profile(requested_name)
lines.append('')
if profile is None:
lines.append(
f'No matching remote profile for "{requested_name}". '
'Run /remote-env to list available profiles.'
)
else:
lines.append(
f'Matched remote profile "{profile.name}" '
f'({profile.mode} -> {profile.target}). '
'Use the npm CLI bridge to actually connect.'
)
return _local_result(input_text, '\n'.join(lines))
def _gh_auth_status() -> tuple[str, str]:
"""Return (status, detail) — status is one of 'not_installed',
'authenticated', 'not_authenticated', 'unknown'."""
import shutil
import subprocess
if shutil.which('gh') is None:
return ('not_installed', 'gh CLI not on PATH')
try:
result = subprocess.run(
['gh', 'auth', 'status'],
capture_output=True,
text=True,
timeout=5,
)
except (subprocess.SubprocessError, OSError) as exc:
return ('unknown', f'gh auth status failed: {exc}')
detail = (result.stderr or result.stdout or '').strip().splitlines()
summary = detail[0] if detail else ''
if result.returncode == 0:
return ('authenticated', summary or 'Authenticated to GitHub')
return ('not_authenticated', summary or 'Not authenticated to GitHub')
def _handle_remote_setup(
agent: 'LocalCodingAgent',
_args: str,
input_text: str,
) -> SlashCommandResult:
code_web_url = 'https://claude.ai/code'
gh_status, gh_detail = _gh_auth_status()
lines = [
'Claude Code on the web setup:',
f' Visit {code_web_url} to manage your environments.',
'',
f'GitHub CLI: {gh_status}',
f' {gh_detail}',
]
if gh_status == 'not_installed':
lines.append(' Install gh from https://cli.github.com to import a GitHub token.')
elif gh_status == 'not_authenticated':
lines.append(' Run `gh auth login` to authenticate before importing your token.')
elif gh_status == 'authenticated':
lines.append(' You can run `gh auth token` to retrieve the token to import on the web.')
lines.append('')
lines.append(
'Token import / default-environment provisioning is not implemented '
'in the Python runtime — complete remote setup from the npm CLI or '
'directly on claude.ai/code.'
)
return _local_result(input_text, '\n'.join(lines))
def _prompt_result(input_text: str, prompt: str) -> SlashCommandResult:
"""Return a prompt-type result — the prompt gets sent to the model."""
return SlashCommandResult(
+109
View File
@@ -0,0 +1,109 @@
"""Tests for remote/bridge slash commands ported from the npm source.
Covers /bridge (aliases /remote-control, /rc) and /remote-setup
(alias /web-setup).
"""
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from src.agent_runtime import LocalCodingAgent
from src.agent_types import AgentRuntimeConfig, ModelConfig
def _make_agent(tmp_dir: str) -> LocalCodingAgent:
return LocalCodingAgent(
model_config=ModelConfig(model='test-model'),
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
)
class BridgeCommandTest(unittest.TestCase):
def test_reports_unsupported_status(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/bridge').final_output
self.assertIn('not implemented', out.lower())
self.assertIn('No active local remote connection', out)
def test_remote_control_alias(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/remote-control').final_output
self.assertIn('Remote-control bridge', out)
def test_rc_alias(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/rc').final_output
self.assertIn('Remote-control bridge', out)
def test_named_lookup_misses_unknown_profile(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/bridge nope').final_output
self.assertIn('No matching remote profile for "nope"', out)
def test_named_lookup_matches_known_profile(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / '.remote.json').write_text(json.dumps({
'profiles': [
{'name': 'edge', 'mode': 'ssh', 'target': 'user@edge.example'},
],
}), encoding='utf-8')
agent = _make_agent(tmp)
out = agent.run('/bridge edge').final_output
self.assertIn('Matched remote profile "edge"', out)
self.assertIn('user@edge.example', out)
class RemoteSetupCommandTest(unittest.TestCase):
def test_includes_web_url(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/remote-setup').final_output
self.assertIn('https://claude.ai/code', out)
self.assertIn('GitHub CLI', out)
def test_web_setup_alias(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/web-setup').final_output
self.assertIn('https://claude.ai/code', out)
def test_handles_missing_gh(self) -> None:
with mock.patch('shutil.which', return_value=None):
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/remote-setup').final_output
self.assertIn('not_installed', out)
self.assertIn('cli.github.com', out)
def test_handles_authenticated_gh(self) -> None:
fake = mock.Mock(returncode=0, stdout='Logged in to github.com as octo', stderr='')
with mock.patch('shutil.which', return_value='/usr/bin/gh'), \
mock.patch('subprocess.run', return_value=fake):
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/remote-setup').final_output
self.assertIn('authenticated', out)
self.assertIn('gh auth token', out)
def test_handles_unauthenticated_gh(self) -> None:
fake = mock.Mock(returncode=1, stdout='', stderr='You are not logged into any GitHub hosts')
with mock.patch('shutil.which', return_value='/usr/bin/gh'), \
mock.patch('subprocess.run', return_value=fake):
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
out = agent.run('/remote-setup').final_output
self.assertIn('not_authenticated', out)
self.assertIn('gh auth login', out)
if __name__ == '__main__':
unittest.main()