lock in slash typeahead hyphen + Tab-accept behaviour (T-278)

Both T-278 issues — typing '-' breaking the list, and flaky Tab-to-complete —
were resolved by the D-88 ClideTypeahead migration (T-286): the popover now
narrows live as you type (ValueNotifier-bridged suggestions) and the field keeps
focus (captureFocus:false), so '-' filters normally and Tab reliably accepts the
highlight. Add regression coverage: hyphenated queries in slash_commands_test and
a composer widget test that types through a hyphen then Tab-completes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 17:03:16 +02:00
co-authored by Claude Opus 4.8
parent b10ff321a3
commit d8b9a4137e
4 changed files with 46 additions and 0 deletions
@@ -950,3 +950,4 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by,
DONE: The editor honours .editorconfig end-to-end. New source-agnostic EditorSettings model (editor_settings.dart) + composition seam (editor_settings_resolver.dart); .editorconfig demoted to a source (editorconfig.dart, own INI parser + glob matcher, root/nearest-wins precedence, no deps). Registry resolves on open and re-resolves open buffers when a .editorconfig is saved in-app (editor.settings-changed; a save hook, not an fs-watch). Editor: Tab/Shift+Tab indent + max_line_length ruler; save applies end_of_line/trim_trailing_whitespace/insert_final_newline. Follow-ups: T-290 (edit from settings panel), T-291 (external fs-watch). 100% coverage on new model/resolver/parser.', NULL, '2026-06-09 10:46:41', '2026-06-09 10:46:41', '2026-06-09 10:46:41', NULL, '96304f51da31f3ea356faa207ea1873e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-29', 'status', 'in_progress', 'done', NULL, '2026-06-09 10:46:41', '2026-06-09 10:46:41', '2026-06-09 10:46:41', NULL, '9419d87a1b9d0cbb938399638d6a1453', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-278', 'status', 'ready', 'in_progress', NULL, '2026-06-09 14:59:45', '2026-06-09 14:59:45', '2026-06-09 14:59:45', NULL, '57787b04f8886096911590b3e6357d90', 1) ON CONFLICT(hash) DO NOTHING;
+9
View File
@@ -2416,3 +2416,12 @@ INSERT INTO tickets (id, type, parent_id, title, description, status, priority,
Root cause: these tests spawn a real /bin/sh via NativePty and wait on the reader ISOLATE to deliver bytes. Under heavy CPU contention the reader isolate is starved and the 20s ioTimeout (test/helpers/timeouts.dart) elapses before the shell''s first byte / echo response is delivered. Already hardened once (T-108 swapped wall-clock sleeps for event-driven waits; first-byte Completer + 25ms-poll _waitForBuffer), but the isolate-scheduling assumption still breaks under load.
These tests are tagged ''pty'' and are meant to run in the gate''s serial pass. Fix options: (a) ensure the parallel coverage pass EXCLUDES tag:pty (run pty only in the serial pass) so they never compete for isolate scheduling verify ci/test.sh / dart_test.yaml tag routing; (b) harden against isolate starvation (e.g. give the reader isolate priority, or assert on delivery via a more robust signal); (c) document that ''flutter test --coverage'' over the whole tree is not a supported invocation (use make test + the gate). Prefer (a). Refs: T-108, T-96 (reader-isolate hangs), T-192 (gate parallel/serial passes), D-23 (test pyramid).', 'backlog', 'medium', NULL, NULL, 'D-23', '2026-06-09 14:57:32', '2026-06-09 14:57:32', NULL, 'aa9a060645d02824f2d6c5389b812c8b', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-278', 'bug', 'T-276', 'Slash-command typeahead breaks on ''-''; Tab-to-complete is flaky', 'Two issues in the Claude composer slash-command typeahead (T-152/T-153, lib/builtin/claude/src/slash_commands.dart + claude_composer.dart):
1. Typing a ''-'' character breaks the typeahead. Many command names contain hyphens (e.g. clear-context-style names, custom commands), but typing ''-'' appears to drop/empty the suggestion list or mis-parse the active query. Suspect activeSlashQuery''s token run or filterSlashCommands prefix matching not handling ''-'' (or the composer treating ''-'' as a boundary).
2. Tab to accept the highlighted suggestion responds flakily sometimes it completes, sometimes nothing happens. Suspect a focus/key-handling race between the composer''s key handler and the typeahead overlay, or Tab being consumed by focus traversal before the accept intent fires.
Repro: open the composer, type ''/'' then a command fragment; (a) include a ''-'' and watch the list; (b) arrow-select an item and press Tab repeatedly.
Acceptance: ''-'' is treated as a normal command-name character (suggestions keep filtering through hyphens); Tab reliably completes the highlighted suggestion every time (insert via completeSlash). Add/extend unit tests in slash_commands_test.dart for hyphenated queries and a composer widget test for Tab-accept.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-08 09:20:00', '2026-06-09 14:59:45', NULL, 'f0cae2f4ba80ad98e2115d4d3cf1a11f', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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);
@@ -191,6 +191,28 @@ void main() {
expect(tester.widget<EditableText>(find.byType(EditableText)).controller.text, '/clear ');
});
testWidgets('a hyphenated command keeps filtering through the dash and Tab completes it (T-278)', (tester) async {
await pumpWithCommands(tester, ['add-dir', 'add-context', 'model']);
await tester.enterText(find.byType(EditableText), '/add-');
await tester.pump();
await tester.pump(); // ClideTypeahead inserts the popover post-frame
expect(find.text('/add-dir'), findsOneWidget);
expect(find.text('/add-context'), findsOneWidget);
expect(find.text('/model'), findsNothing);
// Typing further through the hyphen narrows the list (it does not empty it).
await tester.enterText(find.byType(EditableText), '/add-d');
await tester.pump();
await tester.pump();
expect(find.text('/add-dir'), findsOneWidget);
expect(find.text('/add-context'), findsNothing);
// Tab reliably accepts the highlighted suggestion.
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(tester.widget<EditableText>(find.byType(EditableText)).controller.text, '/add-dir ');
});
testWidgets('Escape dismisses the typeahead', (tester) async {
await pumpWithCommands(tester, ['model']);
await tester.enterText(find.byType(EditableText), '/mo');
@@ -69,6 +69,12 @@ void main() {
// Cursor sits right after "/mo" inside "/model".
expect(activeSlashQuery('/model', 3), const SlashQuery(start: 0, query: 'mo'));
});
test('hyphens are part of the command token, not a boundary (T-278)', () {
expect(activeSlashQuery('/add-dir', 8), const SlashQuery(start: 0, query: 'add-dir'));
expect(activeSlashQuery('/add-', 5), const SlashQuery(start: 0, query: 'add-'));
expect(activeSlashQuery('go /output-st', 13), const SlashQuery(start: 3, query: 'output-st'));
});
});
group('filterSlashCommands', () {
@@ -86,6 +92,14 @@ void main() {
test('de-duplicates', () {
expect(filterSlashCommands('p', ['pql', 'pql', 'plan']), ['plan', 'pql']);
});
test('keeps filtering through a hyphen in the query (T-278)', () {
const hyphenated = ['add-dir', 'add-context', 'agents', 'output-style'];
// Typing the '-' narrows rather than emptying the list.
expect(filterSlashCommands('add', hyphenated), ['add-context', 'add-dir']);
expect(filterSlashCommands('add-', hyphenated), ['add-context', 'add-dir']);
expect(filterSlashCommands('add-d', hyphenated), ['add-dir']);
});
});
group('completeSlash', () {