diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index ebb67c00..24fcd052 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -3436,3 +3436,67 @@ SCOPE CLARIFICATION (2026-06-10, from user): Test additions: (a) two concurrent agents whose sidechain items interleave -> each agent''s run items land under its own card, none cross-attributed; (b) an unattributable sidechain item (no parent_tool_use_id, broken chain) is NOT swept into the nearest agent''s card; (c) regression: consecutive Bash/Read calls still form one Activity cluster.', NULL, '2026-06-10 15:28:52', '2026-06-10 15:28:52', '2026-06-10 15:28:52', NULL, '0352b24d88a4bf24ede628880fe9b502', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4FDREHRYRR7B9ER72KCQKC', 'status', 'backlog', 'in_progress', NULL, '2026-06-10 15:54:23', '2026-06-10 15:54:23', '2026-06-10 15:54:23', NULL, '8061a1d449acdb1e467239de0dce5d1d', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4FDREHRYRR7B9ER72KCQKC', 'status', 'in_progress', 'in_progress', NULL, '2026-06-10 16:02:55', '2026-06-10 16:02:55', '2026-06-10 16:02:55', NULL, '9c00a4d5fefdc3cf8f227b040bc78acb', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4FDREHRYRR7B9ER72KCQKC', 'description', 'Add a row of type-filter toggle chips at the top of the tickets panel, directly below the "Filter tickets…" box (no section header — the chips read on their own). One chip per ticket type the user thinks in: Bug, Ticket, Epic, Initiative. All four ON by default. + +## Behaviour +- Single-click a chip → toggle that type in/out of the list. +- Double-click a chip → isolate (solo) that type: turns it ON and all others OFF. Double-click the same chip again → restore all to ON. This is the chart-legend solo pattern (Plotly/Tableau/Grafana) — learnable and fully reversible. +- Last-off resets to all-on: disabling the final remaining type snaps all chips back ON. An empty type filter means "no filter", so the list is never mysteriously blank. +- Tooltip per chip: "Click to toggle · double-click to isolate". + +## Type mapping (pql → chip) +pql ticket types are initiative, epic, story, task, bug (see lib/builtin/tickets/src/ticket_colors.dart). The four chips map as: +- Bug → bug +- Ticket → story + task (leaf work items) +- Epic → epic +- Initiative → initiative + +Each chip carries its type-colored dot + border using TicketTypeColors (bug #E87D7D, story/task green/grey, epic #78A0F8, initiative #C792EA). + +## Filtering +- Type filter is ANDed with the existing text filter in _TicketsViewState (lib/builtin/tickets/src/tickets_view.dart): a ticket shows only if its type is enabled AND it matches the text filter. +- When the type filter hides all items in a status section, that section collapses out (same as text-filter behaviour today). + +## Implementation notes +- Active-chip visual: filled tint + type-colored border (active) vs muted/no border (inactive) — reuse the _Toggle pattern from lib/builtin/search/src/search_panel_view.dart and ClideTappable. +- Persist nothing across sessions for v1 (always all-on on load); revisit if requested. +- a11y: Semantics(button, toggled) per chip, mirroring the search-panel toggle. + +## Wireframe +docs/design/wireframes/tickets/ticket-type-filters.json (+ .png export)', 'Add a row of type-filter toggle chips at the top of the tickets panel, directly below the "Filter tickets…" box (no section header — the chips read on their own). One chip per ticket type the user thinks in: Bug, Ticket, Epic, Initiative. All four ON by default. + +## Behaviour +- Single-click a chip → toggle that type in/out of the list. +- Double-click a chip → isolate (solo) that type: turns it ON and all others OFF. Double-click the same chip again → restore all to ON. This is the chart-legend solo pattern (Plotly/Tableau/Grafana) — learnable and fully reversible. +- Last-off resets to all-on: disabling the final remaining type snaps all chips back ON. An empty type filter means "no filter", so the list is never mysteriously blank. +- Tooltip per chip: "Click to toggle · double-click to isolate". + +## Type mapping (pql → chip) +pql ticket types are initiative, epic, story, task, bug (see lib/builtin/tickets/src/ticket_colors.dart). The four chips map as: +- Bug → bug +- Ticket → story + task (leaf work items) +- Epic → epic +- Initiative → initiative + +Each chip carries its type-colored dot + border using TicketTypeColors (bug #E87D7D, story/task green/grey, epic #78A0F8, initiative #C792EA). + +## Filtering +- Type filter is ANDed with the existing text filter in _TicketsViewState (lib/builtin/tickets/src/tickets_view.dart): a ticket shows only if its type is enabled AND it matches the text filter. +- When the type filter hides all items in a status section, that section collapses out (same as text-filter behaviour today). + +## Implementation notes +- Active-chip visual: filled tint + type-colored border (active) vs muted/no border (inactive) — reuse the _Toggle pattern from lib/builtin/search/src/search_panel_view.dart and ClideTappable. +- Persist nothing across sessions for v1 (always all-on on load); revisit if requested. +- a11y: Semantics(button, toggled) per chip, mirroring the search-panel toggle. + +## Wireframe +docs/design/wireframes/tickets/ticket-type-filters.json (+ .png export) + +Design revision (2026-06-10, supersedes the chip set/order above): FIVE chips, one per pql type — no story+task grouping. Ordered LARGE→SMALL left to right: Initiative, Epic, Story, Task, Bug. Each maps 1:1 to its pql type (initiative/epic/story/task/bug) with its TicketTypeColors dot+border (initiative #C792EA, epic #78A0F8, story #7DD3A8, task #9AA0AA grey, bug #E87D7D). All five ON by default. Toggle/solo(double-click)/last-off-reset behaviour unchanged. Wireframe updated + approved.', NULL, '2026-06-10 16:06:25', '2026-06-10 16:06:25', '2026-06-10 16:06:25', NULL, '26b5b4d5cf2a48fe6aeacd7b83968848', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5TWC00GW0P3X02HZW', 'status', 'backlog', 'ready', NULL, '2026-06-10 16:09:21', '2026-06-10 16:09:21', '2026-06-10 16:09:21', NULL, 'c89b4ddcbdeea95483c53b1b76220842', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5TWC00GW0P3X02HZW', 'status', 'ready', 'in_progress', NULL, '2026-06-10 16:09:26', '2026-06-10 16:09:26', '2026-06-10 16:09:26', NULL, 'c36e6343d44f59ff4af0eea3178b5a94', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5TWC00GW0P3X02HZW', 'status', 'in_progress', 'backlog', NULL, '2026-06-10 16:09:57', '2026-06-10 16:09:57', '2026-06-10 16:09:57', NULL, 'be22c7ff64f106d600b4a5bd70ced1d2', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4J5E6W983P1S7BE0FDPSMM', 'status', 'backlog', 'ready', NULL, '2026-06-10 16:10:20', '2026-06-10 16:10:20', '2026-06-10 16:10:20', NULL, '6f4b3ce9e2167200ebdde0c1511d4a91', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4J5E6W983P1S7BE0FDPSMM', 'status', 'ready', 'in_progress', NULL, '2026-06-10 16:10:22', '2026-06-10 16:10:22', '2026-06-10 16:10:22', NULL, 'cce7ce3f17ae087f753f4d6d072a702e', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4FDREHRYRR7B9ER72KCQKC', 'status', 'in_progress', 'done', NULL, '2026-06-10 16:12:15', '2026-06-10 16:12:15', '2026-06-10 16:12:15', NULL, '4cb2daa831a115888a9456461bf53bdf', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 3bef95e0..66fe8b9a 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -2959,3 +2959,124 @@ Each chip carries its type-colored dot + border using TicketTypeColors (bug #E87 ## Wireframe docs/design/wireframes/tickets/ticket-type-filters.json (+ .png export)', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 15:54:09', '2026-06-10 15:54:23', NULL, 'f5614424c00196aabab868db75483bf2', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4FDREHRYRR7B9ER72KCQKC', 'story', '06FB0TNQM5TWC00GW0P3X02HZW', 'Ticket panel: per-type filter chips (Bug / Ticket / Epic / Initiative)', 'Add a row of type-filter toggle chips at the top of the tickets panel, directly below the "Filter tickets…" box (no section header — the chips read on their own). One chip per ticket type the user thinks in: Bug, Ticket, Epic, Initiative. All four ON by default. + +## Behaviour +- Single-click a chip → toggle that type in/out of the list. +- Double-click a chip → isolate (solo) that type: turns it ON and all others OFF. Double-click the same chip again → restore all to ON. This is the chart-legend solo pattern (Plotly/Tableau/Grafana) — learnable and fully reversible. +- Last-off resets to all-on: disabling the final remaining type snaps all chips back ON. An empty type filter means "no filter", so the list is never mysteriously blank. +- Tooltip per chip: "Click to toggle · double-click to isolate". + +## Type mapping (pql → chip) +pql ticket types are initiative, epic, story, task, bug (see lib/builtin/tickets/src/ticket_colors.dart). The four chips map as: +- Bug → bug +- Ticket → story + task (leaf work items) +- Epic → epic +- Initiative → initiative + +Each chip carries its type-colored dot + border using TicketTypeColors (bug #E87D7D, story/task green/grey, epic #78A0F8, initiative #C792EA). + +## Filtering +- Type filter is ANDed with the existing text filter in _TicketsViewState (lib/builtin/tickets/src/tickets_view.dart): a ticket shows only if its type is enabled AND it matches the text filter. +- When the type filter hides all items in a status section, that section collapses out (same as text-filter behaviour today). + +## Implementation notes +- Active-chip visual: filled tint + type-colored border (active) vs muted/no border (inactive) — reuse the _Toggle pattern from lib/builtin/search/src/search_panel_view.dart and ClideTappable. +- Persist nothing across sessions for v1 (always all-on on load); revisit if requested. +- a11y: Semantics(button, toggled) per chip, mirroring the search-panel toggle. + +## Wireframe +docs/design/wireframes/tickets/ticket-type-filters.json (+ .png export)', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 15:54:09', '2026-06-10 16:02:55', NULL, '83c5d82ad411e2827733928cdb81c6e0', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4J5E6W983P1S7BE0FDPSMM', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Conversation card font sizes inconsistent: ClideCollapserCard vs ConversationCard', 'Two collapsible-card primitives sit adjacent in the Claude conversation stream but use different font-size tokens for the same visual roles, so labels/summaries are visibly 1-2px off between neighbouring cards. + +- ClideCollapserCard (lib/widgets/src/clide_collapser_card.dart) — the ''Activity'' / agent-run / edit-run cards: header label, collapsedSummary, and counter all use clideFontCaption (14). +- ConversationCard (lib/builtin/claude/src/conversation_card.dart) — tool-use / result / ''denied'' cards: header label uses clideFontSmall (12), collapsedSummary uses clideFontMeta (13). + +Result: an ''Activity Bash …'' collapser and a neighbouring ''Bash · denied'' tool card render their label + collapsed summary at different sizes in the same list. Spotted in a screenshot of three stacked cards (collapser Bash, denied tool result, collapser Read). + +Fix: pick one set of tokens for the shared header/label/summary role across both primitives (likely align ConversationCard''s label to clideFontCaption and standardise the collapsed-summary size), or extract a shared card-header style. Tokens: clideFontCaption=14, clideFontMeta=13, clideFontSmall=12 (lib/widgets/src/typography.dart).', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 16:06:08', '2026-06-10 16:06:08', NULL, 'c858a9b7c474697611c3ff8240c61a2d', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4FDREHRYRR7B9ER72KCQKC', 'story', '06FB0TNQM5TWC00GW0P3X02HZW', 'Ticket panel: per-type filter chips (Bug / Ticket / Epic / Initiative)', 'Add a row of type-filter toggle chips at the top of the tickets panel, directly below the "Filter tickets…" box (no section header — the chips read on their own). One chip per ticket type the user thinks in: Bug, Ticket, Epic, Initiative. All four ON by default. + +## Behaviour +- Single-click a chip → toggle that type in/out of the list. +- Double-click a chip → isolate (solo) that type: turns it ON and all others OFF. Double-click the same chip again → restore all to ON. This is the chart-legend solo pattern (Plotly/Tableau/Grafana) — learnable and fully reversible. +- Last-off resets to all-on: disabling the final remaining type snaps all chips back ON. An empty type filter means "no filter", so the list is never mysteriously blank. +- Tooltip per chip: "Click to toggle · double-click to isolate". + +## Type mapping (pql → chip) +pql ticket types are initiative, epic, story, task, bug (see lib/builtin/tickets/src/ticket_colors.dart). The four chips map as: +- Bug → bug +- Ticket → story + task (leaf work items) +- Epic → epic +- Initiative → initiative + +Each chip carries its type-colored dot + border using TicketTypeColors (bug #E87D7D, story/task green/grey, epic #78A0F8, initiative #C792EA). + +## Filtering +- Type filter is ANDed with the existing text filter in _TicketsViewState (lib/builtin/tickets/src/tickets_view.dart): a ticket shows only if its type is enabled AND it matches the text filter. +- When the type filter hides all items in a status section, that section collapses out (same as text-filter behaviour today). + +## Implementation notes +- Active-chip visual: filled tint + type-colored border (active) vs muted/no border (inactive) — reuse the _Toggle pattern from lib/builtin/search/src/search_panel_view.dart and ClideTappable. +- Persist nothing across sessions for v1 (always all-on on load); revisit if requested. +- a11y: Semantics(button, toggled) per chip, mirroring the search-panel toggle. + +## Wireframe +docs/design/wireframes/tickets/ticket-type-filters.json (+ .png export) + +Design revision (2026-06-10, supersedes the chip set/order above): FIVE chips, one per pql type — no story+task grouping. Ordered LARGE→SMALL left to right: Initiative, Epic, Story, Task, Bug. Each maps 1:1 to its pql type (initiative/epic/story/task/bug) with its TicketTypeColors dot+border (initiative #C792EA, epic #78A0F8, story #7DD3A8, task #9AA0AA grey, bug #E87D7D). All five ON by default. Toggle/solo(double-click)/last-off-reset behaviour unchanged. Wireframe updated + approved.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 15:54:09', '2026-06-10 16:06:25', NULL, '93554cd5afe5bd881b12bef2892e25b8', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5TWC00GW0P3X02HZW', 'epic', NULL, 'UI tweaks & fixes', 'Ongoing umbrella for small, standalone UI polish, cosmetic tweaks, and visual/interaction bug fixes that don''t belong to a feature epic — color/token corrections, control placement, status surfaces, micro-interactions, and the wireframes that frame them. Children are independently shippable; the epic stays open as a rolling home for this class of work. + +**PERMANENT — never close.** This is a standing rolling tracker for loose UI/UX work and bugs, not a deliverable epic. It stays open indefinitely; only its children are completed/closed. Do not mark T-276 done even when all current children are closed — new tweaks/fixes get filed here on an ongoing basis.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-08 07:46:23', '2026-06-10 16:09:21', NULL, 'd9e663378cc6deec9bc85f95f5552db8', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5TWC00GW0P3X02HZW', 'epic', NULL, 'UI tweaks & fixes', 'Ongoing umbrella for small, standalone UI polish, cosmetic tweaks, and visual/interaction bug fixes that don''t belong to a feature epic — color/token corrections, control placement, status surfaces, micro-interactions, and the wireframes that frame them. Children are independently shippable; the epic stays open as a rolling home for this class of work. + +**PERMANENT — never close.** This is a standing rolling tracker for loose UI/UX work and bugs, not a deliverable epic. It stays open indefinitely; only its children are completed/closed. Do not mark T-276 done even when all current children are closed — new tweaks/fixes get filed here on an ongoing basis.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-08 07:46:23', '2026-06-10 16:09:26', NULL, 'fc45344f2d71e9b71573100825c2bce7', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5TWC00GW0P3X02HZW', 'epic', NULL, 'UI tweaks & fixes', 'Ongoing umbrella for small, standalone UI polish, cosmetic tweaks, and visual/interaction bug fixes that don''t belong to a feature epic — color/token corrections, control placement, status surfaces, micro-interactions, and the wireframes that frame them. Children are independently shippable; the epic stays open as a rolling home for this class of work. + +**PERMANENT — never close.** This is a standing rolling tracker for loose UI/UX work and bugs, not a deliverable epic. It stays open indefinitely; only its children are completed/closed. Do not mark T-276 done even when all current children are closed — new tweaks/fixes get filed here on an ongoing basis.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-08 07:46:23', '2026-06-10 16:09:57', NULL, 'a1ab5ae76714ede6d6a473db9b5d8aa9', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4J5E6W983P1S7BE0FDPSMM', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Conversation card font sizes inconsistent: ClideCollapserCard vs ConversationCard', 'Two collapsible-card primitives sit adjacent in the Claude conversation stream but use different font-size tokens for the same visual roles, so labels/summaries are visibly 1-2px off between neighbouring cards. + +- ClideCollapserCard (lib/widgets/src/clide_collapser_card.dart) — the ''Activity'' / agent-run / edit-run cards: header label, collapsedSummary, and counter all use clideFontCaption (14). +- ConversationCard (lib/builtin/claude/src/conversation_card.dart) — tool-use / result / ''denied'' cards: header label uses clideFontSmall (12), collapsedSummary uses clideFontMeta (13). + +Result: an ''Activity Bash …'' collapser and a neighbouring ''Bash · denied'' tool card render their label + collapsed summary at different sizes in the same list. Spotted in a screenshot of three stacked cards (collapser Bash, denied tool result, collapser Read). + +Fix: pick one set of tokens for the shared header/label/summary role across both primitives (likely align ConversationCard''s label to clideFontCaption and standardise the collapsed-summary size), or extract a shared card-header style. Tokens: clideFontCaption=14, clideFontMeta=13, clideFontSmall=12 (lib/widgets/src/typography.dart).', 'ready', 'medium', NULL, NULL, NULL, '2026-06-10 16:06:08', '2026-06-10 16:10:20', NULL, '140190d844d98929ecc83e30cdcc0745', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4J5E6W983P1S7BE0FDPSMM', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Conversation card font sizes inconsistent: ClideCollapserCard vs ConversationCard', 'Two collapsible-card primitives sit adjacent in the Claude conversation stream but use different font-size tokens for the same visual roles, so labels/summaries are visibly 1-2px off between neighbouring cards. + +- ClideCollapserCard (lib/widgets/src/clide_collapser_card.dart) — the ''Activity'' / agent-run / edit-run cards: header label, collapsedSummary, and counter all use clideFontCaption (14). +- ConversationCard (lib/builtin/claude/src/conversation_card.dart) — tool-use / result / ''denied'' cards: header label uses clideFontSmall (12), collapsedSummary uses clideFontMeta (13). + +Result: an ''Activity Bash …'' collapser and a neighbouring ''Bash · denied'' tool card render their label + collapsed summary at different sizes in the same list. Spotted in a screenshot of three stacked cards (collapser Bash, denied tool result, collapser Read). + +Fix: pick one set of tokens for the shared header/label/summary role across both primitives (likely align ConversationCard''s label to clideFontCaption and standardise the collapsed-summary size), or extract a shared card-header style. Tokens: clideFontCaption=14, clideFontMeta=13, clideFontSmall=12 (lib/widgets/src/typography.dart).', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 16:06:08', '2026-06-10 16:10:22', NULL, 'f02a03592e970322a82bf1be7002b7fa', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4FDREHRYRR7B9ER72KCQKC', 'story', '06FB0TNQM5TWC00GW0P3X02HZW', 'Ticket panel: per-type filter chips (Bug / Ticket / Epic / Initiative)', 'Add a row of type-filter toggle chips at the top of the tickets panel, directly below the "Filter tickets…" box (no section header — the chips read on their own). One chip per ticket type the user thinks in: Bug, Ticket, Epic, Initiative. All four ON by default. + +## Behaviour +- Single-click a chip → toggle that type in/out of the list. +- Double-click a chip → isolate (solo) that type: turns it ON and all others OFF. Double-click the same chip again → restore all to ON. This is the chart-legend solo pattern (Plotly/Tableau/Grafana) — learnable and fully reversible. +- Last-off resets to all-on: disabling the final remaining type snaps all chips back ON. An empty type filter means "no filter", so the list is never mysteriously blank. +- Tooltip per chip: "Click to toggle · double-click to isolate". + +## Type mapping (pql → chip) +pql ticket types are initiative, epic, story, task, bug (see lib/builtin/tickets/src/ticket_colors.dart). The four chips map as: +- Bug → bug +- Ticket → story + task (leaf work items) +- Epic → epic +- Initiative → initiative + +Each chip carries its type-colored dot + border using TicketTypeColors (bug #E87D7D, story/task green/grey, epic #78A0F8, initiative #C792EA). + +## Filtering +- Type filter is ANDed with the existing text filter in _TicketsViewState (lib/builtin/tickets/src/tickets_view.dart): a ticket shows only if its type is enabled AND it matches the text filter. +- When the type filter hides all items in a status section, that section collapses out (same as text-filter behaviour today). + +## Implementation notes +- Active-chip visual: filled tint + type-colored border (active) vs muted/no border (inactive) — reuse the _Toggle pattern from lib/builtin/search/src/search_panel_view.dart and ClideTappable. +- Persist nothing across sessions for v1 (always all-on on load); revisit if requested. +- a11y: Semantics(button, toggled) per chip, mirroring the search-panel toggle. + +## Wireframe +docs/design/wireframes/tickets/ticket-type-filters.json (+ .png export) + +Design revision (2026-06-10, supersedes the chip set/order above): FIVE chips, one per pql type — no story+task grouping. Ordered LARGE→SMALL left to right: Initiative, Epic, Story, Task, Bug. Each maps 1:1 to its pql type (initiative/epic/story/task/bug) with its TicketTypeColors dot+border (initiative #C792EA, epic #78A0F8, story #7DD3A8, task #9AA0AA grey, bug #E87D7D). All five ON by default. Toggle/solo(double-click)/last-off-reset behaviour unchanged. Wireframe updated + approved.', 'done', 'medium', NULL, NULL, NULL, '2026-06-10 15:54:09', '2026-06-10 16:12:15', NULL, '3dd40475245455459d9f09d26ef34316', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 005c40cd..fa03348b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Added +- **Per-type filter chips on the tickets panel.** A row of toggle chips + (Initiative · Epic · Story · Task · Bug, large→small) below the filter box. + Click a chip to toggle that type; double-click to isolate it (chart-legend + solo); disabling the last one snaps all back on. ANDed with the text filter. + All on by default. (T-343) - **VS Code keybinding preset.** A `vscode` keymap mapping VS Code's default shortcuts (Ctrl+P, Ctrl+Shift+P, Ctrl+B, Ctrl+J, Ctrl+`, zoom, …) to clide. Activate via the "Keymap: VS Code" command or `app.keymap.preset = vscode`. (T-64) diff --git a/docs/design/wireframes/tickets/ticket-type-filters.json b/docs/design/wireframes/tickets/ticket-type-filters.json index e25aafc9..32cfac99 100644 --- a/docs/design/wireframes/tickets/ticket-type-filters.json +++ b/docs/design/wireframes/tickets/ticket-type-filters.json @@ -42,82 +42,10 @@ "fontSize": 13 }, - "chip-bug": { - "type": "Rectangle", - "parent": "panel", - "left": 96, "top": 118, "width": 66, "height": 24, - "fillColor": "#211519", - "strokeColor": "#E87D7D", - "corners": [4, 4, 4, 4] - }, - "chip-bug-dot": { - "type": "Ellipse", - "parent": "chip-bug", - "left": 104, "top": 126, "width": 8, "height": 8, - "fillColor": "#E87D7D", - "strokeColor": "#E87D7D" - }, - "chip-bug-label": { - "type": "Text", - "parent": "chip-bug", - "left": 117, "top": 124, - "text": "Bug", - "fontColor": "#e8e8ee", - "fontSize": 11 - }, - - "chip-ticket": { - "type": "Rectangle", - "parent": "panel", - "left": 168, "top": 118, "width": 78, "height": 24, - "fillColor": "#15191d", - "strokeColor": "#7DD3A8", - "corners": [4, 4, 4, 4] - }, - "chip-ticket-dot": { - "type": "Ellipse", - "parent": "chip-ticket", - "left": 176, "top": 126, "width": 8, "height": 8, - "fillColor": "#7DD3A8", - "strokeColor": "#7DD3A8" - }, - "chip-ticket-label": { - "type": "Text", - "parent": "chip-ticket", - "left": 189, "top": 124, - "text": "Ticket", - "fontColor": "#e8e8ee", - "fontSize": 11 - }, - - "chip-epic": { - "type": "Rectangle", - "parent": "panel", - "left": 252, "top": 118, "width": 68, "height": 24, - "fillColor": "#14171f", - "strokeColor": "#78A0F8", - "corners": [4, 4, 4, 4] - }, - "chip-epic-dot": { - "type": "Ellipse", - "parent": "chip-epic", - "left": 260, "top": 126, "width": 8, "height": 8, - "fillColor": "#78A0F8", - "strokeColor": "#78A0F8" - }, - "chip-epic-label": { - "type": "Text", - "parent": "chip-epic", - "left": 273, "top": 124, - "text": "Epic", - "fontColor": "#e8e8ee", - "fontSize": 11 - }, - "chip-init": { "type": "Rectangle", "parent": "panel", - "left": 326, "top": 118, "width": 86, "height": 24, + "left": 96, "top": 118, "width": 86, "height": 24, "fillColor": "#1a151f", "strokeColor": "#C792EA", "corners": [4, 4, 4, 4] @@ -125,19 +53,115 @@ "chip-init-dot": { "type": "Ellipse", "parent": "chip-init", - "left": 334, "top": 126, "width": 8, "height": 8, + "left": 104, "top": 126, "width": 8, "height": 8, "fillColor": "#C792EA", "strokeColor": "#C792EA" }, "chip-init-label": { "type": "Text", "parent": "chip-init", - "left": 347, "top": 124, + "left": 117, "top": 124, "text": "Initiative", "fontColor": "#e8e8ee", "fontSize": 11 }, + "chip-epic": { + "type": "Rectangle", + "parent": "panel", + "left": 188, "top": 118, "width": 58, "height": 24, + "fillColor": "#14171f", + "strokeColor": "#78A0F8", + "corners": [4, 4, 4, 4] + }, + "chip-epic-dot": { + "type": "Ellipse", + "parent": "chip-epic", + "left": 196, "top": 126, "width": 8, "height": 8, + "fillColor": "#78A0F8", + "strokeColor": "#78A0F8" + }, + "chip-epic-label": { + "type": "Text", + "parent": "chip-epic", + "left": 209, "top": 124, + "text": "Epic", + "fontColor": "#e8e8ee", + "fontSize": 11 + }, + + "chip-story": { + "type": "Rectangle", + "parent": "panel", + "left": 252, "top": 118, "width": 62, "height": 24, + "fillColor": "#15191d", + "strokeColor": "#7DD3A8", + "corners": [4, 4, 4, 4] + }, + "chip-story-dot": { + "type": "Ellipse", + "parent": "chip-story", + "left": 260, "top": 126, "width": 8, "height": 8, + "fillColor": "#7DD3A8", + "strokeColor": "#7DD3A8" + }, + "chip-story-label": { + "type": "Text", + "parent": "chip-story", + "left": 273, "top": 124, + "text": "Story", + "fontColor": "#e8e8ee", + "fontSize": 11 + }, + + "chip-task": { + "type": "Rectangle", + "parent": "panel", + "left": 320, "top": 118, "width": 56, "height": 24, + "fillColor": "#17181a", + "strokeColor": "#9AA0AA", + "corners": [4, 4, 4, 4] + }, + "chip-task-dot": { + "type": "Ellipse", + "parent": "chip-task", + "left": 328, "top": 126, "width": 8, "height": 8, + "fillColor": "#9AA0AA", + "strokeColor": "#9AA0AA" + }, + "chip-task-label": { + "type": "Text", + "parent": "chip-task", + "left": 341, "top": 124, + "text": "Task", + "fontColor": "#e8e8ee", + "fontSize": 11 + }, + + "chip-bug": { + "type": "Rectangle", + "parent": "panel", + "left": 382, "top": 118, "width": 54, "height": 24, + "fillColor": "#211519", + "strokeColor": "#E87D7D", + "corners": [4, 4, 4, 4] + }, + "chip-bug-dot": { + "type": "Ellipse", + "parent": "chip-bug", + "left": 390, "top": 126, "width": 8, "height": 8, + "fillColor": "#E87D7D", + "strokeColor": "#E87D7D" + }, + "chip-bug-label": { + "type": "Text", + "parent": "chip-bug", + "left": 403, "top": 124, + "text": "Bug", + "fontColor": "#e8e8ee", + "fontSize": 11 + }, + "divider": { "type": "Line", "parent": "panel", @@ -222,7 +246,7 @@ "type": "Text", "parent": "panel", "left": 96, "top": 540, - "text": "All four type filters ON by default — tap a chip to toggle that type out.", + "text": "All five type filters ON by default — click a chip to toggle, double-click to isolate.", "fontColor": "#56565f", "fontSize": 10 } diff --git a/docs/design/wireframes/tickets/ticket-type-filters.png b/docs/design/wireframes/tickets/ticket-type-filters.png index e92fd382..72a8d7e3 100644 Binary files a/docs/design/wireframes/tickets/ticket-type-filters.png and b/docs/design/wireframes/tickets/ticket-type-filters.png differ diff --git a/lib/builtin/tickets/src/tickets_view.dart b/lib/builtin/tickets/src/tickets_view.dart index 5c735a1a..320fd2ce 100644 --- a/lib/builtin/tickets/src/tickets_view.dart +++ b/lib/builtin/tickets/src/tickets_view.dart @@ -21,6 +21,19 @@ class _TicketsViewState extends State { String? _focusedId; final _focusedKey = GlobalKey(); final Set _pinned = {'in_progress', 'ready', 'backlog'}; + + /// Type-filter chips (T-343), ordered large→small. Each maps 1:1 to a pql + /// ticket type; all on by default. An empty set never persists — toggling off + /// the last one snaps all back on, so the list is never mysteriously blank. + static const _allTypes = {'initiative', 'epic', 'story', 'task', 'bug'}; + static const _typeOrder = [ + ('initiative', 'Initiative'), + ('epic', 'Epic'), + ('story', 'Story'), + ('task', 'Task'), + ('bug', 'Bug'), + ]; + final Set _enabledTypes = {..._allTypes}; StreamSubscription? _focusSub; StreamSubscription? _schedulerSub; StreamSubscription? _changedSub; @@ -44,6 +57,31 @@ class _TicketsViewState extends State { }); } + /// Single-click a chip: toggle that type in/out. Removing the last enabled + /// type resets all back on (T-343). + void _toggleType(String type) { + setState(() { + if (_enabledTypes.contains(type)) { + _enabledTypes.remove(type); + if (_enabledTypes.isEmpty) _enabledTypes.addAll(_allTypes); + } else { + _enabledTypes.add(type); + } + }); + } + + /// Double-click a chip: isolate (solo) that type — it on, all others off. + /// Double-clicking the already-soloed chip restores all-on (chart-legend + /// solo pattern, T-343). + void _soloType(String type) { + setState(() { + final soloed = _enabledTypes.length == 1 && _enabledTypes.contains(type); + _enabledTypes + ..clear() + ..addAll(soloed ? _allTypes : {type}); + }); + } + static String _sectionForStatus(String? status) => status ?? 'backlog'; void _onFocus(Message msg) { @@ -132,12 +170,14 @@ class _TicketsViewState extends State { if (_tickets.isEmpty) return const Padding(padding: EdgeInsets.all(12), child: ClideText('No tickets.\nRun `pql ticket new` to create one.', muted: true)); final lf = _filter.toLowerCase(); - final hasFilter = lf.isNotEmpty; - final filtered = hasFilter - ? _tickets - .where((t) => t.id.toLowerCase().contains(lf) || t.title.toLowerCase().contains(lf) || (t.status ?? '').contains(lf) || (t.type ?? '').contains(lf)) - .toList() - : _tickets; + final hasTextFilter = lf.isNotEmpty; + // All-on = "no type filter" (so a full set never hides null-type tickets). + final allTypesOn = _enabledTypes.length == _allTypes.length; + final filtering = hasTextFilter || !allTypesOn; + bool textMatch(_TicketEntry t) => + t.id.toLowerCase().contains(lf) || t.title.toLowerCase().contains(lf) || (t.status ?? '').contains(lf) || (t.type ?? '').contains(lf); + bool typeMatch(_TicketEntry t) => allTypesOn || _enabledTypes.contains(t.type); + final filtered = _tickets.where((t) => (!hasTextFilter || textMatch(t)) && typeMatch(t)).toList(); const sections = [ ('in_progress', 'IN PROGRESS'), @@ -172,6 +212,25 @@ class _TicketsViewState extends State { ), ], ), + // Per-type filter chips (T-343): large→small, all on by default. + Padding( + padding: const EdgeInsets.fromLTRB(8, 2, 8, 6), + child: Wrap( + spacing: 6, + runSpacing: 4, + children: [ + for (final (type, label) in _typeOrder) + _TypeChip( + label: label, + color: typeColors.forType(type), + active: _enabledTypes.contains(type), + onToggle: () => _toggleType(type), + onSolo: () => _soloType(type), + tokens: tokens, + ), + ], + ), + ), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), @@ -183,7 +242,7 @@ class _TicketsViewState extends State { ClideAccordion( label: label, count: items.length, - expanded: hasFilter || _isSectionExpanded(status), + expanded: filtering || _isSectionExpanded(status), onToggle: () => _toggle(status), children: [ for (final t in items) @@ -205,6 +264,64 @@ class _TicketsViewState extends State { } } +/// A type-filter chip (T-343): a type-colored dot + label. Active = filled +/// tint + colored border; inactive = muted, no fill. Single-click toggles the +/// type; double-click isolates it (chart-legend solo). One [GestureDetector] +/// owns both so Flutter disambiguates single vs double. +class _TypeChip extends StatelessWidget { + const _TypeChip({ + required this.label, + required this.color, + required this.active, + required this.onToggle, + required this.onSolo, + required this.tokens, + }); + + final String label; + final Color color; + final bool active; + final VoidCallback onToggle; + final VoidCallback onSolo; + final SurfaceTokens tokens; + + @override + Widget build(BuildContext context) { + final dotColor = active ? color : tokens.globalTextMuted; + return Semantics( + button: true, + toggled: active, + label: '$label type filter', + child: ClideTooltip( + message: 'Click to toggle · double-click to isolate', + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: onToggle, + onDoubleTap: onSolo, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), + decoration: BoxDecoration( + color: active ? color.withAlpha(0x22) : null, + border: Border.all(color: active ? color : tokens.buttonBorder), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container(width: 8, height: 8, decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle)), + const SizedBox(width: 6), + ClideText(label, fontSize: clideFontSmall, fontFamily: clideMonoFamily, color: active ? tokens.globalForeground : tokens.globalTextMuted), + ], + ), + ), + ), + ), + ), + ); + } +} + class _TicketEntry { const _TicketEntry({required this.id, required this.title, this.type, this.status, this.priority, this.parentId}); final String id; diff --git a/test/builtin/tickets/tickets_view_test.dart b/test/builtin/tickets/tickets_view_test.dart index e6161f0b..6a6889a6 100644 --- a/test/builtin/tickets/tickets_view_test.dart +++ b/test/builtin/tickets/tickets_view_test.dart @@ -136,4 +136,67 @@ void main() { await pumpAsync(tester); expect(calls, greaterThan(before)); }); + + // -- Type-filter chips (T-343) ------------------------------------------ + // The chips own one GestureDetector for both onTap (toggle) and onDoubleTap + // (solo), so a single tap's onTap only fires after the ~300ms double-tap + // window — hence the 350ms pumps below. + + Future loadTwoTypes(WidgetTester tester) async { + f.ipc.stub( + 'pql.tickets.list', + (_) async => _list([ + _t('T-1', 'a bug item', 'backlog', type: 'bug'), + _t('T-2', 'a task item', 'backlog', type: 'task'), + ])); + await pumpView(tester); + } + + testWidgets('renders a chip per type, large→small (T-343)', (tester) async { + f.ipc.stub('pql.tickets.list', (_) async => _list([_t('T-1', 'x', 'backlog')])); + await pumpView(tester); + for (final label in ['Initiative', 'Epic', 'Story', 'Task', 'Bug']) { + expect(find.text(label), findsOneWidget); + } + }); + + testWidgets('single-click toggles a type out, others stay (T-343)', (tester) async { + await loadTwoTypes(tester); + expect(find.text('a bug item'), findsOneWidget); + + await tester.tap(find.text('Bug')); + await tester.pump(const Duration(milliseconds: 350)); + + expect(find.text('a bug item'), findsNothing); + expect(find.text('a task item'), findsOneWidget); + }); + + testWidgets('double-click isolates a type (solo) (T-343)', (tester) async { + await loadTwoTypes(tester); + + await tester.tap(find.text('Bug')); + await tester.pump(const Duration(milliseconds: 50)); + await tester.tap(find.text('Bug')); + await tester.pump(const Duration(milliseconds: 350)); + + expect(find.text('a bug item'), findsOneWidget); + expect(find.text('a task item'), findsNothing); + }); + + testWidgets('toggling off the last enabled type resets all on (T-343)', (tester) async { + await loadTwoTypes(tester); + + // Solo Bug → task hidden. + await tester.tap(find.text('Bug')); + await tester.pump(const Duration(milliseconds: 50)); + await tester.tap(find.text('Bug')); + await tester.pump(const Duration(milliseconds: 350)); + expect(find.text('a task item'), findsNothing); + + // Toggle the only-remaining Bug off → snaps all back on → task returns. + await tester.tap(find.text('Bug')); + await tester.pump(const Duration(milliseconds: 350)); + expect(find.text('a task item'), findsOneWidget); + expect(find.text('a bug item'), findsOneWidget); + }); }