diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index b916e66d..1a193903 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -5,3 +5,4 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, 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-198', 'status', 'backlog', 'done', NULL, '2026-06-01 11:31:39', '2026-06-01 11:31:39', '2026-06-01 11:31:39', NULL, '5abb12dc3c3b72a4bccf1760204fe1ae', 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-199', 'status', 'backlog', 'in_progress', NULL, '2026-06-01 11:38:40', '2026-06-01 11:38:40', '2026-06-01 11:38:40', NULL, '44bad6e0417988025dae8606879de2a4', 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-199', 'status', 'in_progress', 'done', NULL, '2026-06-01 12:35:04', '2026-06-01 12:35:04', '2026-06-01 12:35:04', NULL, '56c14e217540c5789e70317aa8b6461f', 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-200', 'status', 'backlog', 'done', NULL, '2026-06-01 14:04:24', '2026-06-01 14:04:24', '2026-06-01 14:04:24', NULL, 'e77f8d64e4f9723af700dd46f962dea6', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index b5f114cf..09649461 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -4,3 +4,4 @@ INSERT INTO tickets (id, type, parent_id, title, description, status, priority, 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-196', 'bug', 'T-186', 'Decision reader: first click switches tab but doesn''t load (lost on tab mount)', 'Clicking a D-record activates the decisions.detail context-panel tab but does not load the record; only the SECOND click (any record) works. Root cause: DecisionDetailView subscribes to the ''selection'' MessageBus channel in didChangeDependencies, which runs only AFTER the extension''s activateTab mounts the view. The bus is broadcast (no replay), so the ''selection'' message that triggered the reveal is already gone — the view subscribes too late and misses it. The markdown extension already solved this: activateTab, then addPostFrameCallback -> re-publish on a separate ''load'' channel the viewer subscribes to, so the load fires after the viewer mounts+subscribes. Fix: mirror that — DecisionDetailView subscribes to ''load''; DecisionsExtension forwards ''selection'' -> post-frame ''load'' after activateTab.', 'done', 'high', NULL, NULL, NULL, '2026-06-01 08:58:10', '2026-06-01 10:45:14', NULL, 'c87a8726edd727831291bd1055e39ae1', 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-198', 'task', 'T-186', 'Reader pin UX: pin icon, toggle, left-toggle/right-navigate', 'Polish on the reader action bar (follows T-190). (1) push-pin glyph instead of the chain/link icon; (2) the pin button TOGGLES pinned state (tap to pin current, tap again to unpin) rather than set/replace; (3) layout split — pin/unpin toggle on the LEFT, jump-to-pin moved into the navigator (back/forward) on the right. Added ReaderNav.togglePin(), PhosphorIcons.pushPin (0xe3e2) + arrowUUpLeft (0xe08a), and an active state on the action button.', 'done', 'low', NULL, NULL, NULL, '2026-06-01 11:11:32', '2026-06-01 11:31:39', NULL, 'd2392d2e56ffc552c8f4f8993350d939', 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-199', 'task', 'T-186', 'Tickets detail: adopt retained ReaderNav (back/forward + pin) + static tab', 'Bring the tickets detail reader in line with markdown/decisions (T-196/D-81). Adopt the retained ReaderNav(''builtin.tickets'', dataKey ''id''): the nav records selections + emits ''load''; the controller loads on ''load'' (not ''selection''); the view grabs nav.current on mount and wraps in ClidePaneChrome with a ReaderActionBar (pin toggle left; back/forward + jump-to-pin right; NO edit pencil since tickets are pql records, not files). Also drop the per-click uncontribute/contribute churn in TicketsExtension (the T-188 anti-pattern) — use the static tickets.detail tab + reveal on selection. TicketDetailController loses its panels dependency (reveal moves to the extension).', 'done', 'medium', NULL, NULL, NULL, '2026-06-01 11:38:32', '2026-06-01 12:35:04', NULL, '82fe1aefd26d3f53872cc6983d1ae52d', 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-200', 'bug', NULL, 'Sidebar icon rail overflows (RenderFlex) when tab count exceeds width', 'Adding the Search sidebar tab (T-52) pushed the bottom icon rail past its width: ''A RenderFlex overflowed by 54 pixels'' from clide_icon_rail.dart — a fixed Row(center, max) with one _RailButton per tab. Fix: ClideIconRail centers when the icons fit and scrolls horizontally when they don''t (LayoutBuilder + SingleChildScrollView + ConstrainedBox(minWidth: maxWidth)). Robust to any tab count. (Related: T-201 will reduce tab count by merging the pql + find-in-files search surfaces.)', 'done', 'high', NULL, NULL, NULL, '2026-06-01 14:04:00', '2026-06-01 14:04:24', NULL, '59e5ac635342f5994afcc76c0fb3cd1e', 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/CHANGELOG.md b/CHANGELOG.md index 83304e7e..92be3e52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,8 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Fixed +- The sidebar icon rail no longer overflows when there are more tabs than fit: + it centers the icons when they fit and scrolls horizontally otherwise. (T-200) - The editor pane now opens over the Claude pane when a file is opened — the reader's edit pencil, a file-tree click, or a decision's edit all reveal the editor tab now (it was contributed but never activated). (T-197) diff --git a/lib/widgets/src/clide_icon_rail.dart b/lib/widgets/src/clide_icon_rail.dart index 74b4e452..f0170a38 100644 --- a/lib/widgets/src/clide_icon_rail.dart +++ b/lib/widgets/src/clide_icon_rail.dart @@ -29,19 +29,31 @@ class ClideIconRail extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - for (final item in items) - _RailButton( - item: item, - active: item.id == activeId, - onTap: () => onSelect(item.id), + // Center the icons when they fit; scroll horizontally when there are + // more tabs than the rail is wide. The ConstrainedBox minWidth keeps + // them centered while there's room but doesn't cap growth, so the + // ScrollView takes over instead of the Row overflowing. + return LayoutBuilder( + builder: (context, constraints) { + final minWidth = constraints.maxWidth.isFinite ? constraints.maxWidth : 0.0; + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: minWidth), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (final item in items) + _RailButton( + item: item, + active: item.id == activeId, + onTap: () => onSelect(item.id), + ), + ], ), - ], - ), + ), + ); + }, ); } } diff --git a/test/widgets/zero_coverage_widgets_test.dart b/test/widgets/zero_coverage_widgets_test.dart index 743a538e..404309fe 100644 --- a/test/widgets/zero_coverage_widgets_test.dart +++ b/test/widgets/zero_coverage_widgets_test.dart @@ -349,6 +349,28 @@ void main() { await tester.tap(find.bySemanticsLabel('B')); expect(selected, 'b'); }); + + testWidgets('scrolls instead of overflowing when items exceed the width', (tester) async { + await tester.pumpWidget(harness( + f, + SizedBox( + width: 120, + height: 30, + child: ClideIconRail( + items: [ + for (var i = 0; i < 10; i++) ClideIconRailItem(id: '$i', icon: PhosphorIcons.folder, tooltip: 'Tab $i'), + ], + activeId: '0', + onSelect: (_) {}, + ), + ), + )); + await tester.pumpAndSettle(); + // No RenderFlex overflow (would surface as a thrown FlutterError), + // and the rail is horizontally scrollable. + expect(tester.takeException(), isNull); + expect(find.byType(SingleChildScrollView), findsWidgets); + }); }); group('ClideSpine', () {