paseo-aydo 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.
Files changed (178) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +142 -2
  3. package/client/AttachmentPill.tsx +145 -0
  4. package/client/Avatar.tsx +94 -0
  5. package/client/BotDialogs.tsx +364 -0
  6. package/client/BotSidebar.tsx +831 -0
  7. package/client/BotsNav.tsx +65 -0
  8. package/client/BotsSurface.tsx +907 -0
  9. package/client/ChatPane.tsx +237 -0
  10. package/client/Markdown.tsx +315 -0
  11. package/client/Splash.tsx +38 -0
  12. package/client/approvals.ts +11 -0
  13. package/client/auto-start.ts +9 -0
  14. package/client/chat/Composer.tsx +722 -0
  15. package/client/chat/FindBar.tsx +115 -0
  16. package/client/chat/Permission.tsx +152 -0
  17. package/client/chat/Stream.tsx +478 -0
  18. package/client/chat/composer/CommandMenu.tsx +94 -0
  19. package/client/chat/composer/ContextMeter.tsx +105 -0
  20. package/client/chat/composer/drafts.ts +103 -0
  21. package/client/chat/composer/height.ts +51 -0
  22. package/client/chat/composer/logic.ts +309 -0
  23. package/client/chat/composer/storage.ts +97 -0
  24. package/client/chat/follow.ts +26 -0
  25. package/client/chat/stream/ApprovalCard.tsx +154 -0
  26. package/client/chat/stream/PlanCard.tsx +79 -0
  27. package/client/chat/stream/ProposalCard.tsx +309 -0
  28. package/client/chat/stream/QuestionForm.tsx +313 -0
  29. package/client/chat/stream/RoutineRunCard.tsx +71 -0
  30. package/client/chat/stream/details.tsx +358 -0
  31. package/client/chat/stream/model.ts +469 -0
  32. package/client/chat/stream/question.ts +103 -0
  33. package/client/chat/stream/rows.tsx +465 -0
  34. package/client/chat/stream/ui.tsx +331 -0
  35. package/client/chat-engine.ts +20 -0
  36. package/client/data.ts +176 -0
  37. package/client/draft.ts +16 -0
  38. package/client/i18n/messages.ts +5 -0
  39. package/client/i18n/use-t.ts +9 -0
  40. package/client/intent.ts +16 -0
  41. package/client/keyboard.ts +92 -0
  42. package/client/library/LibraryList.tsx +180 -0
  43. package/client/library/LibraryView.tsx +287 -0
  44. package/client/library/McpPage.tsx +158 -0
  45. package/client/library/McpSheets.tsx +206 -0
  46. package/client/library/SkillPage.tsx +159 -0
  47. package/client/library/SkillSheets.tsx +197 -0
  48. package/client/library/parts.tsx +86 -0
  49. package/client/native-agent-view.ts +28 -0
  50. package/client/native.ts +177 -0
  51. package/client/nav-hosts.ts +14 -0
  52. package/client/navigation.ts +33 -0
  53. package/client/panel/AgentSections.tsx +366 -0
  54. package/client/panel/ApprovalsList.tsx +62 -0
  55. package/client/panel/BotPanel.tsx +306 -0
  56. package/client/panel/IdentitySection.tsx +137 -0
  57. package/client/panel/KnowledgeSections.tsx +389 -0
  58. package/client/panel/LibraryPicker.tsx +63 -0
  59. package/client/panel/MaintenanceSection.tsx +103 -0
  60. package/client/panel/MemoryActivity.tsx +160 -0
  61. package/client/panel/OverviewSections.tsx +205 -0
  62. package/client/panel/RememberedProjects.tsx +42 -0
  63. package/client/panel/RoutinesSection.tsx +490 -0
  64. package/client/panel/controls.tsx +517 -0
  65. package/client/panel/maintenance-text.ts +32 -0
  66. package/client/panel/picture.tsx +76 -0
  67. package/client/paseo.ts +11 -0
  68. package/client/sent-attachments.ts +31 -0
  69. package/client/settings/BotsSettings.tsx +219 -0
  70. package/client/speech.ts +97 -0
  71. package/client/teams/TeamMap.tsx +191 -0
  72. package/client/teams/TeamSheet.tsx +136 -0
  73. package/client/teams/TeamTabs.tsx +134 -0
  74. package/client/theme.ts +41 -0
  75. package/client/typography.ts +123 -0
  76. package/client/ui/Columns.tsx +120 -0
  77. package/client/ui/Menu.tsx +236 -0
  78. package/client/ui/SearchableSelect.tsx +158 -0
  79. package/client/ui/Tooltip.ts +197 -0
  80. package/client/useBotSettings.ts +48 -0
  81. package/client/useChat.ts +194 -0
  82. package/client/web.ts +444 -0
  83. package/client/workspace-choice.ts +56 -0
  84. package/index.client.tsx +96 -0
  85. package/index.server.ts +296 -0
  86. package/package.json +44 -4
  87. package/paseo-plugin.json +7 -0
  88. package/server/activity.ts +123 -0
  89. package/server/approvals-file.ts +46 -0
  90. package/server/approvals-store.ts +181 -0
  91. package/server/approvals.ts +221 -0
  92. package/server/ask-store.ts +102 -0
  93. package/server/asks.ts +326 -0
  94. package/server/bot-home.ts +121 -0
  95. package/server/chat-send.ts +54 -0
  96. package/server/chats.ts +61 -0
  97. package/server/commands.ts +86 -0
  98. package/server/file-tools.ts +411 -0
  99. package/server/history-files.ts +82 -0
  100. package/server/host-caps.ts +15 -0
  101. package/server/host.ts +85 -0
  102. package/server/identity.ts +69 -0
  103. package/server/journal.ts +174 -0
  104. package/server/library.ts +200 -0
  105. package/server/maintenance.ts +46 -0
  106. package/server/managed-files.ts +93 -0
  107. package/server/mcp-probe.ts +279 -0
  108. package/server/mcp-sources.ts +34 -0
  109. package/server/memory.ts +194 -0
  110. package/server/paseo.ts +6 -0
  111. package/server/project-grants.ts +69 -0
  112. package/server/prompt.ts +91 -0
  113. package/server/proposal-notices.ts +72 -0
  114. package/server/proposals.ts +74 -0
  115. package/server/relay-state.ts +38 -0
  116. package/server/relay.ts +159 -0
  117. package/server/reports.ts +199 -0
  118. package/server/scheduler.ts +335 -0
  119. package/server/setup-files.ts +85 -0
  120. package/server/share.ts +236 -0
  121. package/server/tidy.ts +285 -0
  122. package/server/tools/approvals.ts +60 -0
  123. package/server/tools/bots.ts +231 -0
  124. package/server/tools/chats.ts +76 -0
  125. package/server/tools/index.ts +11 -0
  126. package/server/tools/mcp.ts +92 -0
  127. package/server/tools/routines.ts +27 -0
  128. package/server/tools/setup.ts +53 -0
  129. package/server/tools/skills.ts +23 -0
  130. package/server/tools/workspaces.ts +26 -0
  131. package/server/turn-facts.ts +48 -0
  132. package/server/turn-timeline.ts +47 -0
  133. package/server/uploads.ts +13 -0
  134. package/server/workspaces.ts +170 -0
  135. package/shared/activity.ts +234 -0
  136. package/shared/approvals.ts +108 -0
  137. package/shared/attachments.ts +81 -0
  138. package/shared/avatar.ts +567 -0
  139. package/shared/bot-tools.ts +73 -0
  140. package/shared/bot.ts +794 -0
  141. package/shared/changes.ts +847 -0
  142. package/shared/chat.ts +237 -0
  143. package/shared/commands.ts +9 -0
  144. package/shared/diff.ts +96 -0
  145. package/shared/file-tools.ts +60 -0
  146. package/shared/folder-access.ts +36 -0
  147. package/shared/groups.ts +162 -0
  148. package/shared/host-caps.ts +16 -0
  149. package/shared/identity.ts +106 -0
  150. package/shared/layout.ts +27 -0
  151. package/shared/library.ts +161 -0
  152. package/shared/locales/en.json +1340 -0
  153. package/shared/locales/zh-CN.json +1312 -0
  154. package/shared/markdown.ts +888 -0
  155. package/shared/memory-maintenance.ts +167 -0
  156. package/shared/pages.ts +19 -0
  157. package/shared/paseo-tools.ts +30 -0
  158. package/shared/pixel.ts +156 -0
  159. package/shared/playbooks.ts +36 -0
  160. package/shared/proposals.ts +70 -0
  161. package/shared/reports.ts +157 -0
  162. package/shared/routines.ts +380 -0
  163. package/shared/rpc.ts +402 -0
  164. package/shared/setup.ts +89 -0
  165. package/shared/sidebar.ts +125 -0
  166. package/shared/skills.ts +75 -0
  167. package/shared/speech.ts +32 -0
  168. package/shared/team-logo.ts +325 -0
  169. package/shared/templates.ts +91 -0
  170. package/shared/time.ts +16 -0
  171. package/shared/tools-mode.ts +37 -0
  172. package/shared/tools.ts +481 -0
  173. package/shared/transcript.ts +45 -0
  174. package/shared/translate.ts +38 -0
  175. package/shared/uuid.ts +11 -0
  176. package/shared/version.ts +5 -0
  177. package/shared/workspaces.ts +63 -0
  178. package/tsconfig.json +25 -0
@@ -0,0 +1,364 @@
1
+ import type { PluginTheme } from "@getpaseo/plugin";
2
+ import { useRpc } from "@getpaseo/plugin/client";
3
+ import { Icon, Modal, TextInput, copyText, useToast } from "@getpaseo/plugin/client/react-native";
4
+ import { SettingsAction, SettingsCard, SettingsSection, SettingsSwitch } from "@getpaseo/plugin/client/ui";
5
+ import { useEffect, useState } from "react";
6
+ import { Pressable, Text, View } from "react-native";
7
+ import type { Bot, BotAvatar, Preset } from "../shared/bot";
8
+ import { exportBotRpc } from "../shared/rpc";
9
+ import { BOT_TEMPLATES, templateText, type BotTemplate } from "../shared/templates";
10
+ import type { Translate } from "../shared/translate";
11
+ import { Avatar } from "./Avatar";
12
+ import { errorText, MONO_FONT, MONO_PROPS, nativeTokens, useHover } from "./native";
13
+ import { Button, FormTextArea, SheetActions } from "./panel/controls";
14
+ import { useT } from "./i18n/use-t";
15
+ import type { MenuEntry } from "./ui/Menu";
16
+ import { code, codeLine, ui } from "./typography";
17
+
18
+ type Colors = PluginTheme["colors"];
19
+
20
+ // ------------------------------------------------------------------ new bot
21
+
22
+ export function NewBotDialog({
23
+ colors,
24
+ presets,
25
+ onClose,
26
+ onCreate,
27
+ onImport,
28
+ }: {
29
+ colors: Colors;
30
+ presets: readonly Preset[];
31
+ onClose(): void;
32
+ onCreate(start?: { template?: BotTemplate; preset?: Preset }): void;
33
+ onImport(json: string): Promise<void>;
34
+ }) {
35
+ const { t } = useT();
36
+ const [json, setJson] = useState("");
37
+ const [importing, setImporting] = useState(false);
38
+ const toast = useToast();
39
+ return (
40
+ <Modal title={t("host.newBot")} open onOpenChange={(open) => !open && onClose()}>
41
+ <Modal.Content>
42
+ <SettingsSection title={t("dialogs.newBot.startFrom")}>
43
+ <SettingsCard>
44
+ <StartRow colors={colors} label={t("dialogs.newBot.blank")} hint={t("dialogs.newBot.blankHint")} onPress={() => onCreate()} />
45
+ {BOT_TEMPLATES.map((template) => {
46
+ const text = templateText(template, t);
47
+ return <StartRow key={template.id} colors={colors} label={text.title} hint={text.description} avatar={{ seed: template.avatarSeed }} onPress={() => onCreate({ template })} />;
48
+ })}
49
+ </SettingsCard>
50
+ </SettingsSection>
51
+ {presets.length ? (
52
+ <SettingsSection title={t("dialogs.newBot.presets")} info={t("dialogs.newBot.presetsInfo")}>
53
+ <SettingsCard>
54
+ {presets.map((preset) => (
55
+ <StartRow key={preset.id} colors={colors} label={preset.name} hint={preset.title || preset.description} avatar={preset.avatar} onPress={() => onCreate({ preset })} />
56
+ ))}
57
+ </SettingsCard>
58
+ </SettingsSection>
59
+ ) : null}
60
+ <SettingsSection title={t("dialogs.newBot.importButton")} info={t("dialogs.newBot.importInfo")}>
61
+ <FormTextArea
62
+ colors={colors}
63
+ monospace
64
+ accessibilityLabel={t("dialogs.newBot.importLabel")}
65
+ value={json}
66
+ onChangeText={setJson}
67
+ autoCapitalize="none"
68
+ autoCorrect={false}
69
+ minHeight={120}
70
+ placeholder='{"format": "aydo", …}'
71
+ />
72
+ <SheetActions>
73
+ <Button
74
+ colors={colors}
75
+ variant="default"
76
+ label={importing ? t("dialogs.newBot.importing") : t("dialogs.newBot.importButton")}
77
+ loading={importing}
78
+ disabled={!json.trim()}
79
+ onPress={() => {
80
+ setImporting(true);
81
+ void onImport(json)
82
+ .catch((error: unknown) => toast.error(errorText(error)))
83
+ .finally(() => setImporting(false));
84
+ }}
85
+ />
86
+ </SheetActions>
87
+ </SettingsSection>
88
+ </Modal.Content>
89
+ </Modal>
90
+ );
91
+ }
92
+
93
+ /** A whole-row pressable settings row (settings card geometry: 16 padding, 14/12 text). */
94
+ function StartRow({ colors, label, hint, avatar, onPress }: { colors: Colors; label: string; hint: string; avatar?: Partial<BotAvatar> & { seed: string }; onPress(): void }) {
95
+ const { t } = useT();
96
+ const { hovered, hoverProps } = useHover();
97
+ return (
98
+ <Pressable
99
+ accessibilityRole="button"
100
+ accessibilityLabel={t("dialogs.createLabel", { label })}
101
+ accessibilityHint={hint}
102
+ onPress={onPress}
103
+ {...hoverProps}
104
+ style={({ pressed }) => ({
105
+ flexDirection: "row",
106
+ alignItems: "center",
107
+ gap: 12,
108
+ paddingVertical: 16,
109
+ paddingHorizontal: 16,
110
+ backgroundColor: pressed || hovered ? colors.surface2 : "transparent",
111
+ })}
112
+ >
113
+ {avatar ? (
114
+ <Avatar avatar={avatar} size={28} dark={nativeTokens(colors).dark} />
115
+ ) : (
116
+ <View style={{ width: 28, height: 28, borderRadius: 14, alignItems: "center", justifyContent: "center", backgroundColor: colors.surface2 }}>
117
+ <Icon name="Plus" size={14} color={colors.foregroundMuted} />
118
+ </View>
119
+ )}
120
+ <View style={{ flex: 1, minWidth: 0 }}>
121
+ <Text numberOfLines={1} style={{ fontSize: ui(14), color: colors.foreground }}>
122
+ {label}
123
+ </Text>
124
+ {hint ? (
125
+ <Text numberOfLines={2} style={{ marginTop: 4, fontSize: ui(12), color: colors.foregroundMuted }}>
126
+ {hint}
127
+ </Text>
128
+ ) : null}
129
+ </View>
130
+ <Icon name="ChevronRight" size={14} color={hovered ? colors.foreground : colors.foregroundMuted} />
131
+ </Pressable>
132
+ );
133
+ }
134
+
135
+ // ------------------------------------------------------------------ rename
136
+
137
+ interface RenameDialogProps {
138
+ colors: Colors;
139
+ title: string;
140
+ initialValue: string;
141
+ placeholder?: string;
142
+ submitLabel?: string;
143
+ onClose(): void;
144
+ onSubmit(value: string): Promise<void> | void;
145
+ }
146
+
147
+ /**
148
+ * Paseo's AdaptiveRenameModal (components/rename-modal.tsx): the current name selected
149
+ * in one input, the error inline, Cancel and Rename side by side. It stays open on errors.
150
+ */
151
+ export function RenameDialog({ colors, title, initialValue, placeholder, submitLabel, onClose, onSubmit }: RenameDialogProps) {
152
+ const { t } = useT();
153
+ const [draft, setDraft] = useState(initialValue);
154
+ const [error, setError] = useState<string | null>(null);
155
+ const [pending, setPending] = useState(false);
156
+ const invalid = !draft.trim();
157
+ const submitDisabled = pending || draft === initialValue || invalid;
158
+
159
+ const submit = async () => {
160
+ if (pending || draft === initialValue) return;
161
+ if (invalid) {
162
+ setError(t("dialogs.rename.required"));
163
+ return;
164
+ }
165
+ try {
166
+ setPending(true);
167
+ await onSubmit(draft.trim());
168
+ setPending(false);
169
+ onClose();
170
+ } catch (err) {
171
+ setPending(false);
172
+ setError(err instanceof Error && err.message ? err.message : t("dialogs.rename.unableToSave"));
173
+ }
174
+ };
175
+ const cancel = () => {
176
+ if (!pending) onClose();
177
+ };
178
+
179
+ return (
180
+ <Modal title={title} open onOpenChange={(open) => !open && cancel()}>
181
+ <Modal.Content>
182
+ <View style={{ gap: 12, paddingBottom: 8 }}>
183
+ <TextInput
184
+ accessibilityLabel={title}
185
+ defaultValue={initialValue}
186
+ onChangeText={(value) => {
187
+ setDraft(value);
188
+ setError(null);
189
+ }}
190
+ placeholder={placeholder}
191
+ placeholderTextColor={colors.foregroundMuted}
192
+ autoFocus
193
+ selectTextOnFocus
194
+ autoCapitalize="none"
195
+ autoCorrect={false}
196
+ editable={!pending}
197
+ returnKeyType="done"
198
+ onSubmitEditing={() => void submit()}
199
+ style={{
200
+ backgroundColor: colors.surface0,
201
+ color: colors.foreground,
202
+ paddingVertical: 12,
203
+ paddingHorizontal: 12,
204
+ borderRadius: 6,
205
+ borderWidth: 1,
206
+ borderColor: colors.border,
207
+ fontSize: ui(14),
208
+ }}
209
+ />
210
+ {error ? <Text accessibilityRole="alert" style={{ color: colors.statusDanger, fontSize: ui(14) }}>{error}</Text> : null}
211
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
212
+ <DialogButton colors={colors} label={t("dialogs.cancel")} disabled={pending} onPress={cancel} />
213
+ <DialogButton colors={colors} label={pending ? t("dialogs.saving") : (submitLabel ?? t("dialogs.rename.submit"))} primary disabled={submitDisabled} onPress={() => void submit()} />
214
+ </View>
215
+ </View>
216
+ </Modal.Content>
217
+ </Modal>
218
+ );
219
+ }
220
+
221
+ /** Paseo's Button size="sm": secondary (surface3) or default (accent). */
222
+ function DialogButton({ colors, label, primary, disabled, onPress }: { colors: Colors; label: string; primary?: boolean; disabled?: boolean; onPress(): void }) {
223
+ const fill = primary ? colors.accent : nativeTokens(colors).surface3;
224
+ return (
225
+ <Pressable
226
+ accessibilityRole="button"
227
+ accessibilityLabel={label}
228
+ accessibilityState={{ disabled: !!disabled }}
229
+ disabled={disabled}
230
+ onPress={onPress}
231
+ style={({ pressed }) => ({
232
+ flex: 1,
233
+ minHeight: 32,
234
+ paddingHorizontal: 12,
235
+ borderRadius: 12,
236
+ borderWidth: 1,
237
+ borderColor: fill,
238
+ backgroundColor: fill,
239
+ flexDirection: "row",
240
+ alignItems: "center",
241
+ justifyContent: "center",
242
+ opacity: disabled ? 0.5 : pressed ? 0.85 : 1,
243
+ })}
244
+ >
245
+ <Text style={{ fontSize: ui(14), color: primary ? colors.accentForeground : colors.foreground }}>{label}</Text>
246
+ </Pressable>
247
+ );
248
+ }
249
+
250
+ // ------------------------------------------------------------------ menus
251
+
252
+ export interface BotMenuActions {
253
+ bot: Bot;
254
+ /** Set when the host exposes navigation and the bot's workspace is known. */
255
+ onOpenInPaseo?: () => void;
256
+ onOpenSettings(): void;
257
+ onTogglePin(): void;
258
+ onMoveUp?: () => void;
259
+ onMoveDown?: () => void;
260
+ onRename(): void;
261
+ onDuplicate(): Promise<void>;
262
+ onExport(): void;
263
+ onCopyId(): void;
264
+ onSaveAsPreset(): void;
265
+ onToggleArchive(): void;
266
+ onDelete(): Promise<void>;
267
+ }
268
+
269
+ /** The bot menu, after Paseo's project menu (sidebar-workspace-list.tsx ProjectMenuItems). */
270
+ export function botMenuEntries(actions: BotMenuActions, t: Translate): MenuEntry[] {
271
+ const { bot } = actions;
272
+ const entries: MenuEntry[] = [{ label: t("dialogs.menu.openBotSettings"), icon: "Settings", onSelect: actions.onOpenSettings }];
273
+ if (actions.onOpenInPaseo) entries.push({ label: t("dialogs.menu.openInPaseo"), icon: "ExternalLink", onSelect: actions.onOpenInPaseo });
274
+ entries.push({ label: bot.pinned ? t("dialogs.menu.unpin") : t("dialogs.menu.pin"), icon: bot.pinned ? "PinOff" : "Pin", onSelect: actions.onTogglePin });
275
+ if (actions.onMoveUp || actions.onMoveDown) {
276
+ entries.push({ label: t("dialogs.menu.moveUp"), icon: "ArrowUp", disabled: !actions.onMoveUp, onSelect: () => actions.onMoveUp?.() });
277
+ entries.push({ label: t("dialogs.menu.moveDown"), icon: "ArrowDown", disabled: !actions.onMoveDown, onSelect: () => actions.onMoveDown?.() });
278
+ }
279
+ entries.push(
280
+ { label: t("dialogs.menu.renameBot"), icon: "Pencil", onSelect: actions.onRename },
281
+ { label: t("dialogs.menu.duplicate"), icon: "CopyPlus", pendingLabel: t("dialogs.menu.duplicating"), onSelect: actions.onDuplicate },
282
+ { label: t("dialogs.menu.saveAsPreset"), icon: "BookmarkPlus", onSelect: actions.onSaveAsPreset },
283
+ { label: t("dialogs.menu.export"), icon: "Share", onSelect: actions.onExport },
284
+ { label: t("dialogs.menu.copyBotId"), icon: "Copy", onSelect: actions.onCopyId },
285
+ { kind: "separator" },
286
+ { label: bot.archived ? t("dialogs.menu.unarchiveBot") : t("dialogs.menu.archiveBot"), icon: bot.archived ? "ArchiveRestore" : "Archive", onSelect: actions.onToggleArchive },
287
+ { label: t("dialogs.menu.deleteBot"), icon: "Trash2", destructive: true, pendingLabel: t("dialogs.menu.deleting"), onSelect: actions.onDelete },
288
+ );
289
+ return entries;
290
+ }
291
+
292
+ export interface ChatMenuActions {
293
+ pinned: boolean;
294
+ onCopyPath(): void;
295
+ onCopyId(): void;
296
+ onCopyTranscript(): Promise<void>;
297
+ onTogglePin(): void;
298
+ /** Present in manual order; undefined at an edge. `null` hides the pair (pinned chats, activity order). */
299
+ move: { up?: () => void; down?: () => void } | null;
300
+ onOpenInPaseo?: () => void;
301
+ onArchive(): Promise<void>;
302
+ }
303
+
304
+ /** The chat menu, after Paseo's workspace menu (sidebar/sidebar-workspace-menu.tsx). */
305
+ export function chatMenuEntries(actions: ChatMenuActions, t: Translate): MenuEntry[] {
306
+ const entries: MenuEntry[] = [
307
+ { label: t("dialogs.menu.copyPath"), icon: "Copy", onSelect: actions.onCopyPath },
308
+ { label: t("dialogs.menu.copyChatId"), icon: "Copy", onSelect: actions.onCopyId },
309
+ { label: t("dialogs.menu.copyTranscript"), icon: "FileText", pendingLabel: t("dialogs.menu.copying"), onSelect: actions.onCopyTranscript },
310
+ { label: actions.pinned ? t("dialogs.menu.unpin") : t("dialogs.menu.pin"), icon: actions.pinned ? "PinOff" : "Pin", onSelect: actions.onTogglePin },
311
+ ];
312
+ if (actions.move) {
313
+ entries.push({ label: t("dialogs.menu.moveUp"), icon: "ArrowUp", disabled: !actions.move.up, onSelect: () => actions.move?.up?.() });
314
+ entries.push({ label: t("dialogs.menu.moveDown"), icon: "ArrowDown", disabled: !actions.move.down, onSelect: () => actions.move?.down?.() });
315
+ }
316
+ if (actions.onOpenInPaseo) entries.push({ label: t("dialogs.menu.openInPaseo"), icon: "ExternalLink", onSelect: actions.onOpenInPaseo });
317
+ entries.push({ label: t("dialogs.menu.archive"), icon: "Archive", pendingLabel: t("dialogs.menu.archiving"), onSelect: actions.onArchive });
318
+ return entries;
319
+ }
320
+
321
+ // ------------------------------------------------------------------ export
322
+
323
+ export function ExportDialog({ colors, bot, onClose }: { colors: Colors; bot: Bot; onClose(): void }) {
324
+ const { t } = useT();
325
+ const exportBot = useRpc(exportBotRpc);
326
+ const toast = useToast();
327
+ const [includeMemory, setIncludeMemory] = useState(false);
328
+ const [json, setJson] = useState<string | null>(null);
329
+ useEffect(() => {
330
+ let cancelled = false;
331
+ setJson(null);
332
+ exportBot({ bot, includeMemory })
333
+ .then((result) => !cancelled && setJson(result.json))
334
+ .catch((error: unknown) => toast.error(errorText(error)));
335
+ return () => {
336
+ cancelled = true;
337
+ };
338
+ // The bot is a snapshot for this dialog; rebuild only when the memory choice changes.
339
+ }, [includeMemory]);
340
+ return (
341
+ <Modal title={t("dialogs.export.title", { name: bot.name })} open onOpenChange={(open) => !open && onClose()}>
342
+ <Modal.Content>
343
+ <SettingsCard>
344
+ <SettingsSwitch
345
+ label={t("dialogs.export.includeMemory")}
346
+ hint={t("dialogs.export.includeMemoryHint")}
347
+ value={includeMemory}
348
+ onValueChange={setIncludeMemory}
349
+ />
350
+ <SettingsAction
351
+ label={t("dialogs.export.copyLabel")}
352
+ hint={json ? t("dialogs.export.size", { size: (json.length / 1000).toFixed(1) }) : t("dialogs.export.building")}
353
+ actionLabel={t("dialogs.export.copy")}
354
+ disabled={!json}
355
+ onPress={() => void copyText(json!).then(() => toast.show(t("dialogs.export.copied"), { variant: "success" }))}
356
+ />
357
+ </SettingsCard>
358
+ <Text {...MONO_PROPS} selectable numberOfLines={16} style={{ fontFamily: MONO_FONT, fontSize: code(), lineHeight: codeLine(), color: colors.foregroundMuted }}>
359
+ {json ?? ""}
360
+ </Text>
361
+ </Modal.Content>
362
+ </Modal>
363
+ );
364
+ }