From a62003a8333166ee39fe10a6e4f963e576c363ea Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Mon, 8 Jun 2026 13:49:36 +0200 Subject: [PATCH] tickets panel: parent/child as a tree, bold subject ticket (T-281) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the ambiguous inline "T-1 ← T-9" id row on ticket cards with a small tree: the parent renders as a muted, clickable breadcrumb above, and the card's own ticket sits below it under a "└" connector in bold — so it's clear which id is the subject and which is its parent. Tapping the parent selects it. Co-Authored-By: Claude Opus 4.8 (1M context) --- .pql/changelog/ticket_history/2026-06.sql | 2 ++ .pql/changelog/tickets/2026-06.sql | 30 +++++++++++++++++++++ CHANGELOG.md | 3 +++ lib/builtin/tickets/src/tickets_view.dart | 23 ++++++++++++---- test/builtin/tickets/tickets_view_test.dart | 12 +++++++++ 5 files changed, 65 insertions(+), 5 deletions(-) diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 2b94c67f..76e923b2 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -832,3 +832,5 @@ Bisected (each a 45s-timeout repro, all on this box): Quarantined with `skip:` so the suite/gate stays green. Real fix: find the leaked native async resource (likely a Process/Isolate/FakeDaemonClient port reachable from the open-folder command or app boot under the test harness) and ensure it''s drained/cancelled before teardown — or drive the "no git repo" assertion without booting the resource. Then remove the skip.', NULL, '2026-06-08 11:29:33', '2026-06-08 11:29:33', '2026-06-08 11:29:33', NULL, '3a03d00854f80ca2f8320875fe37e558', 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-273', 'status', 'ready', 'done', NULL, '2026-06-08 11:44:27', '2026-06-08 11:44:27', '2026-06-08 11:44:27', NULL, 'd4dc7a1144ebf85630815e55f59ff99b', 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-281', 'status', 'backlog', 'ready', NULL, '2026-06-08 11:47:56', '2026-06-08 11:47:56', '2026-06-08 11:47:56', NULL, '46cc72de9659468d07dc9eed045367cb', 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-281', 'status', 'ready', 'done', NULL, '2026-06-08 11:49:36', '2026-06-08 11:49:36', '2026-06-08 11:49:36', NULL, 'e302cadedc3e13cf232001de156ef1ec', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 01dd7f64..c7c32a45 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -2053,3 +2053,33 @@ Requirements: - ui-design tokens for the guide lines + bold/muted weights; no hardcoded hex. Scope: tickets panel rendering — lib/builtin/tickets/. See attached screenshot for the current ''T-273 ← T-276'' card style being replaced.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-08 11:46:11', '2026-06-08 11:46:11', NULL, 'bbb16c80956550062c0f2e153ee46a94', 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-281', 'task', 'T-276', 'Clearer parent/child nesting in the tickets panel (tree layout, bold focused ticket)', 'The tickets panel shows a ticket''s parent inline as ''T-273 ← T-276'', which reads ambiguously (which one am I looking at? what''s the direction of the arrow?). + +Replace it with a small tree/indented layout that makes the hierarchy obvious, e.g.: + + T-276 + └ T-273 + +Requirements: +- Parent on top, the current/child ticket indented below with a tree connector (└ / ├ style box-drawing or an equivalent indent + guide). +- The ticket the user is actually looking at (the focused/opened one) is bold; ancestors are normal weight (and ideally muted/secondary). +- Support more than one level if a chain exists (initiative → epic → story → task), indenting each level; at minimum handle the common 2-level parent→child case. +- Make each id in the chain clickable to navigate to that ticket (ties into the reader/board navigation; relates to T-279 cross-ref linking). +- ui-design tokens for the guide lines + bold/muted weights; no hardcoded hex. + +Scope: tickets panel rendering — lib/builtin/tickets/. See attached screenshot for the current ''T-273 ← T-276'' card style being replaced.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-08 11:46:11', '2026-06-08 11:47:56', NULL, '8a9b20a4419a7b15ad14a62e0af8a844', 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-281', 'task', 'T-276', 'Clearer parent/child nesting in the tickets panel (tree layout, bold focused ticket)', 'The tickets panel shows a ticket''s parent inline as ''T-273 ← T-276'', which reads ambiguously (which one am I looking at? what''s the direction of the arrow?). + +Replace it with a small tree/indented layout that makes the hierarchy obvious, e.g.: + + T-276 + └ T-273 + +Requirements: +- Parent on top, the current/child ticket indented below with a tree connector (└ / ├ style box-drawing or an equivalent indent + guide). +- The ticket the user is actually looking at (the focused/opened one) is bold; ancestors are normal weight (and ideally muted/secondary). +- Support more than one level if a chain exists (initiative → epic → story → task), indenting each level; at minimum handle the common 2-level parent→child case. +- Make each id in the chain clickable to navigate to that ticket (ties into the reader/board navigation; relates to T-279 cross-ref linking). +- ui-design tokens for the guide lines + bold/muted weights; no hardcoded hex. + +Scope: tickets panel rendering — lib/builtin/tickets/. See attached screenshot for the current ''T-273 ← T-276'' card style being replaced.', 'done', 'medium', NULL, NULL, NULL, '2026-06-08 11:46:11', '2026-06-08 11:49:36', NULL, '457afd72feff0947506795a2da286447', 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 ca0ceaa4..59c8a5cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -144,6 +144,9 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Changed +- Ticket cards now show parentage as a small **tree** — the parent as a muted, + clickable breadcrumb above and the card's own ticket **bold** under a `└` + connector — instead of the ambiguous inline `T-1 ← T-9` arrow. (T-281) - The in-flight **turn indicator** ("Pondering…") now renders in Claude's coral-orange brand accent instead of muted grey. (T-273) - A **sub-agent's prose and thinking** are now attributed to the **`agent`** (a diff --git a/lib/builtin/tickets/src/tickets_view.dart b/lib/builtin/tickets/src/tickets_view.dart index e122c884..86d5db91 100644 --- a/lib/builtin/tickets/src/tickets_view.dart +++ b/lib/builtin/tickets/src/tickets_view.dart @@ -251,8 +251,25 @@ class _TicketCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Parent shown as a muted breadcrumb above; the card's own ticket + // sits below it under a tree connector and in bold, so it's clear + // which id is the subject and which is its parent (T-281). + if (entry.parentId != null) + ClideTappable( + onTap: () => ClideKernel.of(context).messages.publish('builtin.tickets', 'selection', {'id': entry.parentId}), + builder: (ctx, hovered, _) => Padding( + padding: const EdgeInsets.only(bottom: 1), + child: ClideText( + entry.parentId!, + fontSize: clideFontSmall, + color: hovered ? tokens.globalForeground : tokens.globalTextMuted, + fontFamily: clideMonoFamily, + ), + ), + ), Row( children: [ + if (entry.parentId != null) ClideText('└ ', fontSize: clideFontSmall, color: tokens.globalTextMuted), ClideTooltip( message: entry.type ?? 'task', child: Container( @@ -262,11 +279,7 @@ class _TicketCard extends StatelessWidget { ), ), const SizedBox(width: 6), - ClideText(entry.id, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), - if (entry.parentId != null) ...[ - ClideText(' ← ', fontSize: clideFontSmall, color: tokens.globalTextMuted), - ClideText(entry.parentId!, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), - ], + ClideText(entry.id, fontSize: clideFontSmall, color: tokens.globalForeground, fontFamily: clideMonoFamily, fontWeight: FontWeight.w600), ], ), const SizedBox(height: 4), diff --git a/test/builtin/tickets/tickets_view_test.dart b/test/builtin/tickets/tickets_view_test.dart index f2663e1a..e6161f0b 100644 --- a/test/builtin/tickets/tickets_view_test.dart +++ b/test/builtin/tickets/tickets_view_test.dart @@ -95,6 +95,18 @@ void main() { expect(selections.single.data['id'], 'T-1'); }); + testWidgets('tapping the parent breadcrumb selects the parent (T-281)', (tester) async { + f.ipc.stub('pql.tickets.list', (_) async => _list([_t('T-1', 'Child', 'backlog', parentId: 'T-9')])); + final selections = []; + final sub = f.services.messages.subscribe(publisher: 'builtin.tickets', channel: 'selection').listen(selections.add); + addTearDown(sub.cancel); + await pumpView(tester); + + await tester.tap(find.text('T-9')); // the muted parent breadcrumb + await pumpAsync(tester); + expect(selections.single.data['id'], 'T-9'); + }); + testWidgets('empty list shows the placeholder', (tester) async { f.ipc.stub('pql.tickets.list', (_) async => _list(const [])); await pumpView(tester);