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/shared/parser.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
capIssues,
|
|
3
|
+
findDuplicateIdIssues,
|
|
4
|
+
formatIssues,
|
|
5
|
+
formatJsonParseError,
|
|
6
|
+
formatZodIssues,
|
|
7
|
+
type FormIssue,
|
|
8
|
+
} from "./form-errors";
|
|
9
|
+
import { PfrForm, PfrFormSchema } from "./schema";
|
|
10
|
+
|
|
11
|
+
export interface ParsedBlock {
|
|
12
|
+
type: "text" | "form";
|
|
13
|
+
content?: string;
|
|
14
|
+
form?: PfrForm;
|
|
15
|
+
rawJson?: string;
|
|
16
|
+
/** Structured problems (at most 10, plus an "...and N more" entry); set when the block failed. */
|
|
17
|
+
errors?: FormIssue[];
|
|
18
|
+
/** `errors` joined one per line, for display. */
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface FenceBlock {
|
|
23
|
+
start: number;
|
|
24
|
+
contentStart: number;
|
|
25
|
+
contentEnd: number;
|
|
26
|
+
end: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findBlocks(text: string): FenceBlock[] {
|
|
30
|
+
const blocks: FenceBlock[] = [];
|
|
31
|
+
const fences = /^ {0,3}(`{3,}|~{3,})([^\r\n]*)\r?$/gm;
|
|
32
|
+
let opening: RegExpExecArray | null;
|
|
33
|
+
|
|
34
|
+
while ((opening = fences.exec(text)) !== null) {
|
|
35
|
+
const marker = opening[1];
|
|
36
|
+
const label = opening[2].trim();
|
|
37
|
+
const contentStart = opening.index + opening[0].length;
|
|
38
|
+
const closing = new RegExp(
|
|
39
|
+
`^ {0,3}${marker[0]}{${marker.length},}[ \\t]*\\r?$`,
|
|
40
|
+
"gm",
|
|
41
|
+
);
|
|
42
|
+
closing.lastIndex = contentStart;
|
|
43
|
+
const end = closing.exec(text);
|
|
44
|
+
|
|
45
|
+
if (label === "pfr-form") {
|
|
46
|
+
blocks.push({
|
|
47
|
+
start: opening.index,
|
|
48
|
+
contentStart,
|
|
49
|
+
contentEnd: end ? end.index : text.length,
|
|
50
|
+
end: end ? end.index + end[0].length : text.length,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!end) break;
|
|
55
|
+
fences.lastIndex = end.index + end[0].length;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return blocks;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function hasFormBlock(text: string): boolean {
|
|
62
|
+
return findBlocks(text).length > 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function failedBlock(rawJson: string, issues: FormIssue[]): ParsedBlock {
|
|
66
|
+
const errors = capIssues(issues);
|
|
67
|
+
return { type: "form", rawJson, errors, error: formatIssues(errors).join("\n") };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function parseFormBlock(jsonStr: string): ParsedBlock {
|
|
71
|
+
const trimmed = jsonStr.trim();
|
|
72
|
+
let parsedJson: unknown;
|
|
73
|
+
try {
|
|
74
|
+
parsedJson = JSON.parse(trimmed);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
return failedBlock(trimmed, formatJsonParseError(trimmed, e));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const validation = PfrFormSchema.safeParse(parsedJson);
|
|
80
|
+
const issues = [
|
|
81
|
+
...(validation.success ? [] : formatZodIssues(validation.error.issues, parsedJson)),
|
|
82
|
+
...findDuplicateIdIssues(parsedJson),
|
|
83
|
+
];
|
|
84
|
+
if (issues.length > 0) {
|
|
85
|
+
return failedBlock(trimmed, issues);
|
|
86
|
+
}
|
|
87
|
+
return { type: "form", form: validation.data as PfrForm, rawJson: trimmed };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function parseMessageContent(text: string): ParsedBlock[] {
|
|
91
|
+
const blocks: ParsedBlock[] = [];
|
|
92
|
+
const fenceBlocks = findBlocks(text);
|
|
93
|
+
let cursor = 0;
|
|
94
|
+
|
|
95
|
+
for (const block of fenceBlocks) {
|
|
96
|
+
// Leading text
|
|
97
|
+
if (block.start > cursor) {
|
|
98
|
+
const leadingText = text.slice(cursor, block.start).trim();
|
|
99
|
+
if (leadingText) {
|
|
100
|
+
blocks.push({ type: "text", content: leadingText });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const jsonStr = text.slice(block.contentStart, block.contentEnd).trim();
|
|
105
|
+
blocks.push(parseFormBlock(jsonStr));
|
|
106
|
+
|
|
107
|
+
cursor = block.end;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Trailing text
|
|
111
|
+
if (cursor < text.length) {
|
|
112
|
+
const trailingText = text.slice(cursor).trim();
|
|
113
|
+
if (trailingText) {
|
|
114
|
+
blocks.push({ type: "text", content: trailingText });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return blocks;
|
|
119
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const ProjectFileEntrySchema = z.object({
|
|
5
|
+
name: z.string(),
|
|
6
|
+
isDirectory: z.boolean(),
|
|
7
|
+
relativePath: z.string(),
|
|
8
|
+
fullPath: z.string().optional(),
|
|
9
|
+
});
|
|
10
|
+
export type ProjectFileEntry = z.infer<typeof ProjectFileEntrySchema>;
|
|
11
|
+
|
|
12
|
+
export const FilePickerShortcutSchema = z.object({
|
|
13
|
+
id: z.string(),
|
|
14
|
+
label: z.string(),
|
|
15
|
+
icon: z.string(),
|
|
16
|
+
path: z.string(),
|
|
17
|
+
});
|
|
18
|
+
export type FilePickerShortcut = z.infer<typeof FilePickerShortcutSchema>;
|
|
19
|
+
|
|
20
|
+
export const listProjectFilesRpc = defineRpc({
|
|
21
|
+
name: "pfr.list-project-files",
|
|
22
|
+
input: z.object({
|
|
23
|
+
agentId: z.string(),
|
|
24
|
+
subPath: z.string().optional(),
|
|
25
|
+
targetPath: z.string().optional(),
|
|
26
|
+
}),
|
|
27
|
+
output: z.object({
|
|
28
|
+
currentPath: z.string(),
|
|
29
|
+
parentPath: z.string().nullable(),
|
|
30
|
+
entries: z.array(ProjectFileEntrySchema),
|
|
31
|
+
shortcuts: z.array(FilePickerShortcutSchema).optional(),
|
|
32
|
+
isWorkspaceRelative: z.boolean().optional(),
|
|
33
|
+
error: z.string().optional(),
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const createDirectoryRpc = defineRpc({
|
|
38
|
+
name: "pfr.create-directory",
|
|
39
|
+
input: z.object({
|
|
40
|
+
agentId: z.string(),
|
|
41
|
+
parentPath: z.string().min(1),
|
|
42
|
+
name: z.string().min(1),
|
|
43
|
+
}),
|
|
44
|
+
output: z.object({
|
|
45
|
+
success: z.boolean(),
|
|
46
|
+
fullPath: z.string().optional(),
|
|
47
|
+
relativePath: z.string().optional(),
|
|
48
|
+
error: z.string().optional(),
|
|
49
|
+
}),
|
|
50
|
+
});
|
package/shared/prompt.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { PFR_FIX_MARKER } from "./form-fix";
|
|
2
|
+
|
|
3
|
+
export const PFR_FORM_TAG = "pfr-form";
|
|
4
|
+
|
|
5
|
+
const PROMPT_BODY = `You can render interactive forms, dashboards, parameter inputs, and approval cards by outputting a JSON specification inside a fenced code block with the tag \`\`\`pfr-form.
|
|
6
|
+
|
|
7
|
+
Use this format whenever you need to:
|
|
8
|
+
1. Collect input or configuration from the user (text, choices, toggles).
|
|
9
|
+
2. Ask for user confirmation or approval before running an action.
|
|
10
|
+
3. Present metrics, status progress, or structured data tables clearly.
|
|
11
|
+
|
|
12
|
+
### Protocol Specification
|
|
13
|
+
The JSON is a nested component tree starting with a root node (usually a "Card").
|
|
14
|
+
Available component types:
|
|
15
|
+
- **Card**: { "type": "Card", "title"?: string, "description"?: string, "children": [...] }
|
|
16
|
+
- **Stack**: { "type": "Stack", "direction"?: "vertical"|"horizontal", "children": [...] }
|
|
17
|
+
- **Heading**: { "type": "Heading", "text": string }
|
|
18
|
+
- **Text**: { "type": "Text", "text": string, "muted"?: boolean }
|
|
19
|
+
- **Divider**: { "type": "Divider" }
|
|
20
|
+
- **Metric**: { "type": "Metric", "label": string, "value": string|number }
|
|
21
|
+
- **Badge**: { "type": "Badge", "text": string, "tone"?: "neutral"|"info"|"success"|"warning"|"danger" }
|
|
22
|
+
- **Alert**: { "type": "Alert", "title"?: string, "text": string, "tone"?: "neutral"|"info"|"success"|"warning"|"danger" }
|
|
23
|
+
- **Progress**: { "type": "Progress", "label": string, "value": number (0-100) }
|
|
24
|
+
- **Table**: { "type": "Table", "columns": [{ "key": string, "label": string }], "rows": [{ [key: string]: any }] }
|
|
25
|
+
- **Input**: { "type": "Input", "id": string, "label": string, "placeholder"?: string, "defaultValue"?: string|number, "inputType"?: "text"|"number"|"integer"|"email"|"password"|"url", "multiline"?: boolean, "required"?: boolean, "min"?: number, "max"?: number, "minLength"?: number, "maxLength"?: number, "pattern"?: string }
|
|
26
|
+
- **Select**: { "type": "Select", "id": string, "label": string, "options": [{ "label": string, "value": string }], "defaultValue"?: string, "required"?: boolean }
|
|
27
|
+
- **MultiSelect**: { "type": "MultiSelect", "id": string, "label": string, "placeholder"?: string, "options": [{ "label": string, "value": string }], "defaultValue"?: string[], "required"?: boolean, "min"?: number, "max"?: number }
|
|
28
|
+
- **FilePicker**: { "type": "FilePicker", "id": string, "label": string, "placeholder"?: string, "pickerType"?: "file"|"folder", "extensions"?: string[], "defaultValue"?: string, "required"?: boolean }
|
|
29
|
+
* Unified in-app modal file and folder picker with quick shortcuts (Project, Home, Desktop, Downloads, OneDrive). Returns project-relative path if inside workspace, or host absolute path if outside. Zero cold start latency, foreground-guaranteed. When asking for specific file types (e.g. mp4, mov, png, json), ALWAYS specify "extensions": ["mp4"].
|
|
30
|
+
- **Toggle**: { "type": "Toggle", "id": string, "label": string, "defaultValue"?: boolean, "required"?: boolean }
|
|
31
|
+
- **Button**: { "type": "Button", "label": string, "variant"?: "primary"|"secondary"|"danger", "action": "submit"|"reset", "message"?: string }
|
|
32
|
+
|
|
33
|
+
### Important Rules:
|
|
34
|
+
1. Always give input fields (\`Input\`, \`Select\`, \`MultiSelect\`, \`FilePicker\`, \`Toggle\`) a unique \`id\`.
|
|
35
|
+
2. When filtering files by extension, use \`"extensions": ["mp4"]\` (or comma-separated string, leading dot optional).
|
|
36
|
+
3. When the user clicks a \`Button\` with \`action: "submit"\`, the client automatically sends the button's \`message\` along with all field values back to you.
|
|
37
|
+
4. Keep conversational text outside the code fence. The code block should only contain valid JSON.
|
|
38
|
+
5. If you receive a message starting with \`${PFR_FIX_MARKER}\`, a previous pfr-form block failed validation. Re-output only the corrected, complete pfr-form block(s), addressing every listed issue, with valid JSON and no comments.
|
|
39
|
+
|
|
40
|
+
### Example:
|
|
41
|
+
\`\`\`pfr-form
|
|
42
|
+
{
|
|
43
|
+
"type": "Card",
|
|
44
|
+
"title": "Deployment Confirmation",
|
|
45
|
+
"description": "Please review settings before releasing.",
|
|
46
|
+
"children": [
|
|
47
|
+
{
|
|
48
|
+
"type": "Metric",
|
|
49
|
+
"label": "Target Environment",
|
|
50
|
+
"value": "Production"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"type": "Input",
|
|
54
|
+
"id": "release_tag",
|
|
55
|
+
"label": "Release Tag",
|
|
56
|
+
"placeholder": "e.g., v1.0.4",
|
|
57
|
+
"required": true
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"type": "Select",
|
|
61
|
+
"id": "strategy",
|
|
62
|
+
"label": "Deploy Strategy",
|
|
63
|
+
"options": [
|
|
64
|
+
{ "label": "Rolling Update", "value": "rolling" },
|
|
65
|
+
{ "label": "Canary (10%)", "value": "canary" }
|
|
66
|
+
],
|
|
67
|
+
"defaultValue": "rolling"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "Button",
|
|
71
|
+
"label": "Confirm & Deploy",
|
|
72
|
+
"variant": "primary",
|
|
73
|
+
"action": "submit",
|
|
74
|
+
"message": "Start deployment with the selected parameters."
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
\`\`\`
|
|
79
|
+
`.trim();
|
|
80
|
+
|
|
81
|
+
/** Appended when the "prefer forms" setting is on; the default prompt only says the agent *can* use pfr-form. */
|
|
82
|
+
const PREFER_FORMS_INSTRUCTION = `
|
|
83
|
+
### Required usage:
|
|
84
|
+
Prefer pfr-form over plain text for interacting with the user:
|
|
85
|
+
- When you need anything from the user — a choice, a parameter, a value, or a go/no-go confirmation — you MUST ask with an \`pfr-form\` block, not plain-text questions.
|
|
86
|
+
- When presenting results, status, or structured data (metrics, progress, tables, warnings), use the matching components (Metric, Progress, Table, Alert, Badge) where they make the result clearer.
|
|
87
|
+
Plain text is fine for ordinary explanation and short conversational replies.
|
|
88
|
+
`.trim();
|
|
89
|
+
|
|
90
|
+
export function buildPromptInstruction(options: { preferForms?: boolean } = {}): string {
|
|
91
|
+
const parts = [PROMPT_BODY];
|
|
92
|
+
if (options.preferForms) parts.push(PREFER_FORMS_INSTRUCTION);
|
|
93
|
+
return `[paseo-form-render]\n${parts.join("\n\n")}\n[/paseo-form-render]`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const PFR_PROMPT_INSTRUCTION = buildPromptInstruction();
|
package/shared/schema.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// Basic field options
|
|
4
|
+
export const OptionSchema = z.object({
|
|
5
|
+
label: z.string(),
|
|
6
|
+
value: z.string(),
|
|
7
|
+
});
|
|
8
|
+
export type Option = z.infer<typeof OptionSchema>;
|
|
9
|
+
|
|
10
|
+
// Table column schema
|
|
11
|
+
export const TableColumnSchema = z.object({
|
|
12
|
+
key: z.string(),
|
|
13
|
+
label: z.string(),
|
|
14
|
+
});
|
|
15
|
+
export type TableColumn = z.infer<typeof TableColumnSchema>;
|
|
16
|
+
|
|
17
|
+
// Tone variants for Alert and Badge
|
|
18
|
+
export const ToneSchema = z.enum(["neutral", "info", "success", "warning", "danger"]);
|
|
19
|
+
export type Tone = z.infer<typeof ToneSchema>;
|
|
20
|
+
|
|
21
|
+
// Node base definition
|
|
22
|
+
export type PfrNode =
|
|
23
|
+
| CardNode
|
|
24
|
+
| StackNode
|
|
25
|
+
| HeadingNode
|
|
26
|
+
| TextNode
|
|
27
|
+
| DividerNode
|
|
28
|
+
| MetricNode
|
|
29
|
+
| BadgeNode
|
|
30
|
+
| AlertNode
|
|
31
|
+
| ProgressNode
|
|
32
|
+
| TableNode
|
|
33
|
+
| InputNode
|
|
34
|
+
| SelectNode
|
|
35
|
+
| MultiSelectNode
|
|
36
|
+
| ToggleNode
|
|
37
|
+
| FilePickerNode
|
|
38
|
+
| ButtonNode;
|
|
39
|
+
|
|
40
|
+
/** Every valid node `type`. The compile-time check below fails if a node type is missing from this list. */
|
|
41
|
+
export const PFR_NODE_TYPES = [
|
|
42
|
+
"Card",
|
|
43
|
+
"Stack",
|
|
44
|
+
"Heading",
|
|
45
|
+
"Text",
|
|
46
|
+
"Divider",
|
|
47
|
+
"Metric",
|
|
48
|
+
"Badge",
|
|
49
|
+
"Alert",
|
|
50
|
+
"Progress",
|
|
51
|
+
"Table",
|
|
52
|
+
"Input",
|
|
53
|
+
"Select",
|
|
54
|
+
"MultiSelect",
|
|
55
|
+
"Toggle",
|
|
56
|
+
"FilePicker",
|
|
57
|
+
"Button",
|
|
58
|
+
] as const satisfies readonly PfrNode["type"][];
|
|
59
|
+
type MissingNodeTypes = Exclude<PfrNode["type"], (typeof PFR_NODE_TYPES)[number]>;
|
|
60
|
+
const _allNodeTypesListed: [MissingNodeTypes] extends [never] ? true : never = true;
|
|
61
|
+
void _allNodeTypesListed;
|
|
62
|
+
|
|
63
|
+
/** Node types that collect a value and therefore need a unique `id`. */
|
|
64
|
+
export const PFR_FIELD_TYPES = ["Input", "Select", "MultiSelect", "Toggle", "FilePicker"] as const;
|
|
65
|
+
|
|
66
|
+
export interface CardNode {
|
|
67
|
+
type: "Card";
|
|
68
|
+
title?: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
children: PfrNode[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface StackNode {
|
|
74
|
+
type: "Stack";
|
|
75
|
+
direction?: "vertical" | "horizontal";
|
|
76
|
+
children: PfrNode[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface HeadingNode {
|
|
80
|
+
type: "Heading";
|
|
81
|
+
text: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface TextNode {
|
|
85
|
+
type: "Text";
|
|
86
|
+
text: string;
|
|
87
|
+
muted?: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface DividerNode {
|
|
91
|
+
type: "Divider";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface MetricNode {
|
|
95
|
+
type: "Metric";
|
|
96
|
+
label: string;
|
|
97
|
+
value: string | number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface BadgeNode {
|
|
101
|
+
type: "Badge";
|
|
102
|
+
text: string;
|
|
103
|
+
tone?: Tone;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface AlertNode {
|
|
107
|
+
type: "Alert";
|
|
108
|
+
title?: string;
|
|
109
|
+
text: string;
|
|
110
|
+
tone?: Tone;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface ProgressNode {
|
|
114
|
+
type: "Progress";
|
|
115
|
+
label: string;
|
|
116
|
+
value: number; // 0 - 100
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface TableNode {
|
|
120
|
+
type: "Table";
|
|
121
|
+
columns: TableColumn[];
|
|
122
|
+
rows: Record<string, string | number | boolean | null>[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type InputType = "text" | "number" | "integer" | "email" | "password" | "url";
|
|
126
|
+
|
|
127
|
+
export interface InputNode {
|
|
128
|
+
type: "Input";
|
|
129
|
+
id: string;
|
|
130
|
+
label: string;
|
|
131
|
+
placeholder?: string;
|
|
132
|
+
defaultValue?: string | number;
|
|
133
|
+
inputType?: InputType;
|
|
134
|
+
multiline?: boolean;
|
|
135
|
+
required?: boolean;
|
|
136
|
+
min?: number;
|
|
137
|
+
max?: number;
|
|
138
|
+
minLength?: number;
|
|
139
|
+
maxLength?: number;
|
|
140
|
+
pattern?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface SelectNode {
|
|
144
|
+
type: "Select";
|
|
145
|
+
id: string;
|
|
146
|
+
label: string;
|
|
147
|
+
options: Option[];
|
|
148
|
+
defaultValue?: string;
|
|
149
|
+
required?: boolean;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface MultiSelectNode {
|
|
153
|
+
type: "MultiSelect";
|
|
154
|
+
id: string;
|
|
155
|
+
label: string;
|
|
156
|
+
placeholder?: string;
|
|
157
|
+
options: Option[];
|
|
158
|
+
defaultValue?: string[];
|
|
159
|
+
required?: boolean;
|
|
160
|
+
min?: number;
|
|
161
|
+
max?: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface ToggleNode {
|
|
165
|
+
type: "Toggle";
|
|
166
|
+
id: string;
|
|
167
|
+
label: string;
|
|
168
|
+
defaultValue?: boolean;
|
|
169
|
+
required?: boolean;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function normalizeExtensions(
|
|
173
|
+
extensions?: string[] | string,
|
|
174
|
+
extension?: string,
|
|
175
|
+
): string[] | undefined {
|
|
176
|
+
const rawList: string[] = [];
|
|
177
|
+
if (Array.isArray(extensions)) {
|
|
178
|
+
rawList.push(...extensions);
|
|
179
|
+
} else if (typeof extensions === "string") {
|
|
180
|
+
rawList.push(...extensions.split(","));
|
|
181
|
+
}
|
|
182
|
+
if (typeof extension === "string") {
|
|
183
|
+
rawList.push(...extension.split(","));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const cleaned = rawList
|
|
187
|
+
.map((e) => e.trim().toLowerCase().replace(/^\./, ""))
|
|
188
|
+
.filter(Boolean);
|
|
189
|
+
|
|
190
|
+
return cleaned.length > 0 ? Array.from(new Set(cleaned)) : undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface FilePickerNode {
|
|
194
|
+
type: "FilePicker";
|
|
195
|
+
id: string;
|
|
196
|
+
label: string;
|
|
197
|
+
pickerType?: "file" | "folder";
|
|
198
|
+
scope?: "workspace" | "global";
|
|
199
|
+
placeholder?: string;
|
|
200
|
+
defaultValue?: string;
|
|
201
|
+
required?: boolean;
|
|
202
|
+
extensions?: string[] | string;
|
|
203
|
+
extension?: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface ButtonNode {
|
|
207
|
+
type: "Button";
|
|
208
|
+
label: string;
|
|
209
|
+
variant?: "primary" | "secondary" | "danger";
|
|
210
|
+
action: "submit" | "reset";
|
|
211
|
+
message?: string; // Prompt/message returned to agent when submitted
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Recursive Zod Schema for validation
|
|
215
|
+
export const PfrNodeSchema: z.ZodType<PfrNode> = z.lazy(() =>
|
|
216
|
+
z.discriminatedUnion("type", [
|
|
217
|
+
z.object({
|
|
218
|
+
type: z.literal("Card"),
|
|
219
|
+
title: z.string().optional(),
|
|
220
|
+
description: z.string().optional(),
|
|
221
|
+
children: z.array(PfrNodeSchema),
|
|
222
|
+
}),
|
|
223
|
+
z.object({
|
|
224
|
+
type: z.literal("Stack"),
|
|
225
|
+
direction: z.enum(["vertical", "horizontal"]).optional(),
|
|
226
|
+
children: z.array(PfrNodeSchema),
|
|
227
|
+
}),
|
|
228
|
+
z.object({
|
|
229
|
+
type: z.literal("Heading"),
|
|
230
|
+
text: z.string(),
|
|
231
|
+
}),
|
|
232
|
+
z.object({
|
|
233
|
+
type: z.literal("Text"),
|
|
234
|
+
text: z.string(),
|
|
235
|
+
muted: z.boolean().optional(),
|
|
236
|
+
}),
|
|
237
|
+
z.object({
|
|
238
|
+
type: z.literal("Divider"),
|
|
239
|
+
}),
|
|
240
|
+
z.object({
|
|
241
|
+
type: z.literal("Metric"),
|
|
242
|
+
label: z.string(),
|
|
243
|
+
value: z.union([z.string(), z.number()]),
|
|
244
|
+
}),
|
|
245
|
+
z.object({
|
|
246
|
+
type: z.literal("Badge"),
|
|
247
|
+
text: z.string(),
|
|
248
|
+
tone: ToneSchema.optional(),
|
|
249
|
+
}),
|
|
250
|
+
z.object({
|
|
251
|
+
type: z.literal("Alert"),
|
|
252
|
+
title: z.string().optional(),
|
|
253
|
+
text: z.string(),
|
|
254
|
+
tone: ToneSchema.optional(),
|
|
255
|
+
}),
|
|
256
|
+
z.object({
|
|
257
|
+
type: z.literal("Progress"),
|
|
258
|
+
label: z.string(),
|
|
259
|
+
value: z.number().min(0).max(100),
|
|
260
|
+
}),
|
|
261
|
+
z.object({
|
|
262
|
+
type: z.literal("Table"),
|
|
263
|
+
columns: z.array(TableColumnSchema).min(1),
|
|
264
|
+
rows: z.array(z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()]))),
|
|
265
|
+
}),
|
|
266
|
+
z.object({
|
|
267
|
+
type: z.literal("Input"),
|
|
268
|
+
id: z.string().min(1),
|
|
269
|
+
label: z.string(),
|
|
270
|
+
placeholder: z.string().optional(),
|
|
271
|
+
defaultValue: z.union([z.string(), z.number()]).optional(),
|
|
272
|
+
inputType: z.enum(["text", "number", "integer", "email", "password", "url"]).optional(),
|
|
273
|
+
multiline: z.boolean().optional(),
|
|
274
|
+
required: z.boolean().optional(),
|
|
275
|
+
min: z.number().optional(),
|
|
276
|
+
max: z.number().optional(),
|
|
277
|
+
minLength: z.number().int().nonnegative().optional(),
|
|
278
|
+
maxLength: z.number().int().nonnegative().optional(),
|
|
279
|
+
pattern: z.string().optional(),
|
|
280
|
+
}),
|
|
281
|
+
z.object({
|
|
282
|
+
type: z.literal("Select"),
|
|
283
|
+
id: z.string().min(1),
|
|
284
|
+
label: z.string(),
|
|
285
|
+
options: z.array(OptionSchema).min(1),
|
|
286
|
+
defaultValue: z.string().optional(),
|
|
287
|
+
required: z.boolean().optional(),
|
|
288
|
+
}),
|
|
289
|
+
z.object({
|
|
290
|
+
type: z.literal("MultiSelect"),
|
|
291
|
+
id: z.string().min(1),
|
|
292
|
+
label: z.string(),
|
|
293
|
+
placeholder: z.string().optional(),
|
|
294
|
+
options: z.array(OptionSchema).min(1),
|
|
295
|
+
defaultValue: z.array(z.string()).optional(),
|
|
296
|
+
required: z.boolean().optional(),
|
|
297
|
+
min: z.number().int().nonnegative().optional(),
|
|
298
|
+
max: z.number().int().nonnegative().optional(),
|
|
299
|
+
}),
|
|
300
|
+
z.object({
|
|
301
|
+
type: z.literal("Toggle"),
|
|
302
|
+
id: z.string().min(1),
|
|
303
|
+
label: z.string(),
|
|
304
|
+
defaultValue: z.boolean().optional(),
|
|
305
|
+
required: z.boolean().optional(),
|
|
306
|
+
}),
|
|
307
|
+
z.object({
|
|
308
|
+
type: z.literal("FilePicker"),
|
|
309
|
+
id: z.string().min(1),
|
|
310
|
+
label: z.string(),
|
|
311
|
+
pickerType: z.enum(["file", "folder"]).optional(),
|
|
312
|
+
scope: z.enum(["workspace", "global"]).optional(),
|
|
313
|
+
placeholder: z.string().optional(),
|
|
314
|
+
defaultValue: z.string().optional(),
|
|
315
|
+
required: z.boolean().optional(),
|
|
316
|
+
extensions: z.union([z.string(), z.array(z.string())]).optional(),
|
|
317
|
+
extension: z.string().optional(),
|
|
318
|
+
}),
|
|
319
|
+
z.object({
|
|
320
|
+
type: z.literal("Button"),
|
|
321
|
+
label: z.string(),
|
|
322
|
+
variant: z.enum(["primary", "secondary", "danger"]).optional(),
|
|
323
|
+
action: z.enum(["submit", "reset"]),
|
|
324
|
+
message: z.string().optional(),
|
|
325
|
+
}),
|
|
326
|
+
]),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
export const PfrFormSchema = PfrNodeSchema;
|
|
330
|
+
export type PfrForm = PfrNode;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineSettings } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const MAX_AUTO_FIX_RETRIES = 5;
|
|
5
|
+
export const DEFAULT_AUTO_FIX_RETRIES = 2;
|
|
6
|
+
export const DEFAULT_PREFER_FORMS = true;
|
|
7
|
+
|
|
8
|
+
export const pfrSettings = defineSettings({
|
|
9
|
+
id: "paseo-form-render",
|
|
10
|
+
scope: "host",
|
|
11
|
+
version: 1,
|
|
12
|
+
schema: z.object({
|
|
13
|
+
/** How many times the agent is asked to repair an invalid pfr-form block in a row. 0 disables auto-fix. */
|
|
14
|
+
autoFixMaxRetries: z.number().int().min(0).max(MAX_AUTO_FIX_RETRIES).default(DEFAULT_AUTO_FIX_RETRIES),
|
|
15
|
+
/** When on, the injected prompt requires agents to ask for user input with pfr-form instead of plain text. */
|
|
16
|
+
preferForms: z.boolean().default(DEFAULT_PREFER_FORMS),
|
|
17
|
+
}),
|
|
18
|
+
});
|