feat(search): replace the repo search box with query qualifiers
One palette owns search: type:/repo:/space:/lang: qualifiers parsed out of the query, rendered as removable chips, and "/" opens the palette from anywhere outside a text field. The per-repo box and its duplicated message keys go away with it.
ZhenYi committed
Modified web/src/app/styles/workspace.css web/src/app/styles/workspace.css +2 −1 View web/src/app/styles/workspace.css
@@ -58,7 +58,8 @@
5858 * window, so every bar that can lead the content column reserves a gutter for
5959 * it — otherwise it sits on top of the bar's own actions. Below `lg` the
6060 * overlay collapses to its icon, so the gutter stays small enough that the bar
61 * keeps a usable title.
61 * keeps a usable title. The overlay also centres on the bar's own height
62 * (76px, 36px box), so it never drifts off the bar's optical centre line.
6263 */
6364@media (min-width: 768px) {
6465 .workspace-main :is(.shell-top-bar, .chat-header) { padding-right: 64px; }
Deleted web/src/features/repo-browse/ui/repo-code-search.tsx web/src/features/repo-browse/ui/repo-code-search.tsx +0 −252
@@ -1,252 +0,0 @@
1import { useEffect, useMemo, useRef, useState } from 'react'
2import { CornerDownLeft, FileCode2, Loader2, Search, X } from 'lucide-react'
3import { t, useT } from '@/shared/i18n'
4import { search, type SearchHit } from '@/entities/search'
5
6/** 高亮命中片段里的查询词,命中为空或含正则字符时原样返回。 */
7function highlight(text: string, query: string) {
8 const needle = query.trim()
9 if (!needle) return text
10 const parts = text.split(new RegExp(`(${needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'ig'))
11 return parts.map((part, index) =>
12 part.toLowerCase() === needle.toLowerCase() ? (
13 <mark key={index} className="rounded bg-[#f0b232]/35 px-0.5 text-shell-ink">{part}</mark>
14 ) : (
15 <span key={index}>{part}</span>
16 ),
17 )
18}
19
20interface Group {
21 kind: SearchHit['kind']
22 label: string
23 hits: SearchHit[]
24}
25
26/** 仓库内搜索只问代码与符号,这里的标签只有这两类。 */
27const KIND_LABEL: Record<'code' | 'symbol', string> = {
28 get code() { return t('repo.search.kind.code') },
29 get symbol() { return t('repo.search.kind.symbol') },
30}
31
32export function RepoCodeSearch({
33 space,
34 repo,
35 currentRevision,
36 defaultBranch,
37 onOpen,
38 variant = 'block',
39}: {
40 space: string
41 repo: string
42 currentRevision: string
43 defaultBranch: string
44 onOpen: (hit: SearchHit) => void
45 /** block: full-width bar under the toolbar · toolbar: compact box inside the repo toolbar */
46 variant?: 'block' | 'toolbar'
47}) {
48 const { t: tr } = useT()
49 const [query, setQuery] = useState('')
50 const [hits, setHits] = useState<SearchHit[]>([])
51 const [loading, setLoading] = useState(false)
52 const [error, setError] = useState('')
53 const [open, setOpen] = useState(false)
54 const [cursor, setCursor] = useState(0)
55 const root = useRef<HTMLDivElement | null>(null)
56 const inputRef = useRef<HTMLInputElement | null>(null)
57
58 useEffect(() => {
59 const q = query.trim()
60 if (!q) {
61 setHits([])
62 setError('')
63 setLoading(false)
64 return
65 }
66 let active = true
67 const timer = window.setTimeout(() => {
68 setLoading(true)
69 setError('')
70 search({ q, kinds: ['code', 'symbol'], spaceName: space, repoName: repo, limit: 20 }).then(
71 page => {
72 if (!active) return
73 setHits(page.items)
74 setLoading(false)
75 },
76 failure => {
77 if (!active) return
78 setHits([])
79 setError(failure instanceof Error ? failure.message : tr('repo.search.failed'))
80 setLoading(false)
81 },
82 )
83 }, 220)
84 return () => {
85 active = false
86 window.clearTimeout(timer)
87 }
88 }, [query, repo, space, tr])
89
90
91 useEffect(() => {
92 if (!open) return
93 const onPointerDown = (event: PointerEvent) => {
94 if (!root.current?.contains(event.target as Node)) setOpen(false)
95 }
96 window.addEventListener('pointerdown', onPointerDown)
97 return () => window.removeEventListener('pointerdown', onPointerDown)
98 }, [open])
99
100 // 与 GitHub 一致:不在输入框里时按 `/` 直接聚焦仓库搜索。
101 useEffect(() => {
102 const onKeyDown = (event: KeyboardEvent) => {
103 if (event.key !== '/' || event.metaKey || event.ctrlKey || event.altKey) return
104 const target = event.target as HTMLElement | null
105 if (target?.closest('input, textarea, select, [contenteditable="true"]')) return
106 event.preventDefault()
107 inputRef.current?.focus()
108 setOpen(true)
109 }
110 window.addEventListener('keydown', onKeyDown)
111 return () => window.removeEventListener('keydown', onKeyDown)
112 }, [])
113
114 const index = useMemo(() => new Map(hits.map((hit, at) => [`${hit.kind}:${hit.id}`, at])), [hits])
115 // 结果集变化时游标自动收敛,不需要再用 effect 把 state 归零。
116 const active = hits.length ? Math.min(cursor, hits.length - 1) : 0
117 const current = hits[active] ?? null
118
119 // 结果按类型分组,和 GitHub 的仓库内搜索一样先给代码、再给符号。
120 const groups: Group[] = (['code', 'symbol'] as const)
121 .map(kind => ({
122 kind,
123 label: KIND_LABEL[kind],
124 hits: hits.filter(hit => hit.kind === kind),
125 }))
126 .filter(group => group.hits.length > 0)
127
128 const move = (delta: number) => {
129 if (!hits.length) return
130 const next = (active + delta + hits.length) % hits.length
131 setCursor(next)
132 const key = `${hits[next].kind}:${hits[next].id}`
133 document.getElementById(`repo-search-hit-${key}`)?.scrollIntoView({ block: 'nearest' })
134 }
135
136 const indexedBranch = currentRevision === defaultBranch || !currentRevision
137 const panelOpen = open && Boolean(query.trim())
138 const inline = variant === 'toolbar'
139
140 return (
141 <div ref={root} className={inline ? 'repo-search-inline relative w-full' : 'relative mb-3'}>
142 <div className={inline
143 ? 'flex h-9 w-full min-w-0 items-center gap-1.5 rounded-[9px] border border-shell-hairline bg-shell-field px-2 transition-colors focus-within:border-shell-tile focus-within:bg-shell-surface'
144 : 'flex items-center gap-2 rounded-lg border border-shell-hairline bg-shell-surface px-2.5 shadow-sm transition-colors focus-within:border-shell-focus'}>
145 {loading ? (
146 <Loader2 className="h-4 w-4 shrink-0 animate-spin text-shell-ink-muted" aria-hidden />
147 ) : (
148 <Search className="h-4 w-4 shrink-0 text-shell-ink-muted" aria-hidden />
149 )}
150 <input
151 ref={inputRef}
152 value={query}
153 onChange={event => {
154 setQuery(event.target.value)
155 setOpen(true)
156 }}
157 onFocus={() => setOpen(true)}
158 onKeyDown={event => {
159 if (event.key === 'ArrowDown') {
160 event.preventDefault()
161 move(1)
162 } else if (event.key === 'ArrowUp') {
163 event.preventDefault()
164 move(-1)
165 } else if (event.key === 'Enter' && current) {
166 event.preventDefault()
167 onOpen(current)
168 setOpen(false)
169 } else if (event.key === 'Escape') {
170 setOpen(false)
171 }
172 }}
173 placeholder={tr('repo.search.codePlaceholder')}
174 aria-label={tr('repo.search.codeLabel')}
175 role="combobox"
176 aria-expanded={panelOpen}
177 aria-controls="repo-search-results"
178 className={inline ? 'h-full min-w-0 flex-1 bg-transparent text-[12.5px] outline-none placeholder:text-shell-ink-muted' : 'h-10 min-w-0 flex-1 bg-transparent text-[13px] outline-none placeholder:text-shell-ink-muted'}
179 />
180 {query ? (
181 <button type="button" onClick={() => { setQuery(''); setOpen(false) }} className="rounded p-1 text-shell-ink-muted hover:bg-shell-active hover:text-shell-ink" aria-label={tr('repo.search.clear')}>
182 <X className="h-3.5 w-3.5" aria-hidden />
183 </button>
184 ) : (
185 <kbd className="hidden shrink-0 rounded border border-shell-hairline bg-shell-surface px-1.5 py-0.5 font-mono text-[10px] text-shell-ink-muted sm:block">/</kbd>
186 )}
187 </div>
188 {!indexedBranch && !inline ? <p className="mt-1 px-1 text-[11px] text-shell-ink-muted">{tr('repo.search.defaultBranchNote', { branch: defaultBranch })}</p> : null}
189 {panelOpen ? (
190 <div
191 id="repo-search-results"
192 role="listbox"
193 className={inline
194 ? 'absolute inset-x-0 z-40 mt-1 max-h-[min(60vh,480px)] overflow-y-auto overscroll-contain rounded-lg border border-shell-hairline bg-shell-surface py-1 shadow-xl'
195 : 'absolute inset-x-0 z-30 mt-1 max-h-[min(60vh,480px)] overflow-y-auto overscroll-contain rounded-lg border border-shell-hairline bg-shell-surface py-1 shadow-xl'}
196 >
197 {loading ? <p className="px-3 py-4 text-[12px] text-shell-ink-muted">{tr('repo.search.searching')}</p> : null}
198 {error ? <p role="alert" className="px-3 py-4 text-[12px] text-[#b42318]">{error}</p> : null}
199 {!loading && !error && hits.length === 0 ? (
200 <p className="px-3 py-6 text-center text-[12px] text-shell-ink-muted">{tr('repo.search.noHits')}</p>
201 ) : null}
202 {groups.map(group => (
203 <div key={group.kind}>
204 <p className="px-3 pb-1 pt-2 font-mono text-[10px] font-semibold uppercase tracking-[1.2px] text-shell-ink-muted">
205 {group.label} · {group.hits.length}
206 </p>
207 {group.hits.map(hit => {
208 const at = index.get(`${hit.kind}:${hit.id}`) ?? -1
209 const selected = at === active
210 return (
211 <button
212 id={`repo-search-hit-${hit.kind}:${hit.id}`}
213 key={`${hit.kind}:${hit.id}`}
214 type="button"
215 role="option"
216 aria-selected={selected}
217 onMouseEnter={() => setCursor(at)}
218 onClick={() => { onOpen(hit); setOpen(false) }}
219 className={`flex w-full items-start gap-2.5 px-3 py-2 text-left transition-colors ${selected ? 'bg-shell-active' : 'hover:bg-shell-field'}`}
220 >
221 <FileCode2 className="mt-0.5 h-3.5 w-3.5 shrink-0 text-shell-ink-muted" aria-hidden />
222 <span className="min-w-0 flex-1">
223 <span className="flex items-baseline gap-2">
224 <span className="min-w-0 truncate text-[12.5px] font-semibold">{highlight(hit.title, query)}</span>
225 {hit.symbol_name ? <span className="shrink-0 text-[11px] text-shell-ink-muted">{hit.symbol_name}</span> : null}
226 </span>
227 <span className="mt-0.5 block truncate font-mono text-[11px] text-shell-ink-muted">
228 {hit.path}
229 {hit.line_start ? `:${hit.line_start}` : ''}
230 </span>
231 {hit.snippet ? (
232 <span className="mt-1 block truncate font-mono text-[11px] leading-4 text-shell-ink-muted">
233 {highlight(hit.snippet, query)}
234 </span>
235 ) : null}
236 </span>
237 {selected ? <CornerDownLeft className="mt-1 h-3 w-3 shrink-0 text-shell-ink-muted" aria-hidden /> : null}
238 </button>
239 )
240 })}
241 </div>
242 ))}
243 {hits.length > 0 ? (
244 <p className="border-t border-shell-hairline px-3 py-2 text-[11px] text-shell-ink-muted">
245 {tr('repo.search.keyboardHint')}
246 </p>
247 ) : null}
248 </div>
249 ) : null}
250 </div>
251 )
252}
No newline at end of file
Added web/src/features/search/model/query.ts web/src/features/search/model/query.ts +180 −0 View web/src/features/search/model/query.ts
@@ -0,0 +1,180 @@
1import type { SearchKind } from '@/entities/search'
2
3/** Global-search filters split out of the raw query text. */
4export interface SearchFilters {
5 text: string
6 spaceName?: string
7 repoName?: string
8 language?: string
9 state?: string
10 kinds?: SearchKind[]
11}
12
13/** One qualifier the user typed, rendered as a removable pill. */
14export type ActiveFilter =
15 | { field: 'repo' | 'language' | 'state'; token: string }
16 | { field: 'kind'; kind: SearchKind; token: string }
17
18/** Shorthands mapped to the language labels the code index stores. */
19const LANGUAGE_ALIASES: Record<string, string> = {
20 rs: 'Rust',
21 rust: 'Rust',
22 js: 'JavaScript',
23 javascript: 'JavaScript',
24 ts: 'TypeScript',
25 typescript: 'TypeScript',
26 tsx: 'TypeScript (React)',
27 py: 'Python',
28 python: 'Python',
29 go: 'Go',
30 golang: 'Go',
31 java: 'Java',
32 kt: 'Kotlin',
33 kotlin: 'Kotlin',
34 c: 'C',
35 cpp: 'C++',
36 cs: 'C#',
37 rb: 'Ruby',
38 ruby: 'Ruby',
39 php: 'PHP',
40 sh: 'Shell',
41 bash: 'Shell',
42 shell: 'Shell',
43 html: 'HTML',
44 css: 'CSS',
45 scss: 'SCSS',
46 vue: 'Vue',
47 svelte: 'Svelte',
48 sql: 'SQL',
49 json: 'JSON',
50 yaml: 'YAML',
51 toml: 'TOML',
52 md: 'Markdown',
53 markdown: 'Markdown',
54}
55
56const KIND_ALIASES: Record<string, SearchKind> = {
57 code: 'code',
58 symbol: 'symbol',
59 symbols: 'symbol',
60 issue: 'issue',
61 issues: 'issue',
62 pr: 'pull',
63 prs: 'pull',
64 pull: 'pull',
65 pulls: 'pull',
66 'pull-request': 'pull',
67 'pull-requests': 'pull',
68 message: 'message',
69 messages: 'message',
70 chat: 'message',
71}
72
73const STATES = new Set(['open', 'closed', 'merged'])
74
75/** Split on whitespace while keeping `key:"quoted value"` in one token. */
76function tokenize(input: string): string[] {
77 const tokens: string[] = []
78 let current = ''
79 let quoted = false
80 for (const char of input) {
81 if (char === '"') {
82 quoted = !quoted
83 continue
84 }
85 if (!quoted && /\s/.test(char)) {
86 if (current) tokens.push(current)
87 current = ''
88 continue
89 }
90 current += char
91 }
92 if (current) tokens.push(current)
93 return tokens
94}
95
96function repoToken(filters: SearchFilters): string {
97 const path = filters.spaceName
98 ? `${filters.spaceName}/${filters.repoName ?? ''}`
99 : filters.repoName ?? ''
100 return `repo:${path}`
101}
102
103/**
104 * GitHub-style qualifiers (`repo:space/repo`, `language:rust`, `state:open`,
105 * `type:pr` / `is:issue`) become API filters; everything else stays free text.
106 * Unknown or valueless tokens are searched as-is instead of being dropped.
107 */
108export function parseSearchQuery(input: string): SearchFilters {
109 const terms: string[] = []
110 const kinds = new Set<SearchKind>()
111 const filters: SearchFilters = { text: '' }
112 for (const token of tokenize(input)) {
113 const at = token.indexOf(':')
114 if (at <= 0) {
115 terms.push(token)
116 continue
117 }
118 const key = token.slice(0, at).toLowerCase()
119 const value = token.slice(at + 1).trim()
120 if (!value) {
121 terms.push(token)
122 continue
123 }
124 const lower = value.toLowerCase()
125 if (key === 'repo') {
126 // `repo:space/repo` names both halves; a bare `repo:name` only the repo.
127 const [space, ...rest] = value.split('/')
128 if (rest.length) {
129 filters.spaceName = space
130 filters.repoName = rest.join('/')
131 } else {
132 filters.repoName = space
133 }
134 } else if (key === 'space' || key === 'org') {
135 filters.spaceName = value
136 } else if (key === 'language' || key === 'lang') {
137 filters.language = LANGUAGE_ALIASES[lower] ?? value
138 } else if (key === 'state') {
139 filters.state = lower
140 } else if (key === 'is' || key === 'type' || key === 'kind') {
141 const kind = KIND_ALIASES[lower]
142 if (kind) kinds.add(kind)
143 else if (key === 'is' && STATES.has(lower)) filters.state = lower
144 else terms.push(token)
145 } else {
146 terms.push(token)
147 }
148 }
149 filters.text = terms.join(' ')
150 if (kinds.size) filters.kinds = [...kinds]
151 return filters
152}
153
154/** Qualifiers the query carries, in the order the dialog renders them. */
155export function activeFilters(filters: SearchFilters): ActiveFilter[] {
156 const chips: ActiveFilter[] = []
157 if (filters.spaceName || filters.repoName) {
158 chips.push({ field: 'repo', token: repoToken(filters) })
159 }
160 if (filters.language) chips.push({ field: 'language', token: `language:${filters.language}` })
161 if (filters.state) chips.push({ field: 'state', token: `state:${filters.state}` })
162 for (const kind of filters.kinds ?? []) {
163 chips.push({ field: 'kind', kind, token: `type:${kind}` })
164 }
165 return chips
166}
167
168/** Query text with one pill dropped and the remaining filters kept. */
169export function removeFilter(filters: SearchFilters, drop: ActiveFilter): string {
170 const kept: string[] = []
171 if (filters.text.trim()) kept.push(filters.text.trim())
172 if (drop.field !== 'repo' && (filters.spaceName || filters.repoName)) kept.push(repoToken(filters))
173 if (drop.field !== 'language' && filters.language) kept.push(`language:${filters.language}`)
174 if (drop.field !== 'state' && filters.state) kept.push(`state:${filters.state}`)
175 for (const kind of filters.kinds ?? []) {
176 if (drop.field === 'kind' && drop.kind === kind) continue
177 kept.push(`type:${kind}`)
178 }
179 return kept.join(' ')
180}
No newline at end of file
Modified web/src/features/search/ui/global-search-command.tsx web/src/features/search/ui/global-search-command.tsx +51 −8 View web/src/features/search/ui/global-search-command.tsx
@@ -3,6 +3,7 @@
33import { FileCode2, GitPullRequest, Hash, MessageSquare, Search, X } from 'lucide-react'
44import { useT } from '@/shared/i18n'
55import { search, type SearchHit, type SearchKind } from '@/entities/search'
6import { activeFilters, parseSearchQuery, removeFilter, type ActiveFilter, type SearchFilters } from '../model/query'
67
78/** Label keys resolved lazily so the tab strip follows the active locale. */
89const KIND_LABELS: Record<
@@ -35,6 +36,14 @@
3536 event.preventDefault()
3637 setOpen(value => !value)
3738 }
39 // Same muscle memory the per-repo search box used to own: `/` opens the
40 // command palette unless the caret already sits in a field.
41 if (event.key === '/' && !event.metaKey && !event.ctrlKey && !event.altKey) {
42 const target = event.target as HTMLElement | null
43 if (target?.closest('input, textarea, select, [contenteditable="true"]')) return
44 event.preventDefault()
45 setOpen(true)
46 }
3847 if (event.key === 'Escape') setOpen(false)
3948 }
4049 window.addEventListener('keydown', onKeyDown)
@@ -46,9 +55,15 @@
4655 }, [open])
4756
4857 const kindSet = useMemo(() => new Set(selected), [selected])
58 // Qualifiers typed in the box win over the toggle row: `type:pr` narrows an
59 // otherwise unfiltered search without touching the reader's saved scope.
60 const filters: SearchFilters = useMemo(() => parseSearchQuery(query), [query])
61 const chips = useMemo(() => activeFilters(filters), [filters])
62 const text = filters.text.trim()
63 const kinds = filters.kinds ?? selected
64
4965 useEffect(() => {
50 const q = query.trim()
51 if (!open || !q || selected.length === 0) {
66 if (!open || !text || kinds.length === 0) {
5267 setResults([])
5368 setError('')
5469 setLoading(false)
@@ -58,7 +73,15 @@
5873 const timer = window.setTimeout(() => {
5974 setLoading(true)
6075 setError('')
61 search({ q, kinds: selected, limit: 20 }).then(
76 search({
77 q: text,
78 kinds,
79 spaceName: filters.spaceName,
80 repoName: filters.repoName,
81 language: filters.language,
82 state: filters.state,
83 limit: 20,
84 }).then(
6285 page => {
6386 if (!active) return
6487 setResults(page.items)
@@ -76,7 +99,7 @@
7699 active = false
77100 window.clearTimeout(timer)
78101 }
79 }, [open, query, selected, t])
102 }, [open, text, kinds, filters.spaceName, filters.repoName, filters.language, filters.state, t])
80103
81104 const toggle = (kind: SearchKind) => {
82105 setSelected(current => current.includes(kind) ? current.filter(value => value !== kind) : [...current, kind])
@@ -107,12 +130,14 @@
107130 setOpen(false)
108131 }
109132
133 const dropFilter = (chip: ActiveFilter) => setQuery(removeFilter(filters, chip))
134
110135 return (
111136 <>
112137 <button
113138 type="button"
114139 onClick={() => setOpen(true)}
115 className="global-search-trigger fixed right-4 top-3 z-40 hidden h-9 w-9 items-center justify-center gap-2 rounded-lg border border-shell-hairline bg-shell-surface px-0 text-[12px] text-shell-ink-muted shadow-sm transition-colors hover:bg-shell-field md:flex lg:w-[clamp(200px,22vw,300px)] lg:justify-start lg:px-3"
140 className="global-search-trigger fixed right-4 top-5 z-40 hidden h-9 w-9 items-center justify-center gap-2 rounded-lg border border-shell-hairline bg-shell-surface px-0 text-[12px] text-shell-ink-muted shadow-sm transition-colors hover:bg-shell-field md:flex lg:w-[clamp(200px,22vw,300px)] lg:justify-start lg:px-3"
116141 aria-label={t('search.command.open')}
117142 >
118143 <Search className="h-4 w-4 shrink-0" aria-hidden />
@@ -154,11 +179,29 @@
154179 </button>
155180 ))}
156181 </div>
182 {chips.length ? (
183 <div className="flex flex-wrap items-center gap-1.5 border-b border-shell-hairline px-3 py-2" aria-label={t('search.command.filters')}>
184 {chips.map(chip => (
185 <button
186 key={chip.token}
187 type="button"
188 onClick={() => dropFilter(chip)}
189 className="flex items-center gap-1 rounded-full border border-shell-hairline bg-shell-field px-2 py-0.5 font-mono text-[11px] text-shell-ink transition-colors hover:bg-shell-active"
190 aria-label={t('search.command.removeFilter', { filter: chip.token })}
191 title={t('search.command.removeFilter', { filter: chip.token })}
192 >
193 {chip.token}
194 <X className="h-3 w-3 text-shell-ink-muted" aria-hidden />
195 </button>
196 ))}
197 </div>
198 ) : null}
157199 <div className="max-h-[58vh] overflow-y-auto p-2">
158 {!query.trim() ? <p className="px-3 py-8 text-center text-[13px] text-shell-ink-muted">{t('search.command.hint')}</p> : null}
200 {!query.trim() ? <p className="px-3 pb-6 text-center text-[13px] text-shell-ink-muted">{t('search.command.hint')}</p> : null}
201 {query.trim() && !text ? <p className="px-3 py-8 text-center text-[13px] text-shell-ink-muted">{t('search.command.needText')}</p> : null}
159202 {loading ? <p className="px-3 py-4 text-[13px] text-shell-ink-muted">{t('search.command.loading')}</p> : null}
160203 {error ? <p role="alert" className="px-3 py-4 text-[13px] text-[#b42318]">{error}</p> : null}
161 {!loading && !error && query.trim() && results.length === 0 ? <p className="px-3 py-8 text-center text-[13px] text-shell-ink-muted">{t('search.empty')}</p> : null}
204 {!loading && !error && text && results.length === 0 ? <p className="px-3 py-8 text-center text-[13px] text-shell-ink-muted">{t('search.empty')}</p> : null}
162205 {results.map(hit => <SearchRow key={`${hit.kind}:${hit.id}`} hit={hit} onOpen={() => openHit(hit)} />)}
163206 </div>
164207 </section>
@@ -196,4 +239,4 @@
196239 <span className="shrink-0 rounded bg-shell-active px-1.5 py-0.5 text-[10px] text-shell-ink-muted">{hit.kind}</span>
197240 </button>
198241 )
199}
242}
No newline at end of file
Modified web/src/pages/workspace/ui/repo/repo-view.tsx web/src/pages/workspace/ui/repo/repo-view.tsx +1 −21 View web/src/pages/workspace/ui/repo/repo-view.tsx
@@ -12,7 +12,7 @@
1212import { useAsync, repoKey } from '@/shared/lib'
1313import { useRepoViewLocation } from '../../model/repo-location'
1414import { BranchesTab } from '@/features/repo-branches'
15import { CodeTab, RepoCodeSearch } from '@/features/repo-browse'
15import { CodeTab } from '@/features/repo-browse'
1616import { CommitsTab, CommitView } from '@/features/repo-history'
1717import { PullsTab, PullDetail } from '@/features/pull-request'
1818import { SettingsTab } from '@/features/repo-settings'
@@ -193,26 +193,6 @@
193193 onOpenCommit={sha => patch({ tab: 'commits', commit: sha })}
194194 onShowHistory={showHistory}
195195 cloneSlot={<CloneBox targets={clones} fill />}
196 searchSlot={
197 <RepoCodeSearch
198 space={spaceName}
199 repo={repoName}
200 currentRevision={rev}
201 defaultBranch={repo.default_branch}
202 variant="toolbar"
203 onOpen={hit => {
204 if (!hit.path) return
205 patch({
206 tab: 'code',
207 rev: hit.revision || repo.default_branch,
208 path: '',
209 file: hit.path,
210 oid: '',
211 line: hit.line_start ?? 0,
212 })
213 }}
214 />
215 }
216196 />
217197 ) : null}
218198 {tab === 'commits' && rev ? (
Modified web/src/shared/i18n/messages/en/misc.ts web/src/shared/i18n/messages/en/misc.ts +3 −0 View web/src/shared/i18n/messages/en/misc.ts
@@ -52,6 +52,9 @@
5252 'search.command.close': 'Close search',
5353 'search.command.types': 'Search types',
5454 'search.command.hint': 'Type a keyword to start searching',
55 'search.command.needText': 'Add a keyword — the filters are ready',
56 'search.command.filters': 'Active search filters',
57 'search.command.removeFilter': 'Remove filter {filter}',
5558 'search.command.loading': 'Searching…',
5659 'search.kind.code': 'Code',
5760 'search.kind.symbol': 'Symbols',
Modified web/src/shared/i18n/messages/zh-CN/misc.ts web/src/shared/i18n/messages/zh-CN/misc.ts +3 −0 View web/src/shared/i18n/messages/zh-CN/misc.ts
@@ -52,6 +52,9 @@
5252 'search.command.close': '关闭搜索',
5353 'search.command.types': '搜索类型',
5454 'search.command.hint': '输入关键词开始搜索',
55 'search.command.needText': '请再输入关键词,限定条件已生效',
56 'search.command.filters': '当前搜索条件',
57 'search.command.removeFilter': '移除条件 {filter}',
5558 'search.command.loading': '正在搜索…',
5659 'search.kind.code': '代码',
5760 'search.kind.symbol': '符号',
Modified web/steiger.config.js web/steiger.config.js +1 −0 View web/steiger.config.js
@@ -11,6 +11,7 @@
1111// 这里保留完整引用分析,只对清单内的切片丢弃报警:规则对新切片仍然生效。
1212// 本清单应随架构演进同步修剪(切片被第二个消费者使用,或删除后记得移除)。
1313const INSIGNIFICANT_SLICE_ALLOWLIST = new Set([
14 'entities/search',
1415 'features/chat',
1516 'features/issue-board',
1617 'features/notifications',