From 7e3f2b0fb495a92736b01e53ad0fab161505a573 Mon Sep 17 00:00:00 2001 From: wgqqqqq Date: Tue, 14 Jul 2026 11:58:58 +0800 Subject: [PATCH] fix(agent): advertise inline image display by surface --- src/apps/desktop/src/api/commands.rs | 1 + src/apps/desktop/src/api/path_target.rs | 55 ++++++++++++-- src/apps/server/Cargo.toml | 1 + src/apps/server/src/rpc_dispatcher.rs | 11 ++- .../prompt_builder/prompt_builder_impl.rs | 9 +++ .../src/agentic/coordination/coordinator.rs | 9 +++ .../src/agentic/execution/execution_engine.rs | 10 ++- src/crates/execution/agent-runtime/src/lib.rs | 1 + .../agent-runtime/src/output_surface.rs | 31 ++++++++ .../execution/agent-runtime/src/prompt.rs | 18 ++++- .../agent-runtime/tests/prompt_contracts.rs | 36 +++++++++ .../components/Markdown/Markdown.test.tsx | 27 ++++++- .../components/Markdown/Markdown.tsx | 74 ++++++++++++++----- .../flow_chat/components/FlowTextBlock.tsx | 3 + 14 files changed, 256 insertions(+), 30 deletions(-) create mode 100644 src/crates/execution/agent-runtime/src/output_surface.rs diff --git a/src/apps/desktop/src/api/commands.rs b/src/apps/desktop/src/api/commands.rs index 57b6742c45..7d21cda48b 100644 --- a/src/apps/desktop/src/api/commands.rs +++ b/src/apps/desktop/src/api/commands.rs @@ -2538,6 +2538,7 @@ pub async fn read_file_content( read_text_file( &state, &request.file_path, + request.encoding.as_deref(), request.remote_connection_id.as_deref(), ) .await diff --git a/src/apps/desktop/src/api/path_target.rs b/src/apps/desktop/src/api/path_target.rs index 2e00b16c01..23ea8b1a46 100644 --- a/src/apps/desktop/src/api/path_target.rs +++ b/src/apps/desktop/src/api/path_target.rs @@ -1,6 +1,7 @@ //! Shared desktop resolution and access helpers for local, runtime, and remote paths. use crate::api::app_state::AppState; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; use bitfun_core::agentic::tools::workspace_paths::{ is_bitfun_runtime_uri, parse_bitfun_runtime_uri, }; @@ -241,17 +242,26 @@ pub fn stat_local_path_metadata( pub async fn read_text_file( app_state: &AppState, raw_path: &str, + encoding: Option<&str>, preferred_remote_connection_id: Option<&str>, ) -> Result { let target = resolve_desktop_path_target(app_state, raw_path, preferred_remote_connection_id).await?; match &target { - DesktopPathTarget::Local { resolved_path, .. } => app_state - .filesystem_service - .read_file(&resolved_path.to_string_lossy()) - .await - .map(|result| result.content) - .map_err(|e| format!("Failed to read file content: {}", e)), + DesktopPathTarget::Local { resolved_path, .. } => { + let result = app_state + .filesystem_service + .read_file(&resolved_path.to_string_lossy()) + .await + .map_err(|e| format!("Failed to read file content: {}", e))?; + if encoding.is_some_and(|value| value.eq_ignore_ascii_case("base64")) + && !result.encoding.eq_ignore_ascii_case("base64") + { + Ok(BASE64.encode(result.content.as_bytes())) + } else { + Ok(result.content) + } + } DesktopPathTarget::Remote { requested_path, entry, @@ -264,11 +274,42 @@ pub async fn read_text_file( .read_file(&entry.connection_id, requested_path) .await .map_err(|e| format!("Failed to read remote file: {}", e))?; - String::from_utf8(bytes).map_err(|e| format!("File is not valid UTF-8: {}", e)) + encode_remote_file_bytes(bytes, encoding) } } } +fn encode_remote_file_bytes(bytes: Vec, encoding: Option<&str>) -> Result { + if encoding.is_some_and(|value| value.eq_ignore_ascii_case("base64")) { + return Ok(BASE64.encode(bytes)); + } + + String::from_utf8(bytes).map_err(|e| format!("File is not valid UTF-8: {}", e)) +} + +#[cfg(test)] +mod tests { + use super::encode_remote_file_bytes; + + #[test] + fn remote_file_bytes_support_explicit_base64_encoding() { + let png_header = vec![0x89, b'P', b'N', b'G']; + assert_eq!( + encode_remote_file_bytes(png_header, Some("base64")).expect("base64 should encode"), + "iVBORw==" + ); + } + + #[test] + fn remote_file_bytes_preserve_text_default() { + assert_eq!( + encode_remote_file_bytes(b"hello".to_vec(), None).expect("text should decode"), + "hello" + ); + assert!(encode_remote_file_bytes(vec![0xff], None).is_err()); + } +} + pub async fn write_text_file( app_state: &AppState, raw_path: &str, diff --git a/src/apps/server/Cargo.toml b/src/apps/server/Cargo.toml index c10e62da76..bed10367c5 100644 --- a/src/apps/server/Cargo.toml +++ b/src/apps/server/Cargo.toml @@ -19,6 +19,7 @@ tokio = { workspace = true, features = ["full"] } serde = { workspace = true } serde_json = { workspace = true } anyhow = { workspace = true } +base64 = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } futures-util = { workspace = true } diff --git a/src/apps/server/src/rpc_dispatcher.rs b/src/apps/server/src/rpc_dispatcher.rs index 2b2b824a09..b8c31789a2 100644 --- a/src/apps/server/src/rpc_dispatcher.rs +++ b/src/apps/server/src/rpc_dispatcher.rs @@ -6,6 +6,7 @@ use crate::bootstrap::ServerAppState; use anyhow::{anyhow, Result}; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; use bitfun_core::agentic::agents::SubAgentSource; use bitfun_core::agentic::coordination::{DialogSubmissionPolicy, DialogTriggerSource}; use bitfun_core::agentic::core::SessionConfig; @@ -95,12 +96,20 @@ pub async fn dispatch( "read_file_content" => { let request = extract_request(¶ms)?; let file_path = get_string(&request, "filePath")?; + let encoding = request.get("encoding").and_then(|value| value.as_str()); let result = state .filesystem_service .read_file(&file_path) .await .map_err(|e| anyhow!("{}", e))?; - Ok(serde_json::json!(result.content)) + let content = if encoding.is_some_and(|value| value.eq_ignore_ascii_case("base64")) + && !result.encoding.eq_ignore_ascii_case("base64") + { + BASE64.encode(result.content.as_bytes()) + } else { + result.content + }; + Ok(serde_json::json!(content)) } "write_file_content" => { let request = extract_request(¶ms)?; diff --git a/src/crates/assembly/core/src/agentic/agents/prompt_builder/prompt_builder_impl.rs b/src/crates/assembly/core/src/agentic/agents/prompt_builder/prompt_builder_impl.rs index 8fc2eb25cc..770c387338 100644 --- a/src/crates/assembly/core/src/agentic/agents/prompt_builder/prompt_builder_impl.rs +++ b/src/crates/assembly/core/src/agentic/agents/prompt_builder/prompt_builder_impl.rs @@ -54,6 +54,8 @@ pub struct PromptBuilderContext { pub runtime_context_needs: RuntimeContextNeeds, /// Remote mobile/bot turns need `computer://` links for file delivery. pub remote_file_delivery_channel: bool, + /// The active response surface can render Markdown image syntax inline. + pub inline_markdown_image_display: bool, } impl PromptBuilderContext { @@ -73,6 +75,7 @@ impl PromptBuilderContext { tool_listing_sections: ToolListingSections::default(), runtime_context_needs: RuntimeContextNeeds::default(), remote_file_delivery_channel: false, + inline_markdown_image_display: false, } } @@ -110,6 +113,11 @@ impl PromptBuilderContext { self.remote_file_delivery_channel = enabled; self } + + pub fn with_inline_markdown_image_display(mut self, enabled: bool) -> Self { + self.inline_markdown_image_display = enabled; + self + } } pub async fn build_prompt_context_for_workspace( @@ -244,6 +252,7 @@ impl PromptBuilder { remote_execution: self.context.remote_execution.clone(), local_shell, supports_image_understanding: self.context.supports_image_understanding, + inline_markdown_image_display: self.context.inline_markdown_image_display, }) } diff --git a/src/crates/assembly/core/src/agentic/coordination/coordinator.rs b/src/crates/assembly/core/src/agentic/coordination/coordinator.rs index 216b934b2c..cec07106e3 100644 --- a/src/crates/assembly/core/src/agentic/coordination/coordinator.rs +++ b/src/crates/assembly/core/src/agentic/coordination/coordinator.rs @@ -56,6 +56,9 @@ use crate::service::workspace::{ }; use crate::service_agent_runtime::CoreServiceAgentRuntime; use crate::util::errors::{BitFunError, BitFunResult}; +use bitfun_agent_runtime::output_surface::{ + supports_inline_markdown_images_for_source, TOOL_CONTEXT_INLINE_MARKDOWN_IMAGE_DISPLAY_KEY, +}; use bitfun_agent_runtime::remote_file_delivery::{ needs_computer_links_for_source, remote_file_delivery_reminder, TOOL_CONTEXT_REMOTE_FILE_DELIVERY_KEY, @@ -3546,6 +3549,12 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet "true".to_string(), ); } + if supports_inline_markdown_images_for_source(submission_policy.trigger_source) { + context_vars.insert( + TOOL_CONTEXT_INLINE_MARKDOWN_IMAGE_DISPLAY_KEY.to_string(), + "true".to_string(), + ); + } let session_workspace_path = session_workspace .as_ref() .map(|workspace| workspace.root_path_string()); diff --git a/src/crates/assembly/core/src/agentic/execution/execution_engine.rs b/src/crates/assembly/core/src/agentic/execution/execution_engine.rs index 51daf2d737..e6460b0b48 100644 --- a/src/crates/assembly/core/src/agentic/execution/execution_engine.rs +++ b/src/crates/assembly/core/src/agentic/execution/execution_engine.rs @@ -44,6 +44,7 @@ use crate::util::token_counter::TokenCounter; use crate::util::types::Message as AIMessage; use crate::util::types::ToolDefinition; use crate::util::{elapsed_ms_u64, truncate_at_char_boundary}; +use bitfun_agent_runtime::output_surface::TOOL_CONTEXT_INLINE_MARKDOWN_IMAGE_DISPLAY_KEY; use bitfun_agent_runtime::remote_file_delivery::TOOL_CONTEXT_REMOTE_FILE_DELIVERY_KEY; use bitfun_ai_adapters::ModelExchangeTraceConfig; use log::{debug, error, info, trace, warn}; @@ -971,6 +972,11 @@ impl ExecutionEngine { .get(TOOL_CONTEXT_REMOTE_FILE_DELIVERY_KEY) .and_then(|value| value.parse::().ok()) .unwrap_or(false); + let inline_markdown_image_display = context + .context + .get(TOOL_CONTEXT_INLINE_MARKDOWN_IMAGE_DISPLAY_KEY) + .and_then(|value| value.parse::().ok()) + .unwrap_or(false); build_prompt_context_for_workspace( workspace, @@ -983,7 +989,9 @@ impl ExecutionEngine { ) .await .map(|prompt_context| { - prompt_context.with_remote_file_delivery_channel(remote_file_delivery_channel) + prompt_context + .with_remote_file_delivery_channel(remote_file_delivery_channel) + .with_inline_markdown_image_display(inline_markdown_image_display) }) } diff --git a/src/crates/execution/agent-runtime/src/lib.rs b/src/crates/execution/agent-runtime/src/lib.rs index 0f51e26cc8..ae0ef164a7 100644 --- a/src/crates/execution/agent-runtime/src/lib.rs +++ b/src/crates/execution/agent-runtime/src/lib.rs @@ -17,6 +17,7 @@ pub mod event_router; pub mod events; pub mod evidence_ledger; pub mod file_read_state; +pub mod output_surface; pub mod post_call_hooks; pub mod prompt; pub mod prompt_cache; diff --git a/src/crates/execution/agent-runtime/src/output_surface.rs b/src/crates/execution/agent-runtime/src/output_surface.rs new file mode 100644 index 0000000000..a348f736fc --- /dev/null +++ b/src/crates/execution/agent-runtime/src/output_surface.rs @@ -0,0 +1,31 @@ +use bitfun_runtime_ports::DialogTriggerSource; + +pub const TOOL_CONTEXT_INLINE_MARKDOWN_IMAGE_DISPLAY_KEY: &str = "inline_markdown_image_display"; + +pub const fn supports_inline_markdown_images_for_source(source: DialogTriggerSource) -> bool { + matches!(source, DialogTriggerSource::DesktopUi) +} + +#[cfg(test)] +mod tests { + use super::supports_inline_markdown_images_for_source; + use bitfun_runtime_ports::DialogTriggerSource; + + #[test] + fn inline_markdown_images_are_scoped_to_desktop_ui() { + assert!(supports_inline_markdown_images_for_source( + DialogTriggerSource::DesktopUi + )); + + for source in [ + DialogTriggerSource::DesktopApi, + DialogTriggerSource::AgentSession, + DialogTriggerSource::ScheduledJob, + DialogTriggerSource::RemoteRelay, + DialogTriggerSource::Bot, + DialogTriggerSource::Cli, + ] { + assert!(!supports_inline_markdown_images_for_source(source)); + } + } +} diff --git a/src/crates/execution/agent-runtime/src/prompt.rs b/src/crates/execution/agent-runtime/src/prompt.rs index 0a0e5a43ce..dfabb4bf2c 100644 --- a/src/crates/execution/agent-runtime/src/prompt.rs +++ b/src/crates/execution/agent-runtime/src/prompt.rs @@ -132,15 +132,31 @@ pub struct RuntimeContextFacts { pub remote_execution: Option, pub local_shell: Option, pub supports_image_understanding: Option, + pub inline_markdown_image_display: bool, } pub fn render_runtime_context_reminder(facts: &RuntimeContextFacts) -> Option { - if facts.needs.is_empty() { + if facts.needs.is_empty() && !facts.inline_markdown_image_display { return None; } let mut lines = vec!["# Runtime Context".to_string()]; + if facts.inline_markdown_image_display { + push_runtime_context_section( + &mut lines, + "Chat Image Display", + vec![ + "- The current Desktop/Web chat renders Markdown images inline. To show an image to the user, use `![concise alt text](source)` in the response." + .to_string(), + "- Supported sources are verified HTTP(S) image URLs and workspace-relative image paths. Prefer PNG, JPEG, GIF, or WebP for reliable rendering." + .to_string(), + "- Do not invent image URLs, and do not call image-analysis tools solely to display an image. Use a URL you verified or a path to a file that exists in the active workspace." + .to_string(), + ], + ); + } + if facts.needs.workspace_tools { let mut workspace_lines = Vec::new(); if let Some(remote) = &facts.remote_execution { diff --git a/src/crates/execution/agent-runtime/tests/prompt_contracts.rs b/src/crates/execution/agent-runtime/tests/prompt_contracts.rs index 0d28f3fb86..4d8ea580f4 100644 --- a/src/crates/execution/agent-runtime/tests/prompt_contracts.rs +++ b/src/crates/execution/agent-runtime/tests/prompt_contracts.rs @@ -119,6 +119,7 @@ fn runtime_context_renderer_preserves_local_exec_and_computer_use_guidance() { invocation: "powershell.exe -NoLogo".to_string(), }), supports_image_understanding: None, + inline_markdown_image_display: false, }) .expect("runtime context should render"); @@ -156,6 +157,7 @@ fn runtime_context_renderer_preserves_remote_workspace_split() { invocation: "powershell.exe".to_string(), }), supports_image_understanding: None, + inline_markdown_image_display: false, }) .expect("remote runtime context should render"); @@ -176,6 +178,7 @@ fn runtime_context_renderer_adds_text_only_computer_use_guidance_for_non_visual_ remote_execution: None, local_shell: None, supports_image_understanding: Some(false), + inline_markdown_image_display: false, }) .expect("runtime context should render"); @@ -197,6 +200,7 @@ fn runtime_context_renderer_omits_text_only_guidance_for_visual_or_unknown_model remote_execution: None, local_shell: None, supports_image_understanding, + inline_markdown_image_display: false, }) .expect("runtime context should render"); @@ -206,6 +210,38 @@ fn runtime_context_renderer_omits_text_only_guidance_for_visual_or_unknown_model } } +#[test] +fn runtime_context_renderer_scopes_inline_image_guidance_to_capable_surfaces() { + let reminder = render_runtime_context_reminder(&RuntimeContextFacts { + needs: RuntimeContextNeeds::default(), + host_os: "linux".to_string(), + host_family: "unix".to_string(), + host_arch: "x86_64".to_string(), + remote_execution: None, + local_shell: None, + supports_image_understanding: None, + inline_markdown_image_display: true, + }) + .expect("output-surface context should render without tool runtime facts"); + + assert!(reminder.contains("## Chat Image Display")); + assert!(reminder.contains("`![concise alt text](source)`")); + assert!(reminder.contains("workspace-relative image paths")); + assert!(reminder.contains("do not call image-analysis tools solely to display an image")); + + assert!(render_runtime_context_reminder(&RuntimeContextFacts { + needs: RuntimeContextNeeds::default(), + host_os: "linux".to_string(), + host_family: "unix".to_string(), + host_arch: "x86_64".to_string(), + remote_execution: None, + local_shell: None, + supports_image_understanding: None, + inline_markdown_image_display: false, + }) + .is_none()); +} + #[test] fn workspace_and_user_context_renderers_preserve_section_shape() { let local = render_workspace_context(&WorkspaceContextFacts { diff --git a/src/web-ui/src/component-library/components/Markdown/Markdown.test.tsx b/src/web-ui/src/component-library/components/Markdown/Markdown.test.tsx index 52c58abfce..bc9d5e2923 100644 --- a/src/web-ui/src/component-library/components/Markdown/Markdown.test.tsx +++ b/src/web-ui/src/component-library/components/Markdown/Markdown.test.tsx @@ -247,8 +247,33 @@ describe('Markdown file links', () => { const image = container.querySelector('img[alt="ReLU 图像"]'); expect(image).not.toBeNull(); - expect(mocks.readFileContent).toHaveBeenCalledWith(`${EXAMPLE_WORKSPACE}/relu.png`); + expect(mocks.readFileContent).toHaveBeenCalledWith( + `${EXAMPLE_WORKSPACE}/relu.png`, + 'base64', + undefined, + ); expect(image?.src).toBe('data:image/png;base64,cmVsdS1wbmc='); expect(mocks.getCurrentWorkspacePath).not.toHaveBeenCalled(); }); + + it('routes remote markdown image reads through the session connection', async () => { + await act(async () => { + root.render( + , + ); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(mocks.readFileContent).toHaveBeenCalledWith( + '/srv/project/artifacts/chart.png', + 'base64', + 'remote-connection-1', + ); + }); }); diff --git a/src/web-ui/src/component-library/components/Markdown/Markdown.tsx b/src/web-ui/src/component-library/components/Markdown/Markdown.tsx index 8533ba5602..96b36aa638 100644 --- a/src/web-ui/src/component-library/components/Markdown/Markdown.tsx +++ b/src/web-ui/src/component-library/components/Markdown/Markdown.tsx @@ -451,37 +451,57 @@ function getMimeType(filePath: string): string { return mimeTypes[ext || ''] || 'image/jpeg'; } -async function getLocalImageDataUrl(localPath: string): Promise { - const cachedDataUrl = localImageDataUrlCache.get(localPath); +function getLocalImageCacheKey(localPath: string, remoteConnectionId?: string): string { + return JSON.stringify([remoteConnectionId || null, localPath]); +} + +async function getLocalImageDataUrl( + localPath: string, + remoteConnectionId?: string, +): Promise { + const cacheKey = getLocalImageCacheKey(localPath, remoteConnectionId); + const cachedDataUrl = localImageDataUrlCache.get(cacheKey); if (cachedDataUrl) { return cachedDataUrl; } - const pendingRequest = localImageRequestCache.get(localPath); + const pendingRequest = localImageRequestCache.get(cacheKey); if (pendingRequest) { return pendingRequest; } const request = (async () => { - const base64Content = await workspaceAPI.readFileContent(localPath); + const base64Content = await workspaceAPI.readFileContent( + localPath, + 'base64', + remoteConnectionId, + ); const dataUrl = `data:${getMimeType(localPath)};base64,${base64Content}`; - localImageDataUrlCache.set(localPath, dataUrl); - localImageRequestCache.delete(localPath); + localImageDataUrlCache.set(cacheKey, dataUrl); + localImageRequestCache.delete(cacheKey); return dataUrl; })().catch((error) => { - localImageRequestCache.delete(localPath); + localImageRequestCache.delete(cacheKey); throw error; }); - localImageRequestCache.set(localPath, request); + localImageRequestCache.set(cacheKey, request); return request; } interface MarkdownImageProps extends React.ImgHTMLAttributes { basePath?: string; + remoteConnectionId?: string; } -const MarkdownImage: React.FC = ({ src, alt, className, basePath, ...imgProps }) => { +const MarkdownImage: React.FC = ({ + src, + alt, + className, + basePath, + remoteConnectionId, + ...imgProps +}) => { const rawSrc = typeof src === 'string' ? normalizeExternalImageSrc(src) : ''; const localPath = useMemo(() => { if (!rawSrc || !isLocalAssetPath(rawSrc)) { @@ -490,29 +510,32 @@ const MarkdownImage: React.FC = ({ src, alt, className, base return resolveBaseRelativePath(rawSrc, basePath); }, [basePath, rawSrc]); + const cacheKey = localPath + ? getLocalImageCacheKey(localPath, remoteConnectionId) + : null; const [resolvedSrc, setResolvedSrc] = useState(() => { - if (!localPath) { + if (!localPath || !cacheKey) { return rawSrc; } - return localImageDataUrlCache.get(localPath) || LOCAL_IMAGE_PLACEHOLDER; + return localImageDataUrlCache.get(cacheKey) || LOCAL_IMAGE_PLACEHOLDER; }); const [loadState, setLoadState] = useState<'idle' | 'loading' | 'loaded' | 'error'>(() => { - if (!localPath) { + if (!localPath || !cacheKey) { return 'loaded'; } - return localImageDataUrlCache.has(localPath) ? 'loaded' : 'idle'; + return localImageDataUrlCache.has(cacheKey) ? 'loaded' : 'idle'; }); useEffect(() => { - if (!localPath) { + if (!localPath || !cacheKey) { setResolvedSrc(rawSrc); setLoadState('loaded'); return; } - const cachedDataUrl = localImageDataUrlCache.get(localPath); + const cachedDataUrl = localImageDataUrlCache.get(cacheKey); if (cachedDataUrl) { setResolvedSrc(cachedDataUrl); setLoadState('loaded'); @@ -523,7 +546,7 @@ const MarkdownImage: React.FC = ({ src, alt, className, base setResolvedSrc(LOCAL_IMAGE_PLACEHOLDER); setLoadState('loading'); - void getLocalImageDataUrl(localPath) + void getLocalImageDataUrl(localPath, remoteConnectionId) .then((dataUrl) => { if (cancelled) { return; @@ -537,7 +560,11 @@ const MarkdownImage: React.FC = ({ src, alt, className, base return; } - log.error('Failed to load local markdown image', { path: localPath, error }); + log.error('Failed to load local markdown image', { + path: localPath, + remoteConnectionId, + error, + }); setResolvedSrc(rawSrc); setLoadState('error'); }); @@ -545,7 +572,7 @@ const MarkdownImage: React.FC = ({ src, alt, className, base return () => { cancelled = true; }; - }, [localPath, rawSrc]); + }, [cacheKey, localPath, rawSrc, remoteConnectionId]); return ( (({ content, basePath, + remoteConnectionId, className = '', isStreaming = false, expandDetailsByDefault = false, @@ -1319,7 +1348,13 @@ export const Markdown = React.memo(({ }, img({ node: _node, ...props }: any) { - return ; + return ( + + ); }, blockquote({ children }: any) { @@ -1350,6 +1385,7 @@ export const Markdown = React.memo(({ } }), [ basePath, + remoteConnectionId, expandDetailsByDefault, isStreaming, markdownContent, diff --git a/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx b/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx index 0e1ce7f758..e92c5a429b 100644 --- a/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx +++ b/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx @@ -77,6 +77,8 @@ export const FlowTextBlock = React.memo(({ } = useFlowChatContext(); const markdownBasePath = activeSessionOverride?.workspacePath || activeSessionOverride?.config?.workspacePath; + const markdownRemoteConnectionId = activeSessionOverride?.remoteConnectionId + || activeSessionOverride?.config?.remoteConnectionId; // Normalize content to a string. const content = typeof textItem.content === 'string' @@ -151,6 +153,7 @@ export const FlowTextBlock = React.memo(({