Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@
Save,
Pencil,
Code2,
Search,
} from 'lucide-svelte';
import Spinner from '../../shared/Spinner.svelte';
import FormInput from '../../shared/FormInput.svelte';
import RepoLabel from '../../shared/RepoLabel.svelte';
import RepoBadge from '../../shared/RepoBadge.svelte';
import ConfirmDialog from '../../shared/ConfirmDialog.svelte';
import type { ActionContext, ProjectAction } from '../../api/commands';
import * as commands from '../../api/commands';
import { detectRepoActions, type ActionType } from '../actions/actions';
import { repoBadgeStore } from '../../stores/repoBadges.svelte';
import { matchesRepoContextSearch } from './repoContextSearch';

type RepoAttachment = {
projectId: string;
Expand All @@ -36,6 +39,7 @@
let loadingRepoAttachments = $state(false);
let repoAttachmentsByContext = $state<Record<string, RepoAttachment[]>>({});
let repoAttachmentLoadGeneration = 0;
let repoSearch = $state('');

let actions = $state<ProjectAction[]>([]);
let loadingActions = $state(false);
Expand Down Expand Up @@ -427,6 +431,13 @@
});
});

let filteredContexts = $derived.by(() => {
const query = repoSearch.trim();
if (!query) return sortedContexts;

return sortedContexts.filter((context) => matchesRepoContextSearch(context, query));
});

let groupedActions = $derived.by(() => {
const groups: Record<string, ProjectAction[]> = {
prerun: [],
Expand Down Expand Up @@ -457,13 +468,19 @@
<div class="panel-body">
<aside class="sidebar">
<div class="sidebar-title">Repos</div>
<label class="sidebar-search">
<Search size={14} />
<FormInput bind:value={repoSearch} placeholder="Search" aria-label="Search repos" />
</label>
{#if loadingContexts}
<div class="loading-side"><Spinner size={14} /> Loading...</div>
{:else if contexts.length === 0}
<div class="empty-side">No repo contexts yet</div>
{:else if filteredContexts.length === 0}
<div class="empty-side">No repos match "{repoSearch.trim()}"</div>
{:else}
<div class="context-list">
{#each sortedContexts as context (context.id)}
{#each filteredContexts as context (context.id)}
{@const badge = repoBadgeStore.lookup(context.githubRepo, context.subpath)}
<button
class="context-item"
Expand Down Expand Up @@ -789,6 +806,23 @@
margin: 4px 6px 10px;
}

.sidebar-search {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding: 0 2px;
color: var(--text-faint);
}

.sidebar-search :global(.form-input) {
min-width: 0;
min-height: 36px;
padding: 8px 12px;
font-size: var(--size-sm);
}

.context-list {
display: flex;
flex-direction: column;
Expand Down
43 changes: 43 additions & 0 deletions apps/staged/src/lib/features/settings/repoContextSearch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest';
import { matchesRepoContextSearch } from './repoContextSearch';
import type { ActionContext } from '../../api/commands';

const context: ActionContext = {
id: 'ctx-1',
githubRepo: 'block/builderbot',
subpath: 'apps/staged',
hasDetectedActions: false,
detectingActions: false,
createdAt: 0,
updatedAt: 0,
};

describe('matchesRepoContextSearch', () => {
it('matches tokens across repo and subpath fields', () => {
expect(matchesRepoContextSearch(context, 'staged block')).toBe(true);
});

it('matches tokens regardless of order', () => {
expect(matchesRepoContextSearch(context, 'builderbot apps')).toBe(true);
});

it('matches case-insensitively', () => {
expect(matchesRepoContextSearch(context, 'BLOCK STAGED')).toBe(true);
});

it('handles missing subpaths', () => {
expect(
matchesRepoContextSearch(
{
...context,
subpath: null,
},
'builderbot'
)
).toBe(true);
});

it('requires every token to match some repo term', () => {
expect(matchesRepoContextSearch(context, 'staged unknown')).toBe(false);
});
});
19 changes: 19 additions & 0 deletions apps/staged/src/lib/features/settings/repoContextSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ActionContext } from '../../api/commands';

function searchTerms(context: ActionContext): string[] {
const githubRepo = context.githubRepo.toLowerCase();
const [org = '', repoName = ''] = githubRepo.split('/');
const subpath = context.subpath?.toLowerCase() ?? '';
const subpathParts = subpath.split('/').filter(Boolean);

return [githubRepo, org, repoName, subpath, ...subpathParts].filter(Boolean);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include full repo context string in search terms

The matcher never indexes the combined githubRepo/subpath label, so a query like block/builderbot/apps/staged (which matches how the repo is displayed in the sidebar/title) returns no results even for an exact visible context. This makes exact copy/paste searches fail unless users manually insert spaces between segments, which is a functional mismatch for the new search UX.

Useful? React with 👍 / 👎.

}

export function matchesRepoContextSearch(context: ActionContext, query: string): boolean {
const tokens = query.toLowerCase().trim().split(/\s+/).filter(Boolean);

if (tokens.length === 0) return true;

const terms = searchTerms(context);
return tokens.every((token) => terms.some((term) => term.includes(token)));
}