add clide ui toast — raise a toast from the CLI (T-245)

The drive-half complement to the toast system (D-6 parity, like ui.open):
`clide ui toast "msg" [--severity success|warning|error|info] [--duration MS]`
publishes a message on the kernel MessageBus 'toast' channel, which the
ToastService consumes — so a hosted Claude session or any script can surface
"done/failed" on the user's screen. The channel literal is kept in ui_command
(not imported from the kernel) so the daemon command stays Flutter-free.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 23:30:46 +02:00
co-authored by Claude Opus 4.8
parent 1f09abcfd7
commit 62c6a60ec7
3 changed files with 105 additions and 0 deletions
+53
View File
@@ -75,4 +75,57 @@ void main() {
expect(r.ok, isFalse);
expect(r.error?.kind, IpcErrorKind.toolError);
});
// -- ui.toast (T-50 drive-half) -------------------------------------------
Future<IpcResponse> toast(List<String> positional, {Map<String, Object?>? flags}) => d.dispatch(
IpcRequest(id: '1', cmd: 'ui.toast', args: {'positional': positional, if (flags != null) 'flags': flags}),
);
test('ui toast publishes a toast message (default info severity)', () async {
wire();
final r = await toast(['Build', 'finished']); // unquoted multi-word joins
expect(r.ok, isTrue, reason: r.error?.message);
expect(r.data['shown'], isTrue);
expect(published.single.publisher, 'cli');
expect(published.single.channel, 'toast');
expect(published.single.data, {'message': 'Build finished', 'severity': 'info'});
});
test('ui toast honours --severity and --duration', () async {
wire();
final r = await toast(['Pushed'], flags: {'severity': 'success', 'duration': '2000'});
expect(r.ok, isTrue);
expect(published.single.data, {'message': 'Pushed', 'severity': 'success', 'durationMs': 2000});
});
test('ui toast rejects an unknown severity', () async {
wire();
final r = await toast(['x'], flags: {'severity': 'bogus'});
expect(r.ok, isFalse);
expect(r.error?.kind, IpcErrorKind.userError);
expect(published, isEmpty);
});
test('ui toast rejects a non-integer duration', () async {
wire();
final r = await toast(['x'], flags: {'duration': 'soon'});
expect(r.ok, isFalse);
expect(r.error?.kind, IpcErrorKind.userError);
});
test('ui toast with no message → userError', () async {
wire();
final r = await toast([]);
expect(r.ok, isFalse);
expect(r.error?.kind, IpcErrorKind.userError);
expect(published, isEmpty);
});
test('ui toast with no live UI → toolError', () async {
wire(liveUi: false);
final r = await toast(['hi']);
expect(r.ok, isFalse);
expect(r.error?.kind, IpcErrorKind.toolError);
});
}