diff --git a/src-tauri/crates/core/src/models/provider_pool_model.rs b/src-tauri/crates/core/src/models/provider_pool_model.rs index c9de7c50d..e8c72fea5 100644 --- a/src-tauri/crates/core/src/models/provider_pool_model.rs +++ b/src-tauri/crates/core/src/models/provider_pool_model.rs @@ -289,19 +289,19 @@ impl ProviderCredential { /// 解析当前凭证应采用的 Prompt Cache 模式。 pub fn effective_prompt_cache_mode(&self) -> Option { self.prompt_cache_mode_override.or_else(|| { - if self.provider_type.supports_anthropic_prompt_cache() { - Some(ProviderPromptCacheMode::Automatic) - } else if matches!(self.provider_type, PoolProviderType::AnthropicCompatible) - && is_known_automatic_anthropic_compatible_host( + if matches!(self.provider_type, PoolProviderType::AnthropicCompatible) { + return if is_known_automatic_anthropic_compatible_host( get_base_url(&self.credential).as_deref(), - ) - { - Some(ProviderPromptCacheMode::Automatic) - } else if matches!(self.provider_type, PoolProviderType::AnthropicCompatible) { - Some(ProviderPromptCacheMode::ExplicitOnly) - } else { - None + ) { + Some(ProviderPromptCacheMode::Automatic) + } else { + Some(ProviderPromptCacheMode::ExplicitOnly) + }; } + + self.provider_type + .supports_anthropic_prompt_cache() + .then_some(ProviderPromptCacheMode::Automatic) }) } diff --git a/src-tauri/src/commands/aster_agent_cmd/tool_runtime/workspace_tools.rs b/src-tauri/src/commands/aster_agent_cmd/tool_runtime/workspace_tools.rs index 0753a43a2..20725535d 100644 --- a/src-tauri/src/commands/aster_agent_cmd/tool_runtime/workspace_tools.rs +++ b/src-tauri/src/commands/aster_agent_cmd/tool_runtime/workspace_tools.rs @@ -144,10 +144,6 @@ impl WorkspaceSandboxedBashTool { } } - fn format_output(stdout: &str, stderr: &str, exit_code: i32) -> String { - Self::format_output_with_message(stdout, stderr, exit_code, None) - } - fn format_output_with_message( stdout: &str, stderr: &str, diff --git a/src/components/agent/chat/components/HarnessStatusPanel.test.tsx b/src/components/agent/chat/components/HarnessStatusPanel.test.tsx index e9dac4827..b2c083414 100644 --- a/src/components/agent/chat/components/HarnessStatusPanel.test.tsx +++ b/src/components/agent/chat/components/HarnessStatusPanel.test.tsx @@ -598,7 +598,7 @@ function createAlignedRuntimeToolInventory(): AgentRuntimeToolInventory { caller_allowed: true, visible_in_context: true, }, - ...base.runtime_tools.filter((entry) => entry.name !== "Agent"), + ...(base.runtime_tools ?? []).filter((entry) => entry.name !== "Agent"), ], }; } diff --git a/src/components/agent/chat/components/MarkdownRenderer.test.tsx b/src/components/agent/chat/components/MarkdownRenderer.test.tsx index 7ba590744..ea00e987e 100644 --- a/src/components/agent/chat/components/MarkdownRenderer.test.tsx +++ b/src/components/agent/chat/components/MarkdownRenderer.test.tsx @@ -417,6 +417,36 @@ describe("MarkdownRenderer", () => { ).not.toBeNull(); }); + it("标题后的正文应保持聊天正文排版,不应缩小变灰", () => { + const container = document.createElement("div"); + container.style.setProperty("--foreground", "17 24 39"); + container.style.setProperty("--muted-foreground", "100 116 139"); + container.style.fontSize = "15px"; + container.style.lineHeight = "1.7"; + document.body.appendChild(container); + const root = createRoot(container); + + act(() => { + root.render( + , + ); + }); + + mountedRoots.push({ container, root }); + + const heading = container.querySelector( + 'h2[data-markdown-heading-level="2"]', + ); + const paragraph = container.querySelector("p"); + + expect(heading).not.toBeNull(); + expect(paragraph).not.toBeNull(); + expect(getComputedStyle(paragraph as Element).fontSize).toBe("1em"); + expect(document.head.textContent).not.toContain("h1 + p"); + expect(document.head.textContent).not.toContain("h2 + p"); + expect(document.head.textContent).not.toContain("h3 + p"); + }); + it("非流式时应保留 raw html 渲染能力", () => { const content = [ "前置文本", diff --git a/src/components/agent/chat/components/MarkdownRenderer.tsx b/src/components/agent/chat/components/MarkdownRenderer.tsx index 8b9443f5d..9e5aaebda 100644 --- a/src/components/agent/chat/components/MarkdownRenderer.tsx +++ b/src/components/agent/chat/components/MarkdownRenderer.tsx @@ -39,8 +39,8 @@ const CODE_BLOCK_BUTTON_HOVER_SURFACE = "rgba(248, 250, 252, 0.98)"; // 收紧正文与代码块表面,让消息正文更接近单列执行流的阅读节奏。 const MarkdownContainer = styled.div` - font-size: 14px; - line-height: 1.76; + font-size: inherit; + line-height: inherit; color: hsl(var(--foreground)); overflow-wrap: break-word; word-break: break-word; @@ -57,14 +57,8 @@ const MarkdownContainer = styled.div` p { margin: 0 0 0.95em; color: hsl(var(--foreground)); - } - - h1 + p, - h2 + p, - h3 + p { - color: hsl(var(--muted-foreground)); - font-size: 1.02em; - line-height: 1.8; + font-size: 1em; + line-height: inherit; } h1, @@ -74,9 +68,9 @@ const MarkdownContainer = styled.div` h5, h6 { font-weight: 700; - margin: 1.34em 0 0.58em; - line-height: 1.32; - letter-spacing: -0.01em; + margin: 1.08em 0 0.5em; + line-height: 1.42; + letter-spacing: 0; color: hsl(var(--foreground)); } @@ -87,21 +81,20 @@ const MarkdownContainer = styled.div` } h1 { - font-size: 1.54em; + font-size: 1.16em; } h2 { - font-size: 1.28em; + font-size: 1.1em; } h3 { - font-size: 1.12em; + font-size: 1.04em; } h4 { - font-size: 1.03em; + font-size: 1em; } h5, h6 { - font-size: 0.96em; - color: hsl(var(--muted-foreground)); + font-size: 0.98em; } ul, diff --git a/src/components/agent/chat/components/MessageList.test.tsx b/src/components/agent/chat/components/MessageList.test.tsx index 2a7643df5..aebe60b9f 100644 --- a/src/components/agent/chat/components/MessageList.test.tsx +++ b/src/components/agent/chat/components/MessageList.test.tsx @@ -1653,7 +1653,7 @@ describe("MessageList", () => { content: "正在分析依赖关系。", timestamp: now, runtimeStatus: { - phase: "reasoning", + phase: "routing", title: "处理中", detail: "正在读取多个 crate 的依赖。", }, diff --git a/src/components/agent/chat/hooks/agentStreamTurnEventBinding.test.ts b/src/components/agent/chat/hooks/agentStreamTurnEventBinding.test.ts index feb05c413..35e0447ef 100644 --- a/src/components/agent/chat/hooks/agentStreamTurnEventBinding.test.ts +++ b/src/components/agent/chat/hooks/agentStreamTurnEventBinding.test.ts @@ -288,11 +288,19 @@ describe("agentStreamTurnEventBinding", () => { setIsSending: noopDispatch(), }); - streamHandler?.({ + if (!streamHandler) { + throw new Error("expected stream handler to be registered"); + } + + const activeStreamHandler = streamHandler as (event: { + payload: unknown; + }) => void; + + activeStreamHandler({ payload: { type: "runtime_status", status: { - phase: "reasoning", + phase: "routing", title: "分析中", detail: "正在整理仓库结构", }, diff --git a/src/components/agent/chat/utils/runtimeToolAvailability.test.ts b/src/components/agent/chat/utils/runtimeToolAvailability.test.ts index cb8328344..848e13d1d 100644 --- a/src/components/agent/chat/utils/runtimeToolAvailability.test.ts +++ b/src/components/agent/chat/utils/runtimeToolAvailability.test.ts @@ -1,9 +1,14 @@ +import type { AgentRuntimeToolInventory } from "@/lib/api/agentRuntime"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { deriveRuntimeToolAvailability, RUNTIME_TOOL_AVAILABILITY_OVERRIDE_STORAGE_KEY, } from "./runtimeToolAvailability"; +function asToolInventory(value: unknown): AgentRuntimeToolInventory { + return value as AgentRuntimeToolInventory; +} + describe("runtime tool surface 派生", () => { beforeEach(() => { window.localStorage.clear(); @@ -14,24 +19,26 @@ describe("runtime tool surface 派生", () => { }); it("runtime tool surface 应优先从 runtime_tools 派生 current capability", () => { - const availability = deriveRuntimeToolAvailability({ - agent_initialized: true, - runtime_tools: [ - { name: "WebSearch" }, - { name: "Agent" }, - { name: "SendMessage" }, - { name: "TeamCreate" }, - { name: "TeamDelete" }, - { name: "ListPeers" }, - { name: "TaskCreate" }, - { name: "TaskGet" }, - { name: "TaskList" }, - { name: "TaskUpdate" }, - { name: "TaskOutput" }, - { name: "TaskStop" }, - ], - registry_tools: [{ name: "registry-only" }], - } as Parameters[0]); + const availability = deriveRuntimeToolAvailability( + asToolInventory({ + agent_initialized: true, + runtime_tools: [ + { name: "WebSearch" }, + { name: "Agent" }, + { name: "SendMessage" }, + { name: "TeamCreate" }, + { name: "TeamDelete" }, + { name: "ListPeers" }, + { name: "TaskCreate" }, + { name: "TaskGet" }, + { name: "TaskList" }, + { name: "TaskUpdate" }, + { name: "TaskOutput" }, + { name: "TaskStop" }, + ], + registry_tools: [], + }), + ); expect(availability).toMatchObject({ source: "runtime_tools", @@ -62,11 +69,13 @@ describe("runtime tool surface 派生", () => { }), ); - const availability = deriveRuntimeToolAvailability({ - agent_initialized: true, - runtime_tools: [{ name: "WebSearch" }], - registry_tools: [], - } as Parameters[0]); + const availability = deriveRuntimeToolAvailability( + asToolInventory({ + agent_initialized: true, + runtime_tools: [{ name: "WebSearch" }], + registry_tools: [], + }), + ); expect(availability).toMatchObject({ source: "runtime_tools", diff --git a/src/components/agent/chat/utils/toolBatchGrouping.ts b/src/components/agent/chat/utils/toolBatchGrouping.ts index d9007da8c..2fadd48d9 100644 --- a/src/components/agent/chat/utils/toolBatchGrouping.ts +++ b/src/components/agent/chat/utils/toolBatchGrouping.ts @@ -6,6 +6,7 @@ import { normalizeToolNameKey, parseToolCallArguments, resolveToolFilePath, + type ToolCallArgumentValue, } from "./toolDisplayInfo"; export type ToolBatchKind = "exploration" | "browser"; @@ -39,11 +40,16 @@ interface ToolBatchAccumulator { interface ToolLikeDescriptor { toolName: string; - argumentsValue?: string | Record; + argumentsValue?: string | Record; command?: string | null; query?: string | null; } +type ThreadProcessBatchItem = Extract< + AgentThreadItem, + { type: "tool_call" | "command_execution" | "web_search" } +>; + function shorten(value: string | null | undefined, maxLength = 72): string | null { const normalized = value?.trim(); if (!normalized) { @@ -55,7 +61,7 @@ function shorten(value: string | null | undefined, maxLength = 72): string | nul return `${normalized.slice(0, maxLength - 1).trimEnd()}…`; } -function asRecord(value: unknown): Record { +function asRecord(value: unknown): Record { if (!value) { return {}; } @@ -63,18 +69,28 @@ function asRecord(value: unknown): Record { if (typeof value === "string") { const parsed = parseToolCallArguments(value); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { - return parsed as Record; + return parsed; } return {}; } if (typeof value === "object" && !Array.isArray(value)) { - return value as Record; + return value as Record; } return {}; } +function isThreadProcessBatchItem( + item: AgentThreadItem, +): item is ThreadProcessBatchItem { + return ( + item.type === "tool_call" || + item.type === "command_execution" || + item.type === "web_search" + ); +} + function readString( record: Record, keys: string[], @@ -378,32 +394,32 @@ export function summarizeStreamingToolBatch( export function summarizeThreadProcessBatch( items: AgentThreadItem[], ): ToolBatchSummaryDescriptor | null { - if ( - items.length < 2 || - items.some( - (item) => - item.type !== "tool_call" && - item.type !== "command_execution" && - item.type !== "web_search", - ) - ) { + const processItems = items.filter(isThreadProcessBatchItem); + if (processItems.length < 2 || processItems.length !== items.length) { return null; } - const descriptors: ToolLikeDescriptor[] = items.map((item) => { + const descriptors: ToolLikeDescriptor[] = processItems.map((item) => { if (item.type === "command_execution") { + const argumentsValue: Record = { + command: item.command, + cwd: item.cwd, + }; return { toolName: "exec_command", command: item.command, - argumentsValue: { command: item.command, cwd: item.cwd }, + argumentsValue, }; } if (item.type === "web_search") { + const argumentsValue: Record = { + query: item.query || item.action || "", + }; return { toolName: item.action || "web_search", query: item.query || item.action || null, - argumentsValue: { query: item.query || item.action || "" }, + argumentsValue, }; } @@ -411,7 +427,7 @@ export function summarizeThreadProcessBatch( toolName: item.tool_name, argumentsValue: item.arguments && typeof item.arguments === "object" - ? (item.arguments as Record) + ? (item.arguments as Record) : item.arguments === undefined ? undefined : String(item.arguments), diff --git a/src/components/agent/chat/workspace/useWorkspaceConversationSceneRuntime.tsx b/src/components/agent/chat/workspace/useWorkspaceConversationSceneRuntime.tsx index 7da4c3cd7..802a069d4 100644 --- a/src/components/agent/chat/workspace/useWorkspaceConversationSceneRuntime.tsx +++ b/src/components/agent/chat/workspace/useWorkspaceConversationSceneRuntime.tsx @@ -36,10 +36,7 @@ import { WorkspaceConversationScene } from "./WorkspaceConversationScene"; type InputbarScene = Pick< ReturnType, - | "inputbarNode" - | "generalWorkbenchDialog" - | "teamWorkbenchSurfaceProps" - | "runtimeToolAvailability" + "inputbarNode" | "generalWorkbenchDialog" | "teamWorkbenchSurfaceProps" >; type CanvasScene = Pick< ReturnType, @@ -723,7 +720,6 @@ export function useWorkspaceConversationSceneRuntime({ setAccessMode, onManageProviders: navigationActions.handleManageProviders, toolPreferences: chatToolPreferences, - runtimeToolAvailability: inputbarScene.runtimeToolAvailability, onToolPreferenceChange: (key, enabled) => setChatToolPreferences((previous) => ({ ...previous, diff --git a/src/components/memory/MemoryPage.tsx b/src/components/memory/MemoryPage.tsx index 7e3a98cf8..d671307d1 100644 --- a/src/components/memory/MemoryPage.tsx +++ b/src/components/memory/MemoryPage.tsx @@ -336,6 +336,17 @@ const MEMORY_SCOPE_CARD_META: Array<{ }, ]; +type MemoryAvailabilityStatus = "loaded" | "exists" | "missing"; + +interface MemoryScopeCardView { + key: "user" | "project" | "local" | "auto" | "durable" | "team"; + label: string; + description: string; + status: MemoryAvailabilityStatus; + detail: string; + helper: string; +} + const MEMORY_DO_NOT_SAVE = [ "代码模式、约定、架构、文件路径或项目结构,这些应直接从当前仓库读取。", "Git 历史、最近改动、谁改了什么,`git log` 和 `git blame` 才是事实源。", @@ -637,7 +648,7 @@ function buildCreationPrompt( return lines.join("\n"); } -function getMemoryAvailabilityBadge(status: "loaded" | "exists" | "missing"): { +function getMemoryAvailabilityBadge(status: MemoryAvailabilityStatus): { label: string; className: string; } { @@ -932,10 +943,11 @@ export function MemoryPage({ onNavigate, pageParams }: MemoryPageProps) { [sourceBuckets], ); - const memoryScopeCards = useMemo(() => { + const memoryScopeCards = useMemo(() => { return MEMORY_SCOPE_CARD_META.map((card) => { if (card.key === "team") { - const status = teamSnapshots.length > 0 ? "loaded" : "missing"; + const status: MemoryAvailabilityStatus = + teamSnapshots.length > 0 ? "loaded" : "missing"; return { ...card, status, @@ -961,7 +973,7 @@ export function MemoryPage({ onNavigate, pageParams }: MemoryPageProps) { ? "durable" : "user"; const bucket = sourceBucketMap.get(bucketKey); - const status = bucket?.status || "missing"; + const status: MemoryAvailabilityStatus = bucket?.status || "missing"; if (card.key === "auto") { return { diff --git a/src/lib/base-setup/compat/serviceSkillCatalogProjection.ts b/src/lib/base-setup/compat/serviceSkillCatalogProjection.ts index 6c7a64255..9a69c0f75 100644 --- a/src/lib/base-setup/compat/serviceSkillCatalogProjection.ts +++ b/src/lib/base-setup/compat/serviceSkillCatalogProjection.ts @@ -14,7 +14,6 @@ import type { BaseSetupPackage, BaseSetupPolicyProfile, BaseSetupProjectionIndex, - BaseSetupScorecardProfile, BaseSetupSlotProfile, } from "../types"; diff --git a/src/lib/dev-bridge/http-client.test.ts b/src/lib/dev-bridge/http-client.test.ts index d6259afc4..23a401fd5 100644 --- a/src/lib/dev-bridge/http-client.test.ts +++ b/src/lib/dev-bridge/http-client.test.ts @@ -179,7 +179,7 @@ describe("http-client", () => { expect(fetchMock).toHaveBeenCalledTimes(1); }); - it("事件流在已建立连接后断开时应主动关闭并停止重复告警", async () => { + it("事件流在已建立连接后断开时应保留连接并停止重复告警", async () => { class MockEventSource { static instances: MockEventSource[] = []; @@ -226,10 +226,15 @@ describe("http-client", () => { source.emitError(); source.emitError(); - expect(source.close).toHaveBeenCalledTimes(1); + const secondUnlisten = await listenViaHttpEvent("config-changed", vi.fn()); + + expect(MockEventSource.instances).toHaveLength(1); + expect(source.close).not.toHaveBeenCalled(); expect(warnSpy).toHaveBeenCalledTimes(1); unlisten(); + secondUnlisten(); + expect(source.close).toHaveBeenCalledTimes(1); }); it("事件流在建立后结束不应把整个桥接误标记为 unavailable", async () => { diff --git a/src/lib/dev-bridge/http-client.ts b/src/lib/dev-bridge/http-client.ts index 44c6599db..7b3ecbfe5 100644 --- a/src/lib/dev-bridge/http-client.ts +++ b/src/lib/dev-bridge/http-client.ts @@ -336,6 +336,7 @@ export async function listenViaHttpEvent( let hasOpened = false; let settleOpen: ((value: void | PromiseLike) => void) | null = null; let settleOpenError: ((reason?: unknown) => void) | null = null; + let reconnectWarningShown = false; const openPromise = new Promise((resolve, reject) => { settleOpen = resolve; settleOpenError = reject; @@ -354,6 +355,10 @@ export async function listenViaHttpEvent( }, DEV_BRIDGE_EVENT_CONNECT_TIMEOUT_MS); source.onmessage = (messageEvent) => { + if (reconnectWarningShown) { + reconnectWarningShown = false; + markBridgeHealthy(); + } const parsed = parseBridgeEventPayload(messageEvent.data); if (!parsed) { return; @@ -375,6 +380,7 @@ export async function listenViaHttpEvent( return; } hasOpened = true; + reconnectWarningShown = false; markBridgeHealthy(); window.clearTimeout(connectTimeout); settleOpen?.(); @@ -386,13 +392,17 @@ export async function listenViaHttpEvent( if (!hubActive) { return; } + if (hasOpened) { + if (!reconnectWarningShown) { + reconnectWarningShown = true; + console.warn(`[DevBridge] 事件流异常: ${normalizedEvent}`, error); + } + return; + } console.warn(`[DevBridge] 事件流异常: ${normalizedEvent}`, error); hubActive = false; bridgeEventHubs.delete(normalizedEvent); source.close(); - if (hasOpened) { - return; - } markBridgeUnavailable(); window.clearTimeout(connectTimeout); settleOpenError?.(