You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/10/23 00:56:00 UTC
(maka) 01/03: feat(terminal): load network Markdown images through native image services
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 81b9891ab371b741153d4891ef51562c77b24f20
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Fri Oct 23 08:28:41 2026 +0800
feat(terminal): load network Markdown images through native image services
---
products/maka/terminal-shell/README.md | 2 +-
products/maka/terminal-shell/README.zh-CN.md | 2 +
products/maka/terminal-shell/app/MarkdownImage.tsx | 30 +++++----
products/maka/terminal-shell/app/host.ts | 8 ++-
products/maka/terminal-shell/src/bindings.rs | 8 +--
products/maka/terminal-shell/src/bindings/links.rs | 57 +++++++++++++----
products/maka/terminal-shell/src/entry.rs | 42 +++++++++---
.../tests/real_host/markdown_links.rs | 74 +++++++++++++++++++++-
products/maka/terminal-shell/tests/support/mod.rs | 3 +-
9 files changed, 180 insertions(+), 46 deletions(-)
diff --git a/products/maka/terminal-shell/README.md b/products/maka/terminal-shell/README.md
index 3d8466a2af..f0ad740dd1 100644
--- a/products/maka/terminal-shell/README.md
+++ b/products/maka/terminal-shell/README.md
@@ -82,7 +82,7 @@ Grouped activity has independent native scrolling, capped at half the reading vi
Assistant responses format headings, emphasis, lists, quotes, tables and code. Each code block has its own **Copy code** action. **Show message source** switches to the exact selectable text; **Show formatted message** restores formatting. Partial history stays literal until its opening is loaded.
-Relative Markdown image references are read from the conversation's workspace and render inline. Activate an unlinked image to view it full screen; linked images open their link target. Source mode and message copy preserve the original Markdown.
+Markdown images render inline from the conversation's workspace or an HTTP(S) URL. Workspace images use the existing Host file reader; network images load on the client through NSH's native image loader and cache. The application grants HTTP(S) GET, including the same permission check on each redirect. Activate an unlinked image to view it full screen, then close or press Escape to return to that image without changing the draft; linked images keep their original link target. Loading and [...]
**Todo** sits above the composer and displays the Agent's actual checklist, including its current item and completed count. Token speed reports the **last request**; context figures mark approximate or last-known values. Unavailable metrics are omitted.
diff --git a/products/maka/terminal-shell/README.zh-CN.md b/products/maka/terminal-shell/README.zh-CN.md
index 96f7be658e..c421361f40 100644
--- a/products/maka/terminal-shell/README.zh-CN.md
+++ b/products/maka/terminal-shell/README.zh-CN.md
@@ -78,6 +78,8 @@ cargo run -p maka-cli
助手回复支持标题、强调、列表、引用、表格和代码格式,每个代码块提供独立的 **Copy code**。**Show message source** 切换到可选择的原文,**Show formatted message** 恢复格式。历史片段在开头加载完整前保持原文显示。
+Markdown 图片支持工作区相对路径和 HTTP(S) URL,并在正文中显示。工作区图片仍通过现有 Host 文件服务读取;网络图片由客户端的 NSH 原生加载器和缓存处理,应用只授予 HTTP(S) GET,每次重定向也检查权限。激活未带链接的图片可全屏查看,关闭或按 Escape 返回原图片并保留草稿;带链接的图片仍打开原链接目标。加载和失败都有明确提示,未带链接的失败图片可重试。原文模式和消息复制保留完整原始 Markdown。
+
用户消息经 Host 记录后提供 **Copy user message**;已完成的回复提供 **Copy message**、**Fork from this turn** 和 **View this turn's model usage**,并显示原文切换、已记录的模型与耗时。紧凑的图标操作始终可用,配有完整的悬浮提示和无障碍名称。两种消息复制即使在历史窗口只显示末尾时也会读取完整原始消息。Fork 将历史复制到该回复所在轮次的完成边界,打开独立会话,原草稿仍留在原会话;后续轮次正在运行时,也可以从更早的已完成轮次分叉。Usage 读取原始记录中的本轮主模型请求,分别显示输入、输出、推理、缓存 token 与已记录的费用;缺失的计数或价格明确显示不可用。复制或导入的历史不会产生新的实体模型费用记录。
展开包含图片的工具结果可查看缩略图,激活 **Open image** 可打开较大的预览。Escape 或 **Close** 返回对话并恢复焦点,不会停止当前运行。两处都使用 Host 保留的原始图片,即使工作区文件后来改变也不受影响。在 Unicode 呈现中,内嵌图片在 Sixel 支持和字符单元像素尺寸均可用时使用 Sixel,否则使用彩色 Unicode 半块和象限块。调整窗口大小后,旧测量失效,图片暂时使用 Unicode,等待原查询的回复全部消费后再重新测量;新查询完成后恢复 Sixel。等待终端回复不会阻塞 Unicode 呈现或输入。不支持的图片格式会在结果中显示错误。
diff --git a/products/maka/terminal-shell/app/MarkdownImage.tsx b/products/maka/terminal-shell/app/MarkdownImage.tsx
index ad6b4b6c5c..148606a5a9 100644
--- a/products/maka/terminal-shell/app/MarkdownImage.tsx
+++ b/products/maka/terminal-shell/app/MarkdownImage.tsx
@@ -22,16 +22,15 @@ import type { YNode } from 'nsh';
import { Button } from 'nsh/base';
import { Image } from 'nsh/image';
import type { MarkdownImage } from 'maka/markdown';
-import { host, type FileView } from './host';
+import { host } from './host';
import { ImageViewer } from './ImageViewer';
import { createText, errorText, UiError } from './text';
import { tr } from './locale';
import { C, bodyTextStyle, captionTextStyle, controlFocusStyle } from './ui';
-type ImageFile = Extract<FileView, { kind: 'image' }>;
type Reading = { stream: Awaited<ReturnType<typeof openFile>> | null };
-const openFile = (sessionId: string, target: string) =>
- host.subscribe('agent', 'readFile', { sessionId, path: host.workspaceImagePath(target) });
+const openFile = (sessionId: string, path: { bytesBase64: string }) =>
+ host.subscribe('agent', 'readFile', { sessionId, path });
const close = async (reading: Reading | null) => {
const stream = reading?.stream;
if (reading) reading.stream = null;
@@ -48,7 +47,7 @@ export function MarkdownImageView(props: {
}) {
const target = props.image().target;
const sessionId = props.sessionId;
- const file = signal<ImageFile | null>(null);
+ const source = signal<{ uri: string } | null>(null);
const dimensions = signal<{ width: number; height: number } | null>(null);
const error = createText();
const viewing = signal(false);
@@ -62,12 +61,17 @@ export function MarkdownImageView(props: {
const request: Reading = { stream: null };
reading = request;
error.set('');
- file.set(null);
+ source.set(null);
dimensions.set(null);
void (async () => {
await close(previous);
if (!owns() || reading !== request) return;
- const stream = await openFile(sessionId, target);
+ const selected = host.markdownImageSource(target);
+ if (selected.kind === 'remote') {
+ source.set({ uri: selected.uri });
+ return;
+ }
+ const stream = await openFile(sessionId, selected.path);
if (!owns() || reading !== request) {
await stream.close();
return;
@@ -82,7 +86,7 @@ export function MarkdownImageView(props: {
'This file changed while it was being read. Refresh to capture it again.',
);
if (captured.kind !== 'image') throw new UiError('This file is not a supported image.');
- file.set(captured);
+ source.set({ uri: captured.uri });
} finally {
await close(request);
}
@@ -108,12 +112,12 @@ export function MarkdownImageView(props: {
const activate = (selected: { sessionId: string; target: string }) => {
if (!owns() || selected.sessionId !== sessionId || selected.target !== target) return;
if (error()) read();
- else if (file() && dimensions()) viewing.set(true);
+ else if (source() && dimensions()) viewing.set(true);
};
const content = () => (
<View style={{ gap: 4, width: width() }}>
<Each
- of={file() && !error() && (props.width === undefined || props.width() > 0) ? [file()!] : []}
+ of={source() && !error() && (props.width === undefined || props.width() > 0) ? [source()!] : []}
key={(image) => image.uri}
>
{(image) => {
@@ -122,7 +126,7 @@ export function MarkdownImageView(props: {
cleanup(() => {
mounted = false;
});
- const current = () => owns() && mounted && file() === captured;
+ const current = () => owns() && mounted && source() === captured;
return (
<Image
src={captured.uri}
@@ -174,9 +178,9 @@ export function MarkdownImageView(props: {
{content()}
</Button>
</If>
- <If when={viewing() && file() && dimensions()}>
+ <If when={viewing() && source() && dimensions()}>
<ImageViewer
- image={{ uri: file()!.uri, ...dimensions()!, title: name() }}
+ image={{ uri: source()!.uri, ...dimensions()!, title: name() }}
close={() => {
viewing.set(false);
if (owns()) button?.focus();
diff --git a/products/maka/terminal-shell/app/host.ts b/products/maka/terminal-shell/app/host.ts
index a7ecc9ed2a..0e447680a0 100644
--- a/products/maka/terminal-shell/app/host.ts
+++ b/products/maka/terminal-shell/app/host.ts
@@ -168,9 +168,11 @@ async function subscribe(
// Local journal methods serialize these actual Rust DTOs. Their receipt business
// values remain unknown and continue through the application's existing guards.
export const host = {
- workspaceImagePath(target: string) {
- const path = nativeHost.workspaceImagePath(target);
- return { bytesBase64: path.path };
+ markdownImageSource(target: string) {
+ const source = nativeHost.markdownImageSource(target);
+ return source.kind === 'workspace'
+ ? { kind: 'workspace' as const, path: { bytesBase64: source.path } }
+ : source;
},
async openLink(target: string) {
const link = await nativeHost.openLink(target);
diff --git a/products/maka/terminal-shell/src/bindings.rs b/products/maka/terminal-shell/src/bindings.rs
index 223c6b9139..0aa11da9e6 100644
--- a/products/maka/terminal-shell/src/bindings.rs
+++ b/products/maka/terminal-shell/src/bindings.rs
@@ -196,10 +196,10 @@ fn require_contract(endpoint: &str, method: &str, actual: &Contract) -> Result<(
#[class]
impl ApplicationHost {
- /// Resolve only an exact workspace-relative image selector. Actual access
- /// and immutable image bytes remain with the existing Host file service.
- pub fn workspace_image_path(&self, target: String) -> Result<links::WorkspaceLink, String> {
- links::workspace_image(&target)
+ /// Resolve the original Markdown target once. Workspace bytes remain with
+ /// the Host; HTTP(S) images use NSH's native loader and bundle permissions.
+ pub fn markdown_image_source(&self, target: String) -> Result<links::MarkdownImageSource, String> {
+ links::markdown_image(&target)
}
/// Hand an HTTP(S) link to this client's system browser, or return an exact
diff --git a/products/maka/terminal-shell/src/bindings/links.rs b/products/maka/terminal-shell/src/bindings/links.rs
index 16949571ed..3987d6a9d8 100644
--- a/products/maka/terminal-shell/src/bindings/links.rs
+++ b/products/maka/terminal-shell/src/bindings/links.rs
@@ -22,6 +22,7 @@
use super::{Owner, execute};
use maka_agent_api::contract::WorkspacePath;
use nsh_core::ui::prelude::*;
+use nsh_core::ui::{JsHost, JsIn, JsOwned, PropError, TsTypes};
use std::sync::Arc;
use url::Url;
@@ -88,16 +89,40 @@ fn destination(target: &str) -> Result<Destination, String> {
}
}
-/// Resolve an image without activating an external URL or reading any file.
-pub(super) fn workspace_image(target: &str) -> Result<WorkspaceLink, String> {
+/// An image selector, without reading a file or activating a browser.
+pub enum MarkdownImageSource {
+ Workspace { path: String },
+ Remote { uri: String },
+}
+
+impl JsValue for MarkdownImageSource {
+ fn ts_type(types: &mut TsTypes) -> String {
+ types.declare("MarkdownImageSource", |_| {
+ "export type MarkdownImageSource = { kind: 'workspace'; path: string } | { kind: 'remote'; uri: string };".into()
+ })
+ }
+
+ fn from_js(value: JsIn<'_>) -> Result<Self, PropError> {
+ Err(value.expected("a Markdown image source resolved by the native binding"))
+ }
+
+ fn to_js<'a>(&self, host: &'a dyn JsHost) -> Result<JsOwned<'a>, PropError> {
+ match self {
+ Self::Workspace { path } => serde_json::json!({"kind":"workspace","path":path}),
+ Self::Remote { uri } => serde_json::json!({"kind":"remote","uri":uri}),
+ }
+ .to_js(host)
+ }
+}
+
+pub(super) fn markdown_image(target: &str) -> Result<MarkdownImageSource, String> {
match destination(target)? {
- Destination::Workspace(path, parent) => Ok(WorkspaceLink {
+ Destination::Workspace(path, _) => Ok(MarkdownImageSource::Workspace {
path: path.bytes_base64,
- parent: parent.map(|path| path.bytes_base64),
}),
- Destination::Browser(_) => {
- Err("Network Markdown images are not supported; use a workspace-relative image.".into())
- }
+ Destination::Browser(url) => Ok(MarkdownImageSource::Remote {
+ uri: url.to_string(),
+ }),
}
}
@@ -168,13 +193,20 @@ mod tests {
panic!("expected browser URL")
};
assert_eq!(url.as_str(), "https://user:password@example.test/");
- assert!(workspace_image("https://example.test/image.png").is_err());
+ let MarkdownImageSource::Remote { uri } =
+ markdown_image("HTTPS://example.test/image 你好.png?q=中文#part").unwrap()
+ else {
+ panic!("expected native HTTP(S) image URI")
+ };
assert_eq!(
- STANDARD
- .decode(workspace_image("docs/image%20你好.png").unwrap().path)
- .unwrap(),
- "docs/image 你好.png".as_bytes()
+ uri,
+ "https://example.test/image%20%E4%BD%A0%E5%A5%BD.png?q=%E4%B8%AD%E6%96%87#part"
);
+ let MarkdownImageSource::Workspace { path } = markdown_image("docs/image%20你好.png").unwrap()
+ else {
+ panic!("expected original Host workspace image selector")
+ };
+ assert_eq!(STANDARD.decode(path).unwrap(), "docs/image 你好.png".as_bytes());
for target in ["a.txt#L4", "a.txt?download=1#L4"] {
let Destination::Workspace(path, _) = destination(target).unwrap() else {
panic!("expected file component")
@@ -199,6 +231,7 @@ mod tests {
"https://example.test/\n",
] {
assert!(destination(invalid).is_err(), "accepted {invalid:?}");
+ assert!(markdown_image(invalid).is_err(), "accepted image {invalid:?}");
}
}
}
diff --git a/products/maka/terminal-shell/src/entry.rs b/products/maka/terminal-shell/src/entry.rs
index 8c74ae8f8d..143827ed86 100644
--- a/products/maka/terminal-shell/src/entry.rs
+++ b/products/maka/terminal-shell/src/entry.rs
@@ -173,19 +173,27 @@ impl Args {
}
}
+/// The production bundle and its declared permissions, shared with native UI tests.
+pub fn application_bundle() -> Bundle {
+ let capabilities = nsh_core::access::Capabilities::from_json(&serde_json::json!({
+ "clipboard": true,
+ "network": {"http": [{"host":"*","methods":["GET"],"schemes":["http","https"]}]}
+ }))
+ .expect("static terminal application capabilities");
+ Bundle::source(APPLICATION.into())
+ .with_app_id(Some("org.apache.maka.terminal"))
+ .with_policy(Policy {
+ declared: Some(capabilities),
+ ..Default::default()
+ })
+}
+
fn configuration(
registry: nsh_core::ui::Registry,
runtime: tokio::runtime::Handle,
) -> EngineConfig {
let viewport = Size::new(Points(960.0), Points(640.0));
- let mut capabilities = nsh_core::access::Capabilities::default();
- capabilities.clipboard = true;
- let app = Bundle::source(APPLICATION.into())
- .with_app_id(Some("org.apache.maka.terminal"))
- .with_policy(Policy {
- declared: Some(capabilities),
- ..Default::default()
- });
+ let app = application_bundle();
EngineConfig {
viewport,
environment: Environment {
@@ -240,6 +248,24 @@ mod tests {
args: Args,
}
+ #[test]
+ fn application_bundle_grants_image_gets_without_other_http_methods_or_sockets() {
+ let access = application_bundle().policy.access();
+ for url in [
+ "http://127.0.0.1:1234/image.png",
+ "https://images.example.test/a.png",
+ ] {
+ assert!(access.allows_media(url));
+ assert!(access.allows_http("GET", url));
+ assert!(!access.allows_http("POST", url));
+ assert!(!access.allows_http("DELETE", url));
+ }
+ assert!(!access.allows_media("ftp://images.example.test/a.png"));
+ assert!(!access.allows_http("GET", "wss://images.example.test/a.png"));
+ assert!(!access.allows_socket("127.0.0.1", 1234));
+ assert!(access.declared.clipboard);
+ }
+
#[test]
fn optional_root_selects_the_default_host_and_preserves_explicit_paths() {
const CHILD: &str = "MAKA_TERMINAL_DEFAULT_ROOT_TEST";
diff --git a/products/maka/terminal-shell/tests/real_host/markdown_links.rs b/products/maka/terminal-shell/tests/real_host/markdown_links.rs
index 6c822b8032..c2f13f86fb 100644
--- a/products/maka/terminal-shell/tests/real_host/markdown_links.rs
+++ b/products/maka/terminal-shell/tests/real_host/markdown_links.rs
@@ -37,6 +37,8 @@ fn link_node(view: &AppView, label: &str) -> AccessibleNode {
async fn markdown_links_open_original_workspace_files_without_replacing_the_draft() {
use image::{ExtendedColorType, ImageEncoder, codecs::png};
use nsh_platform_tui::ServiceOutput;
+ use std::sync::{Arc, atomic::AtomicUsize};
+ use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::TcpListener, task::JoinSet};
tokio::time::timeout(Duration::from_secs(90), async {
let mut fixture = Fixture::open().await;
let client = fixture.client().await;
@@ -50,7 +52,39 @@ async fn markdown_links_open_original_workspace_files_without_replacing_the_draf
} else { [32, 220, 96, 255] }).collect();
let mut png_bytes = Vec::new();
png::PngEncoder::new(&mut png_bytes).write_image(&pixels, 64, 48, ExtendedColorType::Rgba8).unwrap();
- std::fs::write(directory.join("scene 你好.png"), png_bytes).unwrap();
+ std::fs::write(directory.join("scene 你好.png"), &png_bytes).unwrap();
+ // Serve the same real PNG to the native HTTP loader. The fixture's
+ // model provider and Host file reader remain unchanged.
+ let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
+ let remote_url = format!("http://{}/image.png", listener.local_addr().unwrap());
+ let missing_url = format!("http://{}/missing.png", listener.local_addr().unwrap());
+ let remote_reads = Arc::new(AtomicUsize::new(0));
+ let reads = remote_reads.clone();
+ // Dropping the task container also aborts the local server
+ // if an original workflow assertion or its outer timeout fails.
+ let mut image_server = JoinSet::<()>::new();
+ image_server.spawn(async move {
+ loop {
+ let (mut socket, _) = listener.accept().await.unwrap();
+ let mut header = Vec::new();
+ while !header.windows(4).any(|bytes| bytes == b"\r\n\r\n") {
+ let mut buffer = [0; 1024];
+ let count = socket.read(&mut buffer).await.unwrap();
+ if count == 0 { break; }
+ header.extend_from_slice(&buffer[..count]);
+ }
+ let requested = header.starts_with(b"GET /image.png HTTP/1.1\r\n");
+ let (status, body) = if requested {
+ reads.fetch_add(1, Ordering::SeqCst);
+ ("200 OK", png_bytes.as_slice())
+ } else {
+ ("404 Not Found", b"missing image".as_slice())
+ };
+ socket.write_all(format!("HTTP/1.1 {status}\r\nContent-Type: image/png\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", body.len()).as_bytes()).await.unwrap();
+ socket.write_all(body).await.unwrap();
+ socket.shutdown().await.unwrap();
+ }
+ });
let (mut view, bindings) = fixture.view(client.clone(), Some(fixture.session.clone()));
view.surface_mut().set_host_services_enabled(true);
wait_editable_composer(&mut view).await;
@@ -59,8 +93,9 @@ async fn markdown_links_open_original_workspace_files_without_replacing_the_draf
})).await;
assert_eq!(sent["kind"], "started");
let mut request = fixture.next_request().await;
- let source = "[**OPEN** *REPORT*](./docs/linked%20%E4%BD%A0%E5%A5%BD.txt#L2)\n\n[OPEN_REFERENCE][report]\n\n[UNSAFE_LINK](javascript:alert%281%29)\n\n[report]: ./docs/linked%20%E4%BD%A0%E5%A5%BD.txt\n\n前  后 [![Reference diagram][image]][report] \n\n[image]: ./docs/scene%20%E4%BD%A0%E5%A5%BD.png\n";
- request.delta(source).await;
+ let workspace_source = "[**OPEN** *REPORT*](./docs/linked%20%E4%BD%A0%E5%A5%BD.txt#L2)\n\n[OPEN_REFERENCE][report]\n\n[UNSAFE_LINK](javascript:alert%281%29)\n\n[report]: ./docs/linked%20%E4%BD%A0%E5%A5%BD.txt\n\n前  后 [![Reference diagram][image]][report] \n\n[image]: ./docs/scene%20%E4%BD%A0%E5%A5%BD.png\n";
+ let source = format!(" [][report] \n\n{workspace_source}");
+ request.delta(&source).await;
request.finish().await;
fixture.idle(&client, &fixture.session).await;
wait_enabled_node(&mut view, "OPEN REPORT").await;
@@ -138,6 +173,35 @@ async fn markdown_links_open_original_workspace_files_without_replacing_the_draf
wait_view(&mut view, |screen| screen.contains("REFRESHED_LINK_CONTENT")).await;
support::click_button(&mut view, "Close workspace preview").await;
wait_view_state(&mut view, |view, screen| screen.contains("UNSENT_LINK_DRAFT 你好") && !view.accessibility_snapshot().iter().any(|node| node.label == "Close workspace preview") && view.accessibility_snapshot().iter().any(|node| node.id == image_link.id && node.role == nsh_core::AccessibilityRole::Link && node.focused)).await;
+ // The network image uses the same loader/cache and full-screen
+ // viewer. Native Tab reveals its original inline button before click.
+ tab_to_label(&mut view, "View full image: Remote diagram 中文").await;
+ wait_view_state(&mut view, |view, screen| screen.contains('▀') && view.accessibility_snapshot().iter().any(|node| node.role == nsh_core::AccessibilityRole::Image && node.label == "Remote diagram 中文") && view.accessibility_snapshot().iter().any(|node| node.role == nsh_core::AccessibilityRole::Image && node.label == "Remote linked diagram" && node.frame.size.width.0 == 64.0 && node.frame.size.height.0 == 48.0)).await;
+ wait_view(&mut view, |screen| screen.contains("Unavailable remote") && screen.contains("HTTP 404")).await;
+ assert!(!view.accessibility_snapshot().iter().any(|node| node.role == nsh_core::AccessibilityRole::Button && node.label == "View full image: Remote linked diagram"), "a network linked image also has only its original Link target");
+ let remote = support::wait_enabled_button(&mut view, "View full image: Remote diagram 中文").await;
+ support::click_node(&mut view, remote.clone());
+ wait_view_state(&mut view, |view, screen| screen.contains('▀') && screen.contains("64 × 48") && view.accessibility_snapshot().iter().any(|node| node.label == "Full screen image viewer") && view.accessibility_snapshot().iter().any(|node| node.role == nsh_core::AccessibilityRole::Image && node.label == "Remote diagram 中文")).await;
+ support::click_button(&mut view, "Close full screen image").await;
+ wait_view_state(&mut view, |view, screen| screen.contains("UNSENT_LINK_DRAFT 你好") && !view.accessibility_snapshot().iter().any(|node| node.label == "Full screen image viewer") && view.surface().focused() == Some(remote.id)).await;
+ view.handle_event(Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)));
+ tab_to_label(&mut view, "View full image: Remote diagram 中文").await;
+ assert_eq!(view.surface().focused(), Some(remote.id));
+ view.handle_event(Event::Key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)));
+ wait_view_state(&mut view, |view, screen| screen.contains("64 × 48") && view.accessibility_snapshot().iter().any(|node| node.label == "Full screen image viewer")).await;
+ view.handle_event(Event::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)));
+ wait_view_state(&mut view, |view, _| !view.accessibility_snapshot().iter().any(|node| node.label == "Full screen image viewer") && view.surface().focused() == Some(remote.id)).await;
+ support::wait_draft(&client, &fixture.session, "UNSENT_LINK_DRAFT 你好").await;
+ assert_eq!(remote_reads.load(Ordering::SeqCst), 1, "opening the loaded URL in the viewer must reuse the native image cache");
+ tab_to_label(&mut view, "Remote linked diagram").await;
+ let remote_link = link_node(&view, "Remote linked diagram");
+ support::click_node(&mut view, remote_link.clone());
+ wait_view(&mut view, |screen| screen.contains("REFRESHED_LINK_CONTENT")).await;
+ assert!(!view.accessibility_snapshot().iter().any(|node| node.label == "Full screen image viewer"));
+ support::click_button(&mut view, "Close workspace preview").await;
+ wait_view_state(&mut view, |view, screen| screen.contains("UNSENT_LINK_DRAFT 你好") && !view.accessibility_snapshot().iter().any(|node| node.label == "Close workspace preview") && view.surface().focused() == Some(remote_link.id)).await;
+ support::wait_draft(&client, &fixture.session, "UNSENT_LINK_DRAFT 你好").await;
+ evidence("markdown-network-image-viewer-and-linked-image-return", &view.snapshot());
// Returning to the image restores that Link, not the message footer.
shortcut(&mut view, 'l');
wait_view_state(&mut view, |view, _| view.surface().focused() == Some(composer)).await;
@@ -169,6 +233,10 @@ async fn markdown_links_open_original_workspace_files_without_replacing_the_draf
assert_eq!(fixture.request_count.load(Ordering::SeqCst), 1, "opening links must not invoke a model");
drop(view);
bindings.close().await.unwrap();
+ image_server.abort_all();
+ while let Some(result) = image_server.join_next().await {
+ assert!(result.unwrap_err().is_cancelled(), "the case-local HTTP server failed");
+ }
fixture.finish().await;
}).await.expect("real Host Markdown link workflow timed out");
}
diff --git a/products/maka/terminal-shell/tests/support/mod.rs b/products/maka/terminal-shell/tests/support/mod.rs
index 5e804ff4b2..a8c8a53736 100644
--- a/products/maka/terminal-shell/tests/support/mod.rs
+++ b/products/maka/terminal-shell/tests/support/mod.rs
@@ -37,7 +37,6 @@ use maka_plugin_core::{
operation::{OperationOutcome, Receipt, receipt_reader_contract},
runtime::Published,
};
-use nsh_engine::Bundle;
use nsh_platform_tui::{AppOptions, AppView};
use serde_json::{Value, json};
use std::{
@@ -67,7 +66,7 @@ fn frame_time() -> f64 {
}
pub fn load_application(view: &mut AppView) {
- view.load(Bundle::source(maka_terminal_shell::APPLICATION.into()).into())
+ view.load(maka_terminal_shell::entry::application_bundle().into())
.unwrap();
// load resets Surface; seed its clock before poll can apply an animation.
view.surface_mut().tick(frame_time());
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org