Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "codeg",
|
"name": "codeg",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.1",
|
"version": "0.2.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
|
|||||||
29
src-tauri/Cargo.lock
generated
29
src-tauri/Cargo.lock
generated
@@ -409,7 +409,7 @@ dependencies = [
|
|||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"shlex",
|
"shlex",
|
||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
"which",
|
"which 4.4.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -792,7 +792,7 @@ checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codeg"
|
name = "codeg"
|
||||||
version = "0.2.1"
|
version = "0.2.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"agent-client-protocol-schema",
|
"agent-client-protocol-schema",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -827,6 +827,7 @@ dependencies = [
|
|||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
"which 7.0.3",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
"zip 2.4.2",
|
"zip 2.4.2",
|
||||||
]
|
]
|
||||||
@@ -1422,6 +1423,12 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_home"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -7185,6 +7192,18 @@ dependencies = [
|
|||||||
"rustix 0.38.44",
|
"rustix 0.38.44",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "which"
|
||||||
|
version = "7.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"env_home",
|
||||||
|
"rustix 1.1.3",
|
||||||
|
"winsafe",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whoami"
|
name = "whoami"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
@@ -7762,6 +7781,12 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winsafe"
|
||||||
|
version = "0.0.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.51.0"
|
version = "0.51.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "codeg"
|
name = "codeg"
|
||||||
version = "0.2.1"
|
version = "0.2.3"
|
||||||
description = "Agent Code Generation App"
|
description = "Agent Code Generation App"
|
||||||
authors = ["feitao"]
|
authors = ["feitao"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -48,6 +48,7 @@ notify = "6"
|
|||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
agent-client-protocol-schema = { version = "0.10", features = ["unstable_session_usage", "unstable_session_fork"] }
|
agent-client-protocol-schema = { version = "0.10", features = ["unstable_session_usage", "unstable_session_fork"] }
|
||||||
kill_tree = { version = "0.2", features = ["tokio"] }
|
kill_tree = { version = "0.2", features = ["tokio"] }
|
||||||
|
which = "7"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-window-state = "2"
|
tauri-plugin-window-state = "2"
|
||||||
|
|||||||
@@ -131,9 +131,13 @@ async fn build_agent(
|
|||||||
parts.push(format!("{k}={v}"));
|
parts.push(format!("{k}={v}"));
|
||||||
}
|
}
|
||||||
parts.push(
|
parts.push(
|
||||||
crate::process::normalized_program(cmd)
|
which::which(cmd)
|
||||||
.to_string_lossy()
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
.to_string(),
|
.unwrap_or_else(|_| {
|
||||||
|
crate::process::normalized_program(cmd)
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
for a in args {
|
for a in args {
|
||||||
parts.push((*a).into());
|
parts.push((*a).into());
|
||||||
|
|||||||
@@ -93,14 +93,24 @@ async fn check_npm_environment(node_required: Option<&str>) -> Vec<CheckItem> {
|
|||||||
return checks;
|
return checks;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run node and npm checks in parallel
|
// Resolve absolute paths via `which` crate to avoid GUI PATH issues,
|
||||||
|
// then run version checks in parallel.
|
||||||
|
let node_path = which::which("node").ok();
|
||||||
|
let npm_path = which::which("npm").ok();
|
||||||
|
|
||||||
let (node_result, npm_result) = tokio::join!(
|
let (node_result, npm_result) = tokio::join!(
|
||||||
crate::process::tokio_command("node")
|
async {
|
||||||
.arg("--version")
|
match &node_path {
|
||||||
.output(),
|
Some(p) => crate::process::tokio_command(p).arg("--version").output().await,
|
||||||
crate::process::tokio_command("npm")
|
None => Err(std::io::Error::new(std::io::ErrorKind::NotFound, "node not found in PATH")),
|
||||||
.arg("--version")
|
}
|
||||||
.output(),
|
},
|
||||||
|
async {
|
||||||
|
match &npm_path {
|
||||||
|
Some(p) => crate::process::tokio_command(p).arg("--version").output().await,
|
||||||
|
None => Err(std::io::Error::new(std::io::ErrorKind::NotFound, "npm not found in PATH")),
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Track the raw node version string for reuse in the version check
|
// Track the raw node version string for reuse in the version check
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ pub fn get_agent_meta(agent_type: AgentType) -> AcpAgentMeta {
|
|||||||
name: "Claude Code",
|
name: "Claude Code",
|
||||||
description: "ACP wrapper for Anthropic's Claude",
|
description: "ACP wrapper for Anthropic's Claude",
|
||||||
distribution: AgentDistribution::Npx {
|
distribution: AgentDistribution::Npx {
|
||||||
version: "0.22.1",
|
version: "0.22.2",
|
||||||
package: "@zed-industries/claude-agent-acp@0.22.1",
|
package: "@zed-industries/claude-agent-acp@0.22.2",
|
||||||
cmd: "claude-agent-acp",
|
cmd: "claude-agent-acp",
|
||||||
args: &[],
|
args: &[],
|
||||||
env: &[],
|
env: &[],
|
||||||
|
|||||||
@@ -81,31 +81,49 @@ fn package_name_from_spec(package: &str) -> String {
|
|||||||
/// Check whether a command is available on the system PATH.
|
/// Check whether a command is available on the system PATH.
|
||||||
/// Uses `which` on unix and `where` on windows — lightweight and does not
|
/// Uses `which` on unix and `where` on windows — lightweight and does not
|
||||||
/// invoke the target binary itself, avoiding side-effects or slow startups.
|
/// invoke the target binary itself, avoiding side-effects or slow startups.
|
||||||
async fn is_cmd_available(cmd: &str) -> bool {
|
fn is_cmd_available(cmd: &str) -> bool {
|
||||||
#[cfg(unix)]
|
which::which(cmd).is_ok()
|
||||||
let check_cmd = "which";
|
}
|
||||||
#[cfg(windows)]
|
|
||||||
let check_cmd = "where";
|
|
||||||
|
|
||||||
crate::process::tokio_command(check_cmd)
|
/// Detect the actual installed version of an npm global package by running
|
||||||
.arg(cmd)
|
/// `npm list -g <package_name> --json` and parsing the JSON output.
|
||||||
.stdout(std::process::Stdio::null())
|
async fn detect_npm_global_version(package_name: &str) -> Option<String> {
|
||||||
.stderr(std::process::Stdio::null())
|
let npm_path = which::which("npm").ok()?;
|
||||||
.status()
|
let output = crate::process::tokio_command(npm_path)
|
||||||
|
.arg("list")
|
||||||
|
.arg("-g")
|
||||||
|
.arg(package_name)
|
||||||
|
.arg("--json")
|
||||||
|
.arg("--depth=0")
|
||||||
|
.output()
|
||||||
.await
|
.await
|
||||||
.map(|s| s.success())
|
.ok()?;
|
||||||
.unwrap_or(false)
|
// npm list --json may exit non-zero when package is missing, but still
|
||||||
|
// outputs valid JSON with an empty dependencies object.
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let json: serde_json::Value = serde_json::from_str(&stdout).ok()?;
|
||||||
|
let version = json
|
||||||
|
.get("dependencies")?
|
||||||
|
.get(package_name)?
|
||||||
|
.get("version")?
|
||||||
|
.as_str()?;
|
||||||
|
normalize_version_candidate(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn detect_local_version(agent_type: AgentType) -> Option<String> {
|
async fn detect_local_version(agent_type: AgentType) -> Option<String> {
|
||||||
let meta = registry::get_agent_meta(agent_type);
|
let meta = registry::get_agent_meta(agent_type);
|
||||||
match meta.distribution {
|
match meta.distribution {
|
||||||
registry::AgentDistribution::Npx { cmd, package, .. } => {
|
registry::AgentDistribution::Npx { cmd, package, .. } => {
|
||||||
if is_cmd_available(cmd).await {
|
if !is_cmd_available(cmd) {
|
||||||
version_from_package_spec(package)
|
return None;
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
// Try `npm list -g <package_name> --json` to get the real installed version.
|
||||||
|
let pkg_name = package_name_from_spec(package);
|
||||||
|
if let Some(v) = detect_npm_global_version(&pkg_name).await {
|
||||||
|
return Some(v);
|
||||||
|
}
|
||||||
|
// Fallback: parse version from registry package spec
|
||||||
|
version_from_package_spec(package)
|
||||||
}
|
}
|
||||||
registry::AgentDistribution::Binary { cmd, .. } => {
|
registry::AgentDistribution::Binary { cmd, .. } => {
|
||||||
binary_cache::detect_installed_version(agent_type, cmd)
|
binary_cache::detect_installed_version(agent_type, cmd)
|
||||||
@@ -1047,7 +1065,7 @@ pub async fn acp_connect(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let registry::AgentDistribution::Npx { cmd, .. } = meta.distribution {
|
if let registry::AgentDistribution::Npx { cmd, .. } = meta.distribution {
|
||||||
if !is_cmd_available(cmd).await {
|
if !is_cmd_available(cmd) {
|
||||||
return Err(AcpError::protocol(format!(
|
return Err(AcpError::protocol(format!(
|
||||||
"{} SDK is not installed. Please install it in Agent Settings.",
|
"{} SDK is not installed. Please install it in Agent Settings.",
|
||||||
meta.name
|
meta.name
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "codeg",
|
"productName": "codeg",
|
||||||
"version": "0.2.1",
|
"version": "0.2.3",
|
||||||
"identifier": "app.codeg",
|
"identifier": "app.codeg",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
|||||||
@@ -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,87 @@ 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 +362,84 @@ 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[conv.status as ConversationStatus] ??
|
||||||
<span className="flex-1 truncate">
|
"bg-gray-400"
|
||||||
{conv.title || t("untitledConversation")}
|
)}
|
||||||
</span>
|
/>
|
||||||
<span className="text-xs text-muted-foreground shrink-0">
|
<span className="flex-1 truncate">
|
||||||
{AGENT_LABELS[conv.agent_type]}
|
{conv.title || t("untitledConversation")}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground shrink-0">
|
<span className="text-xs text-muted-foreground shrink-0">
|
||||||
{formatDistanceToNow(new Date(conv.created_at), {
|
{AGENT_LABELS[conv.agent_type]}
|
||||||
addSuffix: true,
|
</span>
|
||||||
locale: dateFnsLocale,
|
<span className="text-xs text-muted-foreground shrink-0">
|
||||||
})}
|
{formatDistanceToNow(new Date(conv.created_at), {
|
||||||
</span>
|
addSuffix: true,
|
||||||
</CommandItem>
|
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-blue-500" />
|
||||||
|
) : (
|
||||||
|
<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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -13,32 +13,23 @@ import { GitChangesTab } from "./aux-panel-git-changes-tab"
|
|||||||
import { GitLogTab } from "./aux-panel-git-log-tab"
|
import { GitLogTab } from "./aux-panel-git-log-tab"
|
||||||
import { SessionFilesTab } from "./aux-panel-session-files-tab"
|
import { SessionFilesTab } from "./aux-panel-session-files-tab"
|
||||||
|
|
||||||
|
const LAZY_TABS: AuxPanelTab[] = ["file_tree", "changes", "git_log"]
|
||||||
|
|
||||||
export function AuxPanel() {
|
export function AuxPanel() {
|
||||||
const t = useTranslations("Folder.auxPanel.tabs")
|
const t = useTranslations("Folder.auxPanel.tabs")
|
||||||
const { isOpen, activeTab, setActiveTab } = useAuxPanelContext()
|
const { isOpen, activeTab, setActiveTab } = useAuxPanelContext()
|
||||||
const [hasMountedFileTree, setHasMountedFileTree] = useState(
|
const [mountedTabs, setMountedTabs] = useState<Set<AuxPanelTab>>(
|
||||||
activeTab === "file_tree"
|
() => new Set(LAZY_TABS.filter((tab) => tab === activeTab))
|
||||||
)
|
|
||||||
const [hasMountedChanges, setHasMountedChanges] = useState(
|
|
||||||
activeTab === "changes"
|
|
||||||
)
|
|
||||||
const [hasMountedGitLog, setHasMountedGitLog] = useState(
|
|
||||||
activeTab === "git_log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Ensure the active tab is mounted (covers both user clicks and programmatic changes)
|
||||||
|
if (isOpen && LAZY_TABS.includes(activeTab) && !mountedTabs.has(activeTab)) {
|
||||||
|
setMountedTabs((prev) => new Set(prev).add(activeTab))
|
||||||
|
}
|
||||||
|
|
||||||
const handleTabValueChange = useCallback(
|
const handleTabValueChange = useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
const nextTab = value as AuxPanelTab
|
setActiveTab(value as AuxPanelTab)
|
||||||
setActiveTab(nextTab)
|
|
||||||
if (nextTab === "file_tree") {
|
|
||||||
setHasMountedFileTree(true)
|
|
||||||
}
|
|
||||||
if (nextTab === "changes") {
|
|
||||||
setHasMountedChanges(true)
|
|
||||||
}
|
|
||||||
if (nextTab === "git_log") {
|
|
||||||
setHasMountedGitLog(true)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[setActiveTab]
|
[setActiveTab]
|
||||||
)
|
)
|
||||||
@@ -97,21 +88,21 @@ export function AuxPanel() {
|
|||||||
forceMount
|
forceMount
|
||||||
className="mt-0 flex-1 min-h-0 overflow-hidden"
|
className="mt-0 flex-1 min-h-0 overflow-hidden"
|
||||||
>
|
>
|
||||||
{hasMountedFileTree ? <FileTreeTab /> : null}
|
{mountedTabs.has("file_tree") ? <FileTreeTab /> : null}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="changes"
|
value="changes"
|
||||||
forceMount
|
forceMount
|
||||||
className="mt-0 flex-1 min-h-0 overflow-hidden"
|
className="mt-0 flex-1 min-h-0 overflow-hidden"
|
||||||
>
|
>
|
||||||
{hasMountedChanges ? <GitChangesTab /> : null}
|
{mountedTabs.has("changes") ? <GitChangesTab /> : null}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="git_log"
|
value="git_log"
|
||||||
forceMount
|
forceMount
|
||||||
className="mt-0 flex-1 min-h-0 overflow-hidden"
|
className="mt-0 flex-1 min-h-0 overflow-hidden"
|
||||||
>
|
>
|
||||||
{hasMountedGitLog ? <GitLogTab /> : null}
|
{mountedTabs.has("git_log") ? <GitLogTab /> : null}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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": "محادثة بدون عنوان"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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": "無題の会話"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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": "제목 없는 대화"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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": "未命名会话"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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": "未命名會話"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user