feat: add search to the new session project picker (#3408)

* feat: add search to the new session project picker

* fix: punctuate the project picker empty state
This commit is contained in:
Maxim Topciu
2026-09-07 20:25:19 +03:00
committed by GitHub
parent 0e836e7b75
commit fc61b10109
14 changed files with 241 additions and 27 deletions
@@ -189,7 +189,14 @@ and the send path reading the same grammar.
also owns the advisory dirty state for the selected directory, clearing it as also owns the advisory dirty state for the selected directory, clearing it as
soon as the target changes so a warning never names a previous branch. soon as the target changes so a warning never names a previous branch.
- `ui/DraftTargetSelectors.tsx` owns the controlled project/worktree picker - `ui/DraftTargetSelectors.tsx` owns the controlled project/worktree picker
state and registers its application shortcuts locally. The selectors only state and registers its application shortcuts locally. The desktop project
picker is a searchable popup: it ranks the current projects with
`rankByQuery` over display label and path, keeps the query and the active
result as transient local state that resets on every close, and commits
through the existing project-change flow only on explicit activation.
Filtering changes the result area below the anchored input without moving
the search field. The
worktree Select and the mobile bottom sheets are unchanged. The selectors only
consume their shared prefix while the draft target UI is mounted. consume their shared prefix while the draft target UI is mounted.
## Input recall ownership ## Input recall ownership
@@ -1,10 +1,11 @@
/** /**
* Where a new session will run: the project and the directory within it. * Where a new session will run: the project and the directory within it.
* *
* Desktop uses inline selects; mobile uses bottom sheets, because a native * Desktop uses a searchable project popup and an inline branch/worktree
* select over a keyboard-resized viewport is unusable. Both render the same * select; mobile uses bottom sheets, because a native select over a
* options from the same hook, and both offer creating a worktree inline so the * keyboard-resized viewport is unusable. Both render the same options from
* user does not have to leave the draft to make one. * the same hook, and both offer creating a worktree inline so the user does
* not have to leave the draft to make one.
*/ */
import React from 'react'; import React from 'react';
@@ -12,8 +13,19 @@ import React from 'react';
import { Icon } from '@/components/icon/Icon'; import { Icon } from '@/components/icon/Icon';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { shouldDismissDropdown } from '@/components/ui/dropdown-navigation'; import { Popover } from '@base-ui/react/popover';
import { cn } from '@/lib/utils';
import { dropdownMenuPopupClass } from '@/components/ui/dropdown-menu.styles';
import {
Command,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command';
import { handleDropdownNavigationKey, shouldDismissDropdown } from '@/components/ui/dropdown-navigation';
import { isIMECompositionEvent } from '@/lib/ime';
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -28,6 +40,7 @@ import { useI18n } from '@/lib/i18n';
import { matchesRankQuery, rankByQuery } from '@/lib/search/fuzzySearch'; import { matchesRankQuery, rankByQuery } from '@/lib/search/fuzzySearch';
import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, ProjectIconImage } from '@/lib/projectMeta'; import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, ProjectIconImage } from '@/lib/projectMeta';
import { createWorktreeDraft } from '@/lib/worktreeSessionCreator'; import { createWorktreeDraft } from '@/lib/worktreeSessionCreator';
import { shortcutRegistry } from '@/lib/shortcuts';
import { useKeybind } from '@/hooks/useKeybind'; import { useKeybind } from '@/hooks/useKeybind';
import type { Theme } from '@/types/theme'; import type { Theme } from '@/types/theme';
import { normalizePath } from '../attachments/filePaths'; import { normalizePath } from '../attachments/filePaths';
@@ -145,8 +158,34 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
theme, theme,
} = props; } = props;
const [openPicker, setOpenPicker] = React.useState<'project' | 'worktree' | null>(null); const [openPicker, setOpenPicker] = React.useState<'project' | 'worktree' | null>(null);
const [projectQuery, setProjectQuery] = React.useState('');
const [projectActiveId, setProjectActiveId] = React.useState<string | null>(null);
const [projectFocusReturn, setProjectFocusReturn] = React.useState(false);
const projectTriggerRef = React.useRef<HTMLButtonElement>(null); const projectTriggerRef = React.useRef<HTMLButtonElement>(null);
const worktreeTriggerRef = React.useRef<HTMLButtonElement>(null); const worktreeTriggerRef = React.useRef<HTMLButtonElement>(null);
const projectSearchRef = React.useRef<HTMLInputElement>(null);
// Preserve Select's dialog portal and main-area containment.
const [projectPortalContainer, setProjectPortalContainer] = React.useState<HTMLElement | null>(null);
const [projectCollisionBoundary, setProjectCollisionBoundary] = React.useState<Element | null>(null);
const syncProjectPopupContainers = React.useCallback((target: EventTarget | null) => {
const element = target instanceof HTMLElement ? target : null;
const dialog = element?.closest('[data-slot="dialog-content"], [role="dialog"]');
setProjectPortalContainer(dialog instanceof HTMLElement ? dialog : null);
setProjectCollisionBoundary(element?.closest('main') ?? null);
}, []);
// The popover owns no shortcut suspension (unlike Select/DropdownMenu
// wrappers), so suspend global shortcuts while the project popup is
// open and restore them on close/unmount.
const projectSuspendRef = React.useRef<(() => void) | null>(null);
React.useEffect(() => {
if (openPicker !== 'project') return;
projectSuspendRef.current?.();
projectSuspendRef.current = shortcutRegistry.suspend();
return () => {
projectSuspendRef.current?.();
projectSuspendRef.current = null;
};
}, [openPicker]);
const handlePickerKeyDown = (event: React.KeyboardEvent<HTMLElement>) => { const handlePickerKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
if (openPicker === null || !shouldDismissDropdown(event)) return; if (openPicker === null || !shouldDismissDropdown(event)) return;
event.preventDefault(); event.preventDefault();
@@ -156,6 +195,11 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
useKeybind('open_draft_project_picker', () => { useKeybind('open_draft_project_picker', () => {
projectTriggerRef.current?.focus(); projectTriggerRef.current?.focus();
setProjectFocusReturn(false);
setProjectQuery('');
setProjectActiveId(
projects.some((project) => project.id === selectedProject.id) ? selectedProject.id : null,
);
setOpenPicker('project'); setOpenPicker('project');
}); });
useKeybind('open_draft_worktree_picker', () => { useKeybind('open_draft_worktree_picker', () => {
@@ -164,11 +208,43 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
setOpenPicker('worktree'); setOpenPicker('worktree');
}); });
const filteredProjects = React.useMemo(
() => openPicker === 'project'
? rankByQuery(projects, projectQuery, (project) => [getProjectDisplayLabel(project), project.path])
: projects,
[openPicker, projects, projectQuery],
);
// Transient search must never survive to the next opening, including
// picker switches and draft unmounts.
React.useEffect(() => {
if (openPicker !== 'project') {
setProjectQuery('');
setProjectActiveId(null);
}
}, [openPicker]);
// After a query or project-list change, retain the active ID only
// while it stays visible; otherwise fall to the first result (or none
// when the list is empty) so Enter cannot commit a hidden row.
React.useEffect(() => {
if (openPicker !== 'project') return;
setProjectActiveId((current) => {
if (current && filteredProjects.some((project) => project.id === current)) return current;
return filteredProjects[0]?.id ?? null;
});
}, [filteredProjects, openPicker]);
const handleProjectChange = (projectId: string) => { const handleProjectChange = (projectId: string) => {
onProjectChange(projectId); onProjectChange(projectId);
setProjectFocusReturn(true);
setOpenPicker(null); setOpenPicker(null);
}; };
const handleProjectSelect = (projectId: string) => {
if (!filteredProjects.some((project) => project.id === projectId)) return;
handleProjectChange(projectId);
};
const handleDirectoryChange = (directory: string) => { const handleDirectoryChange = (directory: string) => {
onDirectoryChange(directory); onDirectoryChange(directory);
setOpenPicker(null); setOpenPicker(null);
@@ -176,33 +252,152 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
return ( return (
<div className="mb-1.5 flex min-w-0 items-center gap-1.5 px-0.5"> <div className="mb-1.5 flex min-w-0 items-center gap-1.5 px-0.5">
<Select {/* Plain popover (not a menu): the popup owns a combobox +
value={selectedProject.id} listbox, so menu/menuitem semantics would be wrong here. */}
<Popover.Root
open={openPicker === 'project'} open={openPicker === 'project'}
onOpenChange={(open) => setOpenPicker(open ? 'project' : null)} onOpenChange={(open, eventDetails) => {
onValueChange={handleProjectChange} if (open) {
disableGlobalShortcuts setProjectFocusReturn(false);
// Seed the active row from the committed project so
// Enter without typing repeats the current choice.
// The clamp below keeps it when still visible and
// falls to the first result otherwise.
setProjectQuery('');
setProjectActiveId(
projects.some((project) => project.id === selectedProject.id)
? selectedProject.id
: null,
);
setOpenPicker('project');
return;
}
setProjectQuery('');
setProjectActiveId(null);
const reason = eventDetails?.reason;
setProjectFocusReturn(reason === 'escape-key');
setOpenPicker(null);
}}
onOpenChangeComplete={(open) => {
// Return focus after Base UI finishes closing so the
// trigger itself, not document body, keeps keyboard flow.
if (!open && projectFocusReturn) {
projectTriggerRef.current?.focus();
setProjectFocusReturn(false);
}
// Focus the search once the popup mounts; the opening
// shortcut focuses the trigger first.
if (open && openPicker === 'project') projectSearchRef.current?.focus();
}}
> >
<SelectTrigger <Popover.Trigger
ref={projectTriggerRef} render={
onKeyDown={handlePickerKeyDown} <Button
size="sm" ref={projectTriggerRef}
className="h-7 min-w-0 w-fit max-w-[42vw] sm:max-w-[18rem] border-transparent bg-transparent px-1.5 hover:bg-transparent data-[popup-open]:bg-transparent" variant="ghost"
size="sm"
aria-haspopup="dialog"
className="h-7 min-w-0 w-fit max-w-[42vw] justify-start gap-1 px-1.5 normal-case sm:max-w-[18rem]"
onPointerDownCapture={(event) => syncProjectPopupContainers(event.currentTarget)}
onFocusCapture={(event) => syncProjectPopupContainers(event.currentTarget)}
/>
}
> >
<SelectValue> <span className="flex min-w-0 items-center gap-1.5">
{selectedProject.kind === 'chat' {selectedProject.kind === 'chat'
? <span className="truncate">{t('chat.chatInput.chooseProject')}</span> ? <span className="truncate">{t('chat.chatInput.chooseProject')}</span>
: <ProjectLabel project={selectedProject} theme={theme} />} : <ProjectLabel project={selectedProject} theme={theme} />}
</SelectValue> <Icon name="arrow-down-s" className="size-4 shrink-0 opacity-50" />
</SelectTrigger> </span>
<SelectContent side="top" collisionAvoidance={{ side: 'none' }} constrainToMain fitContent onKeyDown={handlePickerKeyDown}> </Popover.Trigger>
{projects.map((project) => ( <Popover.Portal container={projectPortalContainer ?? undefined}>
<SelectItem key={project.id} value={project.id} showSelectedBackground={false} className="max-w-[24rem] truncate"> {/* side="bottom" anchors the popup's top edge at the
<ProjectLabel project={project} theme={theme} /> trigger: the search field stays fixed at the
</SelectItem> selector's level while filtering, and only the
))} results area below changes height. Disabling side
</SelectContent> flips keeps the input stationary during filtering.
</Select> Horizontal shifting keeps the popup inside main. The
--available-height cap comes free from the shared
popup class, so the list scrolls within the space
below the trigger. */}
<Popover.Positioner
side="bottom"
align="start"
sideOffset={4}
collisionAvoidance={{ side: 'none' }}
collisionBoundary={projectCollisionBoundary ?? undefined}
className="app-region-no-drag z-50"
>
<Popover.Popup
role="dialog"
aria-label={t('chat.chatInput.draftPicker.projectTitle')}
className={cn(dropdownMenuPopupClass, 'flex w-72 max-w-[calc(100vw-2rem)] flex-col p-0')}
initialFocus={false}
finalFocus={false}
>
{/* Filtering and ordering are owned by rankByQuery above;
cmdk's own filter would re-filter and reorder the
already-ranked rows. */}
<Command
className="min-h-0 flex-1"
shouldFilter={false}
value={projectActiveId ?? undefined}
onValueChange={setProjectActiveId}
>
<CommandInput
ref={projectSearchRef}
aria-label={t('chat.chatInput.draftPicker.searchProjects')}
placeholder={t('chat.chatInput.draftPicker.searchProjects')}
value={projectQuery}
onValueChange={setProjectQuery}
onKeyDown={(event) => {
// Command owns active-item navigation, so only
// translate the repository's Ctrl+N/P
// convention into arrows at the input.
// IME-composing keys must never move the
// active row or dismiss the popup.
if (isIMECompositionEvent(event)) {
event.stopPropagation();
return;
}
handleDropdownNavigationKey(event, (navigationKey) => {
event.currentTarget.dispatchEvent(new KeyboardEvent('keydown', {
key: navigationKey,
bubbles: true,
cancelable: true,
}));
});
}}
/>
<CommandList label={t('chat.chatInput.draftPicker.projectTitle')}>
{filteredProjects.length === 0 ? (
<div role="status" className="px-3 py-6 text-center typography-ui-label text-muted-foreground">
{t('chat.chatInput.draftPicker.noProjectsFound')}
</div>
) : (
filteredProjects.map((project) => (
<CommandItem
key={project.id}
value={project.id}
onSelect={handleProjectSelect}
aria-current={project.id === selectedProject.id ? true : undefined}
className="max-w-full"
>
<span className="min-w-0 flex-1 truncate">
<ProjectLabel project={project} theme={theme} />
</span>
{project.id === selectedProject.id ? (
<Icon name="check" className="size-4 shrink-0 text-muted-foreground" />
) : null}
</CommandItem>
))
)}
</CommandList>
</Command>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
{showBranchSelector ? ( {showBranchSelector ? (
<Select <Select
+1
View File
@@ -2185,6 +2185,7 @@ export const dict = {
'chat.chatInput.draftPicker.projectTitle': 'Projekt', 'chat.chatInput.draftPicker.projectTitle': 'Projekt',
'chat.chatInput.draftPicker.searchProjects': 'Projekte durchsuchen...', 'chat.chatInput.draftPicker.searchProjects': 'Projekte durchsuchen...',
'chat.chatInput.draftPicker.searchBranches': 'Branches durchsuchen...', 'chat.chatInput.draftPicker.searchBranches': 'Branches durchsuchen...',
'chat.chatInput.draftPicker.noProjectsFound': 'Keine Projekte gefunden.',
'chat.chatInput.worktrees': 'Worktrees', 'chat.chatInput.worktrees': 'Worktrees',
'chat.chatInput.worktreeNew': '+ Neu', 'chat.chatInput.worktreeNew': '+ Neu',
'chat.chatInput.drop.insertMention': 'Hier ablegen, um als Erwähnung einzufügen', 'chat.chatInput.drop.insertMention': 'Hier ablegen, um als Erwähnung einzufügen',
+1
View File
@@ -2406,6 +2406,7 @@ export const dict = {
'chat.chatInput.draftPicker.projectTitle': 'Project', 'chat.chatInput.draftPicker.projectTitle': 'Project',
'chat.chatInput.draftPicker.searchProjects': 'Search projects...', 'chat.chatInput.draftPicker.searchProjects': 'Search projects...',
'chat.chatInput.draftPicker.searchBranches': 'Search branches...', 'chat.chatInput.draftPicker.searchBranches': 'Search branches...',
'chat.chatInput.draftPicker.noProjectsFound': 'No projects found.',
'chat.chatInput.worktrees': 'Worktrees', 'chat.chatInput.worktrees': 'Worktrees',
'chat.chatInput.worktreeNew': '+ New', 'chat.chatInput.worktreeNew': '+ New',
'chat.chatInput.drop.insertMention': 'Drop to insert as mention', 'chat.chatInput.drop.insertMention': 'Drop to insert as mention',
+1
View File
@@ -2372,6 +2372,7 @@ export const dict: Record<I18nKey, string> = {
"chat.chatInput.draftPicker.projectTitle": "Proyecto", "chat.chatInput.draftPicker.projectTitle": "Proyecto",
"chat.chatInput.draftPicker.searchProjects": "Buscar proyectos...", "chat.chatInput.draftPicker.searchProjects": "Buscar proyectos...",
"chat.chatInput.draftPicker.searchBranches": "Buscar ramas...", "chat.chatInput.draftPicker.searchBranches": "Buscar ramas...",
"chat.chatInput.draftPicker.noProjectsFound": "No se encontraron proyectos.",
"chat.chatInput.worktrees": "Worktrees", "chat.chatInput.worktrees": "Worktrees",
"chat.chatInput.worktreeNew": "+ Nuevo", "chat.chatInput.worktreeNew": "+ Nuevo",
"chat.chatInput.drop.insertMention": "Suelta para insertar como mención", "chat.chatInput.drop.insertMention": "Suelta para insertar como mención",
+1
View File
@@ -2114,6 +2114,7 @@ export const dict = {
'chat.chatInput.draftPicker.projectTitle': 'Projet', 'chat.chatInput.draftPicker.projectTitle': 'Projet',
'chat.chatInput.draftPicker.searchProjects': 'Rechercher des projets...', 'chat.chatInput.draftPicker.searchProjects': 'Rechercher des projets...',
'chat.chatInput.draftPicker.searchBranches': 'Rechercher des branches...', 'chat.chatInput.draftPicker.searchBranches': 'Rechercher des branches...',
'chat.chatInput.draftPicker.noProjectsFound': 'Aucun projet trouvé.',
'chat.chatInput.worktrees': 'Worktrees', 'chat.chatInput.worktrees': 'Worktrees',
'chat.chatInput.worktreeNew': '+ Nouveau', 'chat.chatInput.worktreeNew': '+ Nouveau',
'chat.chatInput.drop.insertMention': 'Déposer pour insérer comme mention', 'chat.chatInput.drop.insertMention': 'Déposer pour insérer comme mention',
+1
View File
@@ -2405,6 +2405,7 @@ export const dict: Record<I18nKey, string> = {
'chat.chatInput.draftPicker.projectTitle': 'プロジェクト', 'chat.chatInput.draftPicker.projectTitle': 'プロジェクト',
'chat.chatInput.draftPicker.searchProjects': 'プロジェクトを検索...', 'chat.chatInput.draftPicker.searchProjects': 'プロジェクトを検索...',
'chat.chatInput.draftPicker.searchBranches': 'ブランチを検索...', 'chat.chatInput.draftPicker.searchBranches': 'ブランチを検索...',
'chat.chatInput.draftPicker.noProjectsFound': 'プロジェクトが見つかりません。',
'chat.chatInput.worktrees': 'ワークツリー', 'chat.chatInput.worktrees': 'ワークツリー',
'chat.chatInput.worktreeNew': '+ 新規', 'chat.chatInput.worktreeNew': '+ 新規',
'chat.chatInput.drop.insertMention': 'ドロップしてメンションとして挿入', 'chat.chatInput.drop.insertMention': 'ドロップしてメンションとして挿入',
+1
View File
@@ -2406,6 +2406,7 @@ export const dict: Record<I18nKey, string> = {
'chat.chatInput.draftPicker.projectTitle': '프로젝트', 'chat.chatInput.draftPicker.projectTitle': '프로젝트',
'chat.chatInput.draftPicker.searchProjects': '프로젝트 검색...', 'chat.chatInput.draftPicker.searchProjects': '프로젝트 검색...',
'chat.chatInput.draftPicker.searchBranches': '브랜치 검색...', 'chat.chatInput.draftPicker.searchBranches': '브랜치 검색...',
'chat.chatInput.draftPicker.noProjectsFound': '프로젝트를 찾을 수 없습니다.',
'chat.chatInput.worktrees': '워크트리', 'chat.chatInput.worktrees': '워크트리',
'chat.chatInput.worktreeNew': '+ 새로 만들기', 'chat.chatInput.worktreeNew': '+ 새로 만들기',
'chat.chatInput.drop.insertMention': '여기에 놓아 멘션으로 추가', 'chat.chatInput.drop.insertMention': '여기에 놓아 멘션으로 추가',
+1
View File
@@ -1267,6 +1267,7 @@ export const dict: Record<I18nKey, string> = {
'chat.chatInput.draftPicker.projectTitle': 'Projekt', 'chat.chatInput.draftPicker.projectTitle': 'Projekt',
'chat.chatInput.draftPicker.searchProjects': 'Szukaj projektów...', 'chat.chatInput.draftPicker.searchProjects': 'Szukaj projektów...',
'chat.chatInput.draftPicker.searchBranches': 'Szukaj gałęzi...', 'chat.chatInput.draftPicker.searchBranches': 'Szukaj gałęzi...',
'chat.chatInput.draftPicker.noProjectsFound': 'Nie znaleziono projektów.',
'chat.chatInput.drop.attachFiles': 'Drop files here to attach', 'chat.chatInput.drop.attachFiles': 'Drop files here to attach',
'chat.chatInput.drop.insertMention': 'Drop to insert as mention', 'chat.chatInput.drop.insertMention': 'Drop to insert as mention',
'chat.chatInput.fileFallback': 'file', 'chat.chatInput.fileFallback': 'file',
@@ -2372,6 +2372,7 @@ export const dict: Record<I18nKey, string> = {
"chat.chatInput.draftPicker.projectTitle": "Projeto", "chat.chatInput.draftPicker.projectTitle": "Projeto",
"chat.chatInput.draftPicker.searchProjects": "Buscar projetos...", "chat.chatInput.draftPicker.searchProjects": "Buscar projetos...",
"chat.chatInput.draftPicker.searchBranches": "Buscar branches...", "chat.chatInput.draftPicker.searchBranches": "Buscar branches...",
"chat.chatInput.draftPicker.noProjectsFound": "Nenhum projeto encontrado.",
"chat.chatInput.worktrees": "Worktrees", "chat.chatInput.worktrees": "Worktrees",
"chat.chatInput.worktreeNew": "+ Novo", "chat.chatInput.worktreeNew": "+ Novo",
"chat.chatInput.drop.insertMention": "Solte para inserir como menção", "chat.chatInput.drop.insertMention": "Solte para inserir como menção",
+1
View File
@@ -2346,6 +2346,7 @@ export const dict = {
'chat.chatInput.draftPicker.projectTitle': 'Proje', 'chat.chatInput.draftPicker.projectTitle': 'Proje',
'chat.chatInput.draftPicker.searchProjects': 'Projelerde ara...', 'chat.chatInput.draftPicker.searchProjects': 'Projelerde ara...',
'chat.chatInput.draftPicker.searchBranches': 'Branch\'lerde ara...', 'chat.chatInput.draftPicker.searchBranches': 'Branch\'lerde ara...',
'chat.chatInput.draftPicker.noProjectsFound': 'Proje bulunamadı.',
'chat.chatInput.worktrees': 'Worktree\'ler', 'chat.chatInput.worktrees': 'Worktree\'ler',
'chat.chatInput.worktreeNew': '+ Yeni', 'chat.chatInput.worktreeNew': '+ Yeni',
'chat.chatInput.drop.insertMention': 'Mention olarak eklemek için bırak', 'chat.chatInput.drop.insertMention': 'Mention olarak eklemek için bırak',
+1
View File
@@ -2372,6 +2372,7 @@ export const dict: Record<I18nKey, string> = {
"chat.chatInput.draftPicker.projectTitle": "Проєкт", "chat.chatInput.draftPicker.projectTitle": "Проєкт",
"chat.chatInput.draftPicker.searchProjects": "Пошук проєктів...", "chat.chatInput.draftPicker.searchProjects": "Пошук проєктів...",
"chat.chatInput.draftPicker.searchBranches": "Пошук гілок...", "chat.chatInput.draftPicker.searchBranches": "Пошук гілок...",
"chat.chatInput.draftPicker.noProjectsFound": "Проєктів не знайдено.",
"chat.chatInput.worktrees": "Worktree", "chat.chatInput.worktrees": "Worktree",
"chat.chatInput.worktreeNew": "+ Новий", "chat.chatInput.worktreeNew": "+ Новий",
"chat.chatInput.drop.insertMention": "Відпустіть, щоб вставити як згадку", "chat.chatInput.drop.insertMention": "Відпустіть, щоб вставити як згадку",
@@ -2372,6 +2372,7 @@ export const dict: Record<I18nKey, string> = {
'chat.chatInput.draftPicker.projectTitle': '项目', 'chat.chatInput.draftPicker.projectTitle': '项目',
'chat.chatInput.draftPicker.searchProjects': '搜索项目...', 'chat.chatInput.draftPicker.searchProjects': '搜索项目...',
'chat.chatInput.draftPicker.searchBranches': '搜索分支...', 'chat.chatInput.draftPicker.searchBranches': '搜索分支...',
'chat.chatInput.draftPicker.noProjectsFound': '未找到项目。',
'chat.chatInput.worktrees': '工作树', 'chat.chatInput.worktrees': '工作树',
'chat.chatInput.worktreeNew': '+ 新建', 'chat.chatInput.worktreeNew': '+ 新建',
'chat.chatInput.drop.insertMention': '释放以插入为提及', 'chat.chatInput.drop.insertMention': '释放以插入为提及',
@@ -2376,6 +2376,7 @@ export const dict: Record<I18nKey, string> = {
'chat.chatInput.draftPicker.projectTitle': '專案', 'chat.chatInput.draftPicker.projectTitle': '專案',
'chat.chatInput.draftPicker.searchProjects': '搜尋專案...', 'chat.chatInput.draftPicker.searchProjects': '搜尋專案...',
'chat.chatInput.draftPicker.searchBranches': '搜尋分支...', 'chat.chatInput.draftPicker.searchBranches': '搜尋分支...',
'chat.chatInput.draftPicker.noProjectsFound': '找不到專案。',
'chat.chatInput.worktrees': 'Worktree', 'chat.chatInput.worktrees': 'Worktree',
'chat.chatInput.worktreeNew': '+ 新增', 'chat.chatInput.worktreeNew': '+ 新增',
'chat.chatInput.drop.insertMention': '放開以插入為提及', 'chat.chatInput.drop.insertMention': '放開以插入為提及',