设置页面修改语言和Agent,其它页面实时更新状态
This commit is contained in:
@@ -8,6 +8,8 @@ import { AGENT_LABELS } from "@/lib/types"
|
||||
import { AgentIcon } from "@/components/agent-icon"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ACP_AGENTS_UPDATED_EVENT = "app://acp-agents-updated"
|
||||
|
||||
interface AgentSelectorProps {
|
||||
defaultAgentType?: AgentType
|
||||
onSelect: (agentType: AgentType) => void
|
||||
@@ -31,16 +33,20 @@ export function AgentSelector({
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
acpListAgents()
|
||||
.then((list) => {
|
||||
if (cancelled) return
|
||||
let latestRequestId = 0
|
||||
|
||||
const reloadAgents = async () => {
|
||||
const requestId = latestRequestId + 1
|
||||
latestRequestId = requestId
|
||||
try {
|
||||
const list = await acpListAgents()
|
||||
if (cancelled || requestId !== latestRequestId) return
|
||||
const sorted = [...list].sort(
|
||||
(a, b) => a.sort_order - b.sort_order || a.name.localeCompare(b.name)
|
||||
)
|
||||
const visible = sorted.filter((a) => a.enabled)
|
||||
setAgents(visible)
|
||||
onAgentsLoaded?.(visible)
|
||||
// Auto-select default if it exists in the list
|
||||
if (defaultAgentType) {
|
||||
const found = visible.find(
|
||||
(a) => a.agent_type === defaultAgentType && a.available
|
||||
@@ -48,7 +54,6 @@ export function AgentSelector({
|
||||
if (found) {
|
||||
setSelected(found.agent_type)
|
||||
} else {
|
||||
// Fall back to first available
|
||||
const first = visible.find((a) => a.available)
|
||||
if (first) {
|
||||
setSelected(first.agent_type)
|
||||
@@ -62,15 +67,44 @@ export function AgentSelector({
|
||||
onSelect(first.agent_type)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
} catch {
|
||||
if (!cancelled && requestId === latestRequestId) {
|
||||
setAgents([])
|
||||
onAgentsLoaded?.([])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void reloadAgents()
|
||||
const onWindowFocus = () => {
|
||||
void reloadAgents()
|
||||
}
|
||||
window.addEventListener("focus", onWindowFocus)
|
||||
|
||||
let unlisten: (() => void) | null = null
|
||||
void import("@tauri-apps/api/event")
|
||||
.then(({ listen }) =>
|
||||
listen(ACP_AGENTS_UPDATED_EVENT, () => {
|
||||
void reloadAgents()
|
||||
})
|
||||
)
|
||||
.then((dispose) => {
|
||||
if (cancelled) {
|
||||
dispose()
|
||||
return
|
||||
}
|
||||
unlisten = dispose
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore when non-tauri runtime.
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
window.removeEventListener("focus", onWindowFocus)
|
||||
if (unlisten) {
|
||||
unlisten()
|
||||
}
|
||||
}
|
||||
}, [defaultAgentType, onAgentsLoaded, onSelect])
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ import {
|
||||
import { Message, MessageContent } from "@/components/ai-elements/message"
|
||||
import { ContentPartsRenderer } from "@/components/message/content-parts-renderer"
|
||||
|
||||
const ACP_AGENTS_UPDATED_EVENT = "app://acp-agents-updated"
|
||||
|
||||
interface WelcomeInputPanelProps {
|
||||
defaultAgentType?: AgentType
|
||||
workingDir?: string
|
||||
@@ -56,6 +58,11 @@ interface WelcomeInputPanelProps {
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
interface AgentsUpdatedEventPayload {
|
||||
reason?: string
|
||||
agent_type?: AgentType | null
|
||||
}
|
||||
|
||||
function normalizeErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) return error.message
|
||||
return String(error)
|
||||
@@ -318,6 +325,7 @@ export function WelcomeInputPanel({
|
||||
// the DB conversation ID and the ACP session ID are available.
|
||||
const externalIdSavedRef = useRef(false)
|
||||
const sessionIdRef = useRef<string | null>(null)
|
||||
const refreshingCurrentAgentRef = useRef(false)
|
||||
useEffect(() => {
|
||||
if (connSessionId) {
|
||||
sessionIdRef.current = connSessionId
|
||||
@@ -348,6 +356,75 @@ export function WelcomeInputPanel({
|
||||
const isConnecting =
|
||||
connStatus === "connecting" || connStatus === "downloading"
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
let unlisten: (() => void) | null = null
|
||||
|
||||
const syncCurrentAgentStatus = async () => {
|
||||
if (cancelled) return
|
||||
if (phase !== "welcome") return
|
||||
if (!workingDir) return
|
||||
if (refreshingCurrentAgentRef.current) return
|
||||
if (connStatus === "prompting" || isConnecting) return
|
||||
|
||||
refreshingCurrentAgentRef.current = true
|
||||
try {
|
||||
setAgentConnectError(null)
|
||||
if (connStatus === "connected") {
|
||||
await connDisconnect()
|
||||
}
|
||||
await connConnect(selectedAgentRef.current, workingDir, undefined, {
|
||||
source: "auto_link",
|
||||
})
|
||||
if (!cancelled) {
|
||||
setAgentConnectError(null)
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
setAgentConnectError(normalizeErrorMessage(error))
|
||||
}
|
||||
if (!isExpectedAutoLinkError(error)) {
|
||||
console.error("[WelcomePanel] refresh current agent status:", error)
|
||||
}
|
||||
} finally {
|
||||
refreshingCurrentAgentRef.current = false
|
||||
}
|
||||
}
|
||||
|
||||
void import("@tauri-apps/api/event")
|
||||
.then(({ listen }) =>
|
||||
listen<AgentsUpdatedEventPayload>(ACP_AGENTS_UPDATED_EVENT, (event) => {
|
||||
if (cancelled) return
|
||||
if (event.payload?.reason === "agent_reordered") return
|
||||
const changedAgentType = event.payload?.agent_type
|
||||
if (
|
||||
changedAgentType &&
|
||||
changedAgentType !== selectedAgentRef.current
|
||||
) {
|
||||
return
|
||||
}
|
||||
void syncCurrentAgentStatus()
|
||||
})
|
||||
)
|
||||
.then((dispose) => {
|
||||
if (cancelled) {
|
||||
dispose()
|
||||
return
|
||||
}
|
||||
unlisten = dispose
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore when non-tauri runtime.
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
if (unlisten) {
|
||||
unlisten()
|
||||
}
|
||||
}
|
||||
}, [connConnect, connDisconnect, connStatus, isConnecting, phase, workingDir])
|
||||
|
||||
const prevStatusRef = useRef(connStatus)
|
||||
|
||||
// Accumulate history when prompting completes
|
||||
|
||||
@@ -34,6 +34,7 @@ interface AppI18nContextValue {
|
||||
}
|
||||
|
||||
const AppI18nContext = createContext<AppI18nContextValue | null>(null)
|
||||
const LANGUAGE_SETTINGS_UPDATED_EVENT = "app://language-settings-updated"
|
||||
|
||||
function subscribeSystemLocale(onStoreChange: () => void) {
|
||||
if (typeof window === "undefined") return () => {}
|
||||
@@ -128,6 +129,57 @@ export function AppI18nProvider({
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
const onStorage = (event: StorageEvent) => {
|
||||
if (event.key !== LANGUAGE_SETTINGS_STORAGE_KEY || !event.newValue) return
|
||||
|
||||
try {
|
||||
const next = normalizeLanguageSettings(
|
||||
JSON.parse(event.newValue) as SystemLanguageSettings
|
||||
)
|
||||
setLanguageSettingsState(next)
|
||||
} catch {
|
||||
// Ignore malformed storage payloads.
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("storage", onStorage)
|
||||
|
||||
let unlisten: (() => void) | null = null
|
||||
let cancelled = false
|
||||
|
||||
void import("@tauri-apps/api/event")
|
||||
.then(({ listen }) =>
|
||||
listen<SystemLanguageSettings>(
|
||||
LANGUAGE_SETTINGS_UPDATED_EVENT,
|
||||
(event) => {
|
||||
if (cancelled) return
|
||||
setLanguageSettings(event.payload)
|
||||
}
|
||||
)
|
||||
)
|
||||
.then((dispose) => {
|
||||
if (cancelled) {
|
||||
dispose()
|
||||
return
|
||||
}
|
||||
unlisten = dispose
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore when running in non-tauri environment.
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
window.removeEventListener("storage", onStorage)
|
||||
if (unlisten) {
|
||||
unlisten()
|
||||
}
|
||||
}
|
||||
}, [setLanguageSettings])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
|
||||
Reference in New Issue
Block a user