paseo-prompt-kit 0.5.2

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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +249 -0
  3. package/client/actions/enabled.ts +30 -0
  4. package/client/commands/rewrite-command.ts +54 -0
  5. package/client/composer-bridge/adapter.ts +15 -0
  6. package/client/composer-bridge/dom.ts +101 -0
  7. package/client/composer-bridge/effect.ts +64 -0
  8. package/client/composer-bridge/fiber.ts +97 -0
  9. package/client/composer-bridge/web.ts +58 -0
  10. package/client/icon.ts +13 -0
  11. package/client/pills/agent-pills.ts +207 -0
  12. package/client/pills/rewrite-runner.ts +123 -0
  13. package/client/settings/action-samples.ts +102 -0
  14. package/client/settings/api-endpoints.ts +156 -0
  15. package/client/settings/custom-actions.ts +79 -0
  16. package/client/settings/draft.ts +82 -0
  17. package/client/settings/model-filter.ts +33 -0
  18. package/client/settings/read-settings.ts +45 -0
  19. package/client/settings/readiness.ts +84 -0
  20. package/client/settings/sections/actions-section.tsx +75 -0
  21. package/client/settings/sections/advanced-section.tsx +127 -0
  22. package/client/settings/sections/api-endpoint-section.tsx +388 -0
  23. package/client/settings/sections/custom-actions-section.tsx +163 -0
  24. package/client/settings/sections/dedicated-model-section.tsx +136 -0
  25. package/client/settings/sections/engine-section.tsx +101 -0
  26. package/client/settings/sections/provider-map-card.tsx +89 -0
  27. package/client/settings/sections/stored-key-rows.tsx +106 -0
  28. package/client/settings/selection.ts +46 -0
  29. package/client/settings/settings-saved.ts +17 -0
  30. package/client/settings/settings-screen.tsx +197 -0
  31. package/client/settings/ui/button.tsx +56 -0
  32. package/client/settings/ui/notice.tsx +61 -0
  33. package/client/settings/ui/split-select.tsx +26 -0
  34. package/client/settings/ui/status-bar.tsx +89 -0
  35. package/client/settings/ui/tokens.ts +38 -0
  36. package/client/settings/validation.ts +50 -0
  37. package/client/sheet/rewrite-sheet.tsx +249 -0
  38. package/index.client.tsx +71 -0
  39. package/index.server.ts +98 -0
  40. package/package.json +53 -0
  41. package/paseo-plugin.json +6 -0
  42. package/server/log.ts +20 -0
  43. package/server/model-resolver/provider-catalog.ts +37 -0
  44. package/server/model-resolver/resolver.ts +196 -0
  45. package/server/paseo-types.ts +13 -0
  46. package/server/rewrite-engine/engine.ts +88 -0
  47. package/server/rewrite-engine/handler.ts +94 -0
  48. package/server/rewrite-engine/output-validator.ts +130 -0
  49. package/server/transports/api/anthropic.ts +61 -0
  50. package/server/transports/api/cloudflare.ts +52 -0
  51. package/server/transports/api/gemini.ts +62 -0
  52. package/server/transports/api/key.ts +95 -0
  53. package/server/transports/api/openai.ts +52 -0
  54. package/server/transports/api/protocol.ts +96 -0
  55. package/server/transports/api/runner.ts +284 -0
  56. package/server/transports/api/secrets-store.ts +90 -0
  57. package/server/transports/cli/family.ts +216 -0
  58. package/server/transports/cli/process.ts +118 -0
  59. package/server/transports/cli/runner.ts +89 -0
  60. package/shared/action-registry/loader.ts +63 -0
  61. package/shared/action-registry/registry.ts +47 -0
  62. package/shared/action-registry/rewrite-contract.ts +31 -0
  63. package/shared/action-registry/schema.ts +65 -0
  64. package/shared/action-registry/wrapper.ts +30 -0
  65. package/shared/api-protocol.ts +56 -0
  66. package/shared/cli-families.ts +29 -0
  67. package/shared/language-registry/loader.ts +53 -0
  68. package/shared/language-registry/registry.ts +20 -0
  69. package/shared/language-registry/schema.ts +21 -0
  70. package/shared/languages/en.json +6 -0
  71. package/shared/languages/index.ts +5 -0
  72. package/shared/languages/vi.json +6 -0
  73. package/shared/packs/general.json +17 -0
  74. package/shared/packs/index.ts +12 -0
  75. package/shared/protected-literals.ts +550 -0
  76. package/shared/rpc.ts +187 -0
  77. package/shared/settings.ts +90 -0
@@ -0,0 +1,26 @@
1
+ import type { PluginTheme } from "@getpaseo/plugin";
2
+ import { SettingsSelect, type SettingsSelectProps } from "@getpaseo/plugin/client/ui";
3
+ import { Text } from "react-native";
4
+ import { FONT, SPACE } from "./tokens.js";
5
+
6
+ export interface SplitSelectProps<Value extends string> extends SettingsSelectProps<Value> {
7
+ theme: PluginTheme;
8
+ /** Narrow layouts keep the host's plain hint. */
9
+ compact: boolean;
10
+ }
11
+
12
+ /** Share of the text column the hint may fill, so it stops short of the dropdown. */
13
+ const HINT_MAX_WIDTH = "85%";
14
+
15
+ /** Host select row whose hint wraps before the dropdown; the host renders a non-string hint as given. */
16
+ export function SplitSelect<Value extends string>({ theme, compact, hint, ...select }: SplitSelectProps<Value>) {
17
+ if (compact || !hint) return <SettingsSelect hint={hint} {...select} />;
18
+ const node = (
19
+ <Text
20
+ style={{ color: theme.colors.foregroundMuted, fontSize: FONT.sm, marginTop: SPACE.xs, maxWidth: HINT_MAX_WIDTH }}
21
+ >
22
+ {hint}
23
+ </Text>
24
+ );
25
+ return <SettingsSelect hint={node as unknown as string} {...select} />;
26
+ }
@@ -0,0 +1,89 @@
1
+ import type { PluginTheme } from "@getpaseo/plugin";
2
+ import type { SettingsDraft } from "../draft.js";
3
+ import type { Readiness } from "../readiness.js";
4
+ import { Button } from "./button.js";
5
+ import { Notice } from "./notice.js";
6
+ import type { Tone } from "./tokens.js";
7
+
8
+ export interface StatusBarProps {
9
+ theme: PluginTheme;
10
+ readiness: Readiness;
11
+ draft: SettingsDraft;
12
+ /** True when the draft changed an action toggle, which the pill only sees on reload. */
13
+ actionsChanged: boolean;
14
+ compact: boolean;
15
+ }
16
+
17
+ function tone(readiness: Readiness, draft: SettingsDraft): Tone {
18
+ if (draft.saveError !== null || draft.problem !== null) return "danger";
19
+ if (readiness.kind === "blocked") return "warning";
20
+ if (readiness.kind === "checking") return "info";
21
+ return "success";
22
+ }
23
+
24
+ function title(readiness: Readiness, draft: SettingsDraft): string {
25
+ if (draft.problem !== null) return "Cannot save yet";
26
+ if (draft.saveError !== null) return "Save failed";
27
+ switch (readiness.kind) {
28
+ case "ready":
29
+ return `Ready · ${readiness.path}`;
30
+ case "blocked":
31
+ return `Not ready · ${readiness.path}`;
32
+ case "checking":
33
+ return `Checking · ${readiness.path}`;
34
+ }
35
+ }
36
+
37
+ function lines(readiness: Readiness, draft: SettingsDraft, actionsChanged: boolean): string[] {
38
+ const out: string[] = [];
39
+ if (draft.problem !== null) out.push(draft.problem);
40
+ else if (draft.saveError !== null) out.push(draft.saveError);
41
+ else if (readiness.kind === "blocked") out.push(readiness.reason);
42
+ else out.push(readiness.detail);
43
+
44
+ if (draft.dirty) {
45
+ out.push(
46
+ actionsChanged
47
+ ? "Unsaved changes. Action changes reach the Composer pill when the agent re-opens or the plugin reloads."
48
+ : "Unsaved changes.",
49
+ );
50
+ } else if (draft.justSaved) {
51
+ out.push("Saved.");
52
+ }
53
+ return out;
54
+ }
55
+
56
+ /** Readiness line plus Save/Discard when dirty. */
57
+ export function StatusBar({ theme, readiness, draft, actionsChanged, compact }: StatusBarProps) {
58
+ return (
59
+ <Notice
60
+ theme={theme}
61
+ tone={tone(readiness, draft)}
62
+ title={title(readiness, draft)}
63
+ lines={lines(readiness, draft, actionsChanged)}
64
+ compact={compact}
65
+ testID="prompt-kit-status"
66
+ trailing={
67
+ draft.dirty ? (
68
+ <>
69
+ <Button
70
+ theme={theme}
71
+ label="Discard"
72
+ onPress={draft.discard}
73
+ disabled={draft.saving}
74
+ testID="prompt-kit-discard"
75
+ />
76
+ <Button
77
+ theme={theme}
78
+ label={draft.saving ? "Saving…" : "Save"}
79
+ variant="primary"
80
+ onPress={() => void draft.save()}
81
+ disabled={draft.saving || draft.problem !== null}
82
+ testID="prompt-kit-save"
83
+ />
84
+ </>
85
+ ) : undefined
86
+ }
87
+ />
88
+ );
89
+ }
@@ -0,0 +1,38 @@
1
+ import type { PluginTheme } from "@getpaseo/plugin";
2
+ import type { TextStyle } from "react-native";
3
+
4
+ /** Spacing/radius/font values mirrored from the host theme (not exported to plugins). */
5
+ export const SPACE = { xs: 4, sm: 8, md: 12, lg: 16 } as const;
6
+ export const RADIUS = { md: 6, lg: 8 } as const;
7
+ export const FONT = { sm: 12, base: 14 } as const;
8
+
9
+ export type Tone = "info" | "success" | "warning" | "danger";
10
+
11
+ export function toneColor(theme: PluginTheme, tone: Tone): string {
12
+ switch (tone) {
13
+ case "success":
14
+ return theme.colors.statusSuccess;
15
+ case "warning":
16
+ return theme.colors.statusWarning;
17
+ case "danger":
18
+ return theme.colors.statusDanger;
19
+ default:
20
+ return theme.colors.accent;
21
+ }
22
+ }
23
+
24
+ export interface TextStyles {
25
+ readonly body: TextStyle;
26
+ readonly strong: TextStyle;
27
+ readonly muted: TextStyle;
28
+ readonly danger: TextStyle;
29
+ }
30
+
31
+ export function textStyles(theme: PluginTheme): TextStyles {
32
+ return {
33
+ body: { color: theme.colors.foreground, fontSize: FONT.base },
34
+ strong: { color: theme.colors.foreground, fontSize: FONT.base, fontWeight: "600" },
35
+ muted: { color: theme.colors.foregroundMuted, fontSize: FONT.sm },
36
+ danger: { color: theme.colors.statusDanger, fontSize: FONT.sm },
37
+ };
38
+ }
@@ -0,0 +1,50 @@
1
+ import { TIMEOUT_MS, type PromptKitSettings } from "../../shared/settings.js";
2
+ import { isUsableSecretsDir, validateEndpoint } from "./api-endpoints.js";
3
+
4
+ /** Why the draft cannot be saved, or null. Bounds come from the schema constants. */
5
+ export function findSaveProblem(values: PromptKitSettings): string | null {
6
+ if (
7
+ !Number.isInteger(values.timeoutMs) ||
8
+ values.timeoutMs < TIMEOUT_MS.min ||
9
+ values.timeoutMs > TIMEOUT_MS.max
10
+ ) {
11
+ return `Timeout must be a whole number between ${TIMEOUT_MS.min.toLocaleString()} and ${TIMEOUT_MS.max.toLocaleString()} ms.`;
12
+ }
13
+
14
+ for (const endpoint of values.apiEndpoints) {
15
+ const problem = validateEndpoint(endpoint, values.apiEndpoints, endpoint.id);
16
+ if (problem !== null) return `Endpoint "${endpoint.label || endpoint.id}": ${problem}`;
17
+ }
18
+
19
+ if (values.secretsDir !== null && !isUsableSecretsDir(values.secretsDir)) {
20
+ return "The secrets directory must be an absolute path or start with ~/.";
21
+ }
22
+
23
+ const ids = new Set(values.apiEndpoints.map((endpoint) => endpoint.id));
24
+ if (values.apiEndpointId !== null && !ids.has(values.apiEndpointId)) {
25
+ return `The selected endpoint "${values.apiEndpointId}" is not configured.`;
26
+ }
27
+ for (const [provider, endpointId] of Object.entries(values.apiEndpointByProvider)) {
28
+ if (!ids.has(endpointId)) {
29
+ return `Provider "${provider}" is mapped to endpoint "${endpointId}", which is not configured.`;
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+
35
+ /** Row message for the timeout: range error, or a note above the host cap. */
36
+ export function describeTimeout(timeoutMs: number): { error: string | null; note: string | null } {
37
+ if (!Number.isInteger(timeoutMs) || timeoutMs < TIMEOUT_MS.min || timeoutMs > TIMEOUT_MS.max) {
38
+ return {
39
+ error: `Between ${TIMEOUT_MS.min.toLocaleString()} and ${TIMEOUT_MS.max.toLocaleString()} ms.`,
40
+ note: null,
41
+ };
42
+ }
43
+ if (timeoutMs > TIMEOUT_MS.hostRpcCapMs) {
44
+ return {
45
+ error: null,
46
+ note: `Paseo stops waiting for a plugin call after ${TIMEOUT_MS.hostRpcCapMs / 1000} s, so a longer budget is not reached in practice.`,
47
+ };
48
+ }
49
+ return { error: null, note: null };
50
+ }
@@ -0,0 +1,249 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState, type ComponentType } from "react";
2
+ import { Text, View } from "react-native";
3
+ import { usePaseo, useRpc, useSettings } from "@getpaseo/plugin/client";
4
+ import type { PluginButtonContentProps } from "@getpaseo/plugin/client";
5
+ import { TextInput, useToast } from "@getpaseo/plugin/client/react-native";
6
+ import { actionsListRpc, rewriteRpc, type ActionSummary } from "../../shared/rpc.js";
7
+ import { promptKitSettings, promptKitSettingsSchema } from "../../shared/settings.js";
8
+ import { enabledActions } from "../actions/enabled.js";
9
+ import {
10
+ describeLookupFailure,
11
+ fiberOf,
12
+ findComposerHandle,
13
+ type ComposerLookup,
14
+ } from "../composer-bridge/fiber.js";
15
+ import { Button } from "../settings/ui/button.js";
16
+ import { RADIUS, SPACE, textStyles } from "../settings/ui/tokens.js";
17
+
18
+ /** Locates the agent's Composer from a mounted probe element. */
19
+ export type ComposerLocator = (probe: unknown, agentId: string) => ComposerLookup;
20
+
21
+ export const locateComposerFromProbe: ComposerLocator = (probe, agentId) =>
22
+ findComposerHandle(fiberOf(probe), agentId);
23
+
24
+ /** Host sheet: body padding 12 + page 4 + title bottom padding 12 + half a title line. */
25
+ const HOST_TITLE_CENTER_ABOVE_BODY = 39;
26
+ const CLOSE_BUTTON_HEIGHT = 30;
27
+ /** Pulls the body up into the host's title padding so the field sits close to the title. */
28
+ const BODY_LIFT = SPACE.lg;
29
+
30
+ /** Text kept per agent while the sheet is closed with X, so reopening resumes. */
31
+ const drafts = new Map<string, string>();
32
+
33
+ /** Action buttons per row when several actions are enabled. */
34
+ const ACTIONS_PER_ROW = 2;
35
+
36
+ function rows<T>(items: readonly T[]): T[][] {
37
+ const out: T[][] = [];
38
+ for (let index = 0; index < items.length; index += ACTIONS_PER_ROW) out.push(items.slice(index, index + ACTIONS_PER_ROW));
39
+ return out;
40
+ }
41
+
42
+ /**
43
+ * Pill popover for native mobile: opens with the Composer's text. With one
44
+ * enabled action it rewrites at once and Rewrite runs it again; with several,
45
+ * each action has its own button and nothing runs until one is pressed. Send
46
+ * hands the text to the agent and clears the Composer, X keeps everything.
47
+ */
48
+ export function createRewriteSheet(locate: ComposerLocator): ComponentType<PluginButtonContentProps> {
49
+ return function RewriteSheet(props: PluginButtonContentProps) {
50
+ const { theme, close } = props;
51
+ const agentId = props.context === "agent" ? props.agentId : null;
52
+ const workspaceId = props.workspaceId;
53
+ const paseo = usePaseo();
54
+ const toast = useToast();
55
+ const settings = useSettings(promptKitSettings);
56
+ const rewrite = useRpc(rewriteRpc);
57
+ const listActions = useRpc(actionsListRpc);
58
+ const text = useMemo(() => textStyles(theme), [theme]);
59
+ const probeRef = useRef<unknown>(null);
60
+ const composerRef = useRef<ComposerLookup | null>(null);
61
+ const [value, setValue] = useState("");
62
+ const [busy, setBusy] = useState<"idle" | "rewriting" | "sending">("idle");
63
+ const [note, setNote] = useState<string | null>(null);
64
+ /** Enabled actions, null until read. */
65
+ const [choices, setChoices] = useState<readonly ActionSummary[] | null>(null);
66
+ const started = useRef(false);
67
+
68
+ const remember = useCallback(
69
+ (next: string) => {
70
+ setValue(next);
71
+ if (agentId !== null) drafts.set(agentId, next);
72
+ },
73
+ [agentId],
74
+ );
75
+
76
+ const runRewrite = useCallback(
77
+ async (source: string, actionId: string) => {
78
+ if (settings.status !== "ready") {
79
+ toast.error(settings.status === "loading" ? "Settings are still loading." : settings.error);
80
+ return;
81
+ }
82
+ if (source.trim() === "") {
83
+ toast.error("Write a prompt first.");
84
+ return;
85
+ }
86
+ setBusy("rewriting");
87
+ try {
88
+ const output = await rewrite({
89
+ actionId,
90
+ agentId,
91
+ workspaceId,
92
+ originalPrompt: source,
93
+ settings: promptKitSettingsSchema.parse(settings.values),
94
+ });
95
+ if (output.status === "error") throw new Error(output.error.message);
96
+ remember(output.rewrittenPrompt);
97
+ } catch (error) {
98
+ toast.error(error instanceof Error ? error.message : String(error));
99
+ } finally {
100
+ setBusy("idle");
101
+ }
102
+ },
103
+ [agentId, remember, rewrite, settings, toast, workspaceId],
104
+ );
105
+
106
+ // Read the enabled actions once settings are ready.
107
+ const customActions = settings.status === "ready" ? settings.values.customActions : null;
108
+ useEffect(() => {
109
+ if (settings.status !== "ready" || choices !== null) return;
110
+ const values = settings.values;
111
+ listActions({ customActions: values.customActions })
112
+ .then(({ actions }) => setChoices(enabledActions(actions, values)))
113
+ .catch((error: unknown) => {
114
+ setChoices([]);
115
+ toast.error(error instanceof Error ? error.message : String(error));
116
+ });
117
+ }, [choices, customActions, listActions, settings, toast]);
118
+
119
+ // On open: read the Composer and show its text; with one action, rewrite it straight away.
120
+ useEffect(() => {
121
+ if (started.current || agentId === null || choices === null) return;
122
+ started.current = true;
123
+ const lookup = locate(probeRef.current, agentId);
124
+ composerRef.current = lookup;
125
+ const composerText = lookup.ok ? lookup.handle.getText() : "";
126
+ const initial = composerText.trim() !== "" ? composerText : (drafts.get(agentId) ?? "");
127
+ remember(initial);
128
+ if (!lookup.ok) setNote(describeLookupFailure(lookup.reason));
129
+ else if (choices.length === 0) setNote("No PromptKit action is enabled.");
130
+ if (composerText.trim() !== "" && choices.length === 1) void runRewrite(composerText, choices[0]!.id);
131
+ }, [agentId, choices, remember, runRewrite]);
132
+
133
+ const send = useCallback(async () => {
134
+ if (busy !== "idle" || agentId === null || value.trim() === "") return;
135
+ setBusy("sending");
136
+ try {
137
+ await paseo.agents.ref(agentId).send(value);
138
+ const lookup = composerRef.current;
139
+ if (lookup?.ok) lookup.handle.replaceText("");
140
+ remember("");
141
+ close();
142
+ } catch (error) {
143
+ toast.error(error instanceof Error ? error.message : String(error));
144
+ } finally {
145
+ setBusy("idle");
146
+ }
147
+ }, [agentId, busy, close, paseo, remember, toast, value]);
148
+
149
+ const box = useMemo(
150
+ () => ({
151
+ minHeight: 140,
152
+ maxHeight: 260,
153
+ padding: SPACE.md,
154
+ borderRadius: RADIUS.lg,
155
+ borderWidth: 1,
156
+ borderColor: theme.colors.border,
157
+ backgroundColor: theme.colors.surface1,
158
+ color: theme.colors.foreground,
159
+ opacity: busy === "rewriting" ? 0.45 : 1,
160
+ textAlignVertical: "top" as const,
161
+ }),
162
+ [theme, busy],
163
+ );
164
+
165
+ const several = choices !== null && choices.length > 1;
166
+ const only = choices?.length === 1 ? choices[0] : undefined;
167
+
168
+ // The host pads the body and draws the title row; X is lifted into that row.
169
+ return (
170
+ <View style={{ gap: SPACE.md, marginTop: -BODY_LIFT }}>
171
+ <View ref={probeRef as never} collapsable={false} testID="prompt-kit-sheet-probe" />
172
+ <View
173
+ style={{
174
+ position: "absolute",
175
+ top: -(HOST_TITLE_CENTER_ABOVE_BODY - BODY_LIFT + CLOSE_BUTTON_HEIGHT / 2),
176
+ right: -SPACE.xs,
177
+ zIndex: 1,
178
+ }}
179
+ >
180
+ <Button theme={theme} label="✕" variant="ghost" onPress={close} disabled={busy !== "idle"} testID="prompt-kit-sheet-close" />
181
+ </View>
182
+ <TextInput
183
+ multiline
184
+ value={value}
185
+ onChangeText={remember}
186
+ placeholder={several ? "Write the prompt, then pick an action" : "Write the prompt, then press Rewrite"}
187
+ placeholderTextColor={theme.colors.foregroundMuted}
188
+ editable={busy === "idle"}
189
+ returnKeyType="go"
190
+ blurOnSubmit
191
+ onSubmitEditing={() => {
192
+ if (only !== undefined) void runRewrite(value, only.id);
193
+ }}
194
+ style={box}
195
+ testID="prompt-kit-sheet-input"
196
+ />
197
+ <Text style={text.muted} testID="prompt-kit-sheet-note">
198
+ {busy === "rewriting"
199
+ ? "Rewriting…"
200
+ : (note ??
201
+ (several
202
+ ? "Pick how to rewrite; the result replaces the text here. Send hands it to the agent and clears the Composer."
203
+ : "Rewrite replaces the text here. Send hands it to the agent and clears the Composer."))}
204
+ </Text>
205
+ {several
206
+ ? rows(choices).map((row) => (
207
+ <View key={row.map((action) => action.id).join(",")} style={{ flexDirection: "row", gap: SPACE.md }}>
208
+ {row.map((action) => (
209
+ <Button
210
+ key={action.id}
211
+ theme={theme}
212
+ label={action.title}
213
+ fill
214
+ onPress={() => void runRewrite(value, action.id)}
215
+ disabled={busy !== "idle" || value.trim() === ""}
216
+ testID={`prompt-kit-sheet-action-${action.id}`}
217
+ />
218
+ ))}
219
+ {row.length < ACTIONS_PER_ROW ? <View style={{ flex: 1 }} /> : null}
220
+ </View>
221
+ ))
222
+ : null}
223
+ <View style={{ flexDirection: "row", gap: SPACE.md }}>
224
+ {several ? null : (
225
+ <Button
226
+ theme={theme}
227
+ label={busy === "rewriting" ? "Rewriting…" : "Rewrite"}
228
+ fill
229
+ onPress={() => {
230
+ if (only !== undefined) void runRewrite(value, only.id);
231
+ }}
232
+ disabled={busy !== "idle" || value.trim() === "" || only === undefined}
233
+ testID="prompt-kit-sheet-rewrite"
234
+ />
235
+ )}
236
+ <Button
237
+ theme={theme}
238
+ label={busy === "sending" ? "Sending…" : "Send"}
239
+ variant="primary"
240
+ fill
241
+ onPress={() => void send()}
242
+ disabled={busy !== "idle" || agentId === null || value.trim() === ""}
243
+ testID="prompt-kit-sheet-send"
244
+ />
245
+ </View>
246
+ </View>
247
+ );
248
+ };
249
+ }
@@ -0,0 +1,71 @@
1
+ import type { PluginClientContext } from "@getpaseo/plugin/client";
2
+ import type { ActionPack } from "./shared/action-registry/schema.js";
3
+ import { actionsListRpc } from "./shared/rpc.js";
4
+ import { createWebComposerAdapter } from "./client/composer-bridge/web.js";
5
+ import { PLUGIN_ICON } from "./client/icon.js";
6
+ import { createSettingsReader } from "./client/settings/read-settings.js";
7
+ import { onSettingsSaved } from "./client/settings/settings-saved.js";
8
+ import { registerRewriteCommand } from "./client/commands/rewrite-command.js";
9
+ import { registerAgentPills } from "./client/pills/agent-pills.js";
10
+ import { createRewriteRunner } from "./client/pills/rewrite-runner.js";
11
+ import { PromptKitSettingsScreen } from "./client/settings/settings-screen.js";
12
+ import { createRewriteSheet, locateComposerFromProbe } from "./client/sheet/rewrite-sheet.js";
13
+
14
+ /**
15
+ * Client contribution: a Composer pill per live agent, the `/rewrite` slash
16
+ * command, and the settings screen. Without a Composer DOM (native mobile) the
17
+ * pill is a popover sheet instead of an in-place rewrite.
18
+ */
19
+ // Declared, not re-exported: the host snapshots the CJS export table eagerly, before a
20
+ // module-scope `var` for a re-exported binding would have been assigned.
21
+ export default function contribute(client: PluginClientContext): () => void {
22
+ const readSettings = createSettingsReader(client.rpc);
23
+ const listActions = async (customActions: readonly ActionPack[]) => {
24
+ const output = await client.rpc(actionsListRpc, { customActions: [...customActions] });
25
+ // Fail closed on a malformed registry instead of registering a pill with an
26
+ // unknown enabled set.
27
+ if (!Array.isArray(output.actions)) {
28
+ throw new Error("prompt-kit.actions.list returned no action list");
29
+ }
30
+ return output.actions;
31
+ };
32
+
33
+ const composerSupported = createWebComposerAdapter().isSupported();
34
+ const removePills = registerAgentPills(
35
+ client,
36
+ (agent, isActive) => {
37
+ const runner = createRewriteRunner({
38
+ adapter: createWebComposerAdapter(),
39
+ rpc: client.rpc,
40
+ readSettings,
41
+ agentId: agent.agentId,
42
+ workspaceId: agent.workspaceId,
43
+ isActive,
44
+ });
45
+ return (actionId) => runner.run(actionId);
46
+ },
47
+ {
48
+ listActions,
49
+ readSettings,
50
+ onSettingsSaved,
51
+ ...(composerSupported ? {} : { popover: createRewriteSheet(locateComposerFromProbe) }),
52
+ },
53
+ );
54
+
55
+ const removeCommand = registerRewriteCommand(client, { listActions, readSettings });
56
+
57
+ const removeSettingsScreen = client.addSettingsScreen({
58
+ id: "prompt-kit",
59
+ title: "Settings",
60
+ icon: PLUGIN_ICON,
61
+ Component: PromptKitSettingsScreen,
62
+ });
63
+
64
+ return () => {
65
+ removePills();
66
+ removeCommand();
67
+ removeSettingsScreen();
68
+ };
69
+ }
70
+
71
+ export type { PluginClientContext };
@@ -0,0 +1,98 @@
1
+ import type { PluginServerContext } from "@getpaseo/plugin/server";
2
+ import { listRejectedPacks, summarizeActions } from "./shared/action-registry/registry.js";
3
+ import { listRejectedLanguages } from "./shared/language-registry/registry.js";
4
+ import {
5
+ actionsListRpc,
6
+ apiTestRpc,
7
+ providerCatalogRpc,
8
+ rewriteRpc,
9
+ secretsStatusRpc,
10
+ secretsWriteRpc,
11
+ type ApiTestOutput,
12
+ type ProviderCatalogOutput,
13
+ } from "./shared/rpc.js";
14
+ import { promptKitSettings } from "./shared/settings.js";
15
+ import { testApiEndpoint } from "./server/transports/api/runner.js";
16
+ import { hasApiKey, writeApiKey } from "./server/transports/api/secrets-store.js";
17
+ import { readProviderCatalog } from "./server/model-resolver/provider-catalog.js";
18
+ import { createRewriteHandler, type RewriteHandlerDependencies } from "./server/rewrite-engine/handler.js";
19
+ import { pluginLog } from "./server/log.js";
20
+
21
+ /**
22
+ * The composition root. `dependencies` exists so a test can drive the rewrite
23
+ * RPC without launching a real CLI; the host calls this with one argument.
24
+ */
25
+ export default function contribute(
26
+ server: PluginServerContext,
27
+ dependencies: RewriteHandlerDependencies = {},
28
+ ) {
29
+ server.registerSettings(promptKitSettings);
30
+
31
+ const rejected = listRejectedPacks();
32
+ if (rejected.length > 0) {
33
+ pluginLog.error(
34
+ { count: rejected.length, packs: rejected.map((entry) => entry.source).join(",") },
35
+ "action packs rejected",
36
+ );
37
+ }
38
+
39
+ const rejectedLanguages = listRejectedLanguages();
40
+ if (rejectedLanguages.length > 0) {
41
+ pluginLog.error(
42
+ { count: rejectedLanguages.length, languages: rejectedLanguages.map((entry) => entry.source).join(",") },
43
+ "output languages rejected",
44
+ );
45
+ }
46
+
47
+ server.handle(rewriteRpc, createRewriteHandler(dependencies));
48
+
49
+ server.handle(actionsListRpc, (input) => summarizeActions(input.customActions));
50
+
51
+ server.handle(providerCatalogRpc, async (input, { paseo }) => {
52
+ const providers = await readProviderCatalog(paseo, input.cwd);
53
+ return { providers } satisfies ProviderCatalogOutput;
54
+ });
55
+
56
+ // The settings screen's test button. It shares the rewrite path's key lookup, so
57
+ // "test passed" means the same thing a rewrite would find.
58
+ server.handle(apiTestRpc, async (input) => {
59
+ const result = await testApiEndpoint(
60
+ { endpoint: input.endpoint, secretsDir: input.secretsDir, timeoutMs: 15_000 },
61
+ dependencies.fetch === undefined && dependencies.env === undefined
62
+ ? {}
63
+ : {
64
+ ...(dependencies.fetch === undefined ? {} : { fetch: dependencies.fetch }),
65
+ ...(dependencies.env === undefined ? {} : { env: dependencies.env }),
66
+ },
67
+ );
68
+ if (!result.ok) {
69
+ return {
70
+ status: "error",
71
+ error: { code: result.code, message: result.message },
72
+ } satisfies ApiTestOutput;
73
+ }
74
+ return { status: "ok", models: [...result.models] } satisfies ApiTestOutput;
75
+ });
76
+
77
+ // Write-only key storage for the settings screen; the value is never logged or returned.
78
+ server.handle(secretsWriteRpc, async (input) => {
79
+ const result = await writeApiKey({
80
+ secretsDir: input.secretsDir,
81
+ name: input.name,
82
+ value: input.value,
83
+ ...(dependencies.env === undefined ? {} : { env: dependencies.env }),
84
+ });
85
+ return result.ok ? { status: "ok" as const } : { status: "error" as const, message: result.message };
86
+ });
87
+
88
+ server.handle(secretsStatusRpc, async (input) => {
89
+ const result = await hasApiKey({
90
+ secretsDir: input.secretsDir,
91
+ name: input.name,
92
+ ...(dependencies.env === undefined ? {} : { env: dependencies.env }),
93
+ });
94
+ return result.ok ? { status: "ok" as const, stored: result.stored } : { status: "error" as const, message: result.message };
95
+ });
96
+
97
+ return () => {};
98
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "paseo-prompt-kit",
3
+ "version": "0.5.2",
4
+ "license": "MIT",
5
+ "description": "PromptKit - rewrite and transform prompts directly from the Paseo composer.",
6
+ "keywords": [
7
+ "paseo",
8
+ "paseo-plugin",
9
+ "prompt",
10
+ "prompt-engineering",
11
+ "rewrite"
12
+ ],
13
+ "author": "hungcuong9125",
14
+ "homepage": "https://github.com/hungcuong9125/paseo-prompt-kit#readme",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/hungcuong9125/paseo-prompt-kit.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/hungcuong9125/paseo-prompt-kit/issues"
21
+ },
22
+ "type": "module",
23
+ "files": [
24
+ "paseo-plugin.json",
25
+ "index.client.ts",
26
+ "index.client.tsx",
27
+ "index.server.ts",
28
+ "index.server.tsx",
29
+ "client/",
30
+ "server/",
31
+ "shared/"
32
+ ],
33
+ "scripts": {
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "vitest run",
36
+ "gate": "npm run typecheck && npm test"
37
+ },
38
+ "devDependencies": {
39
+ "@getpaseo/client": "0.9.0",
40
+ "@getpaseo/plugin": "0.9.0",
41
+ "@tanstack/react-query": "^5.90.11",
42
+ "@types/node": "^22.12.0",
43
+ "@types/react": "~19.2.0",
44
+ "@types/react-dom": "~19.2.0",
45
+ "jsdom": "^30.1.0",
46
+ "react": "19.1.0",
47
+ "react-dom": "19.1.0",
48
+ "react-native": "0.81.5",
49
+ "typescript": "^5.9.3",
50
+ "vitest": "5.0.1",
51
+ "zod": "^4.4.3"
52
+ }
53
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "prompt-kit",
3
+ "requirements": {
4
+ "paseo": ">=0.9.0"
5
+ }
6
+ }