tickets panel: parent/child as a tree, bold subject ticket (T-281)

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 13:49:36 +02:00
co-authored by Claude Opus 4.8
parent 67081709ec
commit a62003a833
5 changed files with 65 additions and 5 deletions
@@ -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; 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-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;
+30
View File
@@ -2053,3 +2053,33 @@ Requirements:
- ui-design tokens for the guide lines + bold/muted weights; no hardcoded hex. - 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); 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 parentchild 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 parentchild 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);
+3
View File
@@ -144,6 +144,9 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
### Changed ### 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 - The in-flight **turn indicator** ("Pondering…") now renders in Claude's
coral-orange brand accent instead of muted grey. (T-273) coral-orange brand accent instead of muted grey. (T-273)
- A **sub-agent's prose and thinking** are now attributed to the **`agent`** (a - A **sub-agent's prose and thinking** are now attributed to the **`agent`** (a
+18 -5
View File
@@ -251,8 +251,25 @@ class _TicketCard extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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( Row(
children: [ children: [
if (entry.parentId != null) ClideText('', fontSize: clideFontSmall, color: tokens.globalTextMuted),
ClideTooltip( ClideTooltip(
message: entry.type ?? 'task', message: entry.type ?? 'task',
child: Container( child: Container(
@@ -262,11 +279,7 @@ class _TicketCard extends StatelessWidget {
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
ClideText(entry.id, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), ClideText(entry.id, fontSize: clideFontSmall, color: tokens.globalForeground, fontFamily: clideMonoFamily, fontWeight: FontWeight.w600),
if (entry.parentId != null) ...[
ClideText('', fontSize: clideFontSmall, color: tokens.globalTextMuted),
ClideText(entry.parentId!, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
],
], ],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
@@ -95,6 +95,18 @@ void main() {
expect(selections.single.data['id'], 'T-1'); 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 = <Message>[];
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 { testWidgets('empty list shows the placeholder', (tester) async {
f.ipc.stub('pql.tickets.list', (_) async => _list(const [])); f.ipc.stub('pql.tickets.list', (_) async => _list(const []));
await pumpView(tester); await pumpView(tester);