fix(web): converge session search boundaries (round 9)

This commit is contained in:
Hypatia May
2026-07-27 14:46:08 +08:00
parent ba2925c704
commit 0aa7f8c5cf
29 changed files with 629 additions and 157 deletions
@@ -27,6 +27,19 @@ import css from './WorkspaceBrowser.module.css'
const EXPAND_SLIDE_MS = 300
/** Pause between the latest keystroke and a Host content-search request. */
const SEARCH_DEBOUNCE_MS = 250
/** `session.search` wire bound, measured in JavaScript UTF-16 code units. */
const SEARCH_QUERY_MAX_CODE_UNITS = 500
/** Keep controlled input and RPC payload inside the session.search wire contract. */
function sanitizeSearchQuery(value: string): string {
const withoutNul = value.replaceAll('\0', '')
if (withoutNul.length <= SEARCH_QUERY_MAX_CODE_UNITS) return withoutNul
let end = SEARCH_QUERY_MAX_CODE_UNITS
const last = withoutNul.charCodeAt(end - 1)
const next = withoutNul.charCodeAt(end)
if (last >= 0xD800 && last <= 0xDBFF && next >= 0xDC00 && next <= 0xDFFF) end--
return withoutNul.slice(0, end)
}
const GROUP_BY_ITEMS = [
{ type: 'label' as const, id: 'group-by', text: 'Group by' },
@@ -255,7 +268,7 @@ function SearchResults({
return (
<div className={clsx(css.treeBody, css.wide)}>
<div className={css.list} role="tree" aria-label="搜索结果">
<div className={css.list} role="tree" aria-label="Search results">
{results.items.map(result => (
<SearchResultItem
key={result.id}
@@ -265,18 +278,18 @@ function SearchResults({
/>
))}
{pending && (
<div className={css.searchStatus} role="status"></div>
<div className={css.searchStatus} role="status">Searching session history</div>
)}
{failed && (
<div className={css.searchWarning} role="status">
Content search is temporarily unavailable. Showing name matches.
</div>
)}
{!pending && results.items.length === 0 && (
<div className={css.empty}></div>
<div className={css.empty}>No matching sessions</div>
)}
{results.hasMore && (
<div className={css.searchStatus}> 20 </div>
<div className={css.searchStatus}>Showing the first 20 results. Narrow your search.</div>
)}
</div>
<span className={css.fade} />
@@ -308,7 +321,7 @@ export function WorkspaceBrowser({
// The query outlives the tree and the input (both wide-only) so collapsing
// does not silently drop an in-progress filter.
const [query, setQuery] = useState('')
const normalizedQuery = query.trim()
const normalizedQuery = sanitizeSearchQuery(query).trim()
const [remoteSearch, setRemoteSearch] = useState<RemoteSearchState>({
query: '',
status: 'idle',
@@ -439,11 +452,11 @@ export function WorkspaceBrowser({
{/* Expanded: the row is a click-to-focus field (the leading icon is
decorative). Rail: the icon is the region's search control. */}
<div className={css.search} onClick={() => { if (wide) searchInput.current?.focus() }}>
<Tooltip label="搜索" disabled={wide}>
<Tooltip label="Search" disabled={wide}>
<button
type="button"
className={css.searchButton}
aria-label="搜索会话"
aria-label="Search sessions"
tabIndex={wide ? -1 : 0}
onClick={() => { if (!wide) { setSearchOnExpand(true); expandSidebar() } }}
>
@@ -455,16 +468,17 @@ export function WorkspaceBrowser({
ref={searchInput}
className={clsx(css.searchInput, css.wide)}
type="text"
placeholder="搜索名称或关键词…"
placeholder="Search names or content…"
maxLength={SEARCH_QUERY_MAX_CODE_UNITS}
value={query}
onChange={(e) => { setQuery(e.target.value) }}
onChange={(e) => { setQuery(sanitizeSearchQuery(e.target.value)) }}
/>
)}
{wide && query !== '' && (
<button
type="button"
className={clsx(css.clearButton, css.wide)}
aria-label="清除搜索"
aria-label="Clear search"
onClick={() => { setQuery('') }}
>
<IconCloseFill14 />