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:
@@ -4326,3 +4326,5 @@ accent marks per ui-design tokens), and make the controls feel like controls.
|
||||
Treat ui-design skill as the reference for token/type choices.', NULL, '2026-06-12 10:24:43', '2026-06-12 10:24:43', '2026-06-12 10:24:43', NULL, '5e15c4d2cdabba62bbaba701a93f6e79', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3GM6V0RZBY2PXE9ZQFR88', 'status', 'in_progress', 'done', NULL, '2026-06-12 10:25:53', '2026-06-12 10:25:53', '2026-06-12 10:25:53', NULL, 'f65cf1d99c1d22ced8d18e1579b048ad', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3J7TXMG0F9E2WQDENPVJG', 'status', 'backlog', 'in_progress', NULL, '2026-06-12 10:25:53', '2026-06-12 10:25:53', '2026-06-12 10:25:53', NULL, 'dca846f23cb4353cc826ddd79a6c14f0', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3J7TXMG0F9E2WQDENPVJG', 'status', 'in_progress', 'done', NULL, '2026-06-12 10:56:52', '2026-06-12 10:56:52', '2026-06-12 10:56:52', NULL, 'fd7c4c5f0b0de9144c6220a4ce43476b', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3KRWM65MD3DS251NN9YX0', 'status', 'backlog', 'in_progress', NULL, '2026-06-12 10:56:52', '2026-06-12 10:56:52', '2026-06-12 10:56:52', NULL, 'd1eb3f87ceb5508e4dac1f191750f852', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -4711,3 +4711,9 @@ Where: slash_commands.dart (table), claude_pane._send (dispatch), stream_json_se
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3J7TXMG0F9E2WQDENPVJG', 'story', '06FBP3EZC7AJANXZVF3D91QYWM', 'Own /effort: control-request spike + arg/picker UX', 'Mirror T-408''s /model treatment for /effort (thinking-effort level). SPIKE FIRST (same method as the T-408 set_model spike against claude 2.1.175): does the stream-json control channel accept a set_effort-style control_request subtype? Probe candidate subtypes alongside set_permission_mode/set_model; document the result in the ticket. If no control subtype exists, decide a fallback explicitly — settings-file write + session restart, or declare it unavailable-with-explanation — don''t fake it.
|
||||
|
||||
UX when supported: ''/effort <level>'' sets directly (optimistic status merge, toast rollback on rejection, exactly the T-408 pattern); bare ''/effort'' swaps a picker card into the interaction zone (D-78). Surface the current effort in SessionStatus + the sidebar config row, and as an inline selector per the power-panel story. Levels per CLI docs (e.g. low/medium/high; verify against the CLI version).', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-12 08:58:55', '2026-06-12 10:25:53', NULL, 'd0b0dbfec1edcdd4ffe78372f440cf1b', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3J7TXMG0F9E2WQDENPVJG', 'story', '06FBP3EZC7AJANXZVF3D91QYWM', 'Own /effort: control-request spike + arg/picker UX', 'Mirror T-408''s /model treatment for /effort (thinking-effort level). SPIKE FIRST (same method as the T-408 set_model spike against claude 2.1.175): does the stream-json control channel accept a set_effort-style control_request subtype? Probe candidate subtypes alongside set_permission_mode/set_model; document the result in the ticket. If no control subtype exists, decide a fallback explicitly — settings-file write + session restart, or declare it unavailable-with-explanation — don''t fake it.
|
||||
|
||||
UX when supported: ''/effort <level>'' sets directly (optimistic status merge, toast rollback on rejection, exactly the T-408 pattern); bare ''/effort'' swaps a picker card into the interaction zone (D-78). Surface the current effort in SessionStatus + the sidebar config row, and as an inline selector per the power-panel story. Levels per CLI docs (e.g. low/medium/high; verify against the CLI version).', 'done', 'medium', NULL, NULL, NULL, '2026-06-12 08:58:55', '2026-06-12 10:56:52', NULL, '448346b2ab034e4560d9b9589e318344', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3KRWM65MD3DS251NN9YX0', 'story', '06FBP3EZC7AJANXZVF3D91QYWM', 'Open-in-clide command family: /permissions /config /memory /mcp /agents /todos /status /help', 'The ''owned'' routes that map a TUI command to an existing clide surface — thin per-command glue, one story. /permissions → open the permission-mode picker (T-275 control). /config → focus the Claude sidebar Config tab (T-183). /memory → open CLAUDE.md (and memory files) in the editor. /mcp, /agents, /hooks → their Config-tab sections. /todos → focus the task dock (T-308). /status → focus the sidebar Activity tab. /help → a clide help card listing owned commands + advertised skills (NOT the CLI''s TUI help).
|
||||
|
||||
Each lands in the routing table (T-411) as ''owned''; D-6 parity notes: these are also the navigation targets the catch-all hint card (T-411) points at. Keep each handler a few lines — navigation/publish on the existing MessageBus + panel APIs, no new surfaces. Tests: each command routes to its surface (message published / tab activated), none forwards to the session.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-12 08:59:08', '2026-06-12 10:56:52', NULL, '395cba57154e147b6a35212e23e2d17e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
|
||||
@@ -18,6 +18,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
|
||||
### Added
|
||||
|
||||
- **The TUI command family opens clide surfaces.** `/permissions` sets the mode
|
||||
directly or opens a picker; `/status`, `/config`, `/mcp`, `/agents`, `/hooks`
|
||||
jump to the matching Claude sidebar tab; `/memory` opens CLAUDE.md in the
|
||||
editor; `/help` shows clide's own command summary. (T-413)
|
||||
- **`/effort` works in the Claude pane.** With a level (`/effort xhigh`) the
|
||||
session restarts in place carrying `--effort` — resume keeps the
|
||||
conversation; bare `/effort` opens a picker with the five levels and the
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -133,6 +133,29 @@ void main() {
|
||||
expect(find.text('Claude environment not loaded.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a meta.tab message switches the sub-tab (T-413 slash navigation)', (tester) async {
|
||||
await tester.pumpWidget(harness(f, sidebar(stats: stats)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('TODAY'), findsOneWidget); // starts on Activity
|
||||
|
||||
// /config (and /mcp, /agents, /hooks) publish this from the Claude pane.
|
||||
f.services.messages.publish('builtin.claude', 'meta.tab', {'tab': 'config'});
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(find.text('Claude environment not loaded.'), findsOneWidget); // Config tab (no env in fixture)
|
||||
|
||||
f.services.messages.publish('builtin.claude', 'meta.tab', {'tab': 'activity'});
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(find.text('TODAY'), findsOneWidget); // back on Activity
|
||||
|
||||
// An unknown tab name is ignored.
|
||||
f.services.messages.publish('builtin.claude', 'meta.tab', {'tab': 'bogus'});
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(find.text('TODAY'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a team spawn auto-fronts the Team tab', (tester) async {
|
||||
await tester.pumpWidget(harness(f, sidebar(stats: stats)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -151,7 +151,21 @@ void main() {
|
||||
});
|
||||
|
||||
test('owned beats everything', () {
|
||||
for (final t in ['/clear', '/resume', '/fork', '/model opus', '/effort high']) {
|
||||
for (final t in [
|
||||
'/clear',
|
||||
'/resume',
|
||||
'/fork',
|
||||
'/model opus',
|
||||
'/effort high',
|
||||
'/permissions plan',
|
||||
'/status',
|
||||
'/config',
|
||||
'/mcp',
|
||||
'/agents',
|
||||
'/hooks',
|
||||
'/memory',
|
||||
'/help',
|
||||
]) {
|
||||
expect(routeSlashCommand(t, advertised: advertised), SlashRoute.owned, reason: t);
|
||||
}
|
||||
});
|
||||
@@ -163,13 +177,14 @@ void main() {
|
||||
});
|
||||
|
||||
test('a known TUI-only builtin routes unavailable', () {
|
||||
for (final t in ['/status', '/permissions', '/doctor', '/login']) {
|
||||
for (final t in ['/cost', '/doctor', '/login', '/rewind', '/output-style']) {
|
||||
expect(routeSlashCommand(t, advertised: advertised), SlashRoute.unavailable, reason: t);
|
||||
}
|
||||
});
|
||||
|
||||
test('an advertised name shadows the TUI-only catalog (a skill named like a builtin forwards)', () {
|
||||
expect(routeSlashCommand('/status', advertised: ['status']), SlashRoute.forward);
|
||||
// 'cost' is in the catalog but not owned — advertising it wins.
|
||||
expect(routeSlashCommand('/cost', advertised: ['cost']), SlashRoute.forward);
|
||||
});
|
||||
|
||||
test('an unknown token forwards (stays literal text downstream)', () {
|
||||
@@ -179,8 +194,8 @@ void main() {
|
||||
|
||||
group('tuiOnlyNotice (T-411)', () {
|
||||
test('carries the clide-native pointer when the catalog has one', () {
|
||||
final n = tuiOnlyNotice('status');
|
||||
expect(n, contains('/status is a Claude Code TUI command'));
|
||||
final n = tuiOnlyNotice('cost');
|
||||
expect(n, contains('/cost is a Claude Code TUI command'));
|
||||
expect(n, contains('Activity tab'));
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user