Implemented the next missing parity slice around informational slash commands.

Adds 15 commands ported from npm src/commands/: /output-style,
/release-notes, /feedback (alias /bug), /upgrade, /stickers,
/mobile (aliases /ios, /android), /desktop (alias /app),
/install-github-app, /install-slack-app, /privacy-settings,
/extra-usage, /passes, /rate-limit-options, /chrome, and
/reload-plugins. Browser launches honor CI / CLAUDE_CODE_NO_BROWSER
and fall back to a printed link when a browser is unavailable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Abdelrahman Abdallah
2026-04-19 23:39:07 +02:00
parent 872f4f89b5
commit 1072793487
3 changed files with 520 additions and 15 deletions
+15 -15
View File
@@ -325,34 +325,34 @@ Missing npm slash commands (from `src/commands/` — 80+ commands total):
- [x] `/branch` — Create a branch of the current conversation
- [ ] `/bridge` — Connect for remote-control sessions
- [ ] `/btw` — Quick side question without interrupting main conversation
- [ ] `/chrome` — Chrome extension settings
- [x] `/chrome` — Chrome extension settings
- [x] `/color` — Set the prompt bar color for this session
- [x] `/compact` — Clear history but keep a summary in context
- [x] `/copy` — Copy Claude's last response to clipboard
- [x] `/cost` — Show total cost and duration of session
- [ ] `/desktop` — Continue session in Claude Desktop
- [x] `/desktop` — Continue session in Claude Desktop
- [x] `/diff` — View uncommitted changes and per-turn diffs
- [x] `/doctor` — Diagnose and verify installation and settings
- [x] `/effort` — Set effort level for model usage
- [x] `/exit` — Exit the REPL
- [x] `/export` — Export conversation to file or clipboard
- [ ] `/extra-usage` — Configure extra usage for rate limits
- [x] `/extra-usage` — Configure extra usage for rate limits
- [x] `/fast` — Toggle fast mode
- [ ] `/feedback` — Submit feedback
- [x] `/feedback` — Submit feedback (alias `/bug`)
- [x] `/files` — List all files currently in context
- [ ] `/ide` — Manage IDE integrations and show status
- [ ] `/install-github-app` — Set up GitHub Actions
- [ ] `/install-slack-app` — Install Slack app
- [x] `/install-github-app` — Set up GitHub Actions
- [x] `/install-slack-app` — Install Slack app
- [ ] `/keybindings` — Open keybindings config file
- [ ] `/mobile`QR code for mobile app
- [ ] `/output-style`Change output style
- [ ] `/passes` — Passes management
- [x] `/mobile`Mobile app store links (aliases `/ios`, `/android`)
- [x] `/output-style`Deprecation pointer to `/config`
- [x] `/passes` — Passes management
- [ ] `/plugin` — Plugin management
- [x] `/pr-comments`, `/pr_comments` — Get comments from a GitHub PR (prompt-type)
- [ ] `/privacy-settings` — View/update privacy settings
- [ ] `/rate-limit-options` — Show options when rate limited
- [ ] `/release-notes` — View release notes
- [ ] `/reload-plugins` — Activate pending plugin changes
- [x] `/privacy-settings` — View/update privacy settings
- [x] `/rate-limit-options` — Show options when rate limited
- [x] `/release-notes` — View release notes
- [x] `/reload-plugins` — Activate pending plugin changes
- [ ] `/remote-env` — Configure default remote environment
- [ ] `/remote-setup` — Remote setup configuration
- [x] `/rename` — Rename current conversation
@@ -361,10 +361,10 @@ Missing npm slash commands (from `src/commands/` — 80+ commands total):
- [ ] `/sandbox-toggle` — Toggle sandbox mode
- [x] `/skills` — List available skills
- [x] `/stats` — Usage statistics and activity
- [ ] `/stickers` — Order stickers
- [x] `/stickers` — Order stickers
- [x] `/tag` — Toggle a searchable tag on the session
- [ ] `/theme` — Change the theme
- [ ] `/upgrade` — Upgrade to Max
- [x] `/upgrade` — Upgrade to Max
- [x] `/vim` — Toggle Vim/Normal editing modes
- [ ] `/voice` — Toggle voice mode
- [ ] Feature-gated: `/buddy`, `/fork`, `/peers`, `/proactive`, `/torch`, `/workflows` (full), etc.
+360
View File
@@ -414,6 +414,81 @@ def get_slash_command_specs() -> tuple[SlashCommandSpec, ...]:
description='Restore the conversation to a previous point.',
handler=_handle_rewind,
),
SlashCommandSpec(
names=('output-style',),
description='Deprecated: use /config to change your output style.',
handler=_handle_output_style,
),
SlashCommandSpec(
names=('release-notes',),
description='Show local release notes or a link to the changelog.',
handler=_handle_release_notes,
),
SlashCommandSpec(
names=('feedback', 'bug'),
description='Open the Claude Code feedback page in a browser.',
handler=_handle_feedback,
),
SlashCommandSpec(
names=('upgrade',),
description='Open the Claude.ai upgrade page in a browser.',
handler=_handle_upgrade,
),
SlashCommandSpec(
names=('stickers',),
description='Open the Claude Code sticker order page in a browser.',
handler=_handle_stickers,
),
SlashCommandSpec(
names=('mobile', 'ios', 'android'),
description='Show download links for the Claude mobile apps.',
handler=_handle_mobile,
),
SlashCommandSpec(
names=('desktop', 'app'),
description='Show the Claude Desktop handoff page link.',
handler=_handle_desktop,
),
SlashCommandSpec(
names=('install-github-app',),
description='Open the Claude GitHub Actions setup page.',
handler=_handle_install_github_app,
),
SlashCommandSpec(
names=('install-slack-app',),
description='Open the Claude Slack app installation page.',
handler=_handle_install_slack_app,
),
SlashCommandSpec(
names=('privacy-settings',),
description='Open the Claude.ai privacy controls page.',
handler=_handle_privacy_settings,
),
SlashCommandSpec(
names=('extra-usage',),
description='Show extra-usage configuration link.',
handler=_handle_extra_usage,
),
SlashCommandSpec(
names=('passes',),
description='Show Claude Code guest passes information.',
handler=_handle_passes,
),
SlashCommandSpec(
names=('rate-limit-options',),
description='Show options when the active account hits a rate limit.',
handler=_handle_rate_limit_options,
),
SlashCommandSpec(
names=('chrome',),
description='Open the Claude Chrome extension page.',
handler=_handle_chrome,
),
SlashCommandSpec(
names=('reload-plugins',),
description='Reload local plugin manifests and report counts.',
handler=_handle_reload_plugins,
),
)
@@ -1616,6 +1691,291 @@ def _handle_rewind(agent: 'LocalCodingAgent', args: str, input_text: str) -> Sla
)
_FEEDBACK_URL = 'https://github.com/anthropics/claude-code/issues'
_UPGRADE_URL = 'https://claude.ai/upgrade/max'
_STICKERS_URL = 'https://www.stickermule.com/claudecode'
_MOBILE_IOS_URL = 'https://apps.apple.com/app/claude-by-anthropic/id6473753684'
_MOBILE_ANDROID_URL = 'https://play.google.com/store/apps/details?id=com.anthropic.claude'
_DESKTOP_URL = 'https://claude.ai/download'
_GITHUB_APP_URL = 'https://github.com/apps/claude'
_SLACK_APP_URL = 'https://slack.com/marketplace/A08SF47R6P4-claude'
_PRIVACY_URL = 'https://claude.ai/settings/data-privacy-controls'
_CHROME_EXTENSION_URL = 'https://claude.ai/chrome'
_CHANGELOG_URL = 'https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md'
def _try_open_browser(url: str) -> bool:
import os
import webbrowser
# Avoid spawning a browser in CI / non-interactive environments.
if os.environ.get('CLAUDE_CODE_NO_BROWSER') or os.environ.get('CI'):
return False
try:
return webbrowser.open(url, new=2)
except Exception:
return False
def _open_or_link(url: str, *, opening_message: str, fallback_message: str) -> str:
if _try_open_browser(url):
return f'{opening_message}\n {url}'
return f'{fallback_message}\n {url}'
def _changelog_path(agent: 'LocalCodingAgent') -> 'Path':
from pathlib import Path
cwd = Path(agent.runtime_config.cwd)
for candidate in (cwd / 'CHANGELOG.md', cwd / 'docs' / 'CHANGELOG.md'):
if candidate.exists():
return candidate
return cwd / 'CHANGELOG.md'
def _handle_output_style(
agent: 'LocalCodingAgent',
_args: str,
input_text: str,
) -> SlashCommandResult:
return _local_result(
input_text,
'/output-style has been deprecated. Use /config to change your output style, '
'or set it in your settings file. Changes take effect on the next session.',
)
def _handle_release_notes(
agent: 'LocalCodingAgent',
_args: str,
input_text: str,
) -> SlashCommandResult:
path = _changelog_path(agent)
if path.exists():
try:
content = path.read_text(encoding='utf-8').strip()
except OSError as exc:
return _local_result(input_text, f'Could not read {path}: {exc}')
# Show only the most recent release block (everything up to the second
# second-level heading, mirroring how the npm command surfaces a single
# version chunk by default).
lines = content.splitlines()
chunk: list[str] = []
seen_heading = False
for line in lines:
if line.startswith('## '):
if seen_heading:
break
seen_heading = True
chunk.append(line)
return _local_result(input_text, '\n'.join(chunk).strip() or content)
return _local_result(
input_text,
f'No local CHANGELOG.md found. See the full changelog at:\n {_CHANGELOG_URL}',
)
def _handle_feedback(
agent: 'LocalCodingAgent',
args: str,
input_text: str,
) -> SlashCommandResult:
note = args.strip()
body = _open_or_link(
_FEEDBACK_URL,
opening_message='Opening the Claude Code feedback tracker in your browser…',
fallback_message='Submit feedback at:',
)
if note:
body += f'\n\nDraft note (copy into the report form):\n{note}'
return _local_result(input_text, body)
def _handle_upgrade(
agent: 'LocalCodingAgent',
_args: str,
input_text: str,
) -> SlashCommandResult:
return _local_result(
input_text,
_open_or_link(
_UPGRADE_URL,
opening_message='Opening the Claude.ai upgrade page in your browser…',
fallback_message='Upgrade your account at:',
),
)
def _handle_stickers(
agent: 'LocalCodingAgent',
_args: str,
input_text: str,
) -> SlashCommandResult:
return _local_result(
input_text,
_open_or_link(
_STICKERS_URL,
opening_message='Opening the Claude Code sticker page in your browser…',
fallback_message='Order Claude Code stickers at:',
),
)
def _handle_mobile(
agent: 'LocalCodingAgent',
_args: str,
input_text: str,
) -> SlashCommandResult:
lines = [
'Download the Claude mobile app:',
f' iOS: {_MOBILE_IOS_URL}',
f' Android: {_MOBILE_ANDROID_URL}',
]
return _local_result(input_text, '\n'.join(lines))
def _handle_desktop(
agent: 'LocalCodingAgent',
_args: str,
input_text: str,
) -> SlashCommandResult:
import platform
system = platform.system()
if system not in {'Darwin', 'Windows'}:
return _local_result(
input_text,
f'Claude Desktop is currently available on macOS and Windows only '
f'(detected: {system}). Download:\n {_DESKTOP_URL}',
)
return _local_result(
input_text,
_open_or_link(
_DESKTOP_URL,
opening_message='Opening the Claude Desktop download page in your browser…',
fallback_message='Download Claude Desktop at:',
),
)
def _handle_install_github_app(
agent: 'LocalCodingAgent',
_args: str,
input_text: str,
) -> SlashCommandResult:
return _local_result(
input_text,
_open_or_link(
_GITHUB_APP_URL,
opening_message='Opening the Claude GitHub App installation page in your browser…',
fallback_message='Set up Claude GitHub Actions at:',
),
)
def _handle_install_slack_app(
agent: 'LocalCodingAgent',
_args: str,
input_text: str,
) -> SlashCommandResult:
return _local_result(
input_text,
_open_or_link(
_SLACK_APP_URL,
opening_message='Opening the Claude Slack app marketplace page in your browser…',
fallback_message="Couldn't open browser. Visit:",
),
)
def _handle_privacy_settings(
agent: 'LocalCodingAgent',
_args: str,
input_text: str,
) -> SlashCommandResult:
return _local_result(
input_text,
f'Review and manage your privacy settings at:\n {_PRIVACY_URL}',
)
def _handle_extra_usage(
agent: 'LocalCodingAgent',
_args: str,
input_text: str,
) -> SlashCommandResult:
return _local_result(
input_text,
'Configure extra usage on a Claude.ai account at:\n'
f' {_UPGRADE_URL}\n'
'After upgrading, run /login to refresh your local credentials.',
)
def _handle_passes(
agent: 'LocalCodingAgent',
_args: str,
input_text: str,
) -> SlashCommandResult:
return _local_result(
input_text,
'Claude Code guest passes are managed in your Claude.ai account.\n'
' Visit https://claude.ai to sign in and view remaining passes.',
)
def _handle_rate_limit_options(
agent: 'LocalCodingAgent',
_args: str,
input_text: str,
) -> SlashCommandResult:
lines = [
'When the current account hits a rate limit, you can:',
' - Run /upgrade to move to a higher Claude.ai plan.',
' - Run /extra-usage to enable per-message billing on a Claude.ai plan.',
' - Run /login to switch to an API-key billed account.',
f'See {_UPGRADE_URL} for plan details.',
]
return _local_result(input_text, '\n'.join(lines))
def _handle_chrome(
agent: 'LocalCodingAgent',
_args: str,
input_text: str,
) -> SlashCommandResult:
return _local_result(
input_text,
_open_or_link(
_CHROME_EXTENSION_URL,
opening_message='Opening the Claude Chrome extension page in your browser…',
fallback_message='Install the Claude Chrome extension at:',
),
)
def _handle_reload_plugins(
agent: 'LocalCodingAgent',
_args: str,
input_text: str,
) -> SlashCommandResult:
from pathlib import Path
from .plugin_runtime import PluginRuntime
runtime = PluginRuntime.from_workspace(Path(agent.runtime_config.cwd))
agent.plugin_runtime = runtime
plugin_count = len(runtime.manifests)
tool_count = sum(len(manifest.tool_names) for manifest in runtime.manifests)
hook_count = sum(len(manifest.hook_names) for manifest in runtime.manifests)
virtual_count = sum(len(manifest.virtual_tools) for manifest in runtime.manifests)
return _local_result(
input_text,
'Reloaded plugins: '
f'{plugin_count} plugin(s) · {tool_count} tool(s) · {hook_count} hook(s) · '
f'{virtual_count} virtual tool(s)',
)
def _prompt_result(input_text: str, prompt: str) -> SlashCommandResult:
"""Return a prompt-type result — the prompt gets sent to the model."""
return SlashCommandResult(
+145
View File
@@ -0,0 +1,145 @@
"""Tests for the informational slash commands ported from the npm source.
Covers /output-style, /release-notes, /feedback, /upgrade, /stickers, /mobile,
/desktop, /install-github-app, /install-slack-app, /privacy-settings,
/extra-usage, /passes, /rate-limit-options, /chrome, /reload-plugins.
"""
from __future__ import annotations
import os
import tempfile
import unittest
from pathlib import Path
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 ExternalSlashCommandsTest(unittest.TestCase):
"""Each test runs with CLAUDE_CODE_NO_BROWSER=1 so no browser opens."""
def setUp(self) -> None:
os.environ['CLAUDE_CODE_NO_BROWSER'] = '1'
def tearDown(self) -> None:
os.environ.pop('CLAUDE_CODE_NO_BROWSER', None)
def _run(self, cmd: str) -> str:
with tempfile.TemporaryDirectory() as tmp:
agent = _make_agent(tmp)
return agent.run(cmd).final_output
def test_output_style_is_deprecated(self) -> None:
out = self._run('/output-style')
self.assertIn('deprecated', out.lower())
self.assertIn('/config', out)
def test_release_notes_falls_back_to_link(self) -> None:
out = self._run('/release-notes')
self.assertIn('CHANGELOG.md', out)
self.assertIn('https://github.com/anthropics/claude-code', out)
def test_release_notes_reads_local_changelog(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / 'CHANGELOG.md').write_text(
'# Changelog\n\n## 1.2.3\n- did a thing\n\n## 1.2.2\n- old\n',
encoding='utf-8',
)
agent = _make_agent(tmp)
out = agent.run('/release-notes').final_output
self.assertIn('1.2.3', out)
self.assertIn('did a thing', out)
self.assertNotIn('1.2.2', out)
def test_feedback_returns_link(self) -> None:
out = self._run('/feedback')
self.assertIn('https://github.com/anthropics/claude-code/issues', out)
def test_bug_aliases_to_feedback(self) -> None:
out = self._run('/bug')
self.assertIn('https://github.com/anthropics/claude-code/issues', out)
def test_feedback_includes_user_note(self) -> None:
out = self._run('/feedback the wrap selector keeps eating my prompt')
self.assertIn('Draft note', out)
self.assertIn('wrap selector', out)
def test_upgrade_returns_link(self) -> None:
out = self._run('/upgrade')
self.assertIn('https://claude.ai/upgrade/max', out)
def test_stickers_returns_link(self) -> None:
out = self._run('/stickers')
self.assertIn('stickermule.com/claudecode', out)
def test_mobile_lists_both_stores(self) -> None:
out = self._run('/mobile')
self.assertIn('apps.apple.com', out)
self.assertIn('play.google.com', out)
def test_ios_alias(self) -> None:
out = self._run('/ios')
self.assertIn('apps.apple.com', out)
def test_android_alias(self) -> None:
out = self._run('/android')
self.assertIn('play.google.com', out)
def test_desktop_returns_link(self) -> None:
out = self._run('/desktop')
self.assertIn('claude.ai/download', out)
def test_app_aliases_to_desktop(self) -> None:
out = self._run('/app')
self.assertIn('claude.ai/download', out)
def test_install_github_app(self) -> None:
out = self._run('/install-github-app')
self.assertIn('github.com/apps/claude', out)
def test_install_slack_app(self) -> None:
out = self._run('/install-slack-app')
self.assertIn('slack.com/marketplace/A08SF47R6P4-claude', out)
def test_privacy_settings(self) -> None:
out = self._run('/privacy-settings')
self.assertIn('claude.ai/settings/data-privacy-controls', out)
def test_extra_usage_points_to_upgrade(self) -> None:
out = self._run('/extra-usage')
self.assertIn('claude.ai/upgrade/max', out)
self.assertIn('/login', out)
def test_passes_mentions_claude_ai(self) -> None:
out = self._run('/passes')
self.assertIn('claude.ai', out.lower())
self.assertIn('passes', out.lower())
def test_rate_limit_options_lists_actions(self) -> None:
out = self._run('/rate-limit-options')
self.assertIn('/upgrade', out)
self.assertIn('/extra-usage', out)
self.assertIn('/login', out)
def test_chrome_returns_link(self) -> None:
out = self._run('/chrome')
self.assertIn('claude.ai/chrome', out)
def test_reload_plugins_reports_counts(self) -> None:
out = self._run('/reload-plugins')
self.assertIn('Reloaded plugins', out)
self.assertIn('plugin(s)', out)
self.assertIn('tool(s)', out)
self.assertIn('hook(s)', out)
if __name__ == '__main__':
unittest.main()