From 995ad2f8787c284bc198be24c15bea98f87bab4a Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 21 Oct 2026 19:14:17 +0700 Subject: [PATCH] feat(plugin): render Markdown content with host UI (#7579) * feat(plugin): expose Markdown content component * docs(plugin): translate Markdown component reference --- .../app/e2e/browser/plugin-host-ui.spec.ts | 16 ++++++++++++++- packages/app/src/plugins/evaluate.test.ts | 3 ++- .../app/src/plugins/react-native/markdown.tsx | 6 ++++++ packages/app/src/plugins/react-native/ui.ts | 2 ++ .../app/test-stubs/react-native-unistyles.ts | 3 +++ packages/app/vitest.config.ts | 1 + packages/plugin/src/client/ui.ts | 7 +++++++ plugin-examples/local-plugin/client/main.tsx | 4 ++-- public-docs/plugins/reference.md | 20 +++++++++++++++++++ public-docs/plugins/reference.zh-CN.md | 18 +++++++++++++++++ 10 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 packages/app/src/plugins/react-native/markdown.tsx diff --git a/packages/app/e2e/browser/plugin-host-ui.spec.ts b/packages/app/e2e/browser/plugin-host-ui.spec.ts index d8fd175b5f..a195f30aa2 100644 --- a/packages/app/e2e/browser/plugin-host-ui.spec.ts +++ b/packages/app/e2e/browser/plugin-host-ui.spec.ts @@ -10,7 +10,7 @@ import { connectNewWorkspaceDaemonClient } from "../support/helpers/new-workspac const PLUGIN_ID = "plugin-host-ui-e2e"; const PLUGIN_SOURCE = `import { usePaseo } from "@getpaseo/plugin/client"; -import { Icon, Modal, useToast } from "@getpaseo/plugin/client/ui"; +import { Icon, Modal, useToast, Markdown } from "@getpaseo/plugin/client/ui"; import { useQueryClient } from "@tanstack/react-query"; import React, { useState } from "react"; import { Pressable, Text, View } from "react-native"; @@ -27,6 +27,9 @@ function ModalBody({ onSaved }) { return Plugin modal contexts ready + {"## Plugin report"} + {"Checks **passed**. [Documentation](https://paseo.sh/docs/plugins)"} + {""} Save issue @@ -92,10 +95,20 @@ async function openPluginModal(page: Page): Promise { await page.getByRole("button", { name: "Open plugin modal", exact: true }).click(); } +async function expectPluginMarkdown(page: Page): Promise { + await expect(page.getByRole("heading", { name: "Plugin report" })).toBeVisible(); + await expect(page.getByText("Checks passed.", { exact: false })).toBeVisible(); + await expect(page.getByRole("link", { name: "Documentation", exact: true })).toHaveAttribute( + "href", + "https://paseo.sh/docs/plugins", + ); +} + async function expectCenteredPluginDialog(page: Page): Promise { const dialog = page.getByRole("dialog"); await expect(dialog.getByText("Edit plugin issue", { exact: true })).toBeVisible(); await expect(dialog.getByText("Plugin modal contexts ready", { exact: true })).toBeVisible(); + await expectPluginMarkdown(page); } async function closeCenteredPluginDialog(page: Page): Promise { @@ -107,6 +120,7 @@ async function closeCenteredPluginDialog(page: Page): Promise { async function expectCompactPluginSheet(page: Page): Promise { await expect(page.getByText("Edit plugin issue", { exact: true })).toBeVisible(); await expect(page.getByText("Plugin modal contexts ready", { exact: true })).toBeVisible(); + await expectPluginMarkdown(page); } async function savePluginIssue(page: Page): Promise { diff --git a/packages/app/src/plugins/evaluate.test.ts b/packages/app/src/plugins/evaluate.test.ts index 9d5ed28349..b4062b2376 100644 --- a/packages/app/src/plugins/evaluate.test.ts +++ b/packages/app/src/plugins/evaluate.test.ts @@ -695,7 +695,8 @@ describe("evaluatePluginClientBundle", () => { `(function(require) { const shared = require("@getpaseo/plugin"); const client = require("@getpaseo/plugin/client"); - const { ExternalLink, Loading } = require("@getpaseo/plugin/client/ui"); + const { ExternalLink, Loading, Markdown } = require("@getpaseo/plugin/client/ui"); + if (typeof Markdown !== "function") throw new Error("Markdown"); if (typeof ExternalLink !== "function") throw new Error("ExternalLink"); if (typeof Loading !== "function") throw new Error("Loading"); for (const name of ["usePaseo", "useRpc", "useSettings", "useAgent", "useWorkspace", "openExternalUrl", "useFileText", "useFileBytes", "useFileUrl", "PluginFileConflictError", "PluginFileDeletedError"]) { diff --git a/packages/app/src/plugins/react-native/markdown.tsx b/packages/app/src/plugins/react-native/markdown.tsx new file mode 100644 index 0000000000..109409b0f6 --- /dev/null +++ b/packages/app/src/plugins/react-native/markdown.tsx @@ -0,0 +1,6 @@ +import type { MarkdownProps } from "@getpaseo/plugin/client/ui"; +import { MarkdownRenderer } from "@/components/markdown/renderer"; + +export function Markdown({ children }: MarkdownProps) { + return children ? : null; +} diff --git a/packages/app/src/plugins/react-native/ui.ts b/packages/app/src/plugins/react-native/ui.ts index a33add1d4f..e431aa32eb 100644 --- a/packages/app/src/plugins/react-native/ui.ts +++ b/packages/app/src/plugins/react-native/ui.ts @@ -26,3 +26,5 @@ export { TextInput } from "./text-input"; export { copyText } from "./clipboard"; export { useToast } from "./toast"; export { StreamedText } from "./streamed-text"; + +export { Markdown } from "./markdown"; diff --git a/packages/app/test-stubs/react-native-unistyles.ts b/packages/app/test-stubs/react-native-unistyles.ts index 654053543f..adc8af93bf 100644 --- a/packages/app/test-stubs/react-native-unistyles.ts +++ b/packages/app/test-stubs/react-native-unistyles.ts @@ -1,6 +1,9 @@ +import { lightHighlightColors } from "@getpaseo/highlight"; + const testTheme = { colorScheme: "light", colors: { + syntax: lightHighlightColors, foreground: "#111111", foregroundMuted: "#666666", statusSuccess: "#15803d", diff --git a/packages/app/vitest.config.ts b/packages/app/vitest.config.ts index 35272f336e..fd12cd6189 100644 --- a/packages/app/vitest.config.ts +++ b/packages/app/vitest.config.ts @@ -104,6 +104,7 @@ export default defineConfig({ deps: { fallbackCJS: true, inline: [ + "react-native-enriched-markdown", "zustand", "@tanstack/react-query", "react-native-web", diff --git a/packages/plugin/src/client/ui.ts b/packages/plugin/src/client/ui.ts index 7bd8d6884b..c012753181 100644 --- a/packages/plugin/src/client/ui.ts +++ b/packages/plugin/src/client/ui.ts @@ -178,3 +178,10 @@ export declare const Loading: ComponentType; /** Render a local document URL in an isolated frame. */ export declare const DocumentFrame: ComponentType<{ url: string; testID?: string }>; + +export interface MarkdownProps { + children: string; +} + +/** Renders Markdown using Paseo’s theme, selection, and link behavior. */ +export declare const Markdown: ComponentType; diff --git a/plugin-examples/local-plugin/client/main.tsx b/plugin-examples/local-plugin/client/main.tsx index 749012e0a9..b9fb3dc63f 100644 --- a/plugin-examples/local-plugin/client/main.tsx +++ b/plugin-examples/local-plugin/client/main.tsx @@ -8,7 +8,7 @@ import { } from "@getpaseo/plugin/client"; import { useCallback, useMemo } from "react"; import { Pressable, Text, View } from "react-native"; -import { ExternalLink } from "@getpaseo/plugin/client/ui"; +import { ExternalLink, Markdown } from "@getpaseo/plugin/client/ui"; import { incrementRpc } from "../shared/increment"; export function contributeClient(client: PluginClientContext) { @@ -107,7 +107,7 @@ export function ExamplePanel({ return ( - Workspace plugin panel + {"## Workspace plugin panel\n\nCall a **daemon RPC** from this panel."} Plugin documentation {openBrowser ? ( diff --git a/public-docs/plugins/reference.md b/public-docs/plugins/reference.md index 16fde6470b..3729fa4e0e 100644 --- a/public-docs/plugins/reference.md +++ b/public-docs/plugins/reference.md @@ -1220,6 +1220,26 @@ export function IssueActions({ theme }: PluginSurfaceProps) { } ``` +### Markdown + +Import `Markdown` from `@getpaseo/plugin/client/ui` to render a string with Paseo's +Markdown presentation on web, iOS, and Android: + +```tsx +import { Markdown } from "@getpaseo/plugin/client/ui"; + +{"## Review\n\nThe checks **passed**."}; +``` + +`children` is the only prop and must be a string. Updating it renders the new content +immediately; an empty string renders nothing. The host supplies theme, text selection, +code blocks, tables, diagrams, and link handling. Workspace file links and images use +workspace context when the containing surface supplies it. + +The component adds no page padding or scroll container. Use a surrounding layout or +scroll view when needed. Fetching content and showing loading or error states belong +to your plugin. + ### Modal `Modal` uses a bottom sheet on compact layouts and a centered dialog otherwise. The plugin owns diff --git a/public-docs/plugins/reference.zh-CN.md b/public-docs/plugins/reference.zh-CN.md index a70d91e7bc..fb5d1c9f70 100644 --- a/public-docs/plugins/reference.zh-CN.md +++ b/public-docs/plugins/reference.zh-CN.md @@ -1061,6 +1061,24 @@ export function IssueActions({ theme }: PluginSurfaceProps) { } ``` +### Markdown {#markdown} + +从 `@getpaseo/plugin/client/ui` 导入 `Markdown`,即可在网页、iOS 和 Android 上 +使用 Paseo 的 Markdown 样式渲染字符串: + +```tsx +import { Markdown } from "@getpaseo/plugin/client/ui"; + +{"## 审查\n\n检查**通过**。"}; +``` + +`children` 是唯一的属性,必须为字符串。更新它会立即渲染新内容;空字符串不渲染任何内容。 +宿主提供主题、文本选择、代码块、表格、图表和链接处理。 +当所在界面提供工作区上下文时,工作区文件链接和图片会使用该上下文。 + +组件不添加页面内边距或滚动容器。需要时请在外层使用布局或滚动视图。 +内容获取以及加载和错误状态由插件负责。 + ### Modal {#modal} `Modal` 在紧凑布局中使用底部面板,否则使用居中的对话框。`open` 状态由插件负责。