feat(plugin): render Markdown content with host UI (#7579)

* feat(plugin): expose Markdown content component

* docs(plugin): translate Markdown component reference
This commit is contained in:
Mohamed Boudra
2026-10-21 19:14:17 +07:00
committed by GitHub
parent 1d383519dd
commit 995ad2f878
10 changed files with 76 additions and 4 deletions
@@ -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 <View>
<Text>Plugin modal contexts ready</Text>
<Markdown>{"## Plugin report"}</Markdown>
<Markdown>{"Checks **passed**. [Documentation](https://paseo.sh/docs/plugins)"}</Markdown>
<Markdown>{""}</Markdown>
<Pressable accessibilityRole="button" onPress={save}>
<Text>Save issue</Text>
</Pressable>
@@ -92,10 +95,20 @@ async function openPluginModal(page: Page): Promise<void> {
await page.getByRole("button", { name: "Open plugin modal", exact: true }).click();
}
async function expectPluginMarkdown(page: Page): Promise<void> {
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<void> {
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<void> {
@@ -107,6 +120,7 @@ async function closeCenteredPluginDialog(page: Page): Promise<void> {
async function expectCompactPluginSheet(page: Page): Promise<void> {
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<void> {
+2 -1
View File
@@ -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"]) {
@@ -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 ? <MarkdownRenderer text={children} /> : null;
}
@@ -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";
@@ -1,6 +1,9 @@
import { lightHighlightColors } from "@getpaseo/highlight";
const testTheme = {
colorScheme: "light",
colors: {
syntax: lightHighlightColors,
foreground: "#111111",
foregroundMuted: "#666666",
statusSuccess: "#15803d",
+1
View File
@@ -104,6 +104,7 @@ export default defineConfig({
deps: {
fallbackCJS: true,
inline: [
"react-native-enriched-markdown",
"zustand",
"@tanstack/react-query",
"react-native-web",
+7
View File
@@ -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<MarkdownProps>;
+2 -2
View File
@@ -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 (
<View style={styles.screen}>
<Text style={styles.title}>Workspace plugin panel</Text>
<Markdown>{"## Workspace plugin panel\n\nCall a **daemon RPC** from this panel."}</Markdown>
<ExternalLink href="https://paseo.sh/docs/plugins">Plugin documentation</ExternalLink>
{openBrowser ? (
<Pressable accessibilityRole="button" onPress={handleOpenBrowser} style={styles.button}>
+20
View File
@@ -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";
<Markdown>{"## Review\n\nThe checks **passed**."}</Markdown>;
```
`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
+18
View File
@@ -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";
<Markdown>{"## 审查\n\n检查**通过**。"}</Markdown>;
```
`children` 是唯一的属性,必须为字符串。更新它会立即渲染新内容;空字符串不渲染任何内容。
宿主提供主题、文本选择、代码块、表格、图表和链接处理。
当所在界面提供工作区上下文时,工作区文件链接和图片会使用该上下文。
组件不添加页面内边距或滚动容器。需要时请在外层使用布局或滚动视图。
内容获取以及加载和错误状态由插件负责。
### Modal {#modal}
`Modal` 在紧凑布局中使用底部面板,否则使用居中的对话框。`open` 状态由插件负责。