diff --git a/CHANGELOG.md b/CHANGELOG.md index df94c62b..562d3d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Fixed +- The composer slash typeahead now lists clide-owned commands — `/resume` and + `/fork` (and `/clear`) surface even though the CLI probe doesn't advertise + them, unioned onto whatever command source the composer uses. (T-162) - Clicking a decision opens it in the decision reader again — the decisions extension no longer tears down and re-contributes its panel tab on every selection; it activates a static tab and reveals the panel like the ticket diff --git a/lib/builtin/claude/src/claude_composer.dart b/lib/builtin/claude/src/claude_composer.dart index 56a09e4c..db4ec061 100644 --- a/lib/builtin/claude/src/claude_composer.dart +++ b/lib/builtin/claude/src/claude_composer.dart @@ -116,7 +116,14 @@ class _ClaudeComposerState extends State { if (!_focus.hasFocus) _closeTypeahead(); } - Iterable _commands() => (widget.slashCommandsResolver ?? () => activeClaudeConfig?.slashCommands ?? const [])(); + Iterable _commands() { + // Always union the clide-owned commands (/clear, /resume, /fork) onto the + // command source — whether that's a caller-supplied resolver or the default + // ClaudeConfig probe — since the CLI probe never advertises them (T-162). + // The Set literal de-dupes (e.g. 'clear' present in both). + final base = widget.slashCommandsResolver?.call() ?? activeClaudeConfig?.slashCommands ?? const []; + return {...base, ...kClideOwnedCommands}; + } /// Recompute the active slash query + suggestions from the current text and /// caret, opening/updating/closing the typeahead overlay accordingly. diff --git a/test/builtin/claude/claude_composer_test.dart b/test/builtin/claude/claude_composer_test.dart index a9213c66..27fb29bd 100644 --- a/test/builtin/claude/claude_composer_test.dart +++ b/test/builtin/claude/claude_composer_test.dart @@ -5,6 +5,7 @@ library; import 'package:clide/builtin/claude/src/claude_composer.dart'; import 'package:clide/builtin/claude/src/clipboard_paste.dart'; +import 'package:clide/builtin/claude/src/slash_commands.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -292,5 +293,45 @@ void main() { )); expect(find.text('Stop ⎋'), findsNothing); }); + + // T-162: clide-owned commands surface in typeahead even when absent from the + // CLI probe (slashCommandsResolver). + testWidgets('clide-owned /resume surfaces even when absent from the probe list', (tester) async { + // The probe list (CLI-sourced) only has 'model' — no 'resume' or 'fork'. + await pumpWithCommands(tester, ['model']); + await tester.enterText(find.byType(EditableText), '/res'); + await tester.pump(); + + // /resume must appear (sourced from kClideOwnedCommands). + expect(find.text('/resume'), findsOneWidget); + // /model must not appear (doesn't match '/res'). + expect(find.text('/model'), findsNothing); + }); + + testWidgets('clide-owned /clear surfaces without duplicate when also in probe', (tester) async { + // 'clear' is in both the probe list AND kClideOwnedCommands. + await pumpWithCommands(tester, ['clear', 'model']); + await tester.enterText(find.byType(EditableText), '/cl'); + await tester.pump(); + + // /clear must appear exactly once (filterSlashCommands de-dupes via seen set). + expect(find.text('/clear'), findsOneWidget); + }); + + testWidgets('clide-owned commands are reachable via the default resolver', (tester) async { + // No slashCommandsResolver → default path; kClideOwnedCommands must be included. + final submitted = []; + await tester.pumpWidget(harness( + f, + ClaudeComposer(onSubmit: submitted.add), + )); + await tester.enterText(find.byType(EditableText), '/fo'); + await tester.pump(); + + // /fork is a kClideOwnedCommands member; it must appear without a probe. + expect(find.text('/fork'), findsOneWidget); + // Sanity: kClideOwnedCommands is the source (not a coincidence). + expect(kClideOwnedCommands, contains('fork')); + }); }); }