From 226520cfceedbdcda6109e318b6da77ebfa04f23 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 23 Apr 2026 22:22:16 +0200 Subject: [PATCH] add ranked search to pql sidebar with DSL toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Search tab defaults to ranked text search via pql search — debounced 300ms, scored results with visual score bar. DSL toggle switches to raw PQL query mode for SQL-like queries. Clicking a search result opens it in the markdown context viewer. Adds pql.search IPC command and PqlClient.search method. Controller gains SearchMode enum and search() method alongside existing runQuery. Co-Authored-By: Claude --- CHANGELOG.md | 9 +- lib/builtin/pql/src/pql_controller.dart | 43 +++++++- lib/builtin/pql/src/pql_panel_view.dart | 139 ++++++++++++++++++++++-- lib/src/daemon/pql_commands.dart | 14 +++ lib/src/pql/client.dart | 6 + 5 files changed, 194 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75e5ad9c..66bb0e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,10 +86,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. - `files.read` IPC command for reading file content by path. -- pql sidebar restructured: Search tab (PQL DSL query) is the - default left tab; Markdown tab (filtered to `.md` files) on the - right. Clicking a markdown file opens it in the context panel - markdown viewer with bidirectional focus highlighting. +- pql sidebar restructured: Search tab is the default left tab with + ranked text search (debounced, scored results with score bar) and + a DSL toggle for raw PQL query mode; Markdown tab (filtered to + `.md` files) on the right. Clicking a result opens it in the + context panel markdown viewer with bidirectional focus highlighting. - Graph view in context panel — lists files with inbound/outbound link counts from `pql search --connections` (T-39). diff --git a/lib/builtin/pql/src/pql_controller.dart b/lib/builtin/pql/src/pql_controller.dart index e9f6eccf..1a7bdaad 100644 --- a/lib/builtin/pql/src/pql_controller.dart +++ b/lib/builtin/pql/src/pql_controller.dart @@ -1,7 +1,7 @@ /// State model for the pql sidebar panel. /// -/// Manages schema cache, query execution, file listing, and -/// decision/ticket views. All data comes through pql.* IPC verbs. +/// Manages search, DSL query execution, and markdown file listing. +/// All data comes through pql.* IPC verbs. library; import 'dart:async'; @@ -11,6 +11,8 @@ import 'package:flutter/foundation.dart'; enum PqlView { query, markdown } +enum SearchMode { search, dsl } + class PqlController extends ChangeNotifier { PqlController({required this.ipc}); @@ -19,6 +21,9 @@ class PqlController extends ChangeNotifier { PqlView _view = PqlView.query; PqlView get view => _view; + SearchMode _searchMode = SearchMode.search; + SearchMode get searchMode => _searchMode; + String? _error; String? get error => _error; @@ -45,6 +50,40 @@ class PqlController extends ChangeNotifier { } } + void toggleSearchMode() { + _searchMode = _searchMode == SearchMode.search ? SearchMode.dsl : SearchMode.search; + _results = const []; + _error = null; + notifyListeners(); + } + + Future search(String terms) async { + if (terms.trim().isEmpty) { + _results = const []; + _error = null; + notifyListeners(); + return; + } + _loading = true; + _error = null; + notifyListeners(); + + final r = await ipc.request('pql.search', args: { + 'terms': terms, + 'limit': 50, + }); + + _loading = false; + if (!r.ok) { + _error = r.error?.message; + _results = const []; + notifyListeners(); + return; + } + _results = _castList(r.data['results']); + notifyListeners(); + } + Future loadMarkdownFiles({String? glob}) async { _loading = true; notifyListeners(); diff --git a/lib/builtin/pql/src/pql_panel_view.dart b/lib/builtin/pql/src/pql_panel_view.dart index 7322ed2f..612f92af 100644 --- a/lib/builtin/pql/src/pql_panel_view.dart +++ b/lib/builtin/pql/src/pql_panel_view.dart @@ -1,4 +1,4 @@ -/// Sidebar panel for pql — DSL query input and markdown file listing. +/// Sidebar panel for pql — ranked search, DSL query, and markdown file listing. library; import 'dart:async'; @@ -63,13 +63,9 @@ class _PqlPanelViewState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _ViewTabs(controller: c), - if (c.view == PqlView.query) - ClideFilterBox( - hint: 'PQL query…', - debounce: Duration.zero, - onChanged: (_) {}, - onSubmitted: (v) => unawaited(c.runQuery(v)), - ), + if (c.view == PqlView.query) ...[ + _SearchInput(controller: c), + ], if (c.view == PqlView.markdown) ClideFilterBox( hint: 'Filter markdown…', @@ -82,8 +78,8 @@ class _PqlPanelViewState extends State { ), if (c.loading && c.results.isEmpty) const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)), - if (!c.loading && c.results.isEmpty && c.error == null) - const Padding(padding: EdgeInsets.all(12), child: ClideText('No results.', muted: true)), + if (!c.loading && c.results.isEmpty && c.error == null && c.view == PqlView.markdown) + const Padding(padding: EdgeInsets.all(12), child: ClideText('No markdown files found.', muted: true)), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 4), @@ -98,7 +94,9 @@ class _PqlPanelViewState extends State { focused: (f['path'] as String?) == _focusedPath, focusKey: (f['path'] as String?) == _focusedPath ? _focusedKey : null, ), - if (c.view == PqlView.query) + if (c.view == PqlView.query && c.searchMode == SearchMode.search) + for (final r in c.results) _SearchResultRow(entry: r), + if (c.view == PqlView.query && c.searchMode == SearchMode.dsl) for (final r in c.results) _QueryResultRow(entry: r), ], ), @@ -151,6 +149,125 @@ class _ViewTabs extends StatelessWidget { }; } +class _SearchInput extends StatelessWidget { + const _SearchInput({required this.controller}); + final PqlController controller; + + @override + Widget build(BuildContext context) { + final tokens = ClideTheme.of(context).surface; + final isDsl = controller.searchMode == SearchMode.dsl; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ClideFilterBox( + hint: isDsl ? 'PQL query…' : 'Search vault…', + debounce: isDsl ? Duration.zero : const Duration(milliseconds: 300), + onChanged: isDsl ? (_) {} : (v) => unawaited(controller.search(v)), + onSubmitted: isDsl ? (v) => unawaited(controller.runQuery(v)) : (v) => unawaited(controller.search(v)), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + child: Row( + children: [ + GestureDetector( + onTap: controller.toggleSearchMode, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: isDsl ? tokens.globalFocus.withAlpha(0x30) : null, + borderRadius: BorderRadius.circular(3), + border: Border.all(color: isDsl ? tokens.globalFocus : tokens.panelBorder), + ), + child: ClideText('DSL', fontSize: clideFontBadge, color: isDsl ? tokens.globalFocus : tokens.globalTextMuted, fontFamily: clideMonoFamily), + ), + ), + ), + const SizedBox(width: 6), + ClideText( + isDsl ? 'SQL-like query mode' : 'ranked text search', + fontSize: clideFontBadge, + muted: true, + ), + ], + ), + ), + ], + ); + } +} + +class _SearchResultRow extends StatelessWidget { + const _SearchResultRow({required this.entry}); + final Map entry; + + @override + Widget build(BuildContext context) { + final tokens = ClideTheme.of(context).surface; + final path = entry['path'] as String? ?? ''; + final score = (entry['score'] as num?)?.toDouble() ?? 0; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1), + child: ClideTappable( + onTap: () { + if (path.endsWith('.md')) { + ClideKernel.of(context).messages.publish('builtin.markdown', 'selection', {'path': path}); + } + }, + builder: (context, hovered, _) => Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: hovered ? tokens.sidebarItemHover : null, + borderRadius: BorderRadius.circular(4), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + ClideText(path, fontSize: clideFontCaption, color: tokens.sidebarForeground, maxLines: 1, overflow: TextOverflow.ellipsis), + const SizedBox(height: 3), + _ScoreBar(score: score, tokens: tokens), + ], + ), + ), + ), + ); + } +} + +class _ScoreBar extends StatelessWidget { + const _ScoreBar({required this.score, required this.tokens}); + final double score; + final SurfaceTokens tokens; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SizedBox( + width: 60, + height: 3, + child: ClipRRect( + borderRadius: BorderRadius.circular(1.5), + child: ColoredBox( + color: tokens.panelBorder, + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: score.clamp(0, 1), + child: ColoredBox(color: tokens.globalFocus), + ), + ), + ), + ), + const SizedBox(width: 6), + ClideText('${(score * 100).round()}%', fontSize: clideFontBadge, muted: true, fontFamily: clideMonoFamily), + ], + ); + } +} + class _FileRow extends StatelessWidget { const _FileRow({required this.entry, this.focused = false, this.focusKey}); final Map entry; diff --git a/lib/src/daemon/pql_commands.dart b/lib/src/daemon/pql_commands.dart index 21288743..dc1e34b2 100644 --- a/lib/src/daemon/pql_commands.dart +++ b/lib/src/daemon/pql_commands.dart @@ -94,6 +94,20 @@ void registerPqlCommands(DaemonDispatcher d, PqlClient pql) { } }); + d.register('pql.search', (req) async { + final terms = req.args['terms'] as String?; + if (terms == null || terms.isEmpty) { + return _userError(req.id, 'pql.search requires a terms string'); + } + try { + final limit = (req.args['limit'] as num?)?.toInt(); + final results = await pql.search(terms, limit: limit); + return IpcResponse.ok(id: req.id, data: {'results': results}); + } on PqlException catch (e) { + return _pqlError(req.id, e); + } + }); + d.register('pql.doctor', (req) async { try { final report = await pql.doctor(); diff --git a/lib/src/pql/client.dart b/lib/src/pql/client.dart index 6770a3e7..345d7f35 100644 --- a/lib/src/pql/client.dart +++ b/lib/src/pql/client.dart @@ -59,6 +59,12 @@ class PqlClient { return _runList(args); } + Future>> search(String terms, {int? limit}) async { + final args = ['search', terms]; + if (limit != null) args.addAll(['--limit', '$limit']); + return _runList(args); + } + Future> doctor() async { return _runObject(['doctor']); }