diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 1315572b..4d88245b 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -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 ('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 ('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; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 636fb1bd..c4f39e0f 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -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). 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); diff --git a/CHANGELOG.md b/CHANGELOG.md index c3389805..7cff8677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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.** Approving Claude's plan (the ExitPlanMode tool) transitioned the underlying session out of plan mode, but clide's tracked permission mode didn't follow, diff --git a/lib/kernel/src/facade.dart b/lib/kernel/src/facade.dart index c0a78007..4cad6b30 100644 --- a/lib/kernel/src/facade.dart +++ b/lib/kernel/src/facade.dart @@ -166,7 +166,7 @@ class KernelServices { final readerNav = ReaderNavRegistry(messages); final clipboard = ClideClipboard(); final files = FileServices(events); - final notify = Notifications(); + final notify = Notifications(messages: messages); final dialog = DialogRouter(); final tray = TrayRegistry(); final secrets = SecretsVault(); diff --git a/lib/kernel/src/notify.dart b/lib/kernel/src/notify.dart index a4228cb8..466eb8dc 100644 --- a/lib/kernel/src/notify.dart +++ b/lib/kernel/src/notify.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:clide/kernel/src/events/message_bus.dart'; +import 'package:clide/kernel/src/toast.dart'; import 'package:flutter/foundation.dart'; enum NotificationLevel { info, warning, error, success } @@ -18,6 +20,14 @@ class ClideNotification { } 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 _active = []; final Map _timers = {}; 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)); _active.add(n); _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(); } diff --git a/test/kernel/src/services_stubs_test.dart b/test/kernel/src/services_stubs_test.dart index 085a12fc..3d87f30a 100644 --- a/test/kernel/src/services_stubs_test.dart +++ b/test/kernel/src/services_stubs_test.dart @@ -272,5 +272,36 @@ void main() { expect(calls, 1); 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.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.delayed(Duration.zero); + + expect(toasts.entries.map((e) => e.severity), [ToastSeverity.error, ToastSeverity.success]); + }); }); }