Implemented the next missing parity slice around remote-control slash commands.
This commit is contained in:
+2
-2
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user