paseo-form-render 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +115 -2
- package/client/container.tsx +289 -0
- package/client/host-markdown.tsx +24 -0
- package/client/markdown.tsx +470 -0
- package/client/messages.ts +147 -0
- package/client/project-file-modal.tsx +690 -0
- package/client/react-native-dataset.d.ts +10 -0
- package/client/renderer.tsx +1056 -0
- package/client/rpc-bridge.tsx +27 -0
- package/client/settings-screen.tsx +163 -0
- package/index.client.tsx +103 -0
- package/index.server.ts +38 -0
- package/package.json +50 -4
- package/paseo-plugin.json +8 -0
- package/server/auto-fix.ts +217 -0
- package/server/demo.ts +20 -0
- package/server/file-picker.ts +253 -0
- package/server/settings.ts +26 -0
- package/shared/demo.ts +96 -0
- package/shared/form-errors.ts +390 -0
- package/shared/form-fix.ts +77 -0
- package/shared/form-helpers.ts +214 -0
- package/shared/parser.ts +119 -0
- package/shared/pickers.ts +50 -0
- package/shared/prompt.ts +96 -0
- package/shared/schema.ts +330 -0
- package/shared/settings.ts +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 quickwind
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,116 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Paseo Form Render
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Interactive forms, dashboards, and metrics rendering plugin for Paseo conversations.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Interactive Forms in Chat**: Renders structured forms directly in the timeline from ````pfr-form```` code fences.
|
|
8
|
+
- **Rich Component Catalog**:
|
|
9
|
+
- `Card`: Container with title, description, and child items
|
|
10
|
+
- `Metric`: Key-value statistics display
|
|
11
|
+
- `Input`: Text input with placeholders and required validation
|
|
12
|
+
- `Select`: Dropdown selection menus with customizable options
|
|
13
|
+
- `Toggle`: Boolean switches
|
|
14
|
+
- `Button`: Submit and action buttons with custom payloads
|
|
15
|
+
- **Provider Agnostic**:
|
|
16
|
+
- Fully compatible with **Devin**, **Claude**, **Antigravity**, **Gemini**, and OpenAI providers.
|
|
17
|
+
- Automatically injects protocol instructions on agent initialization.
|
|
18
|
+
- **Localized Interface**:
|
|
19
|
+
- Labels, buttons, and the picker and settings screens follow the Paseo app language (English and 简体中文). Form content and the error details are not translated.
|
|
20
|
+
- **Full Mobile & Hermes Compatibility**:
|
|
21
|
+
- 100% pure React Native UI primitives (`View`, `Text`, `TextInput`, `Pressable`, `Switch`).
|
|
22
|
+
- Zero external runtime dependencies in the client bundle.
|
|
23
|
+
- Rigorously tested against the React Native Hermes JavaScript engine on iOS and Android.
|
|
24
|
+
- **Self-healing Forms**:
|
|
25
|
+
- Invalid `pfr-form` blocks show a precise error list (offending node, missing fields, allowed values, "did you mean" hints, JSON line/column).
|
|
26
|
+
- The agent is automatically asked to re-send a corrected block, up to a configurable number of times in a row.
|
|
27
|
+
- An **Ask agent to fix** button on the error box requests a fix manually (ignores the retry limit).
|
|
28
|
+
- Setting: **Paseo Form Render > Maximum retries** in the plugin settings (0-5, default 2; 0 turns automatic fixes off).
|
|
29
|
+
- Known limitations:
|
|
30
|
+
- A message sent within a few milliseconds of the agent finishing (by a script, another plugin, or an orchestrating agent) can race the automatic fix request. Paseo's `send` interrupts a running turn and has no "send only if idle" option, so in that race the fix may cancel the other message's turn. The plugin re-checks that the agent is idle right before sending, which narrows the window; messages typed by a person are not affected in practice.
|
|
31
|
+
- Not used yet: sending the fix with `activeTurnBehavior: "steer"` would add it to a running turn instead of cancelling that turn, but the agent then handles an unrelated fix request in the middle of another reply, and providers without steering support (ACP-based ones such as Copilot, Cursor, Kimi) fall back to interrupting anyway.
|
|
32
|
+
- Each assistant message is checked on its own, as the client renders it. A provider that splits one `pfr-form` fence across several messages gets a (misleading) JSON error and a fix request.
|
|
33
|
+
- **Prefer forms** (on by default): the **Paseo Form Render > Prefer forms** plugin setting adds a "Required usage" section to the injected prompt, so agents must ask for choices, parameters, and confirmations with an `pfr-form` block instead of plain text. Turn it off to keep the weaker default wording, which only says agents can use forms. Applies to agents created after the change.
|
|
34
|
+
- **Non-blocking `/form-demo`**:
|
|
35
|
+
- Pushes sample forms directly via daemon RPC without triggering model thinking turns or consuming API quota.
|
|
36
|
+
|
|
37
|
+
## Slash Commands
|
|
38
|
+
|
|
39
|
+
- `/form [task]`: Instruct the active agent to adopt the `pfr-form` protocol for interactive questionnaires or parameter collection.
|
|
40
|
+
- `/form-demo`: Display a demonstration interactive configuration form in the conversation timeline without calling the model.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
### From npm
|
|
45
|
+
```bash
|
|
46
|
+
paseo plugin add npm:paseo-form-render
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That is the whole install. Paseo fetches the package and compiles the TypeScript sources with its
|
|
50
|
+
own bundler, so the plugin ships no build artifacts and has no runtime dependencies.
|
|
51
|
+
|
|
52
|
+
### From Local Directory
|
|
53
|
+
```bash
|
|
54
|
+
paseo plugin add /path/to/paseo-form-render
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### From Offline Package
|
|
58
|
+
Unpack `paseo-form-render-<version>-offline.zip` and run:
|
|
59
|
+
```bash
|
|
60
|
+
paseo plugin add /path/to/extracted-folder
|
|
61
|
+
```
|
|
62
|
+
Or register in `~/.paseo/config.json`:
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"plugins": {
|
|
66
|
+
"paseo-form-render": {
|
|
67
|
+
"source": "directory",
|
|
68
|
+
"path": "/path/to/paseo-form-render",
|
|
69
|
+
"enabled": true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Development & Packaging
|
|
76
|
+
|
|
77
|
+
The npm package is the primary release channel: the tarball ships the TypeScript sources plus
|
|
78
|
+
`paseo-plugin.json` (the negated `files` pattern keeps the `*.test.ts` files out), and Paseo compiles
|
|
79
|
+
them itself. Releases run from the **Publish** workflow by hand (Actions tab → Publish → Run workflow,
|
|
80
|
+
or `gh workflow run Publish -f version=0.1.1`): it checks the version against `package.json`, re-runs
|
|
81
|
+
typecheck and the unit tests, publishes with the `NPM_TOKEN` secret, then pushes the `v<version>` tag
|
|
82
|
+
and opens the GitHub release. Pass `dry_run` to verify without publishing.
|
|
83
|
+
|
|
84
|
+
Nothing runs automatically — this repository is private, so Actions minutes are billed. CI (unit +
|
|
85
|
+
e2e on three platforms) and the Snyk scan are manual too.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Typecheck and run test suites
|
|
89
|
+
npm run typecheck
|
|
90
|
+
npm test
|
|
91
|
+
|
|
92
|
+
# Inspect exactly what the npm tarball would contain
|
|
93
|
+
npm pack --dry-run
|
|
94
|
+
|
|
95
|
+
# Build offline archive (outputs to ./dist by default)
|
|
96
|
+
npm run pack:offline
|
|
97
|
+
|
|
98
|
+
# Or specify a custom output directory
|
|
99
|
+
npm run pack:offline -- /path/to/dist
|
|
100
|
+
# or: DIST_DIR=/path/to/dist npm run pack:offline
|
|
101
|
+
|
|
102
|
+
# End-to-end tests against a real Paseo daemon (Linux, macOS, Windows; Node 22+)
|
|
103
|
+
npm ci --prefix e2e
|
|
104
|
+
npx --prefix e2e playwright install chromium # once, for the browser UI checks
|
|
105
|
+
npm run e2e # or: node e2e/run.mjs --all --repeat 2 (--no-ui: skip the browser)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The e2e harness lives in `e2e/` with its own dependencies and is not part of the plugin package; see
|
|
109
|
+
[`e2e/README.md`](e2e/README.md). Besides the auto-fix protocol it checks the client UI (rendered
|
|
110
|
+
forms, the error box and its "Ask agent to fix" button, the settings screen) in the daemon's web UI
|
|
111
|
+
with headless Chromium and saves screenshots. CI (`.github/workflows/ci.yml`) runs the unit tests and
|
|
112
|
+
the e2e suite on Linux, macOS and Windows.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { PluginHostProps } from "@getpaseo/plugin/client";
|
|
2
|
+
import { useTranslation } from "@getpaseo/plugin/client";
|
|
3
|
+
import React, { useMemo, useState } from "react";
|
|
4
|
+
import { View, Text, StyleSheet, Pressable, Platform } from "react-native";
|
|
5
|
+
import { formatIssues } from "../shared/form-errors";
|
|
6
|
+
import { buildFixMessage } from "../shared/form-fix";
|
|
7
|
+
import { parseFormBlock, parseMessageContent, type ParsedBlock } from "../shared/parser";
|
|
8
|
+
import { messages } from "./messages";
|
|
9
|
+
import { PfrFormView } from "./renderer";
|
|
10
|
+
import { HostMarkdown } from "./host-markdown";
|
|
11
|
+
import { PfrRpcProvider, type PfrRpcFn } from "./rpc-bridge";
|
|
12
|
+
|
|
13
|
+
export interface MessageContainerProps {
|
|
14
|
+
text: string;
|
|
15
|
+
theme: PluginHostProps["theme"];
|
|
16
|
+
agentId?: string;
|
|
17
|
+
rpc?: PfrRpcFn;
|
|
18
|
+
onSubmit: (formattedMessage: string) => Promise<void> | void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface FormBlockViewProps {
|
|
22
|
+
rawJson: string;
|
|
23
|
+
blockNumber: number;
|
|
24
|
+
theme: PluginHostProps["theme"];
|
|
25
|
+
agentId?: string;
|
|
26
|
+
rpc?: PfrRpcFn;
|
|
27
|
+
onSubmit: (formattedMessage: string) => Promise<void> | void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface FormErrorBoxProps {
|
|
31
|
+
block: ParsedBlock;
|
|
32
|
+
blockNumber: number;
|
|
33
|
+
theme: PluginHostProps["theme"];
|
|
34
|
+
onSubmit: MessageContainerProps["onSubmit"];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface BlockErrorBoundaryProps {
|
|
38
|
+
fallbackText?: string;
|
|
39
|
+
children: React.ReactNode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface BlockErrorBoundaryState {
|
|
43
|
+
hasError: boolean;
|
|
44
|
+
errorMessage: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ES5 class component for Hermes compatibility (Hermes throws syntax error on ES6 `class` keyword).
|
|
48
|
+
const BlockErrorBoundary: React.ComponentClass<BlockErrorBoundaryProps, BlockErrorBoundaryState> = function (
|
|
49
|
+
this: any,
|
|
50
|
+
props: BlockErrorBoundaryProps
|
|
51
|
+
) {
|
|
52
|
+
(React.Component as any).call(this, props);
|
|
53
|
+
this.state = { hasError: false, errorMessage: "" };
|
|
54
|
+
} as any;
|
|
55
|
+
BlockErrorBoundary.prototype = Object.create(React.Component.prototype);
|
|
56
|
+
BlockErrorBoundary.prototype.constructor = BlockErrorBoundary;
|
|
57
|
+
(BlockErrorBoundary as any).getDerivedStateFromError = function (error: any): BlockErrorBoundaryState {
|
|
58
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
59
|
+
return { hasError: true, errorMessage: msg };
|
|
60
|
+
};
|
|
61
|
+
BlockErrorBoundary.prototype.componentDidCatch = function (error: any, info: any) {
|
|
62
|
+
console.warn("[paseo-form-render] Block render failed:", error, info?.componentStack);
|
|
63
|
+
};
|
|
64
|
+
BlockErrorBoundary.prototype.render = function (this: any) {
|
|
65
|
+
if (this.state.hasError) {
|
|
66
|
+
return (
|
|
67
|
+
<View style={[styles.errorBox, { borderColor: "#ef4444", backgroundColor: "#fee2e2" }]}>
|
|
68
|
+
<Text style={[styles.errorTitle, { color: "#dc2626" }]}>
|
|
69
|
+
[UI Render Error]
|
|
70
|
+
</Text>
|
|
71
|
+
<Text style={[styles.errorDetail, { color: "#991b1b" }]}>
|
|
72
|
+
{this.state.errorMessage}
|
|
73
|
+
</Text>
|
|
74
|
+
{this.props.fallbackText ? (
|
|
75
|
+
<View style={styles.rawJsonBox}>
|
|
76
|
+
<Text style={[styles.rawJsonText, { color: "#1f2937" }]}>
|
|
77
|
+
{this.props.fallbackText}
|
|
78
|
+
</Text>
|
|
79
|
+
</View>
|
|
80
|
+
) : null}
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return this.props.children;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const FormErrorBox: React.FC<FormErrorBoxProps> = ({ block, blockNumber, theme, onSubmit }) => {
|
|
88
|
+
const { t } = useTranslation(messages);
|
|
89
|
+
const [status, setStatus] = useState<"idle" | "sending" | "sent">("idle");
|
|
90
|
+
const errors = block.errors ?? [];
|
|
91
|
+
|
|
92
|
+
const askAgentToFix = async () => {
|
|
93
|
+
setStatus("sending");
|
|
94
|
+
try {
|
|
95
|
+
await onSubmit(buildFixMessage([{ issues: errors, blockNumber }]));
|
|
96
|
+
setStatus("sent");
|
|
97
|
+
} catch (e) {
|
|
98
|
+
setStatus("idle");
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<View
|
|
104
|
+
style={[
|
|
105
|
+
styles.errorBox,
|
|
106
|
+
{
|
|
107
|
+
backgroundColor: theme.colors.surface2,
|
|
108
|
+
borderColor: theme.colors.statusDanger,
|
|
109
|
+
},
|
|
110
|
+
]}
|
|
111
|
+
>
|
|
112
|
+
<Text style={[styles.errorTitle, { color: theme.colors.statusDanger }]}>
|
|
113
|
+
{t("errorBox.title")}
|
|
114
|
+
</Text>
|
|
115
|
+
{formatIssues(errors).map((line, i) => (
|
|
116
|
+
<Text key={i} style={[styles.errorDetail, { color: theme.colors.statusDanger }]}>
|
|
117
|
+
{line}
|
|
118
|
+
</Text>
|
|
119
|
+
))}
|
|
120
|
+
<View
|
|
121
|
+
style={[
|
|
122
|
+
styles.rawJsonBox,
|
|
123
|
+
{
|
|
124
|
+
backgroundColor: theme.colors.surface1,
|
|
125
|
+
},
|
|
126
|
+
]}
|
|
127
|
+
>
|
|
128
|
+
<Text
|
|
129
|
+
style={[
|
|
130
|
+
styles.rawJsonText,
|
|
131
|
+
{
|
|
132
|
+
color: theme.colors.foreground,
|
|
133
|
+
},
|
|
134
|
+
]}
|
|
135
|
+
>
|
|
136
|
+
{block.rawJson}
|
|
137
|
+
</Text>
|
|
138
|
+
</View>
|
|
139
|
+
{status === "sent" ? (
|
|
140
|
+
<Text style={[styles.sentText, { color: theme.colors.statusSuccess }]}>
|
|
141
|
+
{t("errorBox.fixRequested")}
|
|
142
|
+
</Text>
|
|
143
|
+
) : (
|
|
144
|
+
<Pressable
|
|
145
|
+
disabled={status !== "idle"}
|
|
146
|
+
style={[
|
|
147
|
+
styles.fixButton,
|
|
148
|
+
{ backgroundColor: theme.colors.accent },
|
|
149
|
+
status !== "idle" && { opacity: 0.5 },
|
|
150
|
+
]}
|
|
151
|
+
onPress={askAgentToFix}
|
|
152
|
+
>
|
|
153
|
+
<Text style={[styles.fixButtonText, { color: theme.colors.accentForeground }]}>
|
|
154
|
+
{status === "sending" ? t("errorBox.sending") : t("errorBox.askFix")}
|
|
155
|
+
</Text>
|
|
156
|
+
</Pressable>
|
|
157
|
+
)}
|
|
158
|
+
</View>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const FormBlockView: React.FC<FormBlockViewProps> = ({
|
|
163
|
+
rawJson,
|
|
164
|
+
blockNumber,
|
|
165
|
+
theme,
|
|
166
|
+
agentId,
|
|
167
|
+
rpc,
|
|
168
|
+
onSubmit,
|
|
169
|
+
}) => {
|
|
170
|
+
const block = useMemo(() => parseFormBlock(rawJson), [rawJson]);
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<PfrRpcProvider rpc={rpc}>
|
|
174
|
+
{block.error ? (
|
|
175
|
+
<FormErrorBox
|
|
176
|
+
block={block}
|
|
177
|
+
blockNumber={blockNumber}
|
|
178
|
+
theme={theme}
|
|
179
|
+
onSubmit={onSubmit}
|
|
180
|
+
/>
|
|
181
|
+
) : block.form ? (
|
|
182
|
+
<PfrFormView
|
|
183
|
+
form={block.form}
|
|
184
|
+
theme={theme}
|
|
185
|
+
agentId={agentId}
|
|
186
|
+
onSubmit={onSubmit}
|
|
187
|
+
/>
|
|
188
|
+
) : null}
|
|
189
|
+
</PfrRpcProvider>
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const MessageContainer: React.FC<MessageContainerProps> = ({ text, theme, agentId, rpc, onSubmit }) => {
|
|
194
|
+
const blocks = parseMessageContent(text);
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<PfrRpcProvider rpc={rpc}>
|
|
198
|
+
<View style={styles.container} testID="assistant-message">
|
|
199
|
+
{blocks.map((block, idx) => {
|
|
200
|
+
if (block.type === "text" && block.content) {
|
|
201
|
+
return (
|
|
202
|
+
<BlockErrorBoundary key={idx} fallbackText={block.content}>
|
|
203
|
+
<HostMarkdown
|
|
204
|
+
text={block.content}
|
|
205
|
+
theme={theme}
|
|
206
|
+
/>
|
|
207
|
+
</BlockErrorBoundary>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (block.type === "form") {
|
|
212
|
+
if (block.error) {
|
|
213
|
+
return (
|
|
214
|
+
<BlockErrorBoundary key={idx} fallbackText={block.rawJson ?? ""}>
|
|
215
|
+
<FormErrorBox
|
|
216
|
+
block={block}
|
|
217
|
+
blockNumber={blocks.slice(0, idx).filter((b) => b.type === "form").length + 1}
|
|
218
|
+
theme={theme}
|
|
219
|
+
onSubmit={onSubmit}
|
|
220
|
+
/>
|
|
221
|
+
</BlockErrorBoundary>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (block.form) {
|
|
226
|
+
return (
|
|
227
|
+
<BlockErrorBoundary key={idx} fallbackText={block.rawJson ?? ""}>
|
|
228
|
+
<PfrFormView
|
|
229
|
+
form={block.form}
|
|
230
|
+
theme={theme}
|
|
231
|
+
agentId={agentId}
|
|
232
|
+
onSubmit={onSubmit}
|
|
233
|
+
/>
|
|
234
|
+
</BlockErrorBoundary>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return null;
|
|
240
|
+
})}
|
|
241
|
+
</View>
|
|
242
|
+
</PfrRpcProvider>
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const styles = StyleSheet.create({
|
|
247
|
+
container: {
|
|
248
|
+
gap: 8,
|
|
249
|
+
width: "100%",
|
|
250
|
+
...(Platform.OS === "web" ? { userSelect: "text" as const } : {}),
|
|
251
|
+
},
|
|
252
|
+
errorBox: {
|
|
253
|
+
borderWidth: 1,
|
|
254
|
+
borderRadius: 6,
|
|
255
|
+
padding: 12,
|
|
256
|
+
gap: 6,
|
|
257
|
+
marginVertical: 4,
|
|
258
|
+
},
|
|
259
|
+
errorTitle: {
|
|
260
|
+
fontSize: 13,
|
|
261
|
+
fontWeight: "600",
|
|
262
|
+
},
|
|
263
|
+
errorDetail: {
|
|
264
|
+
fontSize: 12,
|
|
265
|
+
},
|
|
266
|
+
fixButton: {
|
|
267
|
+
borderRadius: 6,
|
|
268
|
+
paddingHorizontal: 16,
|
|
269
|
+
paddingVertical: 8,
|
|
270
|
+
alignItems: "center",
|
|
271
|
+
justifyContent: "center",
|
|
272
|
+
},
|
|
273
|
+
fixButtonText: {
|
|
274
|
+
fontSize: 13,
|
|
275
|
+
fontWeight: "600",
|
|
276
|
+
},
|
|
277
|
+
sentText: {
|
|
278
|
+
fontSize: 12,
|
|
279
|
+
fontWeight: "600",
|
|
280
|
+
},
|
|
281
|
+
rawJsonBox: {
|
|
282
|
+
padding: 8,
|
|
283
|
+
borderRadius: 4,
|
|
284
|
+
},
|
|
285
|
+
rawJsonText: {
|
|
286
|
+
fontFamily: Platform.select({ ios: "Menlo", default: "monospace" }),
|
|
287
|
+
fontSize: 11,
|
|
288
|
+
},
|
|
289
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PluginHostProps } from "@getpaseo/plugin/client";
|
|
2
|
+
import * as hostUi from "@getpaseo/plugin/client/ui";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { MarkdownText } from "./markdown";
|
|
5
|
+
|
|
6
|
+
type HostMarkdownComponent = React.ComponentType<{ children: string }>;
|
|
7
|
+
|
|
8
|
+
// Newer hosts export `Markdown` from the plugin UI module; it renders with the host's own
|
|
9
|
+
// typography, links, code highlighting and selection. Older hosts lack it, so fall back to
|
|
10
|
+
// the built-in mini renderer. Look it up per render so a missing export never throws.
|
|
11
|
+
function getHostMarkdown(): HostMarkdownComponent | undefined {
|
|
12
|
+
const candidate = (hostUi as { Markdown?: unknown }).Markdown;
|
|
13
|
+
return typeof candidate === "function" || (typeof candidate === "object" && candidate !== null)
|
|
14
|
+
? (candidate as HostMarkdownComponent)
|
|
15
|
+
: undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function HostMarkdown({
|
|
19
|
+
text,
|
|
20
|
+
theme,
|
|
21
|
+
}: Pick<PluginHostProps, "theme"> & { text: string }) {
|
|
22
|
+
const Host = getHostMarkdown();
|
|
23
|
+
return Host ? <Host>{text}</Host> : <MarkdownText text={text} theme={theme} />;
|
|
24
|
+
}
|