@jetcrab/pi-desk 1.1.18-dev → 1.1.20-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/docs/plugins.md +1 -1
  2. package/next.config.ts +1 -0
  3. package/package.json +3 -2
  4. package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-runtime.ts +1 -1
  5. package/src/client/l2_biz/workbench/hooks/l2-workbench-chat-reload.ts +42 -0
  6. package/src/client/l2_biz/workbench/l2-work-session-sidebar.tsx +19 -1
  7. package/src/client/l2_biz/workbench/l2-workbench-chat.ts +45 -0
  8. package/src/client/l2_biz/workbench/l2-workbench.tsx +4 -0
  9. package/src/client/l3_modules/conversation/l3-conversation-markdown.tsx +10 -1
  10. package/src/client/l3_modules/project-files/l3-markdown-preview.tsx +10 -1
  11. package/src/client/l3_modules/project-files/l3-project-document-view.tsx +2 -0
  12. package/src/client/l3_modules/project-files/l3-project-file-browser.tsx +171 -229
  13. package/src/client/l3_modules/project-files/l3-project-file-runtime.ts +133 -70
  14. package/src/client/l3_modules/project-files/l3-project-reader.ts +4 -3
  15. package/src/client/l3_modules/project-files/views/l3-project-search-controls.tsx +201 -0
  16. package/src/client/l3_modules/project-files/views/l3-project-search-results.tsx +333 -0
  17. package/src/client/l4_foundation/ui/code/l4-code-preview.tsx +47 -3
  18. package/src/client/l4_foundation/ui/markdown/l4-markdown-remark.ts +89 -0
  19. package/src/client/l4_foundation/ui/markdown/l4-markdown.module.css +58 -0
  20. package/src/client/l4_foundation/ui/markdown/l4-markdown.tsx +108 -0
  21. package/src/common/l2_biz/work-session/l2-work-session-file-contract.ts +4 -3
  22. package/src/common/l2_biz/workbench/l2-workbench-locale-messages.ts +6 -0
  23. package/src/common/l3_modules/project-files/l3-project-files-contract.ts +2 -3
  24. package/src/common/l3_modules/project-files/l3-project-files-locale-messages.ts +56 -10
  25. package/src/common/l4_foundation/file/l4-project-file-contract.ts +51 -1
  26. package/src/common/l4_foundation/locale/l4-host-error-messages.ts +11 -0
  27. package/src/common/l4_foundation/locale/l4-locale-messages.ts +16 -0
  28. package/src/instrumentation.ts +0 -2
  29. package/src/server/l1_entry/api/l1-project-read-routes.ts +6 -4
  30. package/src/server/l1_entry/api/l1-project-route.ts +9 -8
  31. package/src/server/l1_entry/api/l1-work-session-file-error.ts +23 -0
  32. package/src/server/l2_biz/plugin-management/l2-plugin-management.ts +0 -3
  33. package/src/server/l2_biz/work-session/l2-work-session-chat-runtime.ts +30 -58
  34. package/src/server/l2_biz/work-session/l2-work-session-manage.ts +0 -4
  35. package/src/server/l4_foundation/file/l4-project-file-index.ts +276 -0
  36. package/src/server/l4_foundation/file/l4-project-search.ts +399 -0
  37. package/src/server/l4_foundation/file/l4-ripgrep-search.ts +192 -0
  38. package/src/server/l4_foundation/file/l4-work-session-project-file.ts +8 -332
  39. package/src/server/l4_foundation/pi/l4-pi-package-maintenance.ts +0 -6
  40. package/src/server/l4_foundation/pi/l4-pi-plugin-capability-probe.ts +1 -3
  41. package/src/server/l4_foundation/pi/l4-pi-plugin-probe.ts +1 -3
  42. package/src/server/l4_foundation/pi/l4-pi-runner-process.ts +2 -2
  43. package/src/server/l4_foundation/process/l4-process-tree.js +23 -4
  44. package/temp/build/pi-desk/release/.next/BUILD_ID +1 -1
  45. package/temp/build/pi-desk/release/.next/build-manifest.json +3 -3
  46. package/temp/build/pi-desk/release/.next/prerender-manifest.json +3 -3
  47. package/temp/build/pi-desk/release/.next/react-loadable-manifest.json +1 -1
  48. package/temp/build/pi-desk/release/.next/required-server-files.js +3 -2
  49. package/temp/build/pi-desk/release/.next/required-server-files.json +3 -2
  50. package/temp/build/pi-desk/release/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  51. package/temp/build/pi-desk/release/.next/server/app/_global-error.html +1 -1
  52. package/temp/build/pi-desk/release/.next/server/app/_global-error.rsc +1 -1
  53. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  54. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  55. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  56. package/temp/build/pi-desk/release/.next/server/app/_not-found/page.js +1 -1
  57. package/temp/build/pi-desk/release/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  58. package/temp/build/pi-desk/release/.next/server/app/api/health/route.js +1 -1
  59. package/temp/build/pi-desk/release/.next/server/app/api/project-files/get/route.js +2 -1
  60. package/temp/build/pi-desk/release/.next/server/app/api/project-files/list/route.js +2 -1
  61. package/temp/build/pi-desk/release/.next/server/app/api/project-files/reveal/route.js +1 -1
  62. package/temp/build/pi-desk/release/.next/server/app/api/project-files/search/route.js +2 -1
  63. package/temp/build/pi-desk/release/.next/server/app/api/project-git/get/route.js +2 -1
  64. package/temp/build/pi-desk/release/.next/server/app/api/project-git-branches/list/route.js +2 -1
  65. package/temp/build/pi-desk/release/.next/server/app/api/project-git-changes/list/route.js +2 -1
  66. package/temp/build/pi-desk/release/.next/server/app/api/project-git-diff/get/route.js +2 -1
  67. package/temp/build/pi-desk/release/.next/server/app/api/project-git-heads/get/route.js +2 -1
  68. package/temp/build/pi-desk/release/.next/server/app/api/project-git-log/list/route.js +2 -1
  69. package/temp/build/pi-desk/release/.next/server/app/login/page.js +1 -1
  70. package/temp/build/pi-desk/release/.next/server/app/login/page_client-reference-manifest.js +1 -1
  71. package/temp/build/pi-desk/release/.next/server/app/page.js +77 -77
  72. package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
  73. package/temp/build/pi-desk/release/.next/server/chunks/{33658.js → 10881.js} +8 -8
  74. package/temp/build/pi-desk/release/.next/server/chunks/23054.js +1 -1
  75. package/temp/build/pi-desk/release/.next/server/chunks/25406.js +3 -3
  76. package/temp/build/pi-desk/release/.next/server/chunks/35985.js +5 -5
  77. package/temp/build/pi-desk/release/.next/server/chunks/50520.js +2 -4
  78. package/temp/build/pi-desk/release/.next/server/chunks/6917.js +1 -1
  79. package/temp/build/pi-desk/release/.next/server/chunks/70780.js +1 -0
  80. package/temp/build/pi-desk/release/.next/server/chunks/74528.js +1 -1
  81. package/temp/build/pi-desk/release/.next/server/chunks/92386.js +1 -1
  82. package/temp/build/pi-desk/release/.next/server/instrumentation.js +1 -1
  83. package/temp/build/pi-desk/release/.next/server/middleware-build-manifest.js +1 -1
  84. package/temp/build/pi-desk/release/.next/server/middleware-react-loadable-manifest.js +1 -1
  85. package/temp/build/pi-desk/release/.next/server/pages/500.html +1 -1
  86. package/temp/build/pi-desk/release/.next/server/server-reference-manifest.json +1 -1
  87. package/temp/build/pi-desk/release/.next/static/chunks/27611-0890482fe76bd9d6.js +238 -0
  88. package/temp/build/pi-desk/release/.next/static/chunks/{31087-ba426d09d6f68bfd.js → 31087-d8e5765d541d5c26.js} +1 -1
  89. package/temp/build/pi-desk/release/.next/static/chunks/{49516-cc4dcd84d64ab4fe.js → 51474-8dc714cb76b2e60a.js} +2 -2
  90. package/temp/build/pi-desk/release/.next/static/chunks/77592-5bef6acb8e7c4b96.js +13 -0
  91. package/temp/build/pi-desk/release/.next/static/chunks/app/layout-a50c456648537afb.js +3 -0
  92. package/temp/build/pi-desk/release/.next/static/chunks/app/{page-a984139e99586041.js → page-0789c43551b29dd2.js} +4 -4
  93. package/temp/build/pi-desk/release/.next/static/chunks/{webpack-40a782fc8a3535b9.js → webpack-d7248206b7fc7ba8.js} +1 -1
  94. package/temp/build/pi-desk/release/.next/static/css/250a6b93cc2222d1.css +1 -0
  95. package/temp/build/pi-desk/release/.next/static/css/6dbae2922f88ca31.css +1 -0
  96. package/temp/build/pi-desk/release/.next/static/css/a4cf28fd76c70165.css +1 -0
  97. package/temp/build/pi-desk/release/.next/trace-build +1 -1
  98. package/temp/build/pi-desk/release/.next/server/chunks/8584.js +0 -2
  99. package/temp/build/pi-desk/release/.next/static/chunks/39654-b5fedd762ea0312e.js +0 -238
  100. package/temp/build/pi-desk/release/.next/static/chunks/75130-d2ca74894aff8085.js +0 -13
  101. package/temp/build/pi-desk/release/.next/static/chunks/app/layout-c0e1c5cef8620cf8.js +0 -3
  102. package/temp/build/pi-desk/release/.next/static/css/6427d02b6edf406f.css +0 -1
  103. package/temp/build/pi-desk/release/.next/static/css/79e06f05df9edff0.css +0 -1
  104. /package/temp/build/pi-desk/release/.next/static/{n2LrXQgq8UkbH_AzhR3fw → RLKcK-RQF5FvEeDmS9uih}/_buildManifest.js +0 -0
  105. /package/temp/build/pi-desk/release/.next/static/{n2LrXQgq8UkbH_AzhR3fw → RLKcK-RQF5FvEeDmS9uih}/_ssgManifest.js +0 -0
@@ -12,10 +12,9 @@ import {
12
12
  ImageIcon,
13
13
  ListFilterIcon,
14
14
  LoaderCircleIcon,
15
- SearchIcon,
16
- XIcon
15
+ SearchIcon
17
16
  } from 'lucide-react'
18
- import { useEffect, useRef, useState, useSyncExternalStore } from 'react'
17
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'
19
18
  import { useTranslation } from 'react-i18next'
20
19
  import type { L4GitRepository } from '@common/l4_foundation/git/l4-git-contract'
21
20
  import { selectL4LocalizedText } from '@common/l4_foundation/locale/l4-localized-text'
@@ -30,12 +29,8 @@ import {
30
29
  } from '@client/l4_foundation/ui/ai-elements/file-tree'
31
30
  import { L4ScrollArea } from '@client/l4_foundation/ui/l4-scroll-area'
32
31
  import { buttonVariants } from '@client/l4_foundation/ui/shadcn/button'
33
- import {
34
- InputGroup,
35
- InputGroupAddon,
36
- InputGroupButton,
37
- InputGroupInput
38
- } from '@client/l4_foundation/ui/shadcn/input-group'
32
+ import { L3ProjectSearchControls } from './views/l3-project-search-controls'
33
+ import { L3ProjectSearchResults } from './views/l3-project-search-results'
39
34
  import type { L3ProjectGitBrowser } from './l3-project-git-runtime'
40
35
  import {
41
36
  getL3GitChangeBadge,
@@ -69,6 +64,7 @@ interface L3ProjectFileBrowserProps {
69
64
  }
70
65
 
71
66
  const EMPTY_GIT_REPOSITORIES: readonly L4GitRepository[] = []
67
+ const composingSearches = new WeakMap<L3ProjectFileRuntime, object>()
72
68
 
73
69
  function compactPath(path: string): string {
74
70
  const parts = path.replaceAll('\\', '/').split('/').filter(Boolean)
@@ -169,8 +165,26 @@ export function L3ProjectFileBrowser({
169
165
  const gitWorkspace = gitRuntime.getWorkspace(cwd)
170
166
  const scrollRef = useRef<HTMLDivElement>(null)
171
167
  const searchInputRef = useRef<HTMLInputElement>(null)
168
+ const searchResultsRef = useRef<HTMLDivElement>(null)
169
+ const searchButtonRef = useRef<HTMLButtonElement>(null)
170
+ const [composing, setComposing] = useState(false)
171
+ const compositionOwner = useRef<object>({})
172
+ const onComposingChange = useCallback(
173
+ (active: boolean): void => {
174
+ if (active) composingSearches.set(runtime, compositionOwner.current)
175
+ else if (composingSearches.get(runtime) === compositionOwner.current)
176
+ composingSearches.delete(runtime)
177
+ setComposing(active)
178
+ },
179
+ [runtime]
180
+ )
172
181
  const searchOpen = workspace.searchOpen
173
182
  const setSearchOpen = (open: boolean): void => runtime.setSearchOpen(open)
183
+ const closeSearch = (): void => {
184
+ setComposing(false)
185
+ setSearchOpen(false)
186
+ requestAnimationFrame(() => searchButtonRef.current?.focus())
187
+ }
174
188
  const [visibleEntries, setVisibleEntries] = useState<Record<string, number>>({})
175
189
  const moreEntries = (key: string, total: number): React.ReactNode => {
176
190
  const count = visibleEntries[key] ?? 300
@@ -200,15 +214,17 @@ export function L3ProjectFileBrowser({
200
214
 
201
215
  useEffect(() => {
202
216
  const scrollElement = scrollRef.current
203
- if (!scrollElement || !workspace) return
217
+ if (!scrollElement || searchOpen) return
204
218
  scrollElement.scrollTop = workspace.treeScrollTop
205
- }, [workspace])
219
+ }, [searchOpen, workspace])
206
220
 
207
221
  useEffect(() => {
208
- if (!searchQuery.trim() || gitWorkspace?.changesOnly) return
209
- const timer = window.setTimeout(() => void runtime.search(searchQuery), 220)
222
+ if (!searchOpen || !searchQuery.trim() || composing) return
223
+ const timer = window.setTimeout(() => {
224
+ if (!composingSearches.has(runtime)) void runtime.search(searchQuery)
225
+ }, 220)
210
226
  return () => window.clearTimeout(timer)
211
- }, [gitWorkspace?.changesOnly, runtime, searchQuery])
227
+ }, [composing, runtime, searchOpen, searchQuery])
212
228
 
213
229
  const expandedRepositoryRootsKey = [...(gitWorkspace?.expandedRepositories ?? [])].join('\u0000')
214
230
  useEffect(() => {
@@ -242,11 +258,20 @@ export function L3ProjectFileBrowser({
242
258
  toggleDirectory(directory, keys.has(keyL3ProjectTree('directory', path)))
243
259
  }
244
260
 
245
- const openPreview = (path: string): void => {
246
- runtime.openFile(path)
261
+ const openPreview = (path: string, line?: number): void => {
262
+ runtime.openFile(path, line)
247
263
  onFileOpened?.()
248
264
  }
249
265
 
266
+ const openFirstSearchResult = (): void => {
267
+ const search = runtime.state.search
268
+ if (search.status !== 'ready') return
269
+ const firstFile = search.fileMatches[0]
270
+ const firstLine = search.contentMatches[0]
271
+ if (firstFile) openPreview(firstFile.path)
272
+ else if (firstLine) openPreview(firstLine.path, firstLine.line)
273
+ }
274
+
250
275
  const openDiff = (change: L3GitProjectChange): void => {
251
276
  runtime.openDiff(change.repositoryRoot, change.path)
252
277
  onFileOpened?.()
@@ -271,16 +296,17 @@ export function L3ProjectFileBrowser({
271
296
  path: string,
272
297
  trigger: React.ReactElement,
273
298
  change: L3GitProjectChange | null = null,
274
- diffOnSelect = false
299
+ diffOnSelect = false,
300
+ line?: number
275
301
  ): React.JSX.Element => (
276
302
  <L3ProjectPathMenu
277
- key={keyL3ProjectTree('file', path)}
303
+ key={line === undefined ? keyL3ProjectTree('file', path) : `${path}:${line}`}
278
304
  cwd={cwd}
279
305
  path={path}
280
306
  type="file"
281
307
  trigger={trigger}
282
308
  openLabel={t(diffOnSelect ? 'viewDiff' : 'openPreview')}
283
- onOpen={() => (diffOnSelect && change ? openDiff(change) : openPreview(path))}
309
+ onOpen={() => (diffOnSelect && change ? openDiff(change) : openPreview(path, line))}
284
310
  oldPath={
285
311
  change?.change.oldPath ? joinPath(change.repositoryRoot, change.change.oldPath) : undefined
286
312
  }
@@ -648,10 +674,6 @@ export function L3ProjectFileBrowser({
648
674
  )
649
675
  }
650
676
 
651
- const changeSearchResults = projectChanges.filter((change) =>
652
- change.projectPath.toLocaleLowerCase().includes(searchQuery.trim().toLocaleLowerCase())
653
- )
654
- const searching = searchOpen && Boolean(searchQuery.trim())
655
677
  const singleProjectRootRepository =
656
678
  repositories.length === 1 && repositories[0]?.root === '' ? repositories[0] : null
657
679
  const showLayoutToggle = repositories.length > 0 && !singleProjectRootRepository
@@ -661,92 +683,82 @@ export function L3ProjectFileBrowser({
661
683
  <section
662
684
  className="flex h-full min-h-0 flex-col bg-sidebar"
663
685
  aria-label={t('projectFiles', { name: projectName })}
686
+ onKeyDown={(event) => {
687
+ if (!searchOpen || event.key !== 'Escape') return
688
+ event.stopPropagation()
689
+ if (event.nativeEvent.isComposing || composing) return
690
+ event.preventDefault()
691
+ closeSearch()
692
+ }}
664
693
  >
665
- <div className="space-y-1.5 border-b px-2.5 py-2">
666
- <div className="flex min-h-7 min-w-0 items-center gap-2">
667
- <L3ProjectPathMenu
668
- cwd={cwd}
669
- path=""
670
- type="directory"
671
- onReveal={
672
- runtime.canReveal ? (path, type) => runtime.revealFile(path, type) : undefined
673
- }
674
- trigger={
675
- rootTitle ? (
676
- <div className="flex min-w-0 flex-1">{rootTitle}</div>
677
- ) : (
678
- <div tabIndex={0} className="flex min-w-0 flex-1 items-baseline gap-1">
679
- <p className="shrink-0 truncate text-xs font-semibold" title={projectName}>
680
- {projectName}
681
- </p>
682
- <span className="text-xs text-muted-foreground">·</span>
683
- <p
684
- className="min-w-0 truncate font-mono text-xs text-muted-foreground"
685
- title={cwd}
686
- >
687
- {compactPath(cwd)}
688
- </p>
689
- </div>
690
- )
691
- }
692
- />
693
- {singleProjectRootRepository ? (
694
- <button
695
- type="button"
696
- disabled={gitReadOnly || singleProjectRootRepository.state !== 'ready'}
697
- className={cn(
698
- buttonVariants({ variant: 'outline', size: 'sm' }),
699
- 'max-w-32 shrink gap-1 font-mono text-xs text-muted-foreground'
700
- )}
701
- title={
702
- singleProjectRootRepository.state === 'ready'
703
- ? t(gitReadOnly ? 'gitBranchReadOnly' : 'gitBranches')
704
- : selectL4LocalizedText(singleProjectRootRepository.message, locale)
694
+ {searchOpen ? (
695
+ <L3ProjectSearchControls
696
+ input={workspace.search}
697
+ inputRef={searchInputRef}
698
+ invalid={workspace.search.status === 'error'}
699
+ onQueryChange={(query) => runtime.setSearchQuery(query)}
700
+ onOptionsChange={(options) => runtime.setSearchOptions(options)}
701
+ onComposingChange={onComposingChange}
702
+ onClose={closeSearch}
703
+ onOpenFirst={openFirstSearchResult}
704
+ onFocusResults={() =>
705
+ searchResultsRef.current?.querySelector<HTMLButtonElement>('button')?.focus()
706
+ }
707
+ />
708
+ ) : (
709
+ <div className="space-y-1.5 border-b px-2.5 py-2">
710
+ <div className="flex min-h-7 min-w-0 items-center gap-2">
711
+ <L3ProjectPathMenu
712
+ cwd={cwd}
713
+ path=""
714
+ type="directory"
715
+ onReveal={
716
+ runtime.canReveal ? (path, type) => runtime.revealFile(path, type) : undefined
717
+ }
718
+ trigger={
719
+ rootTitle ? (
720
+ <div className="flex min-w-0 flex-1">{rootTitle}</div>
721
+ ) : (
722
+ <div tabIndex={0} className="flex min-w-0 flex-1 items-baseline gap-1">
723
+ <p className="shrink-0 truncate text-xs font-semibold" title={projectName}>
724
+ {projectName}
725
+ </p>
726
+ <span className="text-xs text-muted-foreground">·</span>
727
+ <p
728
+ className="min-w-0 truncate font-mono text-xs text-muted-foreground"
729
+ title={cwd}
730
+ >
731
+ {compactPath(cwd)}
732
+ </p>
733
+ </div>
734
+ )
705
735
  }
706
- onClick={() => onOpenGitBranches('')}
707
- >
708
- <GitBranchIcon className="size-3 shrink-0" />
709
- <span className="truncate">
710
- {singleProjectRootRepository.state === 'ready'
711
- ? getL3GitHeadLabel(singleProjectRootRepository.head)
712
- : t('unavailable')}
713
- </span>
714
- </button>
715
- ) : null}
716
- </div>
717
-
718
- {searchOpen ? (
719
- <InputGroup>
720
- <InputGroupAddon>
721
- <SearchIcon className="size-3.5" />
722
- </InputGroupAddon>
723
- <InputGroupInput
724
- ref={searchInputRef}
725
- data-testid="project-file-search"
726
- value={searchQuery}
727
- onChange={(event) => runtime.setSearchQuery(event.target.value)}
728
- onKeyDown={(event) => {
729
- if (event.key !== 'Escape') return
730
- runtime.setSearchQuery('')
731
- setSearchOpen(false)
732
- }}
733
- placeholder={t('searchFiles')}
734
- aria-label={t('searchFiles')}
735
736
  />
736
- <InputGroupAddon align="inline-end">
737
- <InputGroupButton
738
- size="icon-xs"
739
- onClick={() => {
740
- runtime.setSearchQuery('')
741
- setSearchOpen(false)
742
- }}
743
- aria-label={t('closeSearch')}
737
+ {singleProjectRootRepository ? (
738
+ <button
739
+ type="button"
740
+ disabled={gitReadOnly || singleProjectRootRepository.state !== 'ready'}
741
+ className={cn(
742
+ buttonVariants({ variant: 'outline', size: 'sm' }),
743
+ 'max-w-32 shrink gap-1 font-mono text-xs text-muted-foreground'
744
+ )}
745
+ title={
746
+ singleProjectRootRepository.state === 'ready'
747
+ ? t(gitReadOnly ? 'gitBranchReadOnly' : 'gitBranches')
748
+ : selectL4LocalizedText(singleProjectRootRepository.message, locale)
749
+ }
750
+ onClick={() => onOpenGitBranches('')}
744
751
  >
745
- <XIcon />
746
- </InputGroupButton>
747
- </InputGroupAddon>
748
- </InputGroup>
749
- ) : (
752
+ <GitBranchIcon className="size-3 shrink-0" />
753
+ <span className="truncate">
754
+ {singleProjectRootRepository.state === 'ready'
755
+ ? getL3GitHeadLabel(singleProjectRootRepository.head)
756
+ : t('unavailable')}
757
+ </span>
758
+ </button>
759
+ ) : null}
760
+ </div>
761
+
750
762
  <div className="flex min-h-8 min-w-0 items-center gap-1.5">
751
763
  {showLayoutToggle ? (
752
764
  <div
@@ -797,6 +809,7 @@ export function L3ProjectFileBrowser({
797
809
  </button>
798
810
  ) : null}
799
811
  <button
812
+ ref={searchButtonRef}
800
813
  type="button"
801
814
  className={cn(buttonVariants({ variant: 'outline', size: 'icon-sm' }), 'ml-auto')}
802
815
  aria-label={t('searchProject')}
@@ -809,133 +822,62 @@ export function L3ProjectFileBrowser({
809
822
  <SearchIcon className="size-3.5" />
810
823
  </button>
811
824
  </div>
812
- )}
813
- </div>
825
+ </div>
826
+ )}
814
827
 
815
- <L4ScrollArea
816
- className="min-h-0 flex-1"
817
- viewportClassName="py-1.5"
818
- viewportRef={scrollRef}
819
- onViewportScroll={(event) => runtime.setTreeScrollTop(event.currentTarget.scrollTop)}
820
- >
821
- {repositories
822
- .filter((item) => item.state === 'unavailable')
823
- .map((item) =>
824
- item.state === 'unavailable' ? (
825
- <p
826
- key={item.root}
827
- role="alert"
828
- className="mx-2 mb-1 break-words rounded-md bg-muted px-2 py-2 text-sm text-destructive"
829
- >
830
- {item.root || projectName}:{selectL4LocalizedText(item.message, locale)}
831
- </p>
832
- ) : null
833
- )}
834
- {gitWorkspace?.snapshot.status === 'error' ? (
835
- <p className="mx-2 mb-1 rounded-md bg-muted px-2 py-2 text-sm text-destructive">
836
- {gitWorkspace.snapshot.error}
837
- </p>
838
- ) : null}
839
- {searching ? (
840
- <div className="space-y-0.5 px-1">
841
- {gitWorkspace?.changesOnly ? (
842
- changeSearchResults.length > 0 ? (
843
- changeSearchResults.map((change) => {
844
- return fileContextMenu(
845
- change.projectPath,
846
- <button
847
- type="button"
848
- className="flex min-h-10 w-full items-center gap-2 rounded-lg px-2 text-left hover:bg-muted/70"
849
- onClick={() => {
850
- openDiff(change)
851
- }}
852
- title={change.projectPath}
853
- >
854
- {fileIcon(change.projectPath)}
855
- <span className="min-w-0 flex-1">
856
- <span
857
- className={cn(
858
- 'block truncate text-xs font-medium',
859
- changedFileNameClass(change)
860
- )}
861
- >
862
- {change.projectPath.split('/').at(-1)}
863
- </span>
864
- <span className="mt-0.5 block truncate font-mono text-xs text-muted-foreground">
865
- {change.projectPath}
866
- </span>
867
- </span>
868
- </button>,
869
- change,
870
- true
871
- )
872
- })
873
- ) : (
874
- <p className="px-2 py-5 text-center text-xs text-muted-foreground">
875
- {t('noMatches')}
828
+ {searchOpen ? (
829
+ <L3ProjectSearchResults
830
+ key={JSON.stringify([
831
+ workspace.search.query,
832
+ workspace.search.scope,
833
+ workspace.search.caseSensitive,
834
+ workspace.search.wholeWord,
835
+ workspace.search.regex
836
+ ])}
837
+ search={workspace.search}
838
+ resultsRef={searchResultsRef}
839
+ activePath={workspace.activeDiff ? null : workspace.activePath}
840
+ onOpen={openPreview}
841
+ wrapFile={(path, line, trigger) =>
842
+ fileContextMenu(path, trigger, changeByProjectPath.get(path) ?? null, false, line)
843
+ }
844
+ renderFileIcon={fileIcon}
845
+ fileDecoration={(path) => {
846
+ const change = changeByProjectPath.get(path) ?? null
847
+ return {
848
+ className: changedFileNameClass(change),
849
+ badge: change ? getL3GitChangeBadge(change.change) : undefined
850
+ }
851
+ }}
852
+ />
853
+ ) : (
854
+ <L4ScrollArea
855
+ className="min-h-0 flex-1"
856
+ viewportClassName="py-1.5"
857
+ viewportRef={scrollRef}
858
+ onViewportScroll={(event) => runtime.setTreeScrollTop(event.currentTarget.scrollTop)}
859
+ >
860
+ {repositories
861
+ .filter((item) => item.state === 'unavailable')
862
+ .map((item) =>
863
+ item.state === 'unavailable' ? (
864
+ <p
865
+ key={item.root}
866
+ role="alert"
867
+ className="mx-2 mb-1 break-words rounded-md bg-muted px-2 py-2 text-sm text-destructive"
868
+ >
869
+ {item.root || projectName}:{selectL4LocalizedText(item.message, locale)}
876
870
  </p>
877
- )
878
- ) : (
879
- <>
880
- {workspace?.search.status === 'loading' ? (
881
- <div className="flex min-h-10 items-center gap-2 px-2 text-xs text-muted-foreground">
882
- <LoaderCircleIcon className="size-3.5 animate-spin" /> {t('searching')}
883
- </div>
884
- ) : null}
885
- {workspace?.search.status === 'error' ? (
886
- <p className="px-2 py-2 text-xs text-destructive">{workspace.search.error}</p>
887
- ) : null}
888
- {workspace?.search.status === 'ready'
889
- ? workspace.search.matches.map((path) => {
890
- const change = changeByProjectPath.get(path) ?? null
891
- return fileContextMenu(
892
- path,
893
- <button
894
- data-testid={`project-search-result-${path}`}
895
- type="button"
896
- onClick={() => {
897
- openPreview(path)
898
- }}
899
- className="flex min-h-10 w-full items-center gap-2 rounded-lg px-2 text-left hover:bg-muted/70"
900
- title={path}
901
- >
902
- <span className="shrink-0">{fileIcon(path)}</span>
903
- <span className="min-w-0 flex-1">
904
- <span
905
- className={cn(
906
- 'block truncate text-xs font-medium',
907
- changedFileNameClass(change)
908
- )}
909
- >
910
- {path.split('/').at(-1)}
911
- </span>
912
- <span className="mt-0.5 block truncate font-mono text-xs text-muted-foreground">
913
- {path}
914
- </span>
915
- </span>
916
- </button>
917
- )
918
- })
919
- : null}
920
- {workspace?.search.status === 'ready' && workspace.search.matches.length === 0 ? (
921
- <p className="px-2 py-5 text-center text-xs text-muted-foreground">
922
- {t('noMatches')}
923
- </p>
924
- ) : null}
925
- {workspace?.search.status === 'ready' && workspace.search.truncated ? (
926
- <p className="px-2 py-2 text-xs text-amber-600 dark:text-amber-400">
927
- {t('resultLimit')}
928
- </p>
929
- ) : null}
930
- </>
871
+ ) : null
931
872
  )}
932
- </div>
933
- ) : effectiveLayout === 'repository' ? (
934
- renderRepositoryView()
935
- ) : (
936
- renderDirectoryView()
937
- )}
938
- </L4ScrollArea>
873
+ {gitWorkspace?.snapshot.status === 'error' ? (
874
+ <p className="mx-2 mb-1 rounded-md bg-muted px-2 py-2 text-sm text-destructive">
875
+ {gitWorkspace.snapshot.error}
876
+ </p>
877
+ ) : null}
878
+ {effectiveLayout === 'repository' ? renderRepositoryView() : renderDirectoryView()}
879
+ </L4ScrollArea>
880
+ )}
939
881
  </section>
940
882
  )
941
883
  }