surface kernel notifications as toasts (T-382)

The Notifications service had zero widget consumers — anything pushed
through ctx.notify (cli_install's dogfood warnings, install results)
accumulated in a list nothing rendered. The service now takes the
kernel MessageBus and publishes each notification to the toast
channel with mapped severity, so the existing ToastOverlay renders
them; the active list stays for API compatibility. Chose routing over
building a notifications tray nobody asked for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 01:21:16 +02:00
co-authored by Claude Fable 5
parent 01c3de37e1
commit d9ae2d7585
6 changed files with 92 additions and 1 deletions
+13
View File
@@ -4009,3 +4009,16 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
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 ('06FBHCC6AR37VTF4SY8DR99JHC', 'status', 'in_progress', 'done', NULL, '2026-06-11 23:16:28', '2026-06-11 23:16:28', '2026-06-11 23:16:28', NULL, '2fe291251d1a4789c8d2b75a2c32396f', 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 ('06FBHCC6AR37VTF4SY8DR99JHC', 'status', 'in_progress', 'done', NULL, '2026-06-11 23:16:28', '2026-06-11 23:16:28', '2026-06-11 23:16:28', NULL, '2fe291251d1a4789c8d2b75a2c32396f', 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 ('06FBHCJEYHC91PMVNVWVHBR2RG', 'status', 'backlog', 'in_progress', NULL, '2026-06-11 23:16:53', '2026-06-11 23:16:53', '2026-06-11 23:16:53', NULL, 'e6c0cb3a2fce6e257e054f5b3e209814', 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 ('06FBHCJEYHC91PMVNVWVHBR2RG', 'status', 'backlog', 'in_progress', NULL, '2026-06-11 23:16:53', '2026-06-11 23:16:53', '2026-06-11 23:16:53', NULL, 'e6c0cb3a2fce6e257e054f5b3e209814', 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 ('06FBHCJEYHC91PMVNVWVHBR2RG', 'status', 'in_progress', 'done', NULL, '2026-06-11 23:18:22', '2026-06-11 23:18:22', '2026-06-11 23:18:22', NULL, '22809d0fac7e1aa116e682c18dae7455', 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 ('06FBHCJEYHC91PMVNVWVHBR2RG', 'status', 'in_progress', 'done', NULL, '2026-06-11 23:18:22', '2026-06-11 23:18:22', '2026-06-11 23:18:22', NULL, '22809d0fac7e1aa116e682c18dae7455', 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 ('06FBHCQHZQ0NKY1VRWWPSNZT84', 'status', 'backlog', 'in_progress', NULL, '2026-06-11 23:18:47', '2026-06-11 23:18:47', '2026-06-11 23:18:47', NULL, '2ebff93f1c7328538d0075a2a5c34cca', 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 ('06FBHCQHZQ0NKY1VRWWPSNZT84', 'status', 'in_progress', 'done', NULL, '2026-06-11 23:20:52', '2026-06-11 23:20:52', '2026-06-11 23:20:52', NULL, '6a025da791052ffa97803f6ce8afa2f1', 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 ('06FBHCQHZQ0NKY1VRWWPSNZT84', 'description', 'lib/kernel/src/notify.dart has zero widget consumers — anything pushed through the Notifications service (e.g. cli_install''s dogfood warnings) accumulates in a list no surface renders. ToastService exists right next to it and does render.
Fix options (pick one, note it on this ticket): (a) route notify-level messages through ToastService with severity styling; (b) add a notifications tray/indicator surface; (c) delete the service and migrate callers to toasts. Option (a) or (c) is likely right for current scale — avoid building a tray nobody asked for.
Acceptance: a notification posted by cli_install is visibly surfaced in the UI (test via whichever surface is chosen); no silent sink remains.', 'lib/kernel/src/notify.dart has zero widget consumers — anything pushed through the Notifications service (e.g. cli_install''s dogfood warnings) accumulates in a list no surface renders. ToastService exists right next to it and does render.
Fix options (pick one, note it on this ticket): (a) route notify-level messages through ToastService with severity styling; (b) add a notifications tray/indicator surface; (c) delete the service and migrate callers to toasts. Option (a) or (c) is likely right for current scale — avoid building a tray nobody asked for.
Acceptance: a notification posted by cli_install is visibly surfaced in the UI (test via whichever surface is chosen); no silent sink remains.
Resolved with option (a): Notifications now takes the kernel MessageBus and publishes every notification to the toast channel (severity-mapped, ''title — message''); the in-memory active list stays for API compatibility. No tray built.', NULL, '2026-06-11 23:20:59', '2026-06-11 23:20:59', '2026-06-11 23:20:59', NULL, 'a34021fbc6ce201bc9e8f5954129b7cd', 2) ON CONFLICT(hash) DO NOTHING;
+17
View File
@@ -4149,3 +4149,20 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
Fix: emit a newline span for hard breaks; render images as at least an alt-text placeholder chip (full image rendering can be a follow-up — note the existing feedback that live-pane images go through clide image show). Fix: emit a newline span for hard breaks; render images as at least an alt-text placeholder chip (full image rendering can be a follow-up — note the existing feedback that live-pane images go through clide image show).
Acceptance: golden/widget test for hard-break line splitting; image node renders alt text; no regression in existing markdown goldens.', 'done', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:13', '2026-06-11 23:18:22', NULL, '5ec0ffc2676c2f31476249301ae2869b', 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); Acceptance: golden/widget test for hard-break line splitting; image node renders alt text; no regression in existing markdown goldens.', 'done', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:13', '2026-06-11 23:18:22', NULL, '5ec0ffc2676c2f31476249301ae2869b', 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 ('06FBHCQHZQ0NKY1VRWWPSNZT84', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Notifications service renders nowhere — warnings vanish into an unrendered list', 'lib/kernel/src/notify.dart has zero widget consumers — anything pushed through the Notifications service (e.g. cli_install''s dogfood warnings) accumulates in a list no surface renders. ToastService exists right next to it and does render.
Fix options (pick one, note it on this ticket): (a) route notify-level messages through ToastService with severity styling; (b) add a notifications tray/indicator surface; (c) delete the service and migrate callers to toasts. Option (a) or (c) is likely right for current scale — avoid building a tray nobody asked for.
Acceptance: a notification posted by cli_install is visibly surfaced in the UI (test via whichever surface is chosen); no silent sink remains.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:55', '2026-06-11 23:18:47', NULL, '20850287af7c135adc76d53f200f5209', 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 ('06FBHCQHZQ0NKY1VRWWPSNZT84', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Notifications service renders nowhere — warnings vanish into an unrendered list', 'lib/kernel/src/notify.dart has zero widget consumers — anything pushed through the Notifications service (e.g. cli_install''s dogfood warnings) accumulates in a list no surface renders. ToastService exists right next to it and does render.
Fix options (pick one, note it on this ticket): (a) route notify-level messages through ToastService with severity styling; (b) add a notifications tray/indicator surface; (c) delete the service and migrate callers to toasts. Option (a) or (c) is likely right for current scale — avoid building a tray nobody asked for.
Acceptance: a notification posted by cli_install is visibly surfaced in the UI (test via whichever surface is chosen); no silent sink remains.', 'done', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:55', '2026-06-11 23:20:52', NULL, 'fb8aa4e3ae8a67be11cb051975e3804c', 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 ('06FBHCQHZQ0NKY1VRWWPSNZT84', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Notifications service renders nowhere — warnings vanish into an unrendered list', 'lib/kernel/src/notify.dart has zero widget consumers — anything pushed through the Notifications service (e.g. cli_install''s dogfood warnings) accumulates in a list no surface renders. ToastService exists right next to it and does render.
Fix options (pick one, note it on this ticket): (a) route notify-level messages through ToastService with severity styling; (b) add a notifications tray/indicator surface; (c) delete the service and migrate callers to toasts. Option (a) or (c) is likely right for current scale — avoid building a tray nobody asked for.
Acceptance: a notification posted by cli_install is visibly surfaced in the UI (test via whichever surface is chosen); no silent sink remains.
Resolved with option (a): Notifications now takes the kernel MessageBus and publishes every notification to the toast channel (severity-mapped, ''title — message''); the in-memory active list stays for API compatibility. No tray built.', 'done', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:55', '2026-06-11 23:20:59', NULL, 'e98115731bb2c593bd17ae945a79d260', 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);
+5
View File
@@ -120,6 +120,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
together and images vanished; breaks now emit a newline and images render together and images vanished; breaks now emit a newline and images render
an italic `[image: alt]` placeholder. (T-379) an italic `[image: alt]` placeholder. (T-379)
- **Extension notifications actually appear on screen.** Messages pushed
through the kernel Notifications service (e.g. the CLI-install dogfood
warnings) accumulated in a list no surface rendered; they now raise
toasts with matching severity. (T-382)
- **Accepting ExitPlanMode now leaves plan mode in the conversation panel.** - **Accepting ExitPlanMode now leaves plan mode in the conversation panel.**
Approving Claude's plan (the ExitPlanMode tool) transitioned the underlying Approving Claude's plan (the ExitPlanMode tool) transitioned the underlying
session out of plan mode, but clide's tracked permission mode didn't follow, session out of plan mode, but clide's tracked permission mode didn't follow,
+1 -1
View File
@@ -166,7 +166,7 @@ class KernelServices {
final readerNav = ReaderNavRegistry(messages); final readerNav = ReaderNavRegistry(messages);
final clipboard = ClideClipboard(); final clipboard = ClideClipboard();
final files = FileServices(events); final files = FileServices(events);
final notify = Notifications(); final notify = Notifications(messages: messages);
final dialog = DialogRouter(); final dialog = DialogRouter();
final tray = TrayRegistry(); final tray = TrayRegistry();
final secrets = SecretsVault(); final secrets = SecretsVault();
+25
View File
@@ -1,5 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:clide/kernel/src/events/message_bus.dart';
import 'package:clide/kernel/src/toast.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
enum NotificationLevel { info, warning, error, success } enum NotificationLevel { info, warning, error, success }
@@ -18,6 +20,14 @@ class ClideNotification {
} }
class Notifications extends ChangeNotifier { class Notifications extends ChangeNotifier {
Notifications({MessageBus? messages}) : _messages = messages;
/// When wired (the facade passes the kernel bus), every notification is
/// also published to the toast channel so it actually renders — the
/// in-memory list had zero widget consumers and messages vanished
/// silently (T-382).
final MessageBus? _messages;
final List<ClideNotification> _active = []; final List<ClideNotification> _active = [];
final Map<String, Timer> _timers = {}; final Map<String, Timer> _timers = {};
int _seq = 0; int _seq = 0;
@@ -41,6 +51,21 @@ class Notifications extends ChangeNotifier {
final n = ClideNotification(id: id, level: level, message: message, title: title, duration: duration ?? const Duration(seconds: 4)); final n = ClideNotification(id: id, level: level, message: message, title: title, duration: duration ?? const Duration(seconds: 4));
_active.add(n); _active.add(n);
_timers[id] = Timer(n.duration, () => dismiss(id)); _timers[id] = Timer(n.duration, () => dismiss(id));
final bus = _messages;
if (bus != null) {
publishToast(
bus,
'kernel.notify',
title == null ? message : '$title — $message',
severity: switch (level) {
NotificationLevel.info => ToastSeverity.info,
NotificationLevel.warning => ToastSeverity.warning,
NotificationLevel.error => ToastSeverity.error,
NotificationLevel.success => ToastSeverity.success,
},
duration: duration,
);
}
notifyListeners(); notifyListeners();
} }
+31
View File
@@ -272,5 +272,36 @@ void main() {
expect(calls, 1); expect(calls, 1);
n.dispose(); n.dispose();
}); });
// T-382: the in-memory list had no widget consumer — notifications
// vanished silently. Wired to the bus, each one now raises a toast.
test('a bus-wired notification surfaces as a rendered toast (T-382)', () async {
final messages = MessageBus();
final toasts = ToastService(messages: messages);
addTearDown(toasts.dispose);
final n = Notifications(messages: messages);
addTearDown(n.dispose);
n.warn('clide CLI not on PATH', title: 'dogfood');
await Future<void>.delayed(Duration.zero);
expect(toasts.entries, hasLength(1));
expect(toasts.entries.single.message, 'dogfood — clide CLI not on PATH');
expect(toasts.entries.single.severity, ToastSeverity.warning);
});
test('error and success levels map to their toast severities (T-382)', () async {
final messages = MessageBus();
final toasts = ToastService(messages: messages);
addTearDown(toasts.dispose);
final n = Notifications(messages: messages);
addTearDown(n.dispose);
n.error('boom');
n.success('done');
await Future<void>.delayed(Duration.zero);
expect(toasts.entries.map((e) => e.severity), [ToastSeverity.error, ToastSeverity.success]);
});
}); });
} }