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:
@@ -150,7 +150,7 @@ class _DecisionsViewState extends State<DecisionsView> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: ClideFilterBox(hint: 'Filter decisions…', onChanged: (v) => setState(() => _filter = v))),
|
||||
Expanded(child: ClideFilterBox(address: 'decisions.panel', hint: 'Filter decisions…', onChanged: (v) => setState(() => _filter = v))),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ClideTappable(
|
||||
|
||||
@@ -65,7 +65,7 @@ class _FileTreeViewState extends State<FileTreeView> {
|
||||
final rootName = root.split(Platform.pathSeparator).last;
|
||||
return Column(
|
||||
children: [
|
||||
ClideFilterBox(hint: 'Filter files…', onChanged: (v) => setState(() => _filter = v)),
|
||||
ClideFilterBox(address: 'files.tree', hint: 'Filter files…', onChanged: (v) => setState(() => _filter = v)),
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
label: 'file tree — $rootName',
|
||||
|
||||
@@ -75,7 +75,7 @@ class _GitPanelViewState extends State<GitPanelView> {
|
||||
explicitChildNodes: true,
|
||||
child: Column(
|
||||
children: [
|
||||
ClideFilterBox(hint: 'Filter changes…', onChanged: (v) => setState(() => _filter = v)),
|
||||
ClideFilterBox(address: 'git.panel', hint: 'Filter changes…', onChanged: (v) => setState(() => _filter = v)),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
|
||||
@@ -125,7 +125,7 @@ class _OutputViewState extends State<OutputView> {
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClideFilterBox(hint: 'Filter…', onChanged: _c.setText),
|
||||
child: ClideFilterBox(address: 'output.panel', hint: 'Filter…', onChanged: _c.setText),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_Chip(
|
||||
|
||||
@@ -103,11 +103,16 @@ class _PqlSearchBodyState extends State<PqlSearchBody> {
|
||||
children: [
|
||||
if (widget.mode == PqlPaneMode.markdown)
|
||||
ClideFilterBox(
|
||||
address: 'search.pql.markdown',
|
||||
hint: 'Filter markdown…',
|
||||
onChanged: (v) => unawaited(c.loadMarkdownFiles(glob: v.isEmpty ? null : '**/*$v*.md')),
|
||||
)
|
||||
else
|
||||
_PqlSearchInput(controller: c, dsl: widget.mode == PqlPaneMode.query),
|
||||
_PqlSearchInput(
|
||||
controller: c,
|
||||
dsl: widget.mode == PqlPaneMode.query,
|
||||
address: widget.mode == PqlPaneMode.query ? 'search.pql.query' : 'search.pql.vault',
|
||||
),
|
||||
if (c.error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
@@ -146,13 +151,15 @@ class _PqlSearchBodyState extends State<PqlSearchBody> {
|
||||
}
|
||||
|
||||
class _PqlSearchInput extends StatelessWidget {
|
||||
const _PqlSearchInput({required this.controller, required this.dsl});
|
||||
const _PqlSearchInput({required this.controller, required this.dsl, this.address});
|
||||
final PqlController controller;
|
||||
final bool dsl;
|
||||
final String? address;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClideFilterBox(
|
||||
address: address,
|
||||
hint: dsl ? 'PQL query…' : 'Search vault…',
|
||||
debounce: dsl ? Duration.zero : const Duration(milliseconds: 300),
|
||||
onChanged: dsl ? (_) {} : (v) => unawaited(controller.search(v)),
|
||||
|
||||
@@ -54,7 +54,7 @@ class _ProblemsViewState extends State<ProblemsView> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ClideFilterBox(hint: 'Filter problems…', onChanged: (v) => setState(() => _filter = v)),
|
||||
ClideFilterBox(address: 'problems.panel', hint: 'Filter problems…', onChanged: (v) => setState(() => _filter = v)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||
child: Row(
|
||||
|
||||
@@ -105,7 +105,7 @@ class _SearchPanelViewState extends State<SearchPanelView> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ClideFilterBox(hint: 'Search', onChanged: c.run, onSubmitted: c.run),
|
||||
ClideFilterBox(address: 'search.findInFiles', hint: 'Search', onChanged: c.run, onSubmitted: c.run),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
@@ -137,7 +137,9 @@ class _SearchPanelViewState extends State<SearchPanelView> {
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: ClideFilterBox(hint: 'Replace', icon: null, debounce: Duration.zero, onChanged: c.setReplacement)),
|
||||
Expanded(
|
||||
child: ClideFilterBox(
|
||||
address: 'search.findInFiles.replace', hint: 'Replace', icon: null, debounce: Duration.zero, onChanged: c.setReplacement)),
|
||||
const SizedBox(width: 6),
|
||||
_ReplaceAllButton(
|
||||
enabled: c.replacement.isNotEmpty && c.matchCount > 0,
|
||||
@@ -147,9 +149,15 @@ class _SearchPanelViewState extends State<SearchPanelView> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
ClideFilterBox(hint: 'files to include (e.g. *.dart)', icon: null, debounce: Duration.zero, onChanged: (v) => c.include = v),
|
||||
ClideFilterBox(
|
||||
address: 'search.findInFiles.include',
|
||||
hint: 'files to include (e.g. *.dart)',
|
||||
icon: null,
|
||||
debounce: Duration.zero,
|
||||
onChanged: (v) => c.include = v),
|
||||
const SizedBox(height: 4),
|
||||
ClideFilterBox(hint: 'files to exclude', icon: null, debounce: Duration.zero, onChanged: (v) => c.exclude = v),
|
||||
ClideFilterBox(
|
||||
address: 'search.findInFiles.exclude', hint: 'files to exclude', icon: null, debounce: Duration.zero, onChanged: (v) => c.exclude = v),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -159,7 +159,7 @@ class _TicketsViewState extends State<TicketsView> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: ClideFilterBox(hint: 'Filter tickets…', onChanged: (v) => setState(() => _filter = v))),
|
||||
Expanded(child: ClideFilterBox(address: 'tickets.panel', hint: 'Filter tickets…', onChanged: (v) => setState(() => _filter = v))),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ClideTappable(
|
||||
|
||||
Reference in New Issue
Block a user