|
1 | 1 | <script lang="ts"> |
2 | | - import { tick } from "svelte"; |
| 2 | + import { tick, onDestroy } from "svelte"; |
3 | 3 | import { ui } from "../../stores/ui.svelte.js"; |
4 | 4 | import { sessions } from "../../stores/sessions.svelte.js"; |
5 | 5 | import { searchStore } from "../../stores/search.svelte.js"; |
|
10 | 10 | sanitizeSnippet, |
11 | 11 | } from "../../utils/format.js"; |
12 | 12 | import { agentColor } from "../../utils/agents.js"; |
| 13 | + import { copyToClipboard } from "../../utils/clipboard.js"; |
| 14 | + import { stripIdPrefix } from "../../utils/resume.js"; |
13 | 15 | import type { Session, SearchResult } from "../../api/types.js"; |
14 | 16 |
|
15 | 17 | let inputRef: HTMLInputElement | undefined = $state(undefined); |
16 | 18 | let selectedIndex: number = $state(0); |
17 | 19 | let inputValue: string = $state(""); |
18 | 20 |
|
| 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 | +
|
19 | 30 | // Filtered recent sessions (client-side filter) |
20 | 31 | let recentSessions = $derived.by(() => { |
21 | 32 | if (inputValue.length > 0 && inputValue.length < 3) { |
|
92 | 103 |
|
93 | 104 | function selectSearchResult(r: SearchResult) { |
94 | 105 | 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 | + } |
96 | 113 | close(); |
97 | 114 | } |
98 | 115 |
|
99 | 116 | function close() { |
100 | 117 | inputValue = ""; |
101 | | - searchStore.clear(); |
102 | 118 | ui.activeModal = null; |
103 | 119 | } |
104 | 120 |
|
|
149 | 165 |
|
150 | 166 | <div class="palette-results"> |
151 | 167 | {#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> |
152 | 182 | {#if searchStore.isSearching} |
153 | 183 | <div class="palette-empty">Searching...</div> |
154 | 184 | {:else if searchStore.results.length === 0} |
|
161 | 191 | onclick={() => selectSearchResult(result)} |
162 | 192 | onmouseenter={() => (selectedIndex = i)} |
163 | 193 | > |
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} |
169 | 207 | </span> |
170 | 208 | <span class="item-meta"> |
171 | | - {truncate(result.project, 20)} |
| 209 | + {truncate(result.project, 20)}{result.session_ended_at ? ' · ' + formatRelativeTime(result.session_ended_at) : ''} |
172 | 210 | </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> |
173 | 221 | </button> |
174 | 222 | {/each} |
175 | 223 | {/if} |
|
183 | 231 | onmouseenter={() => (selectedIndex = i)} |
184 | 232 | > |
185 | 233 | <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 |
188 | 236 | ? truncate(session.first_message, 60) |
189 | | - : session.project} |
| 237 | + : session.project}</span> |
190 | 238 | </span> |
191 | 239 | <span class="item-meta"> |
192 | 240 | {formatRelativeTime(session.ended_at ?? session.started_at)} |
|
296 | 344 | flex-shrink: 0; |
297 | 345 | } |
298 | 346 |
|
299 | | - .item-role { |
300 | | - width: 18px; |
301 | | - height: 18px; |
| 347 | + .item-body { |
| 348 | + flex: 1; |
| 349 | + min-width: 0; |
302 | 350 | 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; |
311 | 353 | } |
312 | 354 |
|
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); |
316 | 361 | } |
317 | 362 |
|
318 | | - .item-text { |
319 | | - flex: 1; |
320 | | - min-width: 0; |
| 363 | + .item-snippet { |
321 | 364 | white-space: nowrap; |
322 | 365 | overflow: hidden; |
323 | 366 | text-overflow: ellipsis; |
| 367 | + font-size: 11px; |
| 368 | + color: var(--text-muted); |
324 | 369 | } |
325 | 370 |
|
326 | 371 | .item-meta { |
|
336 | 381 | color: var(--text-muted); |
337 | 382 | font-size: 13px; |
338 | 383 | } |
| 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 | + } |
339 | 423 | </style> |
0 commit comments