add ClideFilterBox widget to all sidebar panes

Shared filter box with search icon, debounced input, and clear
button. Applied to all six left-panel panes for consistent
filtering: Files (flat path results when filtering), Git (filter
staged/unstaged/untracked by path), Decisions (by ID/title/domain),
Tickets (by ID/title/status), Problems (by source/message), and
pql Query (replaces custom _QueryInput).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 07:56:39 +02:00
co-authored by Claude Opus 4.6
parent 76029a02a0
commit eb8c9ecf5d
10 changed files with 270 additions and 144 deletions
@@ -38,6 +38,14 @@ class FileTreeController extends ChangeNotifier {
final Map<String, List<FileEntry>> _entries = {};
List<FileEntry>? entriesFor(String path) => _entries[path];
List<FileEntry> allLoadedEntries() {
final out = <FileEntry>[];
for (final list in _entries.values) {
out.addAll(list.where((e) => !e.isDirectory));
}
return out;
}
/// Initial boot: resolve the workspace root, load the root dir,
/// subscribe to `files.changed` events.
Future<void> load() async {
+70 -19
View File
@@ -5,6 +5,8 @@ import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/widgets.dart';
import 'package:clide/src/files/listing.dart' show FileEntry;
import 'file_tree_controller.dart';
/// Sidebar panel rendering the workspace file tree.
@@ -24,6 +26,7 @@ class FileTreeView extends StatefulWidget {
class _FileTreeViewState extends State<FileTreeView> {
FileTreeController? _controller;
String _filter = '';
@override
void didChangeDependencies() {
@@ -61,31 +64,45 @@ class _FileTreeViewState extends State<FileTreeView> {
);
}
final rootName = root.split(Platform.pathSeparator).last;
return Semantics(
label: 'file tree — $rootName',
container: true,
explicitChildNodes: true,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
_DirRow(
name: rootName,
path: '',
controller: c,
depth: 0,
return Column(
children: [
ClideFilterBox(hint: 'Filter files…', onChanged: (v) => setState(() => _filter = v)),
Expanded(
child: Semantics(
label: 'file tree — $rootName',
container: true,
explicitChildNodes: true,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
if (_filter.isEmpty) ...[
_DirRow(name: rootName, path: '', controller: c, depth: 0),
if (c.isExpanded('')) _Children(path: '', controller: c, depth: 1),
] else
..._filteredEntries(c),
],
),
),
if (c.isExpanded(''))
_Children(path: '', controller: c, depth: 1),
],
),
),
),
],
);
},
);
}
List<Widget> _filteredEntries(FileTreeController c) {
final lowerFilter = _filter.toLowerCase();
final matches = c.allLoadedEntries().where((e) {
return e.path.toLowerCase().contains(lowerFilter) || e.name.toLowerCase().contains(lowerFilter);
}).toList();
return [
for (final e in matches) _FilteredFileRow(entry: e),
];
}
}
class _Children extends StatelessWidget {
@@ -269,3 +286,37 @@ class _RowState extends State<_Row> {
}
}
class _FilteredFileRow extends StatefulWidget {
const _FilteredFileRow({required this.entry});
final FileEntry entry;
@override
State<_FilteredFileRow> createState() => _FilteredFileRowState();
}
class _FilteredFileRowState extends State<_FilteredFileRow> {
bool _hover = false;
@override
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => setState(() => _hover = true),
onExit: (_) => setState(() => _hover = false),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
final kernel = ClideKernel.of(context);
unawaited(kernel.ipc.request('editor.open', args: {'path': widget.entry.path}));
},
child: Container(
color: _hover ? tokens.sidebarItemHover : null,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3),
child: ClideText(widget.entry.path, maxLines: 1, overflow: TextOverflow.ellipsis, color: tokens.sidebarForeground),
),
),
);
}
}