add toast notification system (T-50)

Non-modal operation-feedback toasts, bottom-right: a ClideToast card per
severity (success/warning/error/info), auto-dismiss (errors linger), queue
with a visible cap, slide+fade in, manual dismiss, live-region a11y.

ToastService is a MessageBus consumer — components raise a toast by publishing
to the 'toast' channel (publishToast helper), so emitters stay decoupled from
the UI. GitController's push/pull are the first emitters. ToastOverlay mounts
in the app-root Stack.

Also adds comprehensive GitController coverage: importing it for the toast
emitter test first pulled the whole file into the coverage denominator, so the
controller is now tested end to end (status/stage/commit/stash/push/pull).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 23:14:42 +02:00
co-authored by Claude Opus 4.8
parent 55ed3013e1
commit 1f09abcfd7
12 changed files with 747 additions and 4 deletions
+19 -3
View File
@@ -12,13 +12,23 @@ import 'package:clide/kernel/kernel.dart';
import 'package:flutter/foundation.dart';
class GitController extends ChangeNotifier {
GitController({required this.ipc, required this.events}) {
GitController({required this.ipc, required this.events, this.messages}) {
_eventSub = events.on<DaemonEvent>().listen(_onEvent);
}
final DaemonClient ipc;
final DaemonBus events;
/// Optional — the kernel MessageBus, used to publish operation-feedback
/// toasts (push/pull) without depending on the toast service. Null in
/// headless/unit contexts that don't wire it.
final MessageBus? messages;
void _toast(String message, ToastSeverity severity) {
final m = messages;
if (m != null) publishToast(m, 'builtin.git', message, severity: severity);
}
StreamSubscription<DaemonEvent>? _eventSub;
String? _branch;
@@ -112,8 +122,11 @@ class GitController extends ChangeNotifier {
Future<bool> pull() async {
final r = await ipc.request('git.pull');
if (!r.ok) {
if (r.ok) {
_toast('Pulled${_upstream != null ? ' from $_upstream' : ''}', ToastSeverity.success);
} else {
_error = r.error?.message;
_toast('Pull failed: ${r.error?.message ?? 'unknown error'}', ToastSeverity.error);
notifyListeners();
}
return r.ok;
@@ -121,8 +134,11 @@ class GitController extends ChangeNotifier {
Future<bool> push() async {
final r = await ipc.request('git.push');
if (!r.ok) {
if (r.ok) {
_toast('Pushed${_upstream != null ? ' to $_upstream' : ''}', ToastSeverity.success);
} else {
_error = r.error?.message;
_toast('Push failed: ${r.error?.message ?? 'unknown error'}', ToastSeverity.error);
notifyListeners();
}
return r.ok;