decision detail shows full markdown body via pql decisions read

New pql.decisions.read IPC verb returns the full markdown section
body. Decision detail view now shows: title card, full decision
text (rationale, cost, cross-references), and clickable ref cards
for cross-reference navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 17:19:15 +02:00
co-authored by Claude Opus 4.6
parent dad2d0ad52
commit 2345125172
3 changed files with 71 additions and 1 deletions
@@ -33,7 +33,7 @@ class _DecisionDetailViewState extends State<DecisionDetailView> {
Future<void> _load(String id) async {
setState(() => _loading = true);
final kernel = ClideKernel.of(context);
final resp = await kernel.ipc.request('pql.decisions.show', args: {'id': id});
final resp = await kernel.ipc.request('pql.decisions.read', args: {'id': id});
if (!mounted) return;
setState(() {
_loading = false;
@@ -57,6 +57,8 @@ class _DecisionDetailViewState extends State<DecisionDetailView> {
final domain = d['domain'] as String?;
final status = d['status'] as String?;
final date = d['date'] as String?;
final body = d['body'] as String?;
final refs = (d['refs'] as List?)?.cast<Map<String, Object?>>() ?? const [];
final typeColor = typeColors.forType(type);
return SingleChildScrollView(
@@ -104,12 +106,63 @@ class _DecisionDetailViewState extends State<DecisionDetailView> {
],
),
),
if (body != null && body.isNotEmpty) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: tokens.panelBackground,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: tokens.panelBorder),
),
child: ClideText(body, fontSize: 13, fontFamily: clideMonoFamily),
),
],
if (refs.isNotEmpty) ...[
const SizedBox(height: 16),
ClideText('CROSS-REFERENCES', fontSize: 11, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily),
const SizedBox(height: 6),
for (final ref in refs) _RefCard(ref: ref, tokens: tokens),
],
],
),
);
}
}
class _RefCard extends StatelessWidget {
const _RefCard({required this.ref, required this.tokens});
final Map<String, Object?> ref;
final SurfaceTokens tokens;
@override
Widget build(BuildContext context) {
final targetId = (ref['target_id'] ?? ref['source_id']) as String? ?? '';
final refType = ref['ref_type'] as String? ?? '';
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: ClideTappable(
onTap: () => ClideKernel.of(context).messages.publish('builtin.decisions', 'selection', {'id': targetId}),
builder: (ctx, hovered, _) => Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: hovered ? tokens.sidebarItemHover : tokens.panelBackground,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: hovered ? tokens.panelActiveBorder : tokens.panelBorder),
),
child: Row(
children: [
ClideText(targetId, fontSize: 11, color: tokens.globalFocus, fontFamily: clideMonoFamily),
const SizedBox(width: 8),
ClideText(refType, fontSize: 11, color: tokens.globalTextMuted),
],
),
),
),
);
}
}
class _StatusBadge extends StatelessWidget {
const _StatusBadge({required this.status, required this.tokens});
final String status;
+13
View File
@@ -125,6 +125,19 @@ void registerPqlCommands(DaemonDispatcher d, PqlClient pql) {
}
});
d.register('pql.decisions.read', (req) async {
final id = req.args['id'] as String?;
if (id == null || id.isEmpty) {
return _userError(req.id, 'pql.decisions.read requires an id');
}
try {
final result = await pql.decisionRead(id);
return IpcResponse.ok(id: req.id, data: result);
} on PqlException catch (e) {
return _pqlError(req.id, e);
}
});
d.register('pql.decisions.show', (req) async {
final id = req.args['id'] as String?;
if (id == null || id.isEmpty) {
+4
View File
@@ -94,6 +94,10 @@ class PqlClient {
return _runObject(args);
}
Future<Map<String, Object?>> decisionRead(String id) async {
return _runObject(['decisions', 'read', id]);
}
Future<List<Map<String, Object?>>> decisionCoverage() async {
return _runList(['decisions', 'coverage']);
}