make sidebar filter boxes CLI-addressable via the MessageBus (T-270)
The sidebar/dock filter fields (the shared ClideFilterBox) had no CLI peer — a one-way, UI-only affordance that broke D-6 parity. Add the drive+observe verb `clide ui filter <address> [<text>]`, routed entirely through the kernel MessageBus pub/sub so a box reacts to a published message identically whether the trigger was a UI keystroke or the CLI — keeping extensions first-class (no dispatcher→widget wiring). - ClideFilterBox gains an `address`; when set it listens on `filter.set` for its address and republishes its value on `filter.state`. Null address keeps the box a kernel-free UI widget. - FilterStateCache (new kernel service) caches the latest `filter.state` per address — the bus has no retention, so this backs the observe-half. - ui.filter: with text → publishes `filter.set` (drive); without → reads the cache (observe). Honest toolError when there is no live UI. - Address every box: decisions/tickets/files/git/output/problems panes, the four search boxes, and the pql search/query/markdown inputs. Addresses are the ids from `clide pane list` (e.g. decisions.panel). settings.json: allow the `clide` CLI + relevant skills. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clide/kernel/src/events/message_bus.dart';
|
||||
import 'package:clide/kernel/src/facade.dart';
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
import 'package:clide/widgets/src/icons/phosphor.dart';
|
||||
@@ -14,6 +16,7 @@ class ClideFilterBox extends StatefulWidget {
|
||||
this.debounce = const Duration(milliseconds: 200),
|
||||
this.onSubmitted,
|
||||
this.icon = PhosphorIcons.magnifyingGlass,
|
||||
this.address,
|
||||
});
|
||||
|
||||
final ValueChanged<String> onChanged;
|
||||
@@ -25,6 +28,15 @@ class ClideFilterBox extends StatefulWidget {
|
||||
/// that aren't searches (e.g. a replace or glob field).
|
||||
final ClideIconPainter? icon;
|
||||
|
||||
/// Makes this box addressable from the CLI (D-6 parity, T-270). When set,
|
||||
/// the box listens on the MessageBus `filter.set` channel for its address
|
||||
/// — so `clide ui filter <address> <text>` drives it exactly as a UI
|
||||
/// keystroke would — and republishes its value on `filter.state` so the
|
||||
/// observe-half (`clide ui filter <address>`) can read it back. The
|
||||
/// address is the pane/box id surfaced by `clide pane list`. When null the
|
||||
/// box is a plain UI-only widget and never touches the kernel.
|
||||
final String? address;
|
||||
|
||||
@override
|
||||
State<ClideFilterBox> createState() => _ClideFilterBoxState();
|
||||
}
|
||||
@@ -33,18 +45,55 @@ class _ClideFilterBoxState extends State<ClideFilterBox> {
|
||||
final _controller = TextEditingController();
|
||||
final _focus = FocusNode();
|
||||
Timer? _debounceTimer;
|
||||
StreamSubscription<Message>? _busSub;
|
||||
MessageBus? _bus;
|
||||
bool _wired = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final address = widget.address;
|
||||
if (address == null || _wired) return;
|
||||
_wired = true;
|
||||
final bus = ClideKernel.of(context).messages;
|
||||
_bus = bus;
|
||||
_busSub = bus.subscribe(publisher: address, channel: 'filter.set').listen(_onRemoteSet);
|
||||
// Report the initial value so an observe before any change still reads it.
|
||||
_publishState(_controller.text);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounceTimer?.cancel();
|
||||
_busSub?.cancel();
|
||||
_controller.dispose();
|
||||
_focus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Apply a value pushed in over the bus. Programmatic sets take effect
|
||||
/// immediately (no debounce) and report their new state.
|
||||
void _onRemoteSet(Message m) {
|
||||
final value = m.data['query'] as String? ?? '';
|
||||
_debounceTimer?.cancel();
|
||||
_controller.text = value;
|
||||
widget.onChanged(value);
|
||||
_publishState(value);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _publishState(String value) {
|
||||
final address = widget.address;
|
||||
if (address == null) return;
|
||||
_bus?.publish(address, 'filter.state', {'query': value});
|
||||
}
|
||||
|
||||
void _onChanged(String value) {
|
||||
_debounceTimer?.cancel();
|
||||
_debounceTimer = Timer(widget.debounce, () => widget.onChanged(value));
|
||||
_debounceTimer = Timer(widget.debounce, () {
|
||||
widget.onChanged(value);
|
||||
_publishState(value);
|
||||
});
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@@ -52,6 +101,7 @@ class _ClideFilterBoxState extends State<ClideFilterBox> {
|
||||
_controller.clear();
|
||||
_debounceTimer?.cancel();
|
||||
widget.onChanged('');
|
||||
_publishState('');
|
||||
_focus.requestFocus();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user