支持搜索文件和目录

This commit is contained in:
xintaofei
2026-03-19 00:24:50 +08:00
parent f3acb45cb0
commit 33d70b8866
15 changed files with 432 additions and 63 deletions

View File

@@ -1,16 +1,25 @@
"use client" "use client"
import { useState, useEffect, useRef, useCallback } from "react" import { useState, useEffect, useRef, useCallback, useMemo } from "react"
import { formatDistanceToNow } from "date-fns" import { formatDistanceToNow } from "date-fns"
import { enUS, zhCN, zhTW } from "date-fns/locale" import { enUS, zhCN, zhTW } from "date-fns/locale"
import { File, Folder } from "lucide-react"
import ig from "ignore"
import { useLocale, useTranslations } from "next-intl" import { useLocale, useTranslations } from "next-intl"
import { useAuxPanelContext } from "@/contexts/aux-panel-context"
import { useFolderContext } from "@/contexts/folder-context" import { useFolderContext } from "@/contexts/folder-context"
import { useTabContext } from "@/contexts/tab-context" import { useTabContext } from "@/contexts/tab-context"
import { listFolderConversations } from "@/lib/tauri" import { useWorkspaceContext } from "@/contexts/workspace-context"
import {
getFileTree,
listFolderConversations,
readFilePreview,
} from "@/lib/tauri"
import type { import type {
AgentType, AgentType,
ConversationStatus, ConversationStatus,
DbConversationSummary, DbConversationSummary,
FileTreeNode,
} from "@/lib/types" } from "@/lib/types"
import { AGENT_LABELS, STATUS_COLORS, compareAgentType } from "@/lib/types" import { AGENT_LABELS, STATUS_COLORS, compareAgentType } from "@/lib/types"
import { AgentIcon } from "@/components/agent-icon" import { AgentIcon } from "@/components/agent-icon"
@@ -24,6 +33,51 @@ import {
} from "@/components/ui/command" } from "@/components/ui/command"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
type SearchTab = "conversations" | "files"
interface FlatFileEntry {
name: string
/** Relative path from folder root (same as FileTreeNode.path) */
relativePath: string
kind: "file" | "dir"
/** Pre-computed lowercase relativePath for filtering */
lowerPath: string
/** Pre-computed lowercase name for filtering */
lowerName: string
}
function flattenTree(nodes: FileTreeNode[]): FlatFileEntry[] {
const entries: FlatFileEntry[] = []
function walk(node: FileTreeNode) {
entries.push({
name: node.name,
relativePath: node.path,
kind: node.kind,
lowerPath: node.path.toLowerCase(),
lowerName: node.name.toLowerCase(),
})
if (node.kind === "dir" && node.children) {
for (const child of node.children) {
walk(child)
}
}
}
for (const node of nodes) {
walk(node)
}
return entries
}
/** Check whether any ancestor directory of `path` is in `ignoredDirs`. */
function hasIgnoredAncestor(path: string, ignoredDirs: Set<string>): boolean {
let idx = path.indexOf("/")
while (idx !== -1) {
if (ignoredDirs.has(path.slice(0, idx))) return true
idx = path.indexOf("/", idx + 1)
}
return false
}
interface SearchCommandDialogProps { interface SearchCommandDialogProps {
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
@@ -37,20 +91,125 @@ export function SearchCommandDialog({
const locale = useLocale() const locale = useLocale()
const dateFnsLocale = const dateFnsLocale =
locale === "zh-CN" ? zhCN : locale === "zh-TW" ? zhTW : enUS locale === "zh-CN" ? zhCN : locale === "zh-TW" ? zhTW : enUS
const { folderId, conversations } = useFolderContext() const { folderId, folder, conversations } = useFolderContext()
const { openTab } = useTabContext() const { openTab } = useTabContext()
const { openFilePreview } = useWorkspaceContext()
const { revealInFileTree } = useAuxPanelContext()
const [activeTab, setActiveTab] = useState<SearchTab>("conversations")
const [query, setQuery] = useState("") const [query, setQuery] = useState("")
const [agentFilter, setAgentFilter] = useState<AgentType | null>(null) const [agentFilter, setAgentFilter] = useState<AgentType | null>(null)
const [results, setResults] = useState<DbConversationSummary[]>([]) const [results, setResults] = useState<DbConversationSummary[]>([])
const [searching, setSearching] = useState(false) const [searching, setSearching] = useState(false)
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined) const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined)
// File search state
const [allFiles, setAllFiles] = useState<FlatFileEntry[]>([])
const [filesLoading, setFilesLoading] = useState(false)
const filesLoadedRef = useRef(false)
const folderPath = folder?.path ?? ""
// Compute which agent types exist in current folder // Compute which agent types exist in current folder
const availableAgents = Array.from( const availableAgents = Array.from(
new Set(conversations.map((c) => c.agent_type)) new Set(conversations.map((c) => c.agent_type))
).sort(compareAgentType) ).sort(compareAgentType)
// Load file tree when switching to files tab, filtering by .gitignore
useEffect(() => {
if (activeTab !== "files" || !folderPath || filesLoadedRef.current) return
let canceled = false
setFilesLoading(true)
async function load() {
try {
const tree = await getFileTree(folderPath, 10)
const flat = flattenTree(tree)
// Collect all .gitignore files from the tree
const gitignoreEntries = flat.filter(
(f) => f.kind === "file" && f.name === ".gitignore"
)
// Build matchers keyed by directory prefix
const matchers: { prefix: string; matcher: ReturnType<typeof ig> }[] =
[]
await Promise.all(
gitignoreEntries.map(async (entry) => {
try {
const result = await readFilePreview(
folderPath,
entry.relativePath
)
const lastSlash = entry.relativePath.lastIndexOf("/")
const dir = lastSlash === -1 ? "" : entry.relativePath.slice(0, lastSlash)
matchers.push({
prefix: dir ? dir + "/" : "",
matcher: ig().add(result.content),
})
} catch {
// skip unreadable .gitignore
}
})
)
// Sort matchers by prefix length (shortest/root first) so that
// parent rules are evaluated before child rules.
matchers.sort((a, b) => a.prefix.length - b.prefix.length)
// Filter: check each entry against all applicable .gitignore matchers
const ignoredDirs = new Set<string>()
const filtered = flat.filter((f) => {
// Skip .gitignore files themselves from results
if (f.name === ".gitignore") return false
// If an ancestor directory is already ignored, skip — O(depth) lookup
if (hasIgnoredAncestor(f.relativePath, ignoredDirs)) return false
for (const { prefix, matcher } of matchers) {
if (!f.relativePath.startsWith(prefix)) continue
const relPath = f.relativePath.slice(prefix.length)
if (!relPath) continue
const testPath =
f.kind === "dir" ? `${relPath}/` : relPath
if (matcher.ignores(testPath)) {
if (f.kind === "dir") ignoredDirs.add(f.relativePath)
return false
}
}
return true
})
if (!canceled) {
setAllFiles(filtered)
filesLoadedRef.current = true
}
} catch {
if (!canceled) setAllFiles([])
} finally {
if (!canceled) setFilesLoading(false)
}
}
void load()
return () => {
canceled = true
}
}, [activeTab, folderPath])
// Filter files by query using pre-computed lowercase fields
const filteredFiles = useMemo(() => {
const trimmed = query.trim()
if (!trimmed) return allFiles.slice(0, 100)
const lower = trimmed.toLowerCase()
const matched: FlatFileEntry[] = []
for (const f of allFiles) {
if (f.lowerName.includes(lower) || f.lowerPath.includes(lower)) {
matched.push(f)
if (matched.length >= 100) break
}
}
return matched
}, [allFiles, query])
const doSearch = useCallback( const doSearch = useCallback(
async (q: string, agent: AgentType | null) => { async (q: string, agent: AgentType | null) => {
if (!q.trim() && !agent) { if (!q.trim() && !agent) {
@@ -75,8 +234,9 @@ export function SearchCommandDialog({
[folderId] [folderId]
) )
// Debounced search on query change // Debounced search on query change (conversations tab only)
useEffect(() => { useEffect(() => {
if (activeTab !== "conversations") return
if (debounceRef.current) clearTimeout(debounceRef.current) if (debounceRef.current) clearTimeout(debounceRef.current)
debounceRef.current = setTimeout(() => { debounceRef.current = setTimeout(() => {
doSearch(query, agentFilter) doSearch(query, agentFilter)
@@ -84,7 +244,7 @@ export function SearchCommandDialog({
return () => { return () => {
if (debounceRef.current) clearTimeout(debounceRef.current) if (debounceRef.current) clearTimeout(debounceRef.current)
} }
}, [query, agentFilter, doSearch]) }, [query, agentFilter, doSearch, activeTab])
// Reset state when dialog closes // Reset state when dialog closes
useEffect(() => { useEffect(() => {
@@ -92,26 +252,89 @@ export function SearchCommandDialog({
setQuery("") setQuery("")
setAgentFilter(null) setAgentFilter(null)
setResults([]) setResults([])
setActiveTab("conversations")
filesLoadedRef.current = false
setAllFiles([])
} }
}, [open]) }, [open])
const handleSelect = (conv: DbConversationSummary) => { const handleSelectConversation = useCallback(
openTab(conv.id, conv.agent_type, true) (conv: DbConversationSummary) => {
onOpenChange(false) openTab(conv.id, conv.agent_type, true)
} onOpenChange(false)
},
[openTab, onOpenChange]
)
const handleSelectFile = useCallback(
(entry: FlatFileEntry) => {
if (entry.kind === "dir") {
revealInFileTree(entry.relativePath)
} else {
// Reveal parent directory in file tree, then open the file
const lastSlash = entry.relativePath.lastIndexOf("/")
if (lastSlash > 0) {
revealInFileTree(entry.relativePath.slice(0, lastSlash))
}
openFilePreview(entry.relativePath)
}
onOpenChange(false)
},
[revealInFileTree, openFilePreview, onOpenChange]
)
const placeholder =
activeTab === "conversations"
? t("placeholder")
: t("filePlaceholder")
return ( return (
<CommandDialog <CommandDialog
title={t("dialogTitle")} title={t("dialogTitle")}
open={open} open={open}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
shouldFilter={activeTab === "conversations"}
> >
{/* Tabs */}
<div className="flex items-center gap-0 border-b px-3">
<button
onClick={() => setActiveTab("conversations")}
className={cn(
"relative h-9 px-3 text-sm font-medium transition-colors",
activeTab === "conversations"
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
>
{t("tabConversations")}
{activeTab === "conversations" && (
<span className="absolute bottom-0 left-3 right-3 h-0.5 bg-foreground rounded-full" />
)}
</button>
<button
onClick={() => setActiveTab("files")}
className={cn(
"relative h-9 px-3 text-sm font-medium transition-colors",
activeTab === "files"
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
>
{t("tabFiles")}
{activeTab === "files" && (
<span className="absolute bottom-0 left-3 right-3 h-0.5 bg-foreground rounded-full" />
)}
</button>
</div>
<CommandInput <CommandInput
placeholder={t("placeholder")} placeholder={placeholder}
value={query} value={query}
onValueChange={setQuery} onValueChange={setQuery}
/> />
{availableAgents.length > 1 && (
{/* Agent filter (conversations tab only) */}
{activeTab === "conversations" && availableAgents.length > 1 && (
<div className="flex items-center gap-1 px-3 py-2 border-b"> <div className="flex items-center gap-1 px-3 py-2 border-b">
<button <button
onClick={() => setAgentFilter(null)} onClick={() => setAgentFilter(null)}
@@ -141,44 +364,87 @@ export function SearchCommandDialog({
))} ))}
</div> </div>
)} )}
<CommandList className="min-h-96"> <CommandList className="min-h-96">
<CommandEmpty> {/* Conversations tab */}
{searching {activeTab === "conversations" && (
? t("searching") <>
: !query.trim() && !agentFilter <CommandEmpty>
? t("typeToSearch") {searching
: t("noResults")} ? t("searching")
</CommandEmpty> : !query.trim() && !agentFilter
{results.length > 0 && ( ? t("typeToSearch")
<CommandGroup> : t("noResults")}
{results.map((conv) => ( </CommandEmpty>
<CommandItem {results.length > 0 && (
key={conv.id} <CommandGroup>
value={`${conv.id}-${conv.title ?? ""}`} {results.map((conv) => (
onSelect={() => handleSelect(conv)} <CommandItem
> key={conv.id}
<span value={`${conv.id}-${conv.title ?? ""}`}
className={cn( onSelect={() => handleSelectConversation(conv)}
"w-2 h-2 rounded-full shrink-0", >
STATUS_COLORS[conv.status as ConversationStatus] ?? <span
"bg-gray-400" className={cn(
)} "w-2 h-2 rounded-full shrink-0",
/> STATUS_COLORS[
<span className="flex-1 truncate"> conv.status as ConversationStatus
{conv.title || t("untitledConversation")} ] ?? "bg-gray-400"
</span> )}
<span className="text-xs text-muted-foreground shrink-0"> />
{AGENT_LABELS[conv.agent_type]} <span className="flex-1 truncate">
</span> {conv.title || t("untitledConversation")}
<span className="text-xs text-muted-foreground shrink-0"> </span>
{formatDistanceToNow(new Date(conv.created_at), { <span className="text-xs text-muted-foreground shrink-0">
addSuffix: true, {AGENT_LABELS[conv.agent_type]}
locale: dateFnsLocale, </span>
})} <span className="text-xs text-muted-foreground shrink-0">
</span> {formatDistanceToNow(new Date(conv.created_at), {
</CommandItem> addSuffix: true,
))} locale: dateFnsLocale,
</CommandGroup> })}
</span>
</CommandItem>
))}
</CommandGroup>
)}
</>
)}
{/* Files tab */}
{activeTab === "files" && (
<>
<CommandEmpty>
{filesLoading
? t("searching")
: !query.trim()
? t("typeToSearchFiles")
: t("noResults")}
</CommandEmpty>
{filteredFiles.length > 0 && (
<CommandGroup>
{filteredFiles.map((entry) => (
<CommandItem
key={entry.relativePath}
value={entry.relativePath}
onSelect={() => handleSelectFile(entry)}
>
{entry.kind === "dir" ? (
<Folder className="w-4 h-4 shrink-0 text-muted-foreground" />
) : (
<File className="w-4 h-4 shrink-0 text-muted-foreground" />
)}
<span className="flex-1 truncate">
{entry.name}
</span>
<span className="text-xs text-muted-foreground shrink-0 truncate max-w-48">
{entry.relativePath}
</span>
</CommandItem>
))}
</CommandGroup>
)}
</>
)} )}
</CommandList> </CommandList>
</CommandDialog> </CommandDialog>

View File

@@ -744,7 +744,8 @@ function RenderNode({
export function FileTreeTab() { export function FileTreeTab() {
const t = useTranslations("Folder.fileTreeTab") const t = useTranslations("Folder.fileTreeTab")
const tCommon = useTranslations("Folder.common") const tCommon = useTranslations("Folder.common")
const { activeTab } = useAuxPanelContext() const { activeTab, pendingRevealPath, consumePendingRevealPath } =
useAuxPanelContext()
const { folder } = useFolderContext() const { folder } = useFolderContext()
const { tabs, activeTabId } = useTabContext() const { tabs, activeTabId } = useTabContext()
const { createTerminalInDirectory } = useTerminalContext() const { createTerminalInDirectory } = useTerminalContext()
@@ -857,6 +858,24 @@ export function FileTreeTab() {
externalConflictSignatureByPathRef.current.clear() externalConflictSignatureByPathRef.current.clear()
}, [folder?.path]) }, [folder?.path])
// Handle pending reveal path: expand all ancestor directories once tree is loaded
const hasNodes = nodes.length > 0
useEffect(() => {
if (!pendingRevealPath || !hasNodes) return
consumePendingRevealPath()
setExpandedPaths((prev) => {
const next = new Set(prev)
next.add(FILE_TREE_ROOT_PATH)
let idx = pendingRevealPath.indexOf("/")
while (idx !== -1) {
next.add(pendingRevealPath.slice(0, idx))
idx = pendingRevealPath.indexOf("/", idx + 1)
}
next.add(pendingRevealPath)
return next
})
}, [pendingRevealPath, consumePendingRevealPath, hasNodes])
useEffect(() => { useEffect(() => {
if (!activeFileTab || activeFileTab.kind !== "file") return if (!activeFileTab || activeFileTab.kind !== "file") return
if (!activeFileTab.path) return if (!activeFileTab.path) return

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { useCallback, useState } from "react" import { useCallback, useEffect, useState } from "react"
import { FileDiff, Folder, FolderPen, GitCommit } from "lucide-react" import { FileDiff, Folder, FolderPen, GitCommit } from "lucide-react"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { import {
@@ -26,6 +26,14 @@ export function AuxPanel() {
activeTab === "git_log" activeTab === "git_log"
) )
// Sync mount flags when activeTab changes programmatically (e.g. revealInFileTree)
useEffect(() => {
if (!isOpen) return
if (activeTab === "file_tree") setHasMountedFileTree(true)
else if (activeTab === "changes") setHasMountedChanges(true)
else if (activeTab === "git_log") setHasMountedGitLog(true)
}, [isOpen, activeTab])
const handleTabValueChange = useCallback( const handleTabValueChange = useCallback(
(value: string) => { (value: string) => {
const nextTab = value as AuxPanelTab const nextTab = value as AuxPanelTab

View File

@@ -26,15 +26,22 @@ function Command({
function CommandDialog({ function CommandDialog({
title = "Command", title = "Command",
children, children,
shouldFilter,
...props ...props
}: React.ComponentProps<typeof Dialog> & { title?: string }) { }: React.ComponentProps<typeof Dialog> & {
title?: string
shouldFilter?: boolean
}) {
return ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogContent className="overflow-hidden p-0 rounded-2xl max-w-lg"> <DialogContent className="overflow-hidden p-0 rounded-2xl max-w-lg">
<VisuallyHidden.Root> <VisuallyHidden.Root>
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
</VisuallyHidden.Root> </VisuallyHidden.Root>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3"> <Command
shouldFilter={shouldFilter}
className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3"
>
{children} {children}
</Command> </Command>
</DialogContent> </DialogContent>

View File

@@ -31,6 +31,9 @@ interface AuxPanelContextValue {
setWidth: (w: number) => void setWidth: (w: number) => void
setActiveTab: (tab: AuxPanelTab) => void setActiveTab: (tab: AuxPanelTab) => void
openTab: (tab: AuxPanelTab) => void openTab: (tab: AuxPanelTab) => void
pendingRevealPath: string | null
revealInFileTree: (path: string) => void
consumePendingRevealPath: () => void
} }
const AuxPanelContext = createContext<AuxPanelContextValue | null>(null) const AuxPanelContext = createContext<AuxPanelContextValue | null>(null)
@@ -64,6 +67,9 @@ export function AuxPanelProvider({
const [width, setWidthState] = useState(DEFAULT_WIDTH) const [width, setWidthState] = useState(DEFAULT_WIDTH)
const [restored, setRestored] = useState(false) const [restored, setRestored] = useState(false)
const [activeTab, setActiveTab] = useState<AuxPanelTab>("session_files") const [activeTab, setActiveTab] = useState<AuxPanelTab>("session_files")
const [pendingRevealPath, setPendingRevealPath] = useState<string | null>(
null
)
const toggle = useCallback(() => setIsOpen((prev) => !prev), []) const toggle = useCallback(() => setIsOpen((prev) => !prev), [])
@@ -76,6 +82,16 @@ export function AuxPanelProvider({
setIsOpen(true) setIsOpen(true)
}, []) }, [])
const revealInFileTree = useCallback((path: string) => {
setPendingRevealPath(path)
setActiveTab("file_tree")
setIsOpen(true)
}, [])
const consumePendingRevealPath = useCallback(() => {
setPendingRevealPath(null)
}, [])
useEffect(() => { useEffect(() => {
const stored = loadPersistedPanelState(storageKey) const stored = loadPersistedPanelState(storageKey)
// Hydrate from localStorage after mount to keep SSR/CSR markup consistent. // Hydrate from localStorage after mount to keep SSR/CSR markup consistent.
@@ -101,8 +117,21 @@ export function AuxPanelProvider({
setWidth, setWidth,
setActiveTab, setActiveTab,
openTab, openTab,
pendingRevealPath,
revealInFileTree,
consumePendingRevealPath,
}), }),
[isOpen, width, activeTab, toggle, setWidth, openTab] [
isOpen,
width,
activeTab,
toggle,
setWidth,
openTab,
pendingRevealPath,
revealInFileTree,
consumePendingRevealPath,
]
) )
return ( return (

View File

@@ -648,11 +648,15 @@
"save": "حفظ" "save": "حفظ"
}, },
"search": { "search": {
"dialogTitle": "البحث في المحادثات", "dialogTitle": "بحث",
"tabConversations": "المحادثات",
"tabFiles": "الملفات",
"placeholder": "البحث في المحادثات...", "placeholder": "البحث في المحادثات...",
"filePlaceholder": "البحث في الملفات أو المجلدات...",
"allAgents": "الكل", "allAgents": "الكل",
"searching": "جارٍ البحث...", "searching": "جارٍ البحث...",
"typeToSearch": "اكتب للبحث في المحادثات", "typeToSearch": "اكتب للبحث في المحادثات",
"typeToSearchFiles": "اكتب للبحث في الملفات أو المجلدات",
"noResults": "لم يتم العثور على نتائج.", "noResults": "لم يتم العثور على نتائج.",
"untitledConversation": "محادثة بدون عنوان" "untitledConversation": "محادثة بدون عنوان"
}, },

View File

@@ -648,11 +648,15 @@
"save": "Speichern" "save": "Speichern"
}, },
"search": { "search": {
"dialogTitle": "Konversationen suchen", "dialogTitle": "Suchen",
"tabConversations": "Konversationen",
"tabFiles": "Dateien",
"placeholder": "Konversationen suchen...", "placeholder": "Konversationen suchen...",
"filePlaceholder": "Dateien oder Verzeichnisse suchen...",
"allAgents": "Alle", "allAgents": "Alle",
"searching": "Suche...", "searching": "Suche...",
"typeToSearch": "Tippen, um Konversationen zu suchen", "typeToSearch": "Tippen, um Konversationen zu suchen",
"typeToSearchFiles": "Tippen, um Dateien oder Verzeichnisse zu suchen",
"noResults": "Keine Ergebnisse gefunden.", "noResults": "Keine Ergebnisse gefunden.",
"untitledConversation": "Unbenannte Konversation" "untitledConversation": "Unbenannte Konversation"
}, },

View File

@@ -648,11 +648,15 @@
"save": "Save" "save": "Save"
}, },
"search": { "search": {
"dialogTitle": "Search conversations", "dialogTitle": "Search",
"tabConversations": "Conversations",
"tabFiles": "Files",
"placeholder": "Search conversations...", "placeholder": "Search conversations...",
"filePlaceholder": "Search files or directories...",
"allAgents": "All", "allAgents": "All",
"searching": "Searching...", "searching": "Searching...",
"typeToSearch": "Type to search conversations", "typeToSearch": "Type to search conversations",
"typeToSearchFiles": "Type to search files or directories",
"noResults": "No results found.", "noResults": "No results found.",
"untitledConversation": "Untitled conversation" "untitledConversation": "Untitled conversation"
}, },

View File

@@ -648,11 +648,15 @@
"save": "Guardar" "save": "Guardar"
}, },
"search": { "search": {
"dialogTitle": "Buscar conversaciones", "dialogTitle": "Buscar",
"tabConversations": "Conversaciones",
"tabFiles": "Archivos",
"placeholder": "Buscar conversaciones...", "placeholder": "Buscar conversaciones...",
"filePlaceholder": "Buscar archivos o directorios...",
"allAgents": "Todo", "allAgents": "Todo",
"searching": "Buscando...", "searching": "Buscando...",
"typeToSearch": "Escribe para buscar conversaciones", "typeToSearch": "Escribe para buscar conversaciones",
"typeToSearchFiles": "Escribe para buscar archivos o directorios",
"noResults": "No se encontraron resultados.", "noResults": "No se encontraron resultados.",
"untitledConversation": "Conversación sin título" "untitledConversation": "Conversación sin título"
}, },

View File

@@ -648,11 +648,15 @@
"save": "Enregistrer" "save": "Enregistrer"
}, },
"search": { "search": {
"dialogTitle": "Rechercher des conversations", "dialogTitle": "Rechercher",
"tabConversations": "Conversations",
"tabFiles": "Fichiers",
"placeholder": "Rechercher des conversations...", "placeholder": "Rechercher des conversations...",
"filePlaceholder": "Rechercher des fichiers ou répertoires...",
"allAgents": "Tout", "allAgents": "Tout",
"searching": "Recherche...", "searching": "Recherche...",
"typeToSearch": "Tapez pour rechercher des conversations", "typeToSearch": "Tapez pour rechercher des conversations",
"typeToSearchFiles": "Tapez pour rechercher des fichiers ou répertoires",
"noResults": "Aucun résultat trouvé.", "noResults": "Aucun résultat trouvé.",
"untitledConversation": "Conversation sans titre" "untitledConversation": "Conversation sans titre"
}, },

View File

@@ -648,11 +648,15 @@
"save": "保存" "save": "保存"
}, },
"search": { "search": {
"dialogTitle": "会話を検索", "dialogTitle": "検索",
"tabConversations": "会話",
"tabFiles": "ファイル",
"placeholder": "会話を検索...", "placeholder": "会話を検索...",
"filePlaceholder": "ファイルまたはディレクトリを検索...",
"allAgents": "すべて", "allAgents": "すべて",
"searching": "検索中...", "searching": "検索中...",
"typeToSearch": "入力して会話を検索", "typeToSearch": "入力して会話を検索",
"typeToSearchFiles": "入力してファイルまたはディレクトリを検索",
"noResults": "結果が見つかりません。", "noResults": "結果が見つかりません。",
"untitledConversation": "無題の会話" "untitledConversation": "無題の会話"
}, },

View File

@@ -648,11 +648,15 @@
"save": "저장" "save": "저장"
}, },
"search": { "search": {
"dialogTitle": "대화 검색", "dialogTitle": "검색",
"tabConversations": "대화",
"tabFiles": "파일",
"placeholder": "대화 검색...", "placeholder": "대화 검색...",
"filePlaceholder": "파일 또는 디렉토리 검색...",
"allAgents": "전체", "allAgents": "전체",
"searching": "검색 중...", "searching": "검색 중...",
"typeToSearch": "입력하여 대화를 검색하세요", "typeToSearch": "입력하여 대화를 검색하세요",
"typeToSearchFiles": "입력하여 파일 또는 디렉토리를 검색하세요",
"noResults": "검색 결과가 없습니다.", "noResults": "검색 결과가 없습니다.",
"untitledConversation": "제목 없는 대화" "untitledConversation": "제목 없는 대화"
}, },

View File

@@ -648,11 +648,15 @@
"save": "Salvar" "save": "Salvar"
}, },
"search": { "search": {
"dialogTitle": "Buscar conversas", "dialogTitle": "Buscar",
"tabConversations": "Conversas",
"tabFiles": "Arquivos",
"placeholder": "Buscar conversas...", "placeholder": "Buscar conversas...",
"filePlaceholder": "Buscar arquivos ou diretórios...",
"allAgents": "Todos", "allAgents": "Todos",
"searching": "Buscando...", "searching": "Buscando...",
"typeToSearch": "Digite para buscar conversas", "typeToSearch": "Digite para buscar conversas",
"typeToSearchFiles": "Digite para buscar arquivos ou diretórios",
"noResults": "Nenhum resultado encontrado.", "noResults": "Nenhum resultado encontrado.",
"untitledConversation": "Conversa sem título" "untitledConversation": "Conversa sem título"
}, },

View File

@@ -648,11 +648,15 @@
"save": "保存" "save": "保存"
}, },
"search": { "search": {
"dialogTitle": "搜索会话", "dialogTitle": "搜索",
"tabConversations": "会话",
"tabFiles": "文件",
"placeholder": "搜索会话...", "placeholder": "搜索会话...",
"filePlaceholder": "搜索文件或目录...",
"allAgents": "全部", "allAgents": "全部",
"searching": "搜索中...", "searching": "搜索中...",
"typeToSearch": "输入关键词搜索会话", "typeToSearch": "输入关键词搜索会话",
"typeToSearchFiles": "输入关键词搜索文件或目录",
"noResults": "未找到结果。", "noResults": "未找到结果。",
"untitledConversation": "未命名会话" "untitledConversation": "未命名会话"
}, },

View File

@@ -648,11 +648,15 @@
"save": "儲存" "save": "儲存"
}, },
"search": { "search": {
"dialogTitle": "搜尋會話", "dialogTitle": "搜尋",
"tabConversations": "會話",
"tabFiles": "檔案",
"placeholder": "搜尋會話...", "placeholder": "搜尋會話...",
"filePlaceholder": "搜尋檔案或目錄...",
"allAgents": "全部", "allAgents": "全部",
"searching": "搜尋中...", "searching": "搜尋中...",
"typeToSearch": "輸入關鍵字搜尋會話", "typeToSearch": "輸入關鍵字搜尋會話",
"typeToSearchFiles": "輸入關鍵字搜尋檔案或目錄",
"noResults": "找不到結果。", "noResults": "找不到結果。",
"untitledConversation": "未命名會話" "untitledConversation": "未命名會話"
}, },