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.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/client/actions/enabled.ts +30 -0
- package/client/commands/rewrite-command.ts +54 -0
- package/client/composer-bridge/adapter.ts +15 -0
- package/client/composer-bridge/dom.ts +101 -0
- package/client/composer-bridge/effect.ts +64 -0
- package/client/composer-bridge/fiber.ts +97 -0
- package/client/composer-bridge/web.ts +58 -0
- package/client/icon.ts +13 -0
- package/client/pills/agent-pills.ts +207 -0
- package/client/pills/rewrite-runner.ts +123 -0
- package/client/settings/action-samples.ts +102 -0
- package/client/settings/api-endpoints.ts +156 -0
- package/client/settings/custom-actions.ts +79 -0
- package/client/settings/draft.ts +82 -0
- package/client/settings/model-filter.ts +33 -0
- package/client/settings/read-settings.ts +45 -0
- package/client/settings/readiness.ts +84 -0
- package/client/settings/sections/actions-section.tsx +75 -0
- package/client/settings/sections/advanced-section.tsx +127 -0
- package/client/settings/sections/api-endpoint-section.tsx +388 -0
- package/client/settings/sections/custom-actions-section.tsx +163 -0
- package/client/settings/sections/dedicated-model-section.tsx +136 -0
- package/client/settings/sections/engine-section.tsx +101 -0
- package/client/settings/sections/provider-map-card.tsx +89 -0
- package/client/settings/sections/stored-key-rows.tsx +106 -0
- package/client/settings/selection.ts +46 -0
- package/client/settings/settings-saved.ts +17 -0
- package/client/settings/settings-screen.tsx +197 -0
- package/client/settings/ui/button.tsx +56 -0
- package/client/settings/ui/notice.tsx +61 -0
- package/client/settings/ui/split-select.tsx +26 -0
- package/client/settings/ui/status-bar.tsx +89 -0
- package/client/settings/ui/tokens.ts +38 -0
- package/client/settings/validation.ts +50 -0
- package/client/sheet/rewrite-sheet.tsx +249 -0
- package/index.client.tsx +71 -0
- package/index.server.ts +98 -0
- package/package.json +53 -0
- package/paseo-plugin.json +6 -0
- package/server/log.ts +20 -0
- package/server/model-resolver/provider-catalog.ts +37 -0
- package/server/model-resolver/resolver.ts +196 -0
- package/server/paseo-types.ts +13 -0
- package/server/rewrite-engine/engine.ts +88 -0
- package/server/rewrite-engine/handler.ts +94 -0
- package/server/rewrite-engine/output-validator.ts +130 -0
- package/server/transports/api/anthropic.ts +61 -0
- package/server/transports/api/cloudflare.ts +52 -0
- package/server/transports/api/gemini.ts +62 -0
- package/server/transports/api/key.ts +95 -0
- package/server/transports/api/openai.ts +52 -0
- package/server/transports/api/protocol.ts +96 -0
- package/server/transports/api/runner.ts +284 -0
- package/server/transports/api/secrets-store.ts +90 -0
- package/server/transports/cli/family.ts +216 -0
- package/server/transports/cli/process.ts +118 -0
- package/server/transports/cli/runner.ts +89 -0
- package/shared/action-registry/loader.ts +63 -0
- package/shared/action-registry/registry.ts +47 -0
- package/shared/action-registry/rewrite-contract.ts +31 -0
- package/shared/action-registry/schema.ts +65 -0
- package/shared/action-registry/wrapper.ts +30 -0
- package/shared/api-protocol.ts +56 -0
- package/shared/cli-families.ts +29 -0
- package/shared/language-registry/loader.ts +53 -0
- package/shared/language-registry/registry.ts +20 -0
- package/shared/language-registry/schema.ts +21 -0
- package/shared/languages/en.json +6 -0
- package/shared/languages/index.ts +5 -0
- package/shared/languages/vi.json +6 -0
- package/shared/packs/general.json +17 -0
- package/shared/packs/index.ts +12 -0
- package/shared/protected-literals.ts +550 -0
- package/shared/rpc.ts +187 -0
- 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
|
+
}
|
package/index.client.tsx
ADDED
|
@@ -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 };
|
package/index.server.ts
ADDED
|
@@ -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
|
+
}
|