own the open-in-clide command family (T-413)
/permissions, /status, /config, /mcp, /agents, /hooks, /memory, and /help move from the TUI-only notice catalog to clide-owned commands with real behavior: - /permissions <mode> sets the mode over set_permission_mode; bare /permissions opens a picker in the interaction zone — the same card /model and /effort use (kPermissionModes, bypass last and explicit per T-181). - /status → Claude sidebar Activity tab; /config, /mcp, /agents, /hooks → Config tab. The pane activates the claude.meta sidebar tab and publishes a meta.tab message; the sidebar subscribes and switches its sub-tab — the same MessageBus addressing `clide ui open` uses (D-6), so the CLI can drive it too. - /memory opens the workspace CLAUDE.md via editor.open. - /help renders a local summary card (clide-owned + advertised commands) — the CLI's TUI help doesn't exist headless. The catalog keeps empty-hint entries for these tokens as safety nets if they're ever removed from owned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,7 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
StreamSubscription<TeamMemberJoined>? _joinSub;
|
||||
StreamSubscription<TeamMemberLeft>? _leftSub;
|
||||
StreamSubscription<Message>? _statusSub;
|
||||
StreamSubscription<Message>? _tabSub;
|
||||
StreamSubscription<SessionStatus>? _primarySub;
|
||||
StreamSubscription<void>? _brokerChangeSub;
|
||||
Timer? _timer;
|
||||
@@ -163,6 +164,13 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
_memberStatus.remove(m.agentId);
|
||||
});
|
||||
});
|
||||
// Slash-command navigation (T-413): /status, /config, /mcp, … publish a
|
||||
// meta.tab message; switch the sub-tab to match.
|
||||
_tabSub = kernel.messages.subscribe(publisher: 'builtin.claude', channel: 'meta.tab').listen((msg) {
|
||||
final name = msg.data['tab'] as String?;
|
||||
final tab = SidebarTab.values.where((t) => t.name == name).firstOrNull;
|
||||
if (tab != null && mounted) setState(() => _tab = tab);
|
||||
});
|
||||
// Live per-member status forwarded by the observer (T-157).
|
||||
_statusSub = kernel.messages.subscribe(channel: ClaudeConversation.memberStatusChannel).listen((msg) {
|
||||
final agentId = msg.data['agentId'] as String?;
|
||||
@@ -247,6 +255,7 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
_joinSub?.cancel();
|
||||
_leftSub?.cancel();
|
||||
_statusSub?.cancel();
|
||||
_tabSub?.cancel();
|
||||
_primarySub?.cancel();
|
||||
_brokerChangeSub?.cancel();
|
||||
_injectCtl.dispose();
|
||||
|
||||
@@ -92,6 +92,7 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
/// resolves.
|
||||
bool _modelPickerOpen = false;
|
||||
bool _effortPickerOpen = false;
|
||||
bool _permissionPickerOpen = false;
|
||||
|
||||
/// Effort level this pane's session runs at (`--effort`, T-412). Null =
|
||||
/// the CLI default. Set by /effort; carried by every respawn.
|
||||
@@ -255,6 +256,7 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
_modelErrorSub = null;
|
||||
_modelPickerOpen = false;
|
||||
_effortPickerOpen = false;
|
||||
_permissionPickerOpen = false;
|
||||
await activeSessionOrchestrator?.close(_orchId); // kills the old repo's session
|
||||
_conversation = null;
|
||||
_session = null;
|
||||
@@ -417,6 +419,24 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
case 'effort':
|
||||
_effortCommand(slashCommandArg(text) ?? '');
|
||||
return;
|
||||
case 'permissions':
|
||||
_permissionsCommand(slashCommandArg(text) ?? '');
|
||||
return;
|
||||
case 'status':
|
||||
_openMetaTab('activity');
|
||||
return;
|
||||
case 'config':
|
||||
case 'mcp':
|
||||
case 'agents':
|
||||
case 'hooks':
|
||||
_openMetaTab('config');
|
||||
return;
|
||||
case 'memory':
|
||||
_openMemory();
|
||||
return;
|
||||
case 'help':
|
||||
_helpCommand();
|
||||
return;
|
||||
}
|
||||
// Route the rest (T-411): a known TUI-only builtin never reaches the
|
||||
// session — forwarded it would error (or, un-advertised, bracket-paste to
|
||||
@@ -486,6 +506,59 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
unawaited(_respawnWithSession(sid));
|
||||
}
|
||||
|
||||
/// clide-owned `/permissions` (T-413): with a mode, set it directly over
|
||||
/// set_permission_mode; bare, open a picker — the same interaction-zone
|
||||
/// pattern as /model and /effort.
|
||||
void _permissionsCommand(String arg) {
|
||||
final s = _session;
|
||||
if (s == null) return;
|
||||
if (arg.isEmpty) {
|
||||
setState(() => _permissionPickerOpen = true);
|
||||
return;
|
||||
}
|
||||
if (!kPermissionModes.any((m) => m.value == arg)) {
|
||||
s.addLocalNotice('unknown permission mode "$arg" — modes: ${kPermissionModes.map((m) => m.value).join(', ')}');
|
||||
return;
|
||||
}
|
||||
s.setPermissionMode(arg);
|
||||
}
|
||||
|
||||
void _pickPermissionMode(String value) {
|
||||
_closePermissionPicker();
|
||||
_session?.setPermissionMode(value);
|
||||
}
|
||||
|
||||
void _closePermissionPicker() {
|
||||
setState(() => _permissionPickerOpen = false);
|
||||
_composerFocus.requestFocus();
|
||||
}
|
||||
|
||||
/// Navigate to the Claude sidebar and select a sub-tab (T-413): the
|
||||
/// /status//config//mcp//agents//hooks commands land here.
|
||||
void _openMetaTab(String tab) {
|
||||
final k = _kernel;
|
||||
if (k == null) return;
|
||||
k.panels.activateTab(Slots.sidebar, 'claude.meta');
|
||||
k.messages.publish('builtin.claude', 'meta.tab', {'tab': tab});
|
||||
}
|
||||
|
||||
/// clide-owned `/memory` (T-413): open the workspace CLAUDE.md in the editor.
|
||||
void _openMemory() {
|
||||
final root = _repoRoot;
|
||||
if (root == null) return;
|
||||
unawaited(_ipc()?.request('editor.open', args: {'path': '$root/CLAUDE.md'}));
|
||||
}
|
||||
|
||||
/// clide-owned `/help` (T-413): a local summary card — never the CLI's TUI
|
||||
/// help, which doesn't exist headless.
|
||||
void _helpCommand() {
|
||||
final advertised = (activeClaudeConfig?.slashCommands ?? kFallbackSlashCommands).where((c) => !kClideOwnedCommands.contains(c)).toList()..sort();
|
||||
_session?.addLocalNotice(
|
||||
'clide commands: ${(kClideOwnedCommands.toList()..sort()).map((c) => '/$c').join(' ')}\n'
|
||||
'claude commands & skills: ${advertised.map((c) => '/$c').join(' ')}',
|
||||
);
|
||||
}
|
||||
|
||||
/// Record a submitted prompt in the active session's history (T-163),
|
||||
/// de-duping immediate repeats. Empty/whitespace prompts are skipped.
|
||||
void _appendHistory(String text) {
|
||||
@@ -510,7 +583,7 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
/// background tap must never pull focus from (or resurrect) the composer
|
||||
/// over an open prompt.
|
||||
void _focusComposerOnTap() {
|
||||
if (_session?.pendingPrompt != null || _modelPickerOpen || _effortPickerOpen) return;
|
||||
if (_session?.pendingPrompt != null || _modelPickerOpen || _effortPickerOpen || _permissionPickerOpen) return;
|
||||
_composerFocus.requestFocus();
|
||||
}
|
||||
|
||||
@@ -586,6 +659,7 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
_modelErrorSub = null;
|
||||
_modelPickerOpen = false;
|
||||
_effortPickerOpen = false;
|
||||
_permissionPickerOpen = false;
|
||||
await activeSessionOrchestrator?.close(_orchId); // kills the old session
|
||||
// Erase only after the process is dead, so claude isn't mid-write.
|
||||
final root = _repoRoot;
|
||||
@@ -678,6 +752,15 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
onPick: _pickEffort,
|
||||
onCancel: _closeEffortPicker,
|
||||
)
|
||||
else if (_permissionPickerOpen && _session != null)
|
||||
ModelPickerCard(
|
||||
title: 'permissions',
|
||||
models: kPermissionModes,
|
||||
currentModel: _status.permissionMode,
|
||||
isCurrent: (o, c) => c != null && o.value == c,
|
||||
onPick: _pickPermissionMode,
|
||||
onCancel: _closePermissionPicker,
|
||||
)
|
||||
else
|
||||
StreamBuilder<bool>(
|
||||
stream: _session?.busyStream,
|
||||
|
||||
@@ -34,8 +34,25 @@ bool isKnownSlashCommand(String text, Iterable<String> known) {
|
||||
/// is interactive in the CLI's TUI only — forwarded it does nothing — so
|
||||
/// clide owns it as a set_model control request / picker (T-408). `/effort`
|
||||
/// has no control subtype, so clide owns it as a respawn-with-resume
|
||||
/// carrying `--effort` (T-412).
|
||||
const Set<String> kClideOwnedCommands = {'clear', 'resume', 'fork', 'model', 'effort'};
|
||||
/// carrying `--effort` (T-412). `/permissions` is a picker over
|
||||
/// set_permission_mode; the rest navigate to clide surfaces (T-413):
|
||||
/// /status//config//mcp//agents//hooks → the Claude sidebar tabs,
|
||||
/// /memory → CLAUDE.md in the editor, /help → a local command summary.
|
||||
const Set<String> kClideOwnedCommands = {
|
||||
'clear',
|
||||
'resume',
|
||||
'fork',
|
||||
'model',
|
||||
'effort',
|
||||
'permissions',
|
||||
'status',
|
||||
'config',
|
||||
'mcp',
|
||||
'agents',
|
||||
'hooks',
|
||||
'memory',
|
||||
'help',
|
||||
};
|
||||
|
||||
/// The clide-owned command in [text] (a single-line leading-slash token in
|
||||
/// [kClideOwnedCommands]), or null.
|
||||
@@ -67,16 +84,16 @@ enum SlashRoute {
|
||||
/// notice card. Commands clide later implements move to [kClideOwnedCommands].
|
||||
const Map<String, String> kTuiOnlyCommands = {
|
||||
'effort': '', // owned (T-412) — only routes here if ever removed from owned
|
||||
'status': 'session status lives in the Claude sidebar (Activity tab)',
|
||||
'status': '', // owned (T-413)
|
||||
'cost': 'cost and context usage live in the Claude sidebar (Activity tab)',
|
||||
'context': '', // advertised on current CLIs — only routes here on older ones
|
||||
'help': 'type / to browse commands; clide owns /clear /resume /fork /model',
|
||||
'config': 'open the Claude sidebar Config tab',
|
||||
'permissions': 'use the permission-mode control beside the composer',
|
||||
'memory': 'open CLAUDE.md in the editor',
|
||||
'mcp': 'MCP servers are listed in the Claude sidebar Config tab',
|
||||
'agents': 'agents are listed in the Claude sidebar Config tab',
|
||||
'hooks': 'hooks are listed in the Claude sidebar Config tab',
|
||||
'help': '', // owned (T-413)
|
||||
'config': '', // owned (T-413)
|
||||
'permissions': '', // owned (T-413)
|
||||
'memory': '', // owned (T-413)
|
||||
'mcp': '', // owned (T-413)
|
||||
'agents': '', // owned (T-413)
|
||||
'hooks': '', // owned (T-413)
|
||||
'todos': "Claude's task list docks above the composer",
|
||||
'model': '', // owned (T-408) — only routes here if ever removed from owned
|
||||
'doctor': 'run `claude doctor` in a terminal',
|
||||
|
||||
@@ -174,6 +174,16 @@ const List<ModelOption> kEffortLevels = [
|
||||
ModelOption(value: 'max', displayName: 'max', description: 'maximum thinking budget'),
|
||||
];
|
||||
|
||||
/// Permission modes for the /permissions picker (T-413), set over the
|
||||
/// set_permission_mode control request. Bypass is last and explicit — the
|
||||
/// footgun stays visible but never the default reach (T-181).
|
||||
const List<ModelOption> kPermissionModes = [
|
||||
ModelOption(value: 'default', displayName: 'default', description: 'ask before sensitive tools'),
|
||||
ModelOption(value: 'acceptEdits', displayName: 'acceptEdits', description: 'auto-approve file edits'),
|
||||
ModelOption(value: 'plan', displayName: 'plan', description: 'read-only planning mode'),
|
||||
ModelOption(value: 'bypassPermissions', displayName: 'bypassPermissions', description: 'no prompts at all — careful'),
|
||||
];
|
||||
|
||||
/// Fallback picker entries for when the `initialize` response hasn't arrived
|
||||
/// (or carried no models): the stable aliases every claude build accepts
|
||||
/// (T-408). `default` resets to the CLI's configured model.
|
||||
|
||||
Reference in New Issue
Block a user