Skip to content

Commit ce74682

Browse files
Tom Maloneywesm
authored andcommitted
feat: search pane refinement -- session-grouped results, sort, name search, is_system backfill
- Remove planning documents
1 parent a36bb0f commit ce74682

34 files changed

+4778
-153
lines changed

cmd/agentsview/main.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,4 +664,3 @@ func startUnwatchedPoll(engine *sync.Engine) {
664664
engine.SyncAll(context.Background(), nil)
665665
}
666666
}
667-

cmd/agentsview/pg.go

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ func runPGPush(args []string) {
4242
fs := flag.NewFlagSet("pg push", flag.ExitOnError)
4343
full := fs.Bool("full", false,
4444
"Force full local resync and PG push")
45+
clearBackfill := fs.String("clear-backfill", "",
46+
"Clear backfill_pending flag for a retired machine "+
47+
"that can no longer push")
4548
if err := fs.Parse(args); err != nil {
4649
log.Fatalf("parsing flags: %v", err)
4750
}
@@ -55,6 +58,41 @@ func runPGPush(args []string) {
5558
}
5659
setupLogFile(appCfg.DataDir)
5760

61+
pgCfg, err := appCfg.ResolvePG()
62+
if err != nil {
63+
fatal("pg push: %v", err)
64+
}
65+
if pgCfg.URL == "" {
66+
fatal("pg push: url not configured")
67+
}
68+
69+
// --clear-backfill is a PG-only operation: skip local
70+
// sync/SQLite setup entirely.
71+
if *clearBackfill != "" {
72+
pgDB, oErr := postgres.Open(
73+
pgCfg.URL, pgCfg.Schema, pgCfg.AllowInsecure,
74+
)
75+
if oErr != nil {
76+
fatal("pg push: %v", oErr)
77+
}
78+
defer pgDB.Close()
79+
ctx, stop := signal.NotifyContext(
80+
context.Background(), os.Interrupt,
81+
)
82+
defer stop()
83+
if err := postgres.ClearBackfillPending(
84+
ctx, pgDB, *clearBackfill,
85+
); err != nil {
86+
fatal("pg push: clearing backfill for %s: %v",
87+
*clearBackfill, err)
88+
}
89+
fmt.Printf(
90+
"Cleared backfill_pending for machine %q\n",
91+
*clearBackfill,
92+
)
93+
return
94+
}
95+
5896
database, err := db.Open(appCfg.DBPath)
5997
if err != nil {
6098
fatal("opening database: %v", err)
@@ -78,14 +116,6 @@ func runPGPush(args []string) {
78116
didResync := runLocalSync(appCfg, database, *full)
79117
forceFull := *full || didResync
80118

81-
pgCfg, err := appCfg.ResolvePG()
82-
if err != nil {
83-
fatal("pg push: %v", err)
84-
}
85-
if pgCfg.URL == "" {
86-
fatal("pg push: url not configured")
87-
}
88-
89119
ps, err := postgres.New(
90120
pgCfg.URL, pgCfg.Schema, database,
91121
pgCfg.MachineName, pgCfg.AllowInsecure,
@@ -100,9 +130,34 @@ func runPGPush(args []string) {
100130
)
101131
defer stop()
102132

133+
// Read the schema version before EnsureSchema upgrades it so
134+
// we can detect a v1→v2 transition and force a full push.
135+
// Errors (e.g. pre-v1 schema where sync_metadata doesn't
136+
// exist yet) are treated as version 0.
137+
priorVersion, _ := postgres.GetSchemaVersion(ctx, ps.DB())
103138
if err := ps.EnsureSchema(ctx); err != nil {
104139
fatal("pg push schema: %v", err)
105140
}
141+
if !forceFull && priorVersion < postgres.SchemaVersion {
142+
forceFull = true
143+
log.Printf(
144+
"pg push: schema upgrade v%d→v%d detected; "+
145+
"forcing full push to backfill is_system",
146+
priorVersion, postgres.SchemaVersion,
147+
)
148+
}
149+
// Also force full if a previous push was interrupted
150+
// mid-backfill (backfill_pending flag is still set).
151+
if !forceFull && postgres.IsBackfillPendingForMachine(
152+
ctx, ps.DB(), pgCfg.MachineName,
153+
) {
154+
forceFull = true
155+
log.Printf(
156+
"pg push: is_system backfill incomplete " +
157+
"(previous push interrupted); " +
158+
"forcing full push to resume",
159+
)
160+
}
106161
result, err := ps.Push(ctx, forceFull)
107162
if err != nil {
108163
fatal("pg push: %v", err)
@@ -216,7 +271,10 @@ func runPGServe(args []string) {
216271
if err := postgres.CheckSchemaCompat(
217272
ctx, store.DB(),
218273
); err != nil {
219-
fatal("pg serve: schema incompatible: %v", err)
274+
fatal("pg serve: schema incompatible: %v\n"+
275+
"Run 'agentsview pg push --full' from a machine "+
276+
"with write access to migrate the schema and "+
277+
"backfill any newly added columns.", err)
220278
}
221279

222280
appCfg.Host = *host

frontend/src/lib/api/client.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,16 @@ describe("query serialization", () => {
591591
expect(lastUrl()).toBe("/api/v1/search?q=hello");
592592
});
593593

594+
it("includes sort param when provided", async () => {
595+
await search("hello", { sort: "recency" });
596+
expect(lastUrl()).toBe("/api/v1/search?q=hello&sort=recency");
597+
});
598+
599+
it("omits sort param when not provided", async () => {
600+
await search("hello");
601+
expect(lastUrl()).toBe("/api/v1/search?q=hello");
602+
});
603+
594604
it("rejects empty query string", () => {
595605
expect(() => search("")).toThrow(
596606
"search query must not be empty",

frontend/src/lib/api/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ export function search(
212212
project?: string;
213213
limit?: number;
214214
cursor?: number;
215+
sort?: "relevance" | "recency";
215216
} = {},
216217
init?: RequestInit,
217218
): Promise<SearchResponse> {

frontend/src/lib/api/types/core.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export interface Message {
6969
context_tokens: number;
7070
output_tokens: number;
7171
tool_calls?: ToolCall[];
72+
is_system: boolean;
7273
}
7374

7475
/** Matches Go MinimapEntry struct */
@@ -81,9 +82,10 @@ export type MinimapEntry = Pick<
8182
export interface SearchResult {
8283
session_id: string;
8384
project: string;
85+
agent: string;
86+
name: string;
8487
ordinal: number;
85-
role: string;
86-
timestamp: string;
88+
session_ended_at: string;
8789
snippet: string;
8890
rank: number;
8991
}

frontend/src/lib/components/command-palette/CommandPalette.svelte

Lines changed: 113 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { tick } from "svelte";
2+
import { tick, onDestroy } from "svelte";
33
import { ui } from "../../stores/ui.svelte.js";
44
import { sessions } from "../../stores/sessions.svelte.js";
55
import { searchStore } from "../../stores/search.svelte.js";
@@ -10,12 +10,23 @@
1010
sanitizeSnippet,
1111
} from "../../utils/format.js";
1212
import { agentColor } from "../../utils/agents.js";
13+
import { copyToClipboard } from "../../utils/clipboard.js";
14+
import { stripIdPrefix } from "../../utils/resume.js";
1315
import type { Session, SearchResult } from "../../api/types.js";
1416
1517
let inputRef: HTMLInputElement | undefined = $state(undefined);
1618
let selectedIndex: number = $state(0);
1719
let inputValue: string = $state("");
1820
21+
// Clear state and reset sort whenever the palette is unmounted, regardless
22+
// of close path (Escape key, overlay click, Cmd+K toggle, or any other
23+
// mechanism). This ensures stale results and in-flight requests are always
24+
// cancelled even when the caller bypasses close().
25+
onDestroy(() => {
26+
searchStore.clear();
27+
searchStore.resetSort();
28+
});
29+
1930
// Filtered recent sessions (client-side filter)
2031
let recentSessions = $derived.by(() => {
2132
if (inputValue.length > 0 && inputValue.length < 3) {
@@ -92,13 +103,18 @@
92103
93104
function selectSearchResult(r: SearchResult) {
94105
sessions.selectSession(r.session_id);
95-
ui.scrollToOrdinal(r.ordinal, r.session_id);
106+
if (r.ordinal !== -1) {
107+
ui.scrollToOrdinal(r.ordinal, r.session_id);
108+
} else {
109+
// Name-only match: clear any stale selection/scroll state so the
110+
// previously highlighted ordinal is not left active.
111+
ui.clearScrollState();
112+
}
96113
close();
97114
}
98115
99116
function close() {
100117
inputValue = "";
101-
searchStore.clear();
102118
ui.activeModal = null;
103119
}
104120
@@ -149,6 +165,20 @@
149165

150166
<div class="palette-results">
151167
{#if showSearchResults}
168+
<div class="palette-sort">
169+
<button
170+
class="sort-btn"
171+
class:active={searchStore.sort === "relevance"}
172+
onmousedown={(e: MouseEvent) => e.preventDefault()}
173+
onclick={() => { searchStore.setSort("relevance"); selectedIndex = 0; }}
174+
>Relevance</button>
175+
<button
176+
class="sort-btn"
177+
class:active={searchStore.sort === "recency"}
178+
onmousedown={(e: MouseEvent) => e.preventDefault()}
179+
onclick={() => { searchStore.setSort("recency"); selectedIndex = 0; }}
180+
>Recency</button>
181+
</div>
152182
{#if searchStore.isSearching}
153183
<div class="palette-empty">Searching...</div>
154184
{:else if searchStore.results.length === 0}
@@ -161,15 +191,33 @@
161191
onclick={() => selectSearchResult(result)}
162192
onmouseenter={() => (selectedIndex = i)}
163193
>
164-
<span class="item-role" class:user={result.role === "user"}>
165-
{result.role === "user" ? "U" : "A"}
166-
</span>
167-
<span class="item-text">
168-
{@html sanitizeSnippet(result.snippet)}
194+
<span
195+
class="item-dot"
196+
style:background={agentColor(result.agent)}
197+
></span>
198+
<span class="item-body">
199+
{#if result.name}
200+
<span class="item-name">{truncate(result.name, 60)}</span>
201+
{/if}
202+
{#if result.snippet && result.snippet.replace(/<\/?mark>/g, '') !== result.name}
203+
<span class="item-snippet">
204+
{@html sanitizeSnippet(result.snippet)}
205+
</span>
206+
{/if}
169207
</span>
170208
<span class="item-meta">
171-
{truncate(result.project, 20)}
209+
{truncate(result.project, 20)}{result.session_ended_at ? ' · ' + formatRelativeTime(result.session_ended_at) : ''}
172210
</span>
211+
<!-- svelte-ignore a11y_click_events_have_key_events -->
212+
<!-- svelte-ignore a11y_no_static_element_interactions -->
213+
<span
214+
class="item-id"
215+
title="Copy session ID"
216+
onclick={(e) => {
217+
e.stopPropagation();
218+
copyToClipboard(result.session_id);
219+
}}
220+
>{stripIdPrefix(result.session_id, result.agent).slice(0, 8)}</span>
173221
</button>
174222
{/each}
175223
{/if}
@@ -183,10 +231,10 @@
183231
onmouseenter={() => (selectedIndex = i)}
184232
>
185233
<span class="item-dot" style:background={agentColor(session.agent)}></span>
186-
<span class="item-text">
187-
{session.first_message
234+
<span class="item-body">
235+
<span class="item-name">{session.first_message
188236
? truncate(session.first_message, 60)
189-
: session.project}
237+
: session.project}</span>
190238
</span>
191239
<span class="item-meta">
192240
{formatRelativeTime(session.ended_at ?? session.started_at)}
@@ -296,31 +344,28 @@
296344
flex-shrink: 0;
297345
}
298346
299-
.item-role {
300-
width: 18px;
301-
height: 18px;
347+
.item-body {
348+
flex: 1;
349+
min-width: 0;
302350
display: flex;
303-
align-items: center;
304-
justify-content: center;
305-
border-radius: var(--radius-sm);
306-
font-size: 10px;
307-
font-weight: 700;
308-
flex-shrink: 0;
309-
background: var(--assistant-bg);
310-
color: var(--accent-purple);
351+
flex-direction: column;
352+
gap: 1px;
311353
}
312354
313-
.item-role.user {
314-
background: var(--user-bg);
315-
color: var(--accent-blue);
355+
.item-name {
356+
white-space: nowrap;
357+
overflow: hidden;
358+
text-overflow: ellipsis;
359+
font-size: 13px;
360+
color: var(--text-primary);
316361
}
317362
318-
.item-text {
319-
flex: 1;
320-
min-width: 0;
363+
.item-snippet {
321364
white-space: nowrap;
322365
overflow: hidden;
323366
text-overflow: ellipsis;
367+
font-size: 11px;
368+
color: var(--text-muted);
324369
}
325370
326371
.item-meta {
@@ -336,4 +381,43 @@
336381
color: var(--text-muted);
337382
font-size: 13px;
338383
}
384+
385+
.palette-sort {
386+
display: flex;
387+
gap: 4px;
388+
padding: 6px 14px 2px;
389+
}
390+
391+
.sort-btn {
392+
padding: 2px 8px;
393+
font-size: 11px;
394+
border: 1px solid var(--border-default);
395+
border-radius: var(--radius-sm);
396+
background: none;
397+
color: var(--text-muted);
398+
cursor: pointer;
399+
font-family: var(--font-sans);
400+
}
401+
402+
.sort-btn.active {
403+
background: var(--bg-surface-hover);
404+
color: var(--text-primary);
405+
border-color: var(--accent-purple);
406+
}
407+
408+
.item-id {
409+
font-family: var(--font-mono);
410+
font-size: 10px;
411+
color: var(--text-muted);
412+
white-space: nowrap;
413+
flex-shrink: 0;
414+
cursor: pointer;
415+
padding: 1px 3px;
416+
border-radius: var(--radius-sm);
417+
}
418+
419+
.item-id:hover {
420+
background: var(--bg-inset);
421+
color: var(--text-primary);
422+
}
339423
</style>

0 commit comments

Comments
 (0)