feat(dashboard): sticky search bar with GitHub syntax for workflow definitions#42360
feat(dashboard): sticky search bar with GitHub syntax for workflow definitions#42360Copilot wants to merge 3 commits into
Conversation
- Add GitHub search syntax parser (name:, engine:, label: qualifiers) - Add definitionsSearch state initialized from localStorage - Filter definitions before paginating; persist query across sessions - Add sticky search bar UI with clear button to the Workflows tab - Rebuild web/app.js from updated src/app.ts Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
- Clarify rawValue extraction comment (match[3] = quoted content, match[2] = unquoted value) - Unknown qualifiers now fall back to text search on the value instead of literal field:value - Fix HTML tag structure in Box-title (no more Prettier ></span> collapse) - Update placeholder to document supported qualifiers: name:, engine:, label: Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
| import { paginate } from "./pagination.js"; | ||
| import type { ReportWindow } from "./dashboard-config.js"; | ||
|
|
||
| const DEFINITIONS_SEARCH_KEY = "awd-definitions-search"; |
There was a problem hiding this comment.
@copilot move search related code to search.ts
There was a problem hiding this comment.
Done — DEFINITIONS_SEARCH_KEY, SearchTerm, parseDefinitionsSearch, and matchesDefinitionSearch have been moved to src/search.ts, and app.ts now imports from it. Committed in the latest push.
|
🧠 Matt Pocock Skills Reviewer failed during the skills-based review. |
|
✅ Test Quality Sentinel completed test quality analysis. No test files were added or modified in this PR. Test Quality Sentinel skipped. The PR only changes dashboard UI files: app.ts, app.js, index.html, and styles.css. |
|
✅ PR Code Quality Reviewer completed the code quality review. |
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR does not have the 'implementation' label and has ≤100 new lines of code in business logic directories. |
There was a problem hiding this comment.
Pull request overview
Adds a persistent, qualifier-aware search experience to the Agentic Workflows Dashboard “Workflow definitions” view to make large definition lists easier to navigate, including UI updates and filtering/pagination integration.
Changes:
- Introduces parsing + matching logic for GitHub-style qualifier queries (
name:,engine:,label:) and applies filtering before pagination. - Adds a sticky search bar UI with a “Clear” affordance, plus a filtered counter display and updated empty-state messaging.
- Persists the definitions search query via
localStorageto survive reloads.
Show a summary per file
| File | Description |
|---|---|
| .github/extensions/agentic-workflows-dashboard/src/app.ts | Adds definitions search parsing/matching, filtering, pagination integration, and localStorage persistence. |
| .github/extensions/agentic-workflows-dashboard/web/index.html | Adds the search bar UI, filtered counter formatting, and revised empty-state messaging. |
| .github/extensions/agentic-workflows-dashboard/web/styles.css | Styles the new sticky search bar container. |
| .github/extensions/agentic-workflows-dashboard/web/app.js | Updates the generated/bundled dashboard script to include the new search behavior. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 4/4 changed files
- Comments generated: 4
- Review effort level: Low
| <h2 class="Box-title"> | ||
| Workflow definitions <span class="Counter ml-1" x-text="definitionsPaged.totalItems"></span> | ||
| <span class="color-fg-muted text-small ml-1" x-cloak x-show="definitionsSearch && definitionsPaged.totalItems !== definitions.length">/ <span x-text="definitions.length"></span></span> | ||
| </h2> | ||
| </div> | ||
| <!-- search bar --> | ||
| <div class="awd-search-bar"> | ||
| <div class="d-flex flex-items-center gap-2"> | ||
| <input | ||
| class="form-control input-sm flex-1" | ||
| type="search" | ||
| placeholder="Search: name:workflow, engine:claude, label:tag, or bare text" | ||
| aria-label="Search workflow definitions" | ||
| x-model="definitionsSearch" | ||
| @input="applyDefinitionsSearch()" /> | ||
| <button class="btn btn-sm" x-cloak x-show="definitionsSearch" @click="definitionsSearch = ''; applyDefinitionsSearch()">Clear</button> | ||
| </div> |
| <div class="Box-row color-fg-muted text-center py-3" x-show="!loadingDefinitions && definitionsPaged.totalItems === 0"> | ||
| <span x-show="!definitionsSearch">No workflow definitions found. Run <code>make build</code> then open this canvas again.</span><span x-show="definitionsSearch">No workflows match your search.</span> | ||
| </div> |
| definitionsSearch: (() => { | ||
| try { | ||
| return localStorage.getItem(DEFINITIONS_SEARCH_KEY) ?? ""; | ||
| } catch { | ||
| return ""; | ||
| } | ||
| })(), |
| applyDefinitionsSearch() { | ||
| try { | ||
| localStorage.setItem(DEFINITIONS_SEARCH_KEY, this.definitionsSearch); | ||
| } catch { | ||
| // localStorage unavailable (e.g. private browsing restrictions) | ||
| } | ||
| this.loadDefinitionPage(1); | ||
| }, |
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
There was a problem hiding this comment.
Review: sticky search bar for workflow definitions
The feature is well-designed — localStorage persistence, x-cloak on the counter/clear button, and graceful try/catch around storage access are all the right calls. Three non-blocking suggestions inline:
Summary of findings
| # | File | Severity | Issue |
|---|---|---|---|
| 1 | src/app.ts line 18-19 |
Low | Misleading comment for match[2]; .replace() is dead code |
| 2 | web/index.html line 119 |
Low | Empty-state inner <span> elements lack x-cloak (brief double-flash) |
| 3 | web/index.html line 90 |
Low | localStorage.setItem fires on every keystroke; suggest .debounce.300ms |
None of these block merging — the core logic is correct and the UX is a clear improvement over no search at all.
🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 62.9 AIC · ⌖ 9.08 AIC · ⊞ 4.9K
| if (match[1]) { | ||
| const field = match[1].toLowerCase(); | ||
| const rawValue = match[3] ?? match[2] ?? ""; // match[3]: content inside quotes; match[2]: unquoted value | ||
| const value = rawValue.replace(/^"|"$/g, "").toLowerCase(); |
There was a problem hiding this comment.
The comment on the previous line says match[2]: unquoted value, but when the quoted branch of the regex matches, match[2] is actually the full quoted string (e.g. "foo bar" with quotes included). match[3] is the content inside the quotes, so the ?? chain correctly prefers it.
As a consequence, the .replace(/^"|"$/g, "") on this line is effectively dead code — match[3] is already quote-free, and a bare unquoted match[2] won't have surrounding quotes to strip.
Suggested fix:
// match[3] = content inside quotes; match[2] = full value (quoted or bare)
const rawValue = match[3] ?? match[2] ?? "";
const value = rawValue.toLowerCase();@copilot please address this.
| </template> | ||
| <div class="Box-row color-fg-muted text-center py-3" x-show="!loadingDefinitions && definitionsPaged.totalItems === 0">No workflow definitions found. Run <code>make build</code> then open this canvas again.</div> | ||
| <div class="Box-row color-fg-muted text-center py-3" x-show="!loadingDefinitions && definitionsPaged.totalItems === 0"> | ||
| <span x-show="!definitionsSearch">No workflow definitions found. Run <code>make build</code> then open this canvas again.</span><span x-show="definitionsSearch">No workflows match your search.</span> |
There was a problem hiding this comment.
Both inner <span> elements lack x-cloak, so during Alpine's initialization phase they are both briefly visible together — the user sees both messages stacked for a flash.
Add x-cloak to each span so Alpine hides them until it has evaluated the x-show conditions:
<span x-cloak x-show="!definitionsSearch">No workflow definitions found. Run <code>make build</code> then open this canvas again.</span><span x-cloak x-show="definitionsSearch">No workflows match your search.</span>@copilot please address this.
| placeholder="Search: name:workflow, engine:claude, label:tag, or bare text" | ||
| aria-label="Search workflow definitions" | ||
| x-model="definitionsSearch" | ||
| @input="applyDefinitionsSearch()" /> |
There was a problem hiding this comment.
applyDefinitionsSearch() calls localStorage.setItem on every keystroke. For a search box that will be typed into rapidly this means many synchronous writes — localStorage writes block the main thread.
Use Alpine's built-in debounce modifier to throttle:
`@input`.debounce.300ms="applyDefinitionsSearch()"This persists the last value after 300 ms of idle time instead of on every character.
@copilot please address this.
There was a problem hiding this comment.
REQUEST_CHANGES — Three medium issues need fixing before merge: redundant parse-per-definition, missing debounce, and tab counter inconsistency. One low-severity dead-code cleanup is also flagged.
### Blocking themes
Performance — query reparsed per definition (medium)
matchesDefinitionSearch calls parseDefinitionsSearch inside a .filter() loop, so the regex tokenizer runs once per workflow definition instead of once per search query. Combined with the un-debounced @input, this creates O(N × keystrokes) work where O(1 × keystrokes + N) is correct. Fixable by hoisting parseDefinitionsSearch into filteredDefinitions().
Input handling — no debounce (medium)
@input fires applyDefinitionsSearch() synchronously on every character, triggering a localStorage write + full filter + paginate on each keystroke. Alpine 3's @input.debounce.200ms modifier fixes this with one word change.
UX correctness — tab badge out of sync (medium)
tabCount returns this.definitions.length regardless of active search. A user who filters down to 3 of 12 workflows sees "12" on the tab badge but "3 / 12" in the header — the counts disagree about what's currently shown.
Dead code — rawValue.replace(...) (low)
The replace is always a no-op given the regex group semantics. Should be removed to avoid misleading future readers.
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
patchdiff.githubusercontent.com
To allow these domains, add them to the
network.allowedlist in your workflow frontmatter:
network:
allowed:
- defaults
- "patchdiff.githubusercontent.com"See Network Configuration for more information.
🔎 Code quality review by PR Code Quality Reviewer · 84.4 AIC · ⌖ 7.76 AIC · ⊞ 5.2K
Comment /review to run again
Comments that could not be inline-anchored
.github/extensions/agentic-workflows-dashboard/src/app.ts:35
parseDefinitionsSearch runs once per definition, not once per query — this makes filteredDefinitions() O(N × parse) when it should be O(parse + N × match), causing avoidable UI jank as the definitions list grows.
<details>
<summary>💡 Suggested fix</summary>
Hoist the parse out of matchesDefinitionSearch and into filteredDefinitions(), then pass the already-parsed terms:
function matchesDefinitionTerms(definition: WorkflowDefinition, terms: SearchTerm[]): boolean {
const …
</details>
<details><summary>.github/extensions/agentic-workflows-dashboard/web/index.html:90</summary>
**No debounce on search input causes a localStorage write + full re-filter on every keystroke** — `applyDefinitionsSearch()` is synchronous, writes to `localStorage`, then calls `loadDefinitionPage(1)`, which re-filters all definitions. Rapid typing fires this work for each character without any delay.
<details>
<summary>💡 Suggested fix</summary>
Alpine 3 ships a native debounce event modifier — use it:
```html
`@input`.debounce.200ms="applyDefinitionsSearch()"This single change batch…
.github/extensions/agentic-workflows-dashboard/src/app.ts:454
tabCount returns unfiltered definitions.length while the inline counter shows the filtered count — when a search is active, the tab badge shows the total definition count but the box header shows filtered / total. This inconsistency will confuse users who see "12" on the tab but "3 / 12" inside the panel.
<details>
<summary>💡 Suggested fix</summary>
tabCount(tab) {
if (tab.counter === "definitions") {
return this.definitionsSearch ? this.filteredDefinitions().length : t…
</details>
<details><summary>.github/extensions/agentic-workflows-dashboard/src/app.ts:19</summary>
**`rawValue.replace(/^"|"$/g, "")` is dead code** — `match[3]` already holds the unquoted inner content of a quoted string, and `match[2]` for an unquoted token has no surrounding quotes. The replace never strips anything and implies quote-handling coverage that isn't actually tested.
<details>
<summary>💡 Suggested simplification</summary>
Given the regex groups:
- `label:"my tag"` → `match[3]` = `my tag` (no quotes) → replace is a no-op
- `engine:claude` → `match[2]` = `claude` (no quotes) …
</details>
The Workflows tab had no way to filter definitions — with large workflow lists, finding a specific workflow required manually scanning. Adds a sticky, localStorage-persisted search bar supporting GitHub-style qualifier syntax.
Search syntax
Qualifiers are AND-combined; bare terms match across all fields:
claudename:ci-doctorengine:claudelabel:productionengine:claude label:prodUnknown qualifiers fall back to text search on the value. All matching is case-insensitive.
Changes
src/app.tsparseDefinitionsSearch(query)— tokenises query into typedSearchTerm[]; handles quoted values (label:"my tag")matchesDefinitionSearch(definition, query)— applies terms againstworkflow,engine_id,labelsdefinitionsSearchstate — initialised fromlocalStorageat component creation time so queries survive reloadsapplyDefinitionsSearch()— persists tolocalStorage, resets to page 1loadDefinitionPage()— now paginatesfilteredDefinitions()instead of rawdefinitionsweb/index.html— sticky search bar between Box-header and scroll area; "Clear" button when active; header counter shows3 / 12format when filtered; empty-state message adapts to indicate no search matches vs no workflows installedweb/styles.css—.awd-search-barwithposition: sticky; top: 0; z-index: 2