Files
clide/lib/builtin/tickets/src/ticket_detail_controller.dart
T
jpmschweitzerandClaude Opus 4.6 de321bfe92 bidirectional focus: detail broadcasts, list highlights
Detail controller publishes builtin.tickets/focus with the loaded
ticket ID after every load. Ticket list subscribes and highlights
the matching card with selected background and accent border.
Works regardless of what triggered the detail load — sidebar
click, parent tree navigation, or future command palette.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 16:59:21 +02:00

98 lines
2.9 KiB
Dart

import 'dart:async';
import 'package:clide/kernel/kernel.dart';
import 'package:clide/kernel/src/events/message_bus.dart';
import 'package:flutter/foundation.dart';
class TicketDetail {
const TicketDetail({required this.ticket, this.parents = const [], this.decisions = const []});
final Map<String, Object?> ticket;
final List<Map<String, Object?>> parents;
final List<Map<String, Object?>> decisions;
String get id => ticket['id'] as String? ?? '';
String get title => ticket['title'] as String? ?? '';
String? get type => ticket['type'] as String?;
String? get status => ticket['status'] as String?;
String? get priority => ticket['priority'] as String?;
String? get description => ticket['description'] as String?;
String? get parentId => ticket['parent_id'] as String?;
String? get decisionRef => ticket['decision_ref'] as String?;
String? get assignedTo => ticket['assigned_to'] as String?;
}
class TicketDetailController extends ChangeNotifier {
TicketDetailController({required this.ipc, required this.messages, required this.panels}) {
_sub = messages.subscribe(publisher: 'builtin.tickets', channel: 'selection').listen(_onSelection);
}
final DaemonClient ipc;
final MessageBus messages;
final PanelRegistry panels;
StreamSubscription<Message>? _sub;
TicketDetail? _detail;
TicketDetail? get detail => _detail;
bool _loading = false;
bool get loading => _loading;
void _onSelection(Message msg) {
final id = msg.data['id'] as String?;
if (id != null) {
panels.activateTab(Slots.contextPanel, 'tickets.detail');
unawaited(load(id));
}
}
Future<void> load(String id) async {
_loading = true;
notifyListeners();
final resp = await ipc.request('pql.tickets.show', args: {'id': id});
if (!resp.ok) {
_loading = false;
notifyListeners();
return;
}
final ticket = resp.data;
final parents = <Map<String, Object?>>[];
final decisions = <Map<String, Object?>>[];
// Walk parent chain
var pid = ticket['parent_id'] as String?;
while (pid != null) {
final pr = await ipc.request('pql.tickets.show', args: {'id': pid});
if (!pr.ok) break;
parents.add(pr.data);
pid = pr.data['parent_id'] as String?;
}
// Collect decision refs from ticket and parents
final refs = <String>{};
final dr = ticket['decision_ref'] as String?;
if (dr != null) refs.add(dr);
for (final p in parents) {
final pdr = p['decision_ref'] as String?;
if (pdr != null) refs.add(pdr);
}
for (final ref in refs) {
final dr = await ipc.request('pql.decisions.show', args: {'id': ref});
if (dr.ok) decisions.add(dr.data);
}
_detail = TicketDetail(ticket: ticket, parents: parents, decisions: decisions);
_loading = false;
messages.publish('builtin.tickets', 'focus', {'id': id});
notifyListeners();
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
}