继续填充多语言处理

This commit is contained in:
xintaofei
2026-03-07 14:00:29 +08:00
parent a356b813a6
commit 47189318e5
11 changed files with 322 additions and 69 deletions

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import { memo, useMemo, useState } from "react" import { memo, useMemo, useState } from "react"
import { useTranslations } from "next-intl"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import type { LiveMessage } from "@/contexts/acp-connections-context" import type { LiveMessage } from "@/contexts/acp-connections-context"
@@ -36,29 +37,41 @@ function getLatestPlanEntries(message: LiveMessage | null): PlanEntryInfo[] {
return [] return []
} }
function getStatusLabel(status: string): string { function getStatusKey(
status: string
):
| "status.completed"
| "status.inProgress"
| "status.pending"
| "status.unknown" {
switch (status) { switch (status) {
case "completed": case "completed":
return "Completed" return "status.completed"
case "in_progress": case "in_progress":
return "In Progress" return "status.inProgress"
case "pending": case "pending":
return "Pending" return "status.pending"
default: default:
return "Unknown" return "status.unknown"
} }
} }
function getPriorityLabel(priority: string): string { type PriorityKey =
| "priority.high"
| "priority.medium"
| "priority.low"
| "priority.unknown"
function getPriorityKey(priority: string): PriorityKey {
switch (priority) { switch (priority) {
case "high": case "high":
return "High" return "priority.high"
case "medium": case "medium":
return "Medium" return "priority.medium"
case "low": case "low":
return "Low" return "priority.low"
default: default:
return "Unknown" return "priority.unknown"
} }
} }
@@ -94,6 +107,7 @@ export const AgentPlanOverlay = memo(function AgentPlanOverlay({
visible = true, visible = true,
defaultExpanded = true, defaultExpanded = true,
}: AgentPlanOverlayProps) { }: AgentPlanOverlayProps) {
const t = useTranslations("Folder.chat.agentPlanOverlay")
const liveEntries = useMemo( const liveEntries = useMemo(
() => getLatestPlanEntries(message ?? null), () => getLatestPlanEntries(message ?? null),
[message] [message]
@@ -146,7 +160,10 @@ export const AgentPlanOverlay = memo(function AgentPlanOverlay({
} }
> >
<ListTodoIcon className="h-4 w-4" /> <ListTodoIcon className="h-4 w-4" />
Plan {completedCount}/{resolvedEntries.length} {t("collapsedSummary", {
completed: completedCount,
total: resolvedEntries.length,
})}
<ChevronUpIcon className="h-4 w-4" /> <ChevronUpIcon className="h-4 w-4" />
</Button> </Button>
</div> </div>
@@ -162,7 +179,7 @@ export const AgentPlanOverlay = memo(function AgentPlanOverlay({
<div className="flex items-center justify-between border-b px-3 py-2"> <div className="flex items-center justify-between border-b px-3 py-2">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<ListTodoIcon className="h-4 w-4 text-muted-foreground" /> <ListTodoIcon className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium truncate">Agent Plan</span> <span className="text-sm font-medium truncate">{t("title")}</span>
<Badge variant="secondary" className="h-5"> <Badge variant="secondary" className="h-5">
{completedCount}/{resolvedEntries.length} {completedCount}/{resolvedEntries.length}
</Badge> </Badge>
@@ -171,7 +188,7 @@ export const AgentPlanOverlay = memo(function AgentPlanOverlay({
type="button" type="button"
variant="ghost" variant="ghost"
size="icon-xs" size="icon-xs"
aria-label="Collapse plan" aria-label={t("collapsePlanAria")}
onClick={() => onClick={() =>
setCollapsedByPlanKey((prev) => ({ setCollapsedByPlanKey((prev) => ({
...prev, ...prev,
@@ -204,7 +221,7 @@ export const AgentPlanOverlay = memo(function AgentPlanOverlay({
</div> </div>
<div className="mt-2 flex items-center gap-1.5 pl-5"> <div className="mt-2 flex items-center gap-1.5 pl-5">
<Badge variant="outline" className="h-5 text-[10px] uppercase"> <Badge variant="outline" className="h-5 text-[10px] uppercase">
{getStatusLabel(entry.status)} {t(getStatusKey(entry.status))}
</Badge> </Badge>
<Badge <Badge
variant="outline" variant="outline"
@@ -213,7 +230,7 @@ export const AgentPlanOverlay = memo(function AgentPlanOverlay({
getPriorityClassName(entry.priority) getPriorityClassName(entry.priority)
)} )}
> >
{getPriorityLabel(entry.priority)} {t(getPriorityKey(entry.priority))}
</Badge> </Badge>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useTranslations } from "next-intl"
import { acpListAgents } from "@/lib/tauri" import { acpListAgents } from "@/lib/tauri"
import type { AgentType, AcpAgentInfo } from "@/lib/types" import type { AgentType, AcpAgentInfo } from "@/lib/types"
import { AGENT_LABELS } from "@/lib/types" import { AGENT_LABELS } from "@/lib/types"
@@ -22,6 +23,7 @@ export function AgentSelector({
onOpenAgentsSettings, onOpenAgentsSettings,
disabled = false, disabled = false,
}: AgentSelectorProps) { }: AgentSelectorProps) {
const t = useTranslations("Folder.chat.agentSelector")
const [agents, setAgents] = useState<AcpAgentInfo[]>([]) const [agents, setAgents] = useState<AcpAgentInfo[]>([])
const [selected, setSelected] = useState<AgentType | null>( const [selected, setSelected] = useState<AgentType | null>(
defaultAgentType ?? null defaultAgentType ?? null
@@ -80,14 +82,14 @@ export function AgentSelector({
if (agents.length === 0) { if (agents.length === 0) {
return ( return (
<div className="rounded-lg border border-dashed bg-muted/30 px-4 py-3 text-center text-sm text-muted-foreground"> <div className="rounded-lg border border-dashed bg-muted/30 px-4 py-3 text-center text-sm text-muted-foreground">
<div> Agent</div> <div>{t("noEnabledAgents")}</div>
{onOpenAgentsSettings ? ( {onOpenAgentsSettings ? (
<button <button
type="button" type="button"
onClick={onOpenAgentsSettings} onClick={onOpenAgentsSettings}
className="mt-2 inline-flex items-center rounded-md border px-2 py-1 text-xs text-foreground transition-colors hover:bg-accent cursor-pointer" className="mt-2 inline-flex items-center rounded-md border px-2 py-1 text-xs text-foreground transition-colors hover:bg-accent cursor-pointer"
> >
Agents {t("openAgentsSettings")}
</button> </button>
) : null} ) : null}
</div> </div>

View File

@@ -1,5 +1,6 @@
"use client" "use client"
import { useTranslations } from "next-intl"
import type { import type {
ConnectionStatus, ConnectionStatus,
PromptDraft, PromptDraft,
@@ -44,6 +45,7 @@ export function ChatInput({
attachmentTabId, attachmentTabId,
draftStorageKey, draftStorageKey,
}: ChatInputProps) { }: ChatInputProps) {
const t = useTranslations("Folder.chat.chatInput")
const isConnected = status === "connected" const isConnected = status === "connected"
const isPrompting = status === "prompting" const isPrompting = status === "prompting"
const isConnecting = status === "connecting" || status === "downloading" const isConnecting = status === "connecting" || status === "downloading"
@@ -69,10 +71,10 @@ export function ChatInput({
draftStorageKey={draftStorageKey} draftStorageKey={draftStorageKey}
placeholder={ placeholder={
isConnecting isConnecting
? "Connecting..." ? t("connecting")
: isPrompting : isPrompting
? "Agent is responding..." ? t("agentResponding")
: "Send a message..." : t("sendMessage")
} }
className="min-h-28 max-h-60" className="min-h-28 max-h-60"
/> />

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import { memo, useMemo } from "react" import { memo, useMemo } from "react"
import { useTranslations } from "next-intl"
import type { LiveMessage } from "@/contexts/acp-connections-context" import type { LiveMessage } from "@/contexts/acp-connections-context"
import { ContentPartsRenderer } from "@/components/message/content-parts-renderer" import { ContentPartsRenderer } from "@/components/message/content-parts-renderer"
import { adaptLiveMessageFromAcp } from "@/lib/adapters/ai-elements-adapter" import { adaptLiveMessageFromAcp } from "@/lib/adapters/ai-elements-adapter"
@@ -13,6 +14,7 @@ interface LiveMessageBlockProps {
export const LiveMessageBlock = memo(function LiveMessageBlock({ export const LiveMessageBlock = memo(function LiveMessageBlock({
message, message,
}: LiveMessageBlockProps) { }: LiveMessageBlockProps) {
const t = useTranslations("Folder.chat.liveMessageBlock")
const hasContent = message.content.length > 0 const hasContent = message.content.length > 0
const adapted = useMemo(() => adaptLiveMessageFromAcp(message), [message]) const adapted = useMemo(() => adaptLiveMessageFromAcp(message), [message])
@@ -24,7 +26,7 @@ export const LiveMessageBlock = memo(function LiveMessageBlock({
) : ( ) : (
<div <div
className="flex items-center gap-1.5 text-muted-foreground py-1" className="flex items-center gap-1.5 text-muted-foreground py-1"
aria-label="Assistant is thinking" aria-label={t("assistantThinkingAria")}
> >
<span className="h-2 w-2 rounded-full bg-primary animate-bounce [animation-delay:-0.3s]" /> <span className="h-2 w-2 rounded-full bg-primary animate-bounce [animation-delay:-0.3s]" />
<span className="h-2 w-2 rounded-full bg-primary animate-bounce [animation-delay:-0.15s]" /> <span className="h-2 w-2 rounded-full bg-primary animate-bounce [animation-delay:-0.15s]" />

View File

@@ -2,6 +2,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { open } from "@tauri-apps/plugin-dialog" import { open } from "@tauri-apps/plugin-dialog"
import { useTranslations } from "next-intl"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { FileSearch, Plus, Send, Square, X } from "lucide-react" import { FileSearch, Plus, Send, Square, X } from "lucide-react"
@@ -114,7 +115,7 @@ function SelectorLoadingChip({ label }: { label: string }) {
export function MessageInput({ export function MessageInput({
onSend, onSend,
placeholder = "Ask anything...", placeholder,
defaultPath, defaultPath,
disabled = false, disabled = false,
autoFocus = false, autoFocus = false,
@@ -133,7 +134,9 @@ export function MessageInput({
attachmentTabId, attachmentTabId,
draftStorageKey, draftStorageKey,
}: MessageInputProps) { }: MessageInputProps) {
const t = useTranslations("Folder.chat.messageInput")
const effectiveDraftStorageKey = draftStorageKey ?? attachmentTabId ?? null const effectiveDraftStorageKey = draftStorageKey ?? attachmentTabId ?? null
const resolvedPlaceholder = placeholder ?? t("askAnything")
const [text, setText] = useState(() => { const [text, setText] = useState(() => {
if (!effectiveDraftStorageKey) return "" if (!effectiveDraftStorageKey) return ""
return loadMessageInputDraft(effectiveDraftStorageKey) ?? "" return loadMessageInputDraft(effectiveDraftStorageKey) ?? ""
@@ -388,7 +391,7 @@ export function MessageInput({
onCompositionStart={() => (composingRef.current = true)} onCompositionStart={() => (composingRef.current = true)}
onCompositionEnd={() => (composingRef.current = false)} onCompositionEnd={() => (composingRef.current = false)}
onFocus={onFocus} onFocus={onFocus}
placeholder={placeholder} placeholder={resolvedPlaceholder}
className={cn( className={cn(
"text-sm pr-12 resize-none bg-transparent", "text-sm pr-12 resize-none bg-transparent",
topPaddingClass, topPaddingClass,
@@ -411,7 +414,9 @@ export function MessageInput({
type="button" type="button"
onClick={() => removeAttachment(attachment.path)} onClick={() => removeAttachment(attachment.path)}
className="rounded-sm p-0.5 hover:bg-muted-foreground/15" className="rounded-sm p-0.5 hover:bg-muted-foreground/15"
aria-label={`Remove ${attachment.name}`} aria-label={t("removeAttachmentAria", {
name: attachment.name,
})}
> >
<X className="h-3 w-3" /> <X className="h-3 w-3" />
</button> </button>
@@ -428,12 +433,12 @@ export function MessageInput({
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-6 w-6 shrink-0" className="h-6 w-6 shrink-0"
title="Attach files" title={t("attachFiles")}
> >
<Plus className="size-4" /> <Plus className="size-4" />
</Button> </Button>
{showConfigLoading && ( {showConfigLoading && (
<SelectorLoadingChip label="Loading settings..." /> <SelectorLoadingChip label={t("loadingSettings")} />
)} )}
{hasConfigOptions && {hasConfigOptions &&
availableConfigOptions.map((option) => ( availableConfigOptions.map((option) => (
@@ -445,7 +450,7 @@ export function MessageInput({
} }
/> />
))} ))}
{showModeLoading && <SelectorLoadingChip label="Loading mode..." />} {showModeLoading && <SelectorLoadingChip label={t("loadingMode")} />}
{showModeSelector && effectiveModeId && ( {showModeSelector && effectiveModeId && (
<ModeSelector <ModeSelector
modes={availableModes} modes={availableModes}
@@ -461,7 +466,7 @@ export function MessageInput({
variant="destructive" variant="destructive"
size="icon" size="icon"
className="absolute right-2 bottom-2" className="absolute right-2 bottom-2"
title="Cancel" title={t("cancel")}
> >
<Square className="h-4 w-4" /> <Square className="h-4 w-4" />
</Button> </Button>
@@ -471,7 +476,7 @@ export function MessageInput({
disabled={disabled || !hasSendableContent} disabled={disabled || !hasSendableContent}
size="icon" size="icon"
className="absolute right-2 bottom-2" className="absolute right-2 bottom-2"
title="Send" title={t("send")}
> >
<Send className="h-4 w-4" /> <Send className="h-4 w-4" />
</Button> </Button>

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import { useMemo } from "react" import { useMemo } from "react"
import { useTranslations } from "next-intl"
import { import {
ShieldAlert, ShieldAlert,
Terminal, Terminal,
@@ -19,15 +20,16 @@ interface PermissionDialogProps {
onRespond: (requestId: string, optionId: string) => void onRespond: (requestId: string, optionId: string) => void
} }
function formatKindLabel(kind: string): string { function formatKindLabel(kind: string, fallbackLabel: string): string {
const normalized = kind.replace(/_/g, " ").trim() const normalized = kind.replace(/_/g, " ").trim()
return normalized.length > 0 ? normalized : "tool" return normalized.length > 0 ? normalized : fallbackLabel
} }
export function PermissionDialog({ export function PermissionDialog({
permission, permission,
onRespond, onRespond,
}: PermissionDialogProps) { }: PermissionDialogProps) {
const t = useTranslations("Folder.chat.permissionDialog")
const parsed = useMemo( const parsed = useMemo(
() => parsePermissionToolCall(permission?.tool_call), () => parsePermissionToolCall(permission?.tool_call),
[permission?.tool_call] [permission?.tool_call]
@@ -51,12 +53,10 @@ export function PermissionDialog({
<ShieldAlert className="h-4 w-4 shrink-0 text-amber-500" /> <ShieldAlert className="h-4 w-4 shrink-0 text-amber-500" />
<span className="truncate">{parsed.title}</span> <span className="truncate">{parsed.title}</span>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">{t("subtitle")}</p>
Agent requests permission to continue this turn.
</p>
</div> </div>
<Badge variant="outline" className="shrink-0 text-[10px]"> <Badge variant="outline" className="shrink-0 text-[10px]">
{formatKindLabel(parsed.normalizedKind)} {formatKindLabel(parsed.normalizedKind, t("kindFallbackTool"))}
</Badge> </Badge>
</div> </div>
@@ -65,12 +65,12 @@ export function PermissionDialog({
<div className="space-y-1.5 rounded-md border border-border/60 bg-muted/20 p-2"> <div className="space-y-1.5 rounded-md border border-border/60 bg-muted/20 p-2">
<div className="flex items-center gap-1 text-xs text-muted-foreground"> <div className="flex items-center gap-1 text-xs text-muted-foreground">
<Terminal className="h-3.5 w-3.5" /> <Terminal className="h-3.5 w-3.5" />
<span>Command</span> <span>{t("command")}</span>
</div> </div>
<CodeBlock code={parsed.command} language="bash" /> <CodeBlock code={parsed.command} language="bash" />
{parsed.cwd && ( {parsed.cwd && (
<div className="break-all text-xs text-muted-foreground"> <div className="break-all text-xs text-muted-foreground">
CWD: {parsed.cwd} {t("cwd", { cwd: parsed.cwd })}
</div> </div>
)} )}
</div> </div>
@@ -80,7 +80,9 @@ export function PermissionDialog({
<div className="space-y-1.5 rounded-md border border-border/60 bg-muted/20 p-2"> <div className="space-y-1.5 rounded-md border border-border/60 bg-muted/20 p-2">
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground"> <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<FilePenLine className="h-3.5 w-3.5" /> <FilePenLine className="h-3.5 w-3.5" />
<span>Files: {parsed.fileChanges.length}</span> <span>
{t("filesSummary", { count: parsed.fileChanges.length })}
</span>
{(parsed.additions > 0 || parsed.deletions > 0) && ( {(parsed.additions > 0 || parsed.deletions > 0) && (
<span> <span>
+{parsed.additions} / -{parsed.deletions} +{parsed.additions} / -{parsed.deletions}
@@ -98,7 +100,7 @@ export function PermissionDialog({
))} ))}
{parsed.fileChanges.length > 8 && ( {parsed.fileChanges.length > 8 && (
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
+{parsed.fileChanges.length - 8} more files {t("moreFiles", { count: parsed.fileChanges.length - 8 })}
</div> </div>
)} )}
</div> </div>
@@ -112,7 +114,7 @@ export function PermissionDialog({
<div className="space-y-1.5 rounded-md border border-border/60 bg-muted/20 p-2"> <div className="space-y-1.5 rounded-md border border-border/60 bg-muted/20 p-2">
<div className="flex items-center gap-1 text-xs text-muted-foreground"> <div className="flex items-center gap-1 text-xs text-muted-foreground">
<ListTodo className="h-3.5 w-3.5" /> <ListTodo className="h-3.5 w-3.5" />
<span>Plan</span> <span>{t("plan")}</span>
</div> </div>
{parsed.planExplanation && ( {parsed.planExplanation && (
<p className="text-xs text-foreground/90"> <p className="text-xs text-foreground/90">
@@ -140,7 +142,7 @@ export function PermissionDialog({
<div className="rounded-md border border-border/60 bg-muted/20 p-2 text-xs"> <div className="rounded-md border border-border/60 bg-muted/20 p-2 text-xs">
<div className="flex items-center gap-1 text-muted-foreground"> <div className="flex items-center gap-1 text-muted-foreground">
<Compass className="h-3.5 w-3.5" /> <Compass className="h-3.5 w-3.5" />
<span>Target mode: {parsed.modeTarget}</span> <span>{t("targetMode", { mode: parsed.modeTarget })}</span>
</div> </div>
</div> </div>
)} )}

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useTranslations } from "next-intl"
import { MessageInput } from "@/components/chat/message-input" import { MessageInput } from "@/components/chat/message-input"
import type { AgentType, PromptDraft, SessionStats } from "@/lib/types" import type { AgentType, PromptDraft, SessionStats } from "@/lib/types"
import { useFolderContext } from "@/contexts/folder-context" import { useFolderContext } from "@/contexts/folder-context"
@@ -65,18 +66,18 @@ function isExpectedAutoLinkError(error: unknown): boolean {
return (error as { alerted?: unknown }).alerted === true return (error as { alerted?: unknown }).alerted === true
} }
function buildInlineAutoConnectErrorMessage(raw: string): string { function buildInlineAutoConnectErrorMessage(
const normalized = raw.trim().replace(/[.!?,;:]+$/u, "") raw: string,
if (!normalized) return "点击前往设置 > Agents 管理安装。" options: {
const hasSdkNotInstalled = /SDK\s*$/u.test(normalized) fallback: string
const message = append: (message: string) => string
!hasSdkNotInstalled && normalized.endsWith("尚未安装") alreadyContainsPath: (message: string) => boolean
? normalized.replace(/$/u, "SDK ")
: normalized
if (message.includes("设置 > Agents 管理安装")) {
return `${message}`
} }
return `${message},点击前往设置 > Agents 管理安装。` ): string {
const normalized = raw.trim().replace(/[.!?,;:]+$/u, "")
if (!normalized) return options.fallback
if (options.alreadyContainsPath(normalized)) return normalized
return options.append(normalized)
} }
export function WelcomeInputPanel({ export function WelcomeInputPanel({
@@ -85,6 +86,7 @@ export function WelcomeInputPanel({
tabId, tabId,
isActive = true, isActive = true,
}: WelcomeInputPanelProps) { }: WelcomeInputPanelProps) {
const t = useTranslations("Folder.chat.welcomeInputPanel")
const fallbackContextId = useMemo(() => crypto.randomUUID(), []) const fallbackContextId = useMemo(() => crypto.randomUUID(), [])
const contextKey = tabId ?? `new-${fallbackContextId}` const contextKey = tabId ?? `new-${fallbackContextId}`
@@ -569,6 +571,23 @@ export function WelcomeInputPanel({
}) })
}, [selectedAgent]) }, [selectedAgent])
const buildAutoConnectErrorMessage = useCallback(
(raw: string) =>
buildInlineAutoConnectErrorMessage(raw, {
fallback: t("autoConnectFallback"),
append: (message) =>
t("autoConnectAppend", {
message,
path: t("agentsSettingsPath"),
}),
alreadyContainsPath: (message) =>
[t("agentsSettingsPath"), "Settings > Agents"].some((path) =>
message.includes(path)
),
}),
[t]
)
// Track live message visibility across turn completion. // Track live message visibility across turn completion.
// Hooks must be called before any conditional returns. // Hooks must be called before any conditional returns.
const prevConnStatusForLiveRef = useRef(connStatus) const prevConnStatusForLiveRef = useRef(connStatus)
@@ -617,7 +636,7 @@ export function WelcomeInputPanel({
className="w-full cursor-pointer rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-center text-xs text-destructive transition-colors hover:bg-destructive/10" className="w-full cursor-pointer rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-center text-xs text-destructive transition-colors hover:bg-destructive/10"
> >
{(() => { {(() => {
const inlineMessage = buildInlineAutoConnectErrorMessage( const inlineMessage = buildAutoConnectErrorMessage(
autoConnectError ?? agentConnectError ?? "" autoConnectError ?? agentConnectError ?? ""
) )
return ( return (
@@ -638,8 +657,8 @@ export function WelcomeInputPanel({
defaultPath={workingDir} defaultPath={workingDir}
placeholder={ placeholder={
agentsLoaded && usableAgentCount === 0 agentsLoaded && usableAgentCount === 0
? "请先启用至少一个 Agent 后开始会话..." ? t("enableAgentFirstPlaceholder")
: "Ask anything..." : t("askAnythingPlaceholder")
} }
autoFocus autoFocus
attachmentTabId={tabId ?? null} attachmentTabId={tabId ?? null}

View File

@@ -3,6 +3,7 @@
import { useCallback, useEffect, useMemo, useRef } from "react" import { useCallback, useEffect, useMemo, useRef } from "react"
import dynamic from "next/dynamic" import dynamic from "next/dynamic"
import type { editor as MonacoEditorNs } from "monaco-editor" import type { editor as MonacoEditorNs } from "monaco-editor"
import { useTranslations } from "next-intl"
import { useFolderContext } from "@/contexts/folder-context" import { useFolderContext } from "@/contexts/folder-context"
import { defineMonacoThemes, useMonacoThemeSync } from "@/lib/monaco-themes" import { defineMonacoThemes, useMonacoThemeSync } from "@/lib/monaco-themes"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@@ -414,11 +415,13 @@ function parseUnifiedDiff(diffText: string): ParsedDiffFile[] {
.filter((file) => file.hunks.length > 0) .filter((file) => file.hunks.length > 0)
} }
function modeLabel(mode: DiffFileMode): string { function modeKey(
if (mode === "added") return "新增" mode: DiffFileMode
if (mode === "deleted") return "删除" ): "mode.added" | "mode.deleted" | "mode.renamed" | "mode.modified" {
if (mode === "renamed") return "重命名" if (mode === "added") return "mode.added"
return "修改" if (mode === "deleted") return "mode.deleted"
if (mode === "renamed") return "mode.renamed"
return "mode.modified"
} }
function toDisplayPath(filePath: string, folderPath: string | null): string { function toDisplayPath(filePath: string, folderPath: string | null): string {
@@ -436,11 +439,6 @@ function toDisplayPath(filePath: string, folderPath: string | null): string {
return normalizedPath return normalizedPath
} }
function hunkLabel(hunk: ParsedDiffHunk, index: number): string {
void hunk
return `Hunk ${index + 1}`
}
function countHunkChanges(hunk: ParsedDiffHunk): { function countHunkChanges(hunk: ParsedDiffHunk): {
additions: number additions: number
deletions: number deletions: number
@@ -485,6 +483,7 @@ function HunkMonacoPreview({
modelId: string modelId: string
theme: string theme: string
}) { }) {
const t = useTranslations("Folder.diffPreview")
const editorRef = useRef<MonacoEditorNs.IStandaloneCodeEditor | null>(null) const editorRef = useRef<MonacoEditorNs.IStandaloneCodeEditor | null>(null)
const decorationsRef = useRef<string[]>([]) const decorationsRef = useRef<string[]>([])
@@ -567,7 +566,7 @@ function HunkMonacoPreview({
theme={theme} theme={theme}
loading={ loading={
<div className="h-28 flex items-center justify-center text-xs text-muted-foreground"> <div className="h-28 flex items-center justify-center text-xs text-muted-foreground">
Loading hunk... {t("loadingHunk")}
</div> </div>
} }
options={{ options={{
@@ -601,6 +600,7 @@ export function UnifiedDiffPreview({
modelId?: string modelId?: string
className?: string className?: string
}) { }) {
const t = useTranslations("Folder.diffPreview")
const { folder } = useFolderContext() const { folder } = useFolderContext()
const files = useMemo(() => parseUnifiedDiff(diffText), [diffText]) const files = useMemo(() => parseUnifiedDiff(diffText), [diffText])
const theme = useMonacoThemeSync() const theme = useMonacoThemeSync()
@@ -613,7 +613,7 @@ export function UnifiedDiffPreview({
className className
)} )}
> >
No diff data {t("noDiffData")}
</div> </div>
) )
} }
@@ -638,7 +638,7 @@ export function UnifiedDiffPreview({
> >
<header className="flex items-center gap-2 border-b border-border bg-muted/40 px-3 py-2 text-[11px]"> <header className="flex items-center gap-2 border-b border-border bg-muted/40 px-3 py-2 text-[11px]">
<span className="shrink-0 rounded border border-border bg-background px-1.5 py-0.5 text-[10px] text-muted-foreground"> <span className="shrink-0 rounded border border-border bg-background px-1.5 py-0.5 text-[10px] text-muted-foreground">
{modeLabel(file.mode)} {t(modeKey(file.mode))}
</span> </span>
<span <span
className="min-w-0 flex-1 truncate font-mono text-foreground" className="min-w-0 flex-1 truncate font-mono text-foreground"
@@ -666,7 +666,7 @@ export function UnifiedDiffPreview({
className="rounded-md border border-border" className="rounded-md border border-border"
> >
<div className="flex items-center gap-2 border-b border-border bg-muted/30 px-2 py-1 text-[10px] font-mono text-muted-foreground"> <div className="flex items-center gap-2 border-b border-border bg-muted/30 px-2 py-1 text-[10px] font-mono text-muted-foreground">
<span>{hunkLabel(hunk, index)}</span> <span>{t("hunkLabel", { index: index + 1 })}</span>
<span className="ml-auto inline-flex items-center gap-2"> <span className="ml-auto inline-flex items-center gap-2">
<span className="text-green-700 dark:text-green-400"> <span className="text-green-700 dark:text-green-400">
+{hunkStats.additions} +{hunkStats.additions}

View File

@@ -1022,6 +1022,74 @@
"add": "Add", "add": "Add",
"saving": "Saving..." "saving": "Saving..."
} }
},
"chat": {
"chatInput": {
"connecting": "Connecting...",
"agentResponding": "Agent is responding...",
"sendMessage": "Send a message..."
},
"messageInput": {
"askAnything": "Ask anything...",
"removeAttachmentAria": "Remove {name}",
"attachFiles": "Attach files",
"loadingSettings": "Loading settings...",
"loadingMode": "Loading mode...",
"cancel": "Cancel",
"send": "Send"
},
"welcomeInputPanel": {
"agentsSettingsPath": "Settings > Agents",
"autoConnectFallback": "Click to open {path} and manage installation.",
"autoConnectAppend": "{message}. Click to open {path} and manage installation.",
"enableAgentFirstPlaceholder": "Enable at least one agent before starting a session...",
"askAnythingPlaceholder": "Ask anything..."
},
"agentSelector": {
"noEnabledAgents": "No enabled agents",
"openAgentsSettings": "Open Agents settings"
},
"liveMessageBlock": {
"assistantThinkingAria": "Assistant is thinking"
},
"agentPlanOverlay": {
"title": "Agent Plan",
"collapsePlanAria": "Collapse plan",
"collapsedSummary": "Plan {completed}/{total}",
"status": {
"completed": "Completed",
"inProgress": "In Progress",
"pending": "Pending",
"unknown": "Unknown"
},
"priority": {
"high": "High",
"medium": "Medium",
"low": "Low",
"unknown": "Unknown"
}
},
"permissionDialog": {
"subtitle": "Agent requests permission to continue this turn.",
"kindFallbackTool": "tool",
"command": "Command",
"cwd": "CWD: {cwd}",
"filesSummary": "Files: {count}",
"moreFiles": "+{count} more files",
"plan": "Plan",
"targetMode": "Target mode: {mode}"
}
},
"diffPreview": {
"mode": {
"added": "Added",
"deleted": "Deleted",
"renamed": "Renamed",
"modified": "Modified"
},
"hunkLabel": "Hunk {index}",
"loadingHunk": "Loading hunk...",
"noDiffData": "No diff data"
} }
} }
} }

View File

@@ -1022,6 +1022,74 @@
"add": "添加", "add": "添加",
"saving": "保存中..." "saving": "保存中..."
} }
},
"chat": {
"chatInput": {
"connecting": "连接中...",
"agentResponding": "Agent 正在响应...",
"sendMessage": "发送消息..."
},
"messageInput": {
"askAnything": "请开始输入...",
"removeAttachmentAria": "移除 {name}",
"attachFiles": "附加文件",
"loadingSettings": "正在加载设置...",
"loadingMode": "正在加载模式...",
"cancel": "取消",
"send": "发送"
},
"welcomeInputPanel": {
"agentsSettingsPath": "设置 > Agents",
"autoConnectFallback": "点击前往 {path} 管理安装。",
"autoConnectAppend": "{message},点击前往 {path} 管理安装。",
"enableAgentFirstPlaceholder": "请先启用至少一个 Agent 后开始会话...",
"askAnythingPlaceholder": "请开始输入..."
},
"agentSelector": {
"noEnabledAgents": "暂无已启用的 Agent",
"openAgentsSettings": "打开 Agents 设置"
},
"liveMessageBlock": {
"assistantThinkingAria": "助手正在思考"
},
"agentPlanOverlay": {
"title": "Agent 计划",
"collapsePlanAria": "折叠计划",
"collapsedSummary": "计划 {completed}/{total}",
"status": {
"completed": "已完成",
"inProgress": "进行中",
"pending": "待处理",
"unknown": "未知"
},
"priority": {
"high": "高",
"medium": "中",
"low": "低",
"unknown": "未知"
}
},
"permissionDialog": {
"subtitle": "Agent 请求继续当前轮次的权限。",
"kindFallbackTool": "工具",
"command": "命令",
"cwd": "工作目录:{cwd}",
"filesSummary": "文件:{count}",
"moreFiles": "+{count} 个更多文件",
"plan": "计划",
"targetMode": "目标模式:{mode}"
}
},
"diffPreview": {
"mode": {
"added": "新增",
"deleted": "删除",
"renamed": "重命名",
"modified": "修改"
},
"hunkLabel": "代码块 {index}",
"loadingHunk": "正在加载代码块...",
"noDiffData": "无差异数据"
} }
} }
} }

View File

@@ -1022,6 +1022,74 @@
"add": "新增", "add": "新增",
"saving": "儲存中..." "saving": "儲存中..."
} }
},
"chat": {
"chatInput": {
"connecting": "連線中...",
"agentResponding": "Agent 正在回應...",
"sendMessage": "傳送訊息..."
},
"messageInput": {
"askAnything": "請開始輸入...",
"removeAttachmentAria": "移除 {name}",
"attachFiles": "附加檔案",
"loadingSettings": "正在載入設定...",
"loadingMode": "正在載入模式...",
"cancel": "取消",
"send": "傳送"
},
"welcomeInputPanel": {
"agentsSettingsPath": "設定 > Agents",
"autoConnectFallback": "點擊前往 {path} 管理安裝。",
"autoConnectAppend": "{message},點擊前往 {path} 管理安裝。",
"enableAgentFirstPlaceholder": "請先啟用至少一個 Agent 後開始會話...",
"askAnythingPlaceholder": "請開始輸入..."
},
"agentSelector": {
"noEnabledAgents": "暫無已啟用的 Agent",
"openAgentsSettings": "開啟 Agents 設定"
},
"liveMessageBlock": {
"assistantThinkingAria": "助手正在思考"
},
"agentPlanOverlay": {
"title": "Agent 計畫",
"collapsePlanAria": "摺疊計畫",
"collapsedSummary": "計畫 {completed}/{total}",
"status": {
"completed": "已完成",
"inProgress": "進行中",
"pending": "待處理",
"unknown": "未知"
},
"priority": {
"high": "高",
"medium": "中",
"low": "低",
"unknown": "未知"
}
},
"permissionDialog": {
"subtitle": "Agent 請求繼續目前輪次的權限。",
"kindFallbackTool": "工具",
"command": "命令",
"cwd": "工作目錄:{cwd}",
"filesSummary": "檔案:{count}",
"moreFiles": "+{count} 個更多檔案",
"plan": "計畫",
"targetMode": "目標模式:{mode}"
}
},
"diffPreview": {
"mode": {
"added": "新增",
"deleted": "刪除",
"renamed": "重新命名",
"modified": "修改"
},
"hunkLabel": "區塊 {index}",
"loadingHunk": "正在載入區塊...",
"noDiffData": "無差異資料"
} }
} }
} }