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
2 changes: 2 additions & 0 deletions KNOWLEDGE_BASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ Sources: [log types](app/Enums/Log.php), [log queries](app/Queries/LogQueries),

Open search with **⌘/Ctrl+K** or **/**. Developer search finds people by name or GitHub username, and lists the GitHub organizations that already have a page on /dev/null below them, by name or handle. Commit search, also available at `/search`, finds posts by meaning rather than requiring an exact phrase: describe the topic you want to read about. Commit queries must contain at least three characters and at most 100, and cover replies as well as top-level commits. An AI relevance check filters the nearest matches, so a search can return fewer results than asked. Results respect visibility, mutes, and blocks; semantic matching does not guarantee that every relevant commit will be returned. Guests can search developers and organizations; searching commits, and `/search`, need sign-in, since each commit search pays for AI. Commit search is limited to 15 searches a minute per person.

The search results page opens on **Most relevant**. **Latest** shows only matches from the past seven days, newest first, from the same result set. Switching tabs is instant and does not run another search; Latest does not look for additional recent matches outside that set.

Signed-in users see their three most recent distinct searches above the suggested searches, newest first. Repeating a search moves it to the top; adding a fourth discards the oldest from storage, not just from the display. Recent searches stay in this browser, scoped to the signed-in account, and do not sync to other devices or create server-side history. They are cleared on sign-out or account deletion in that browser. Guests do not retain recent searches.

The public search searches developers, organizations, and commits, not this knowledge-base file. Feature documentation is available to the internal knowledge-base retriever once indexed; there is no public knowledge-base search endpoint yet.
Expand Down
25 changes: 25 additions & 0 deletions resources/js/lib/commit-search-results.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Commit } from '@/types';

export class CommitSearchResults {
term: string;
commits: Commit[];

constructor(term: string, commits: Commit[]) {
this.term = term;
this.commits = commits;
}

latest(): CommitSearchResults {
const since = Date.now() - 7 * 24 * 60 * 60 * 1000;

return new CommitSearchResults(
this.term,
this.commits
.filter((commit) => Date.parse(commit.created_at) >= since)
.sort(
(a, b) =>
Date.parse(b.created_at) - Date.parse(a.created_at),
),
);
}
}
54 changes: 49 additions & 5 deletions resources/js/pages/search/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react';
import { Head, router, usePage } from '@inertiajs/react';
import { ArrowRight, History, Search, Sparkles, X } from 'lucide-react';
import type { FormEvent } from 'react';
Expand All @@ -7,11 +8,14 @@ import { SearchResults } from '@/components/commit-search-results';
import { rememberSearch, useRecentSearches } from '@/hooks/use-recent-searches';
import AppLayout from '@/layouts/app-layout';
import { searchCommits, suggestedQueries } from '@/lib/commit-search';
import type { Commit } from '@/types';
import { CommitSearchResults } from '@/lib/commit-search-results';

const skeletonDelay = 300;

type Results = { term: string; commits: Commit[] };
type Results = {
relevant: CommitSearchResults;
latest: CommitSearchResults;
};

export default function Index({ query }: { query: string }) {
const userId = usePage().props.auth.user?.id ?? null;
Expand Down Expand Up @@ -85,7 +89,43 @@ export default function Index({ query }: { query: string }) {
</header>

{query !== '' && (
<SearchResults results={results} isWaiting={isWaiting} />
<TabGroup key={query}>
<TabList
aria-label="Search result order"
className="flex border-b"
>
{['Most relevant', 'Latest'].map((title) => (
<Tab
key={title}
className="relative flex min-h-12 flex-1 justify-center py-3.5 text-[15px] font-medium text-muted-foreground outline-offset-[-3px] transition-colors hover:bg-accent hover:text-foreground data-selected:text-foreground"
>
<span className="relative">
{title}
<span
aria-hidden
className="absolute inset-x-0 -bottom-3.5 h-[3px] bg-primary opacity-0 in-data-selected:opacity-100"
/>
</span>
</Tab>
))}
</TabList>
<TabPanels>
<TabPanel>
Comment on lines +112 to +113
<SearchResults
results={results?.relevant ?? null}
isWaiting={isWaiting}
/>
</TabPanel>
<TabPanel>
<SearchResults
results={results?.latest ?? null}
isWaiting={isWaiting}
heading="Latest commits · past week"
empty="No matches from the past week. Try Most relevant for older matches, or search for another idea."
/>
</TabPanel>
</TabPanels>
</TabGroup>
)}
{recentSearches.length > 0 && (
<Suggestions onPick={pick} recent={recentSearches} />
Expand Down Expand Up @@ -114,10 +154,14 @@ function useCommitSearch(query: string) {
const timeout = setTimeout(() => setIsWaiting(true), skeletonDelay);

searchCommits(query, undefined, controller.signal)
.then((commits) => setResults({ term: query, commits }))
.then((commits) => {
const relevant = new CommitSearchResults(query, commits);
setResults({ relevant, latest: relevant.latest() });
})
.catch(() => {
if (!controller.signal.aborted) {
setResults({ term: query, commits: [] });
const relevant = new CommitSearchResults(query, []);
setResults({ relevant, latest: relevant.latest() });
}
})
.finally(() => {
Expand Down
42 changes: 42 additions & 0 deletions tests/Unit/CommitSearchResults.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { CommitSearchResults } from '../../resources/js/lib/commit-search-results.ts';
import type { Commit } from '../../resources/js/types';

await test('latest keeps the past week newest first without changing relevance order', (context) => {
context.mock.method(Date, 'now', () => Date.parse('2026-11-03T12:00:00Z'));
const commits = [
{ id: 'old', created_at: '2026-10-27T11:59:59.999Z' },
{ id: 'boundary', created_at: '2026-10-27T12:00:00Z' },
{ id: 'recent', created_at: '2026-11-02T13:00:00+01:00' },
{ id: 'newest', created_at: '2026-11-03T11:00:00Z' },
{ id: 'middle', created_at: '2026-10-30T12:00:00Z' },
] as Commit[];
const relevant = new CommitSearchResults('testing strategies', commits);

const latest = relevant.latest();

assert.equal(latest.term, 'testing strategies');
assert.deepEqual(
latest.commits.map((commit) => commit.id),
['newest', 'recent', 'middle', 'boundary'],
);
assert.deepEqual(
relevant.commits.map((commit) => commit.id),
['old', 'boundary', 'recent', 'newest', 'middle'],
);
});

await test('latest is empty when there are no recent matches', (context) => {
context.mock.method(Date, 'now', () => Date.parse('2026-11-03T12:00:00Z'));

for (const commits of [
[],
[{ id: 'old', created_at: '2026-10-20T12:00:00Z' }] as Commit[],
]) {
assert.deepEqual(
new CommitSearchResults('an idea', commits).latest().commits,
[],
);
}
});
Loading