diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 606fb918..33f5548a 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -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; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index ab44a2be..9fee573a 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -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); diff --git a/test/builtin/claude/claude_composer_test.dart b/test/builtin/claude/claude_composer_test.dart index 957ba342..129281d7 100644 --- a/test/builtin/claude/claude_composer_test.dart +++ b/test/builtin/claude/claude_composer_test.dart @@ -191,6 +191,28 @@ void main() { expect(tester.widget(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(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'); diff --git a/test/builtin/claude/slash_commands_test.dart b/test/builtin/claude/slash_commands_test.dart index ee6eaebf..614178c5 100644 --- a/test/builtin/claude/slash_commands_test.dart +++ b/test/builtin/claude/slash_commands_test.dart @@ -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', () {