paseo-form-render 0.1.0 → 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/README.md +2 -0
- package/client/container.tsx +6 -3
- package/client/messages.ts +147 -0
- package/client/project-file-modal.tsx +20 -17
- package/client/renderer.tsx +13 -10
- package/client/settings-screen.tsx +18 -23
- package/index.client.tsx +3 -2
- package/package.json +3 -3
- package/paseo-plugin.json +1 -1
- package/server/auto-fix.ts +20 -2
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@ Interactive forms, dashboards, and metrics rendering plugin for Paseo conversati
|
|
|
15
15
|
- **Provider Agnostic**:
|
|
16
16
|
- Fully compatible with **Devin**, **Claude**, **Antigravity**, **Gemini**, and OpenAI providers.
|
|
17
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.
|
|
18
20
|
- **Full Mobile & Hermes Compatibility**:
|
|
19
21
|
- 100% pure React Native UI primitives (`View`, `Text`, `TextInput`, `Pressable`, `Switch`).
|
|
20
22
|
- Zero external runtime dependencies in the client bundle.
|
package/client/container.tsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { PluginHostProps } from "@getpaseo/plugin/client";
|
|
2
|
+
import { useTranslation } from "@getpaseo/plugin/client";
|
|
2
3
|
import React, { useMemo, useState } from "react";
|
|
3
4
|
import { View, Text, StyleSheet, Pressable, Platform } from "react-native";
|
|
4
5
|
import { formatIssues } from "../shared/form-errors";
|
|
5
6
|
import { buildFixMessage } from "../shared/form-fix";
|
|
6
7
|
import { parseFormBlock, parseMessageContent, type ParsedBlock } from "../shared/parser";
|
|
8
|
+
import { messages } from "./messages";
|
|
7
9
|
import { PfrFormView } from "./renderer";
|
|
8
10
|
import { HostMarkdown } from "./host-markdown";
|
|
9
11
|
import { PfrRpcProvider, type PfrRpcFn } from "./rpc-bridge";
|
|
@@ -83,6 +85,7 @@ BlockErrorBoundary.prototype.render = function (this: any) {
|
|
|
83
85
|
};
|
|
84
86
|
|
|
85
87
|
const FormErrorBox: React.FC<FormErrorBoxProps> = ({ block, blockNumber, theme, onSubmit }) => {
|
|
88
|
+
const { t } = useTranslation(messages);
|
|
86
89
|
const [status, setStatus] = useState<"idle" | "sending" | "sent">("idle");
|
|
87
90
|
const errors = block.errors ?? [];
|
|
88
91
|
|
|
@@ -107,7 +110,7 @@ const FormErrorBox: React.FC<FormErrorBoxProps> = ({ block, blockNumber, theme,
|
|
|
107
110
|
]}
|
|
108
111
|
>
|
|
109
112
|
<Text style={[styles.errorTitle, { color: theme.colors.statusDanger }]}>
|
|
110
|
-
|
|
113
|
+
{t("errorBox.title")}
|
|
111
114
|
</Text>
|
|
112
115
|
{formatIssues(errors).map((line, i) => (
|
|
113
116
|
<Text key={i} style={[styles.errorDetail, { color: theme.colors.statusDanger }]}>
|
|
@@ -135,7 +138,7 @@ const FormErrorBox: React.FC<FormErrorBoxProps> = ({ block, blockNumber, theme,
|
|
|
135
138
|
</View>
|
|
136
139
|
{status === "sent" ? (
|
|
137
140
|
<Text style={[styles.sentText, { color: theme.colors.statusSuccess }]}>
|
|
138
|
-
|
|
141
|
+
{t("errorBox.fixRequested")}
|
|
139
142
|
</Text>
|
|
140
143
|
) : (
|
|
141
144
|
<Pressable
|
|
@@ -148,7 +151,7 @@ const FormErrorBox: React.FC<FormErrorBoxProps> = ({ block, blockNumber, theme,
|
|
|
148
151
|
onPress={askAgentToFix}
|
|
149
152
|
>
|
|
150
153
|
<Text style={[styles.fixButtonText, { color: theme.colors.accentForeground }]}>
|
|
151
|
-
{status === "sending" ? "
|
|
154
|
+
{status === "sending" ? t("errorBox.sending") : t("errorBox.askFix")}
|
|
152
155
|
</Text>
|
|
153
156
|
</Pressable>
|
|
154
157
|
)}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { defineTranslations } from "@getpaseo/plugin/client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Labels shown in the plugin UI. English is the source of truth; every other locale must use the
|
|
5
|
+
* same keys and placeholders (checked by the SDK and by `client/messages.test.ts`). The host picks the
|
|
6
|
+
* locale from the app language.
|
|
7
|
+
*/
|
|
8
|
+
const en = {
|
|
9
|
+
slash: {
|
|
10
|
+
form: "Instruct the agent to use pfr-form protocol",
|
|
11
|
+
formDemo: "Show a demo interactive form",
|
|
12
|
+
},
|
|
13
|
+
errorBox: {
|
|
14
|
+
title: "[pfr-form Syntax Error]",
|
|
15
|
+
askFix: "Ask agent to fix",
|
|
16
|
+
sending: "Sending…",
|
|
17
|
+
fixRequested: "✓ Fix requested; the agent will reply with a corrected form",
|
|
18
|
+
},
|
|
19
|
+
form: {
|
|
20
|
+
submitted: "✓ Form Submitted",
|
|
21
|
+
},
|
|
22
|
+
dropdown: {
|
|
23
|
+
placeholder: "Select an option...",
|
|
24
|
+
multiPlaceholder: "Select options...",
|
|
25
|
+
close: "Close dropdown menu",
|
|
26
|
+
},
|
|
27
|
+
file: {
|
|
28
|
+
folderPlaceholder: "Enter or select folder...",
|
|
29
|
+
filePlaceholder: "Enter or select file...",
|
|
30
|
+
browse: "Browse...",
|
|
31
|
+
fullPath: "Full path:",
|
|
32
|
+
},
|
|
33
|
+
picker: {
|
|
34
|
+
selectFolder: "Select Folder",
|
|
35
|
+
selectFile: "Select File",
|
|
36
|
+
chooseFolder: "Choose Folder",
|
|
37
|
+
chooseFile: "Choose File",
|
|
38
|
+
quick: "Quick:",
|
|
39
|
+
searchPlaceholder: "Search files/folders in directory...",
|
|
40
|
+
newFolderPlaceholder: "New folder name...",
|
|
41
|
+
create: "Create",
|
|
42
|
+
upToParent: ".. (Up to parent)",
|
|
43
|
+
loading: "Loading files...",
|
|
44
|
+
noMatches: "No matching entries found",
|
|
45
|
+
selected: "✓ Selected",
|
|
46
|
+
selectedLabel: "Selected:",
|
|
47
|
+
cancel: "Cancel",
|
|
48
|
+
projectRoot: "Project Root",
|
|
49
|
+
project: "Project",
|
|
50
|
+
invalidFolderName: 'Folder name contains invalid characters (/ \\ : * ? " < > |).',
|
|
51
|
+
createFailed: "Failed to create folder.",
|
|
52
|
+
},
|
|
53
|
+
settings: {
|
|
54
|
+
loadError: "Could not load settings: {{error}}",
|
|
55
|
+
retry: "Retry",
|
|
56
|
+
resetDefaults: "Reset to defaults",
|
|
57
|
+
autoFixTitle: "Automatic form fixes",
|
|
58
|
+
autoFixDescription:
|
|
59
|
+
'When an agent replies with a pfr-form block that fails validation, ask it to re-send a corrected block automatically. This sets how many times in a row the agent is asked; 0 turns automatic fixes off. The "Ask agent to fix" button on an error box always works.',
|
|
60
|
+
maxRetries: "Maximum retries",
|
|
61
|
+
decreaseRetries: "Decrease retries",
|
|
62
|
+
increaseRetries: "Increase retries",
|
|
63
|
+
hintOff: "Automatic fixes are off.",
|
|
64
|
+
hint_one: "Up to {{count}} automatic request per reply.",
|
|
65
|
+
hint_other: "Up to {{count}} automatic requests per reply.",
|
|
66
|
+
saving: "Saving…",
|
|
67
|
+
preferFormsTitle: "Prefer forms",
|
|
68
|
+
preferFormsDescription:
|
|
69
|
+
"By default the agent is only told it can use pfr-form. Turn this on to require it to ask for choices, parameters, and confirmations with a form instead of plain text. Applies to agents created after the change.",
|
|
70
|
+
requireForms: "Require forms for user input",
|
|
71
|
+
saveError: "Could not save: {{error}}",
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const zhCN = {
|
|
76
|
+
slash: {
|
|
77
|
+
form: "让智能体使用 pfr-form 协议",
|
|
78
|
+
formDemo: "显示一个交互表单示例",
|
|
79
|
+
},
|
|
80
|
+
errorBox: {
|
|
81
|
+
title: "[pfr-form 语法错误]",
|
|
82
|
+
askFix: "请求智能体修复",
|
|
83
|
+
sending: "发送中…",
|
|
84
|
+
fixRequested: "✓ 已请求修复,智能体将回复更正后的表单",
|
|
85
|
+
},
|
|
86
|
+
form: {
|
|
87
|
+
submitted: "✓ 表单已提交",
|
|
88
|
+
},
|
|
89
|
+
dropdown: {
|
|
90
|
+
placeholder: "请选择...",
|
|
91
|
+
multiPlaceholder: "请选择多项...",
|
|
92
|
+
close: "关闭下拉菜单",
|
|
93
|
+
},
|
|
94
|
+
file: {
|
|
95
|
+
folderPlaceholder: "输入或选择文件夹...",
|
|
96
|
+
filePlaceholder: "输入或选择文件...",
|
|
97
|
+
browse: "浏览...",
|
|
98
|
+
fullPath: "完整路径:",
|
|
99
|
+
},
|
|
100
|
+
picker: {
|
|
101
|
+
selectFolder: "选择文件夹",
|
|
102
|
+
selectFile: "选择文件",
|
|
103
|
+
chooseFolder: "选择此文件夹",
|
|
104
|
+
chooseFile: "选择此文件",
|
|
105
|
+
quick: "快捷:",
|
|
106
|
+
searchPlaceholder: "在目录中搜索文件/文件夹...",
|
|
107
|
+
newFolderPlaceholder: "新文件夹名称...",
|
|
108
|
+
create: "创建",
|
|
109
|
+
upToParent: ".. (返回上级)",
|
|
110
|
+
loading: "正在加载文件...",
|
|
111
|
+
noMatches: "没有匹配的条目",
|
|
112
|
+
selected: "✓ 已选择",
|
|
113
|
+
selectedLabel: "已选择:",
|
|
114
|
+
cancel: "取消",
|
|
115
|
+
projectRoot: "项目根目录",
|
|
116
|
+
project: "项目",
|
|
117
|
+
invalidFolderName: '文件夹名称包含非法字符 (/ \\ : * ? " < > |)。',
|
|
118
|
+
createFailed: "创建文件夹失败。",
|
|
119
|
+
},
|
|
120
|
+
settings: {
|
|
121
|
+
loadError: "无法加载设置:{{error}}",
|
|
122
|
+
retry: "重试",
|
|
123
|
+
resetDefaults: "恢复默认值",
|
|
124
|
+
autoFixTitle: "自动修复表单",
|
|
125
|
+
autoFixDescription:
|
|
126
|
+
"当智能体回复的 pfr-form 代码块未通过校验时,自动请求它重新发送更正后的代码块。此处设置连续请求的最多次数;设为 0 则关闭自动修复。错误框上的“请求智能体修复”按钮始终可用。",
|
|
127
|
+
maxRetries: "最大重试次数",
|
|
128
|
+
decreaseRetries: "减少重试次数",
|
|
129
|
+
increaseRetries: "增加重试次数",
|
|
130
|
+
hintOff: "自动修复已关闭。",
|
|
131
|
+
hint_other: "每条回复最多自动请求 {{count}} 次。",
|
|
132
|
+
saving: "保存中…",
|
|
133
|
+
preferFormsTitle: "优先使用表单",
|
|
134
|
+
preferFormsDescription:
|
|
135
|
+
"默认情况下,智能体只被告知可以使用 pfr-form。开启后,它必须通过表单询问选项、参数和确认,而不是纯文本。仅对之后创建的智能体生效。",
|
|
136
|
+
requireForms: "用户输入必须使用表单",
|
|
137
|
+
saveError: "无法保存:{{error}}",
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const messages = defineTranslations({
|
|
142
|
+
resources: {
|
|
143
|
+
en,
|
|
144
|
+
"zh-CN": zhCN,
|
|
145
|
+
zh: zhCN,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PluginHostProps } from "@getpaseo/plugin/client";
|
|
2
|
+
import { useTranslation } from "@getpaseo/plugin/client";
|
|
2
3
|
import React, { useEffect, useMemo, useState } from "react";
|
|
3
4
|
import {
|
|
4
5
|
Modal,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
type FilePickerShortcut,
|
|
17
18
|
} from "../shared/pickers";
|
|
18
19
|
import { normalizeExtensions } from "../shared/schema";
|
|
20
|
+
import { messages } from "./messages";
|
|
19
21
|
import { usePfrRpc } from "./rpc-bridge";
|
|
20
22
|
|
|
21
23
|
interface ProjectFileModalProps {
|
|
@@ -43,6 +45,7 @@ export function ProjectFileModal({
|
|
|
43
45
|
}: ProjectFileModalProps) {
|
|
44
46
|
const listProjectFiles = usePfrRpc(listProjectFilesRpc);
|
|
45
47
|
const createDir = usePfrRpc(createDirectoryRpc);
|
|
48
|
+
const { t } = useTranslation(messages);
|
|
46
49
|
const [currentPath, setCurrentPath] = useState("");
|
|
47
50
|
const [parentPath, setParentPath] = useState<string | null>(null);
|
|
48
51
|
const [entries, setEntries] = useState<ProjectFileEntry[]>([]);
|
|
@@ -112,7 +115,7 @@ export function ProjectFileModal({
|
|
|
112
115
|
if (!agentId || !newFolderName.trim() || creatingFolderLoading) return;
|
|
113
116
|
const name = newFolderName.trim();
|
|
114
117
|
if (/[<>:"/\\|?*]/.test(name) || name === "." || name === "..") {
|
|
115
|
-
setNewFolderError(
|
|
118
|
+
setNewFolderError(t("picker.invalidFolderName"));
|
|
116
119
|
return;
|
|
117
120
|
}
|
|
118
121
|
setCreatingFolderLoading(true);
|
|
@@ -132,7 +135,7 @@ export function ProjectFileModal({
|
|
|
132
135
|
setSelectedPath(res.relativePath || res.fullPath);
|
|
133
136
|
}
|
|
134
137
|
} else {
|
|
135
|
-
setNewFolderError(res.error || "
|
|
138
|
+
setNewFolderError(res.error || t("picker.createFailed"));
|
|
136
139
|
}
|
|
137
140
|
} catch (err) {
|
|
138
141
|
setNewFolderError(err instanceof Error ? err.message : String(err));
|
|
@@ -159,9 +162,9 @@ export function ProjectFileModal({
|
|
|
159
162
|
|
|
160
163
|
const breadcrumbs = useMemo(() => {
|
|
161
164
|
if (isWorkspaceRelative) {
|
|
162
|
-
if (!currentPath) return [{ label: "
|
|
165
|
+
if (!currentPath) return [{ label: t("picker.projectRoot"), path: "" }];
|
|
163
166
|
const parts = currentPath.split("/").filter(Boolean);
|
|
164
|
-
const crumbs = [{ label: "
|
|
167
|
+
const crumbs = [{ label: t("picker.project"), path: "" }];
|
|
165
168
|
let accum = "";
|
|
166
169
|
for (const part of parts) {
|
|
167
170
|
accum = accum ? `${accum}/${part}` : part;
|
|
@@ -190,7 +193,7 @@ export function ProjectFileModal({
|
|
|
190
193
|
}
|
|
191
194
|
return crumbs;
|
|
192
195
|
}
|
|
193
|
-
}, [currentPath, isWorkspaceRelative]);
|
|
196
|
+
}, [currentPath, isWorkspaceRelative, t]);
|
|
194
197
|
|
|
195
198
|
const handleEntryPress = (entry: ProjectFileEntry) => {
|
|
196
199
|
if (entry.isDirectory) {
|
|
@@ -227,7 +230,7 @@ export function ProjectFileModal({
|
|
|
227
230
|
<View style={[styles.header, { borderBottomColor: colors.border }]}>
|
|
228
231
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, flex: 1, marginRight: 8 }}>
|
|
229
232
|
<Text style={[styles.title, { color: colors.foreground }]}>
|
|
230
|
-
{pickerType === "folder" ? "
|
|
233
|
+
{pickerType === "folder" ? t("picker.selectFolder") : t("picker.selectFile")}
|
|
231
234
|
</Text>
|
|
232
235
|
{cleanExts && cleanExts.length > 0 && (
|
|
233
236
|
<View style={[styles.filterBadge, { backgroundColor: colors.surface1, borderColor: colors.border }]}>
|
|
@@ -245,7 +248,7 @@ export function ProjectFileModal({
|
|
|
245
248
|
{/* Quick Shortcuts Bar */}
|
|
246
249
|
{shortcuts.length > 0 && (
|
|
247
250
|
<View style={[styles.shortcutsBar, { borderBottomColor: colors.border, backgroundColor: colors.surface1 }]}>
|
|
248
|
-
<Text style={[styles.shortcutsLabel, { color: colors.foregroundMuted }]}>
|
|
251
|
+
<Text style={[styles.shortcutsLabel, { color: colors.foregroundMuted }]}>{t("picker.quick")}</Text>
|
|
249
252
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.shortcutsList}>
|
|
250
253
|
{shortcuts.map((sc) => (
|
|
251
254
|
<Pressable
|
|
@@ -298,7 +301,7 @@ export function ProjectFileModal({
|
|
|
298
301
|
borderColor: colors.border,
|
|
299
302
|
},
|
|
300
303
|
]}
|
|
301
|
-
placeholder="
|
|
304
|
+
placeholder={t("picker.searchPlaceholder")}
|
|
302
305
|
placeholderTextColor={colors.foregroundMuted}
|
|
303
306
|
value={search}
|
|
304
307
|
onChangeText={setSearch}
|
|
@@ -353,7 +356,7 @@ export function ProjectFileModal({
|
|
|
353
356
|
borderColor: colors.border,
|
|
354
357
|
},
|
|
355
358
|
]}
|
|
356
|
-
placeholder="
|
|
359
|
+
placeholder={t("picker.newFolderPlaceholder")}
|
|
357
360
|
placeholderTextColor={colors.foregroundMuted}
|
|
358
361
|
value={newFolderName}
|
|
359
362
|
onChangeText={(t) => {
|
|
@@ -375,7 +378,7 @@ export function ProjectFileModal({
|
|
|
375
378
|
]}
|
|
376
379
|
>
|
|
377
380
|
<Text style={{ color: colors.surface0, fontSize: 12, fontWeight: "600" }}>
|
|
378
|
-
{creatingFolderLoading ? "..." : "
|
|
381
|
+
{creatingFolderLoading ? "..." : t("picker.create")}
|
|
379
382
|
</Text>
|
|
380
383
|
</Pressable>
|
|
381
384
|
<Pressable
|
|
@@ -407,16 +410,16 @@ export function ProjectFileModal({
|
|
|
407
410
|
onPress={() => loadDirectory(parentPath)}
|
|
408
411
|
>
|
|
409
412
|
<Text style={styles.entryIcon}>📁</Text>
|
|
410
|
-
<Text style={{ color: colors.foregroundMuted, fontSize: 14 }}
|
|
413
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 14 }}>{t("picker.upToParent")}</Text>
|
|
411
414
|
</Pressable>
|
|
412
415
|
)}
|
|
413
416
|
|
|
414
417
|
{loading ? (
|
|
415
|
-
<Text style={[styles.emptyText, { color: colors.foregroundMuted }]}>
|
|
418
|
+
<Text style={[styles.emptyText, { color: colors.foregroundMuted }]}>{t("picker.loading")}</Text>
|
|
416
419
|
) : error ? (
|
|
417
420
|
<Text style={[styles.emptyText, { color: colors.statusDanger }]}>{error}</Text>
|
|
418
421
|
) : filteredEntries.length === 0 ? (
|
|
419
|
-
<Text style={[styles.emptyText, { color: colors.foregroundMuted }]}>
|
|
422
|
+
<Text style={[styles.emptyText, { color: colors.foregroundMuted }]}>{t("picker.noMatches")}</Text>
|
|
420
423
|
) : (
|
|
421
424
|
filteredEntries.map((entry) => {
|
|
422
425
|
const isSelected = selectedPath === entry.relativePath;
|
|
@@ -445,7 +448,7 @@ export function ProjectFileModal({
|
|
|
445
448
|
{entry.name}
|
|
446
449
|
</Text>
|
|
447
450
|
{isSelected && (
|
|
448
|
-
<Text style={{ color: colors.accent, fontWeight: "600", fontSize: 13 }}
|
|
451
|
+
<Text style={{ color: colors.accent, fontWeight: "600", fontSize: 13 }}>{t("picker.selected")}</Text>
|
|
449
452
|
)}
|
|
450
453
|
</Pressable>
|
|
451
454
|
);
|
|
@@ -456,7 +459,7 @@ export function ProjectFileModal({
|
|
|
456
459
|
{/* Footer */}
|
|
457
460
|
<View style={[styles.footer, { borderTopColor: colors.border }]}>
|
|
458
461
|
<View style={{ flex: 1, marginRight: 10, overflow: "hidden" }}>
|
|
459
|
-
<Text style={{ color: colors.foregroundMuted, fontSize: 12 }}>
|
|
462
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 12 }}>{t("picker.selectedLabel")}</Text>
|
|
460
463
|
{effectiveSelected ? (
|
|
461
464
|
<ScrollView
|
|
462
465
|
horizontal
|
|
@@ -489,7 +492,7 @@ export function ProjectFileModal({
|
|
|
489
492
|
onPress={onClose}
|
|
490
493
|
style={[styles.actionBtn, { borderColor: colors.border, borderWidth: 1 }]}
|
|
491
494
|
>
|
|
492
|
-
<Text style={{ color: colors.foreground, fontSize: 13 }}>
|
|
495
|
+
<Text style={{ color: colors.foreground, fontSize: 13 }}>{t("picker.cancel")}</Text>
|
|
493
496
|
</Pressable>
|
|
494
497
|
|
|
495
498
|
<Pressable
|
|
@@ -504,7 +507,7 @@ export function ProjectFileModal({
|
|
|
504
507
|
]}
|
|
505
508
|
>
|
|
506
509
|
<Text style={{ color: "#fff", fontWeight: "600", fontSize: 13 }}>
|
|
507
|
-
{pickerType === "folder" ? "
|
|
510
|
+
{pickerType === "folder" ? t("picker.chooseFolder") : t("picker.chooseFile")}
|
|
508
511
|
</Text>
|
|
509
512
|
</Pressable>
|
|
510
513
|
</View>
|
package/client/renderer.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluginHostProps } from "@getpaseo/plugin/client";
|
|
2
|
-
import { useRpc } from "@getpaseo/plugin/client";
|
|
2
|
+
import { useRpc, useTranslation } from "@getpaseo/plugin/client";
|
|
3
3
|
import React, { createContext, useContext, useRef, useState } from "react";
|
|
4
4
|
import { View, Text, TextInput, Pressable, StyleSheet, Switch, ScrollView, Platform } from "react-native";
|
|
5
5
|
import {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
normalizeExtensions,
|
|
12
12
|
} from "../shared/schema";
|
|
13
13
|
import { extractInitialValues, validateForm, formatSubmissionMessage } from "../shared/form-helpers";
|
|
14
|
+
import { messages } from "./messages";
|
|
14
15
|
import { ProjectFileModal } from "./project-file-modal";
|
|
15
16
|
|
|
16
17
|
const MENU_WIDTH = 220;
|
|
@@ -83,6 +84,7 @@ function DropdownSelect({
|
|
|
83
84
|
const buttonRef = useRef<View>(null);
|
|
84
85
|
const isOpen = menuCtx?.openMenuId === node.id;
|
|
85
86
|
const selectedOption = node.options.find((opt) => opt.value === value);
|
|
87
|
+
const { t } = useTranslation(messages);
|
|
86
88
|
|
|
87
89
|
return (
|
|
88
90
|
<View style={styles.fieldContainer}>
|
|
@@ -111,7 +113,7 @@ function DropdownSelect({
|
|
|
111
113
|
{ color: selectedOption ? theme.colors.foreground : theme.colors.foregroundMuted },
|
|
112
114
|
]}
|
|
113
115
|
>
|
|
114
|
-
{selectedOption ? selectedOption.label : "
|
|
116
|
+
{selectedOption ? selectedOption.label : t("dropdown.placeholder")}
|
|
115
117
|
</Text>
|
|
116
118
|
<Text style={[styles.dropdownChevron, { color: theme.colors.foregroundMuted }]}>
|
|
117
119
|
{isOpen ? "▲" : "▼"}
|
|
@@ -143,6 +145,7 @@ function DropdownMultiSelect({
|
|
|
143
145
|
const buttonRef = useRef<View>(null);
|
|
144
146
|
const isOpen = menuCtx?.openMenuId === node.id;
|
|
145
147
|
const selectedOptions = node.options.filter((opt) => value.includes(opt.value));
|
|
148
|
+
const { t } = useTranslation(messages);
|
|
146
149
|
|
|
147
150
|
return (
|
|
148
151
|
<View style={styles.fieldContainer}>
|
|
@@ -174,7 +177,7 @@ function DropdownMultiSelect({
|
|
|
174
177
|
>
|
|
175
178
|
{selectedOptions.length > 0
|
|
176
179
|
? selectedOptions.map((o) => o.label).join(", ")
|
|
177
|
-
: (node.placeholder ?? "
|
|
180
|
+
: (node.placeholder ?? t("dropdown.multiPlaceholder"))}
|
|
178
181
|
</Text>
|
|
179
182
|
<Text style={[styles.dropdownChevron, { color: theme.colors.foregroundMuted }]}>
|
|
180
183
|
{isOpen ? "▲" : "▼"}
|
|
@@ -205,6 +208,7 @@ function FilePickerField({
|
|
|
205
208
|
onChange: (val: string) => void;
|
|
206
209
|
}) {
|
|
207
210
|
const [modalVisible, setModalVisible] = useState(false);
|
|
211
|
+
const { t } = useTranslation(messages);
|
|
208
212
|
const colors = theme.colors;
|
|
209
213
|
const cleanExts = normalizeExtensions(node.extensions, node.extension);
|
|
210
214
|
|
|
@@ -239,9 +243,7 @@ function FilePickerField({
|
|
|
239
243
|
onChangeText={onChange}
|
|
240
244
|
placeholder={
|
|
241
245
|
node.placeholder ||
|
|
242
|
-
(node.pickerType === "folder"
|
|
243
|
-
? "Enter or select folder..."
|
|
244
|
-
: "Enter or select file...")
|
|
246
|
+
(node.pickerType === "folder" ? t("file.folderPlaceholder") : t("file.filePlaceholder"))
|
|
245
247
|
}
|
|
246
248
|
placeholderTextColor={colors.foregroundMuted}
|
|
247
249
|
editable={!disabled}
|
|
@@ -262,14 +264,14 @@ function FilePickerField({
|
|
|
262
264
|
onPress={() => setModalVisible(true)}
|
|
263
265
|
style={[styles.pickerBrowseBtn, { backgroundColor: colors.surface1, borderColor: colors.border }]}
|
|
264
266
|
>
|
|
265
|
-
<Text style={{ color: colors.foreground, fontSize: 12, fontWeight: "500" }}>
|
|
267
|
+
<Text style={{ color: colors.foreground, fontSize: 12, fontWeight: "500" }}>{t("file.browse")}</Text>
|
|
266
268
|
</Pressable>
|
|
267
269
|
</View>
|
|
268
270
|
</View>
|
|
269
271
|
|
|
270
272
|
{value ? (
|
|
271
273
|
<View style={[styles.pathInspector, { backgroundColor: colors.surface1, borderColor: colors.border }]}>
|
|
272
|
-
<Text style={[styles.pathInspectorLabel, { color: colors.foregroundMuted }]}>
|
|
274
|
+
<Text style={[styles.pathInspectorLabel, { color: colors.foregroundMuted }]}>{t("file.fullPath")}</Text>
|
|
273
275
|
<ScrollView
|
|
274
276
|
horizontal
|
|
275
277
|
showsHorizontalScrollIndicator={true}
|
|
@@ -608,6 +610,7 @@ export const PfrFormView: React.FC<PfrFormViewProps> = ({ form, theme, agentId,
|
|
|
608
610
|
// The dropdown menu floats over the form at its root (measured position), so opening it never moves anything.
|
|
609
611
|
const [menuPos, setMenuPos] = useState<OpenDropdownMenu | null>(null);
|
|
610
612
|
const rootRef = useRef<View>(null);
|
|
613
|
+
const { t } = useTranslation(messages);
|
|
611
614
|
|
|
612
615
|
const toggleMenu = (
|
|
613
616
|
id: string,
|
|
@@ -683,13 +686,13 @@ export const PfrFormView: React.FC<PfrFormViewProps> = ({ form, theme, agentId,
|
|
|
683
686
|
/>
|
|
684
687
|
{status === "submitted" && (
|
|
685
688
|
<View style={[styles.submittedBanner, { backgroundColor: theme.colors.surface2, borderColor: theme.colors.statusSuccess, borderWidth: 1 }]}>
|
|
686
|
-
<Text style={[styles.submittedText, { color: theme.colors.statusSuccess }]}
|
|
689
|
+
<Text style={[styles.submittedText, { color: theme.colors.statusSuccess }]}>{t("form.submitted")}</Text>
|
|
687
690
|
</View>
|
|
688
691
|
)}
|
|
689
692
|
{menuPos ? (
|
|
690
693
|
<>
|
|
691
694
|
<Pressable
|
|
692
|
-
accessibilityLabel="
|
|
695
|
+
accessibilityLabel={t("dropdown.close")}
|
|
693
696
|
onPress={() => setMenuPos(null)}
|
|
694
697
|
style={[StyleSheet.absoluteFill, { zIndex: 999 }]}
|
|
695
698
|
/>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
2
|
-
import { useSettings } from "@getpaseo/plugin/client";
|
|
2
|
+
import { useSettings, useTranslation } from "@getpaseo/plugin/client";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { ActivityIndicator, Pressable, StyleSheet, Switch, Text, View } from "react-native";
|
|
5
5
|
import { MAX_AUTO_FIX_RETRIES, pfrSettings } from "../shared/settings";
|
|
6
|
+
import { messages } from "./messages";
|
|
6
7
|
|
|
7
8
|
export const PfrSettingsScreen: React.FC<PluginSurfaceProps> = ({ theme }) => {
|
|
8
9
|
const settings = useSettings(pfrSettings);
|
|
10
|
+
const { t } = useTranslation(messages);
|
|
9
11
|
const { colors } = theme;
|
|
10
12
|
|
|
11
13
|
if (settings.status === "loading") {
|
|
@@ -20,20 +22,20 @@ export const PfrSettingsScreen: React.FC<PluginSurfaceProps> = ({ theme }) => {
|
|
|
20
22
|
return (
|
|
21
23
|
<View style={styles.container}>
|
|
22
24
|
<Text style={[styles.errorText, { color: colors.statusDanger }]}>
|
|
23
|
-
|
|
25
|
+
{t("settings.loadError", { error: settings.error })}
|
|
24
26
|
</Text>
|
|
25
27
|
<Pressable
|
|
26
28
|
style={[styles.stepButton, styles.retryButton, { borderColor: colors.border, backgroundColor: colors.surface2 }]}
|
|
27
29
|
onPress={() => void settings.reload()}
|
|
28
30
|
>
|
|
29
|
-
<Text style={[styles.stepText, { color: colors.foreground }]}>
|
|
31
|
+
<Text style={[styles.stepText, { color: colors.foreground }]}>{t("settings.retry")}</Text>
|
|
30
32
|
</Pressable>
|
|
31
33
|
{settings.status === "invalid" && (
|
|
32
34
|
<Pressable
|
|
33
35
|
style={[styles.stepButton, styles.retryButton, { borderColor: colors.border, backgroundColor: colors.surface2 }]}
|
|
34
36
|
onPress={() => void settings.reset()}
|
|
35
37
|
>
|
|
36
|
-
<Text style={[styles.stepText, { color: colors.foreground }]}>
|
|
38
|
+
<Text style={[styles.stepText, { color: colors.foreground }]}>{t("settings.resetDefaults")}</Text>
|
|
37
39
|
</Pressable>
|
|
38
40
|
)}
|
|
39
41
|
</View>
|
|
@@ -53,18 +55,14 @@ export const PfrSettingsScreen: React.FC<PluginSurfaceProps> = ({ theme }) => {
|
|
|
53
55
|
|
|
54
56
|
return (
|
|
55
57
|
<View style={styles.container}>
|
|
56
|
-
<Text style={[styles.title, { color: colors.foreground }]}>
|
|
57
|
-
<Text style={[styles.description, { color: colors.foregroundMuted }]}>
|
|
58
|
-
When an agent replies with a pfr-form block that fails validation, ask it to re-send a corrected block
|
|
59
|
-
automatically. This sets how many times in a row the agent is asked; 0 turns automatic fixes off. The
|
|
60
|
-
"Ask agent to fix" button on an error box always works.
|
|
61
|
-
</Text>
|
|
58
|
+
<Text style={[styles.title, { color: colors.foreground }]}>{t("settings.autoFixTitle")}</Text>
|
|
59
|
+
<Text style={[styles.description, { color: colors.foregroundMuted }]}>{t("settings.autoFixDescription")}</Text>
|
|
62
60
|
|
|
63
61
|
<View style={styles.row}>
|
|
64
|
-
<Text style={[styles.label, { color: colors.foreground }]}>
|
|
62
|
+
<Text style={[styles.label, { color: colors.foreground }]}>{t("settings.maxRetries")}</Text>
|
|
65
63
|
<View style={styles.stepper}>
|
|
66
64
|
<Pressable
|
|
67
|
-
accessibilityLabel="
|
|
65
|
+
accessibilityLabel={t("settings.decreaseRetries")}
|
|
68
66
|
disabled={value <= 0 || settings.saving}
|
|
69
67
|
style={stepStyle(value <= 0 || settings.saving)}
|
|
70
68
|
onPress={() => change(value - 1)}
|
|
@@ -73,7 +71,7 @@ export const PfrSettingsScreen: React.FC<PluginSurfaceProps> = ({ theme }) => {
|
|
|
73
71
|
</Pressable>
|
|
74
72
|
<Text style={[styles.value, { color: colors.foreground }]}>{value}</Text>
|
|
75
73
|
<Pressable
|
|
76
|
-
accessibilityLabel="
|
|
74
|
+
accessibilityLabel={t("settings.increaseRetries")}
|
|
77
75
|
disabled={value >= MAX_AUTO_FIX_RETRIES || settings.saving}
|
|
78
76
|
style={stepStyle(value >= MAX_AUTO_FIX_RETRIES || settings.saving)}
|
|
79
77
|
onPress={() => change(value + 1)}
|
|
@@ -84,26 +82,23 @@ export const PfrSettingsScreen: React.FC<PluginSurfaceProps> = ({ theme }) => {
|
|
|
84
82
|
</View>
|
|
85
83
|
|
|
86
84
|
<Text style={[styles.hint, { color: colors.foregroundMuted }]}>
|
|
87
|
-
{value === 0 ? "
|
|
88
|
-
{settings.saving ?
|
|
85
|
+
{value === 0 ? t("settings.hintOff") : t("settings.hint", { count: value })}
|
|
86
|
+
{settings.saving ? ` ${t("settings.saving")}` : ""}
|
|
89
87
|
</Text>
|
|
90
88
|
|
|
91
|
-
<Text style={[styles.title, { color: colors.foreground }]}>
|
|
92
|
-
<Text style={[styles.description, { color: colors.foregroundMuted }]}>
|
|
93
|
-
By default the agent is only told it can use pfr-form. Turn this on to require it to ask for choices,
|
|
94
|
-
parameters, and confirmations with a form instead of plain text. Applies to agents created after the change.
|
|
95
|
-
</Text>
|
|
89
|
+
<Text style={[styles.title, { color: colors.foreground }]}>{t("settings.preferFormsTitle")}</Text>
|
|
90
|
+
<Text style={[styles.description, { color: colors.foregroundMuted }]}>{t("settings.preferFormsDescription")}</Text>
|
|
96
91
|
<View style={styles.row}>
|
|
97
|
-
<Text style={[styles.label, { color: colors.foreground }]}>
|
|
92
|
+
<Text style={[styles.label, { color: colors.foreground }]}>{t("settings.requireForms")}</Text>
|
|
98
93
|
<Switch
|
|
99
|
-
accessibilityLabel="
|
|
94
|
+
accessibilityLabel={t("settings.requireForms")}
|
|
100
95
|
value={settings.values.preferForms}
|
|
101
96
|
disabled={settings.saving}
|
|
102
97
|
onValueChange={(next) => void settings.save({ ...settings.values, preferForms: next }, settings.revision)}
|
|
103
98
|
/>
|
|
104
99
|
</View>
|
|
105
100
|
{settings.saveError && (
|
|
106
|
-
<Text style={[styles.errorText, { color: colors.statusDanger }]}>
|
|
101
|
+
<Text style={[styles.errorText, { color: colors.statusDanger }]}>{t("settings.saveError", { error: settings.saveError })}</Text>
|
|
107
102
|
)}
|
|
108
103
|
</View>
|
|
109
104
|
);
|
package/index.client.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { Platform } from "react-native";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { hasFormBlock } from "./shared/parser";
|
|
7
7
|
import { MessageContainer } from "./client/container";
|
|
8
|
+
import { messages } from "./client/messages";
|
|
8
9
|
import { PfrSettingsScreen } from "./client/settings-screen";
|
|
9
10
|
import { showDemoForm } from "./shared/demo";
|
|
10
11
|
import { PFR_PROMPT_INSTRUCTION } from "./shared/prompt";
|
|
@@ -78,7 +79,7 @@ export default function contribute(client: PluginClientContext) {
|
|
|
78
79
|
// Slash command to enable pfr-form in an existing agent session
|
|
79
80
|
client.addSlashCommand({
|
|
80
81
|
name: "form",
|
|
81
|
-
description: "
|
|
82
|
+
description: messages.message("slash.form"),
|
|
82
83
|
argumentHint: "[task]",
|
|
83
84
|
context: "agent",
|
|
84
85
|
async onSubmit({ args, paseo, agent }) {
|
|
@@ -90,7 +91,7 @@ export default function contribute(client: PluginClientContext) {
|
|
|
90
91
|
// Slash command to show a sample form
|
|
91
92
|
client.addSlashCommand({
|
|
92
93
|
name: "form-demo",
|
|
93
|
-
description: "
|
|
94
|
+
description: messages.message("slash.formDemo"),
|
|
94
95
|
argumentHint: "",
|
|
95
96
|
context: "agent",
|
|
96
97
|
async onSubmit({ rpc, agent }) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-form-render",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Interactive forms, dashboards, and metric displays in Paseo conversations using pfr-form code fences.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"e2e": "npm --prefix e2e run e2e --"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@getpaseo/client": "^0.
|
|
25
|
-
"@getpaseo/plugin": "^0.
|
|
24
|
+
"@getpaseo/client": "^0.13.0",
|
|
25
|
+
"@getpaseo/plugin": "^0.13.0",
|
|
26
26
|
"@tanstack/react-query": "^5.90.11",
|
|
27
27
|
"@types/node": "^22.20.4",
|
|
28
28
|
"@types/react": "~19.2.0",
|
package/paseo-plugin.json
CHANGED
package/server/auto-fix.ts
CHANGED
|
@@ -147,10 +147,29 @@ export function registerAutoFix(server: PluginServerContext, settings: PfrSettin
|
|
|
147
147
|
return (await readPfrSettings(settings)).autoFixMaxRetries;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/** The newest history of the agent, or null when it cannot be read (the caller then sends nothing). */
|
|
151
|
+
async function readTail(agent: {
|
|
152
|
+
timeline: { refetch(options: { direction: "tail"; limit: number; projection: "projected" }): Promise<unknown> };
|
|
153
|
+
}): Promise<TimelineEntry[] | null> {
|
|
154
|
+
try {
|
|
155
|
+
const history = timelineFromPayload(
|
|
156
|
+
await agent.timeline.refetch({ direction: "tail", limit: HISTORY_LIMIT, projection: "projected" }),
|
|
157
|
+
);
|
|
158
|
+
return history?.items ?? null;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.warn("[paseo-form-render] could not fetch agent history, skipping auto-fix", error);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
150
165
|
return server.on("agent.turn_ended", async (event, { paseo, signal }) => {
|
|
151
166
|
try {
|
|
167
|
+
const agent = paseo.agents.ref(event.agent.id);
|
|
168
|
+
// Paseo 0.13 sends an always-empty `timeline` on this event (deprecated), so the finished turn is read from history.
|
|
169
|
+
const turnTimeline = event.timeline.length > 0 ? event.timeline : await readTail(agent);
|
|
170
|
+
if (!turnTimeline) return;
|
|
152
171
|
const turn: TurnInput = {
|
|
153
|
-
turnTimeline
|
|
172
|
+
turnTimeline,
|
|
154
173
|
outcome: event.outcome,
|
|
155
174
|
maxRetries: await readMaxRetries(),
|
|
156
175
|
};
|
|
@@ -161,7 +180,6 @@ export function registerAutoFix(server: PluginServerContext, settings: PfrSettin
|
|
|
161
180
|
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
162
181
|
if (signal.aborted) return;
|
|
163
182
|
|
|
164
|
-
const agent = paseo.agents.ref(event.agent.id);
|
|
165
183
|
let history: ReturnType<typeof timelineFromPayload> = null;
|
|
166
184
|
try {
|
|
167
185
|
history = timelineFromPayload(
|