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,101 @@
|
|
|
1
|
+
import type { PluginTheme } from "@getpaseo/plugin";
|
|
2
|
+
import { SettingsCard, SettingsSection } from "@getpaseo/plugin/client/ui";
|
|
3
|
+
import { listLanguages } from "../../../shared/language-registry/registry.js";
|
|
4
|
+
import { SOURCE_LANGUAGE } from "../../../shared/language-registry/schema.js";
|
|
5
|
+
import type { PromptKitSettings } from "../../../shared/settings.js";
|
|
6
|
+
import type { SettingsPatch } from "../draft.js";
|
|
7
|
+
import { SplitSelect } from "../ui/split-select.js";
|
|
8
|
+
|
|
9
|
+
export interface EngineSectionProps {
|
|
10
|
+
theme: PluginTheme;
|
|
11
|
+
compact: boolean;
|
|
12
|
+
values: PromptKitSettings;
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
patch(update: SettingsPatch): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TRANSPORT_OPTIONS = [
|
|
18
|
+
{ label: "Provider CLI", value: "cli" },
|
|
19
|
+
{ label: "Direct API", value: "api" },
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
const MODEL_OPTIONS = [
|
|
23
|
+
{ label: "Current agent model", value: "current" },
|
|
24
|
+
{ label: "Dedicated model", value: "dedicated" },
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
const TRANSPORT_HINT = {
|
|
28
|
+
cli: "The provider's own CLI runs the rewrite headlessly in an empty scratch directory. Needs that CLI on the daemon's PATH.",
|
|
29
|
+
api: "One HTTP request to an endpoint you configure below. Fastest, and works for providers with no CLI. Needs a key.",
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
const MODEL_HINT = {
|
|
33
|
+
current: "The model the Composer shows for the agent whose pill you press. Nothing else to choose.",
|
|
34
|
+
dedicated: "A fixed provider and model, whatever the agent itself runs. Pick them below.",
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
/** The built-in default first, then every loaded language in barrel order. */
|
|
38
|
+
const LANGUAGE_OPTIONS = [
|
|
39
|
+
{ label: "Same as the prompt", value: SOURCE_LANGUAGE },
|
|
40
|
+
...listLanguages().map((language) => ({ label: language.label, value: language.id })),
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
/** Transport, the CLI's model source, and output language. Model source exists only for Provider CLI. */
|
|
44
|
+
export function EngineSection({ theme, compact, values, disabled, patch }: EngineSectionProps) {
|
|
45
|
+
return (
|
|
46
|
+
<SettingsSection
|
|
47
|
+
title="Rewrite engine"
|
|
48
|
+
info="Transport answers how the model is reached. On Provider CLI, Model source answers which model; on Direct API the model is chosen under API endpoint. Output language answers what language the rewrite is written in."
|
|
49
|
+
>
|
|
50
|
+
<SettingsCard>
|
|
51
|
+
<SplitSelect
|
|
52
|
+
theme={theme}
|
|
53
|
+
compact={compact}
|
|
54
|
+
label="Transport"
|
|
55
|
+
hint={TRANSPORT_HINT[values.transport]}
|
|
56
|
+
value={values.transport}
|
|
57
|
+
options={TRANSPORT_OPTIONS}
|
|
58
|
+
disabled={disabled}
|
|
59
|
+
onValueChange={(transport) => patch({ transport: transport === "api" ? "api" : "cli" })}
|
|
60
|
+
/>
|
|
61
|
+
{values.transport === "cli" ? (
|
|
62
|
+
<SplitSelect
|
|
63
|
+
theme={theme}
|
|
64
|
+
compact={compact}
|
|
65
|
+
label="Model source"
|
|
66
|
+
hint={MODEL_HINT[values.modelMode]}
|
|
67
|
+
value={values.modelMode}
|
|
68
|
+
options={MODEL_OPTIONS}
|
|
69
|
+
disabled={disabled}
|
|
70
|
+
onValueChange={(modelMode) =>
|
|
71
|
+
patch({ modelMode: modelMode === "dedicated" ? "dedicated" : "current" })
|
|
72
|
+
}
|
|
73
|
+
/>
|
|
74
|
+
) : null}
|
|
75
|
+
<SplitSelect
|
|
76
|
+
theme={theme}
|
|
77
|
+
compact={compact}
|
|
78
|
+
label="Output language"
|
|
79
|
+
hint={
|
|
80
|
+
values.outputLanguage === SOURCE_LANGUAGE
|
|
81
|
+
? "The rewrite keeps the language the prompt was written in."
|
|
82
|
+
: "The prose is translated; paths, commands, code and names stay exactly as written."
|
|
83
|
+
}
|
|
84
|
+
error={
|
|
85
|
+
LANGUAGE_OPTIONS.some((option) => option.value === values.outputLanguage)
|
|
86
|
+
? null
|
|
87
|
+
: `"${values.outputLanguage}" is not a loaded language. See docs/guides/output-languages.md.`
|
|
88
|
+
}
|
|
89
|
+
value={values.outputLanguage}
|
|
90
|
+
options={
|
|
91
|
+
LANGUAGE_OPTIONS.some((option) => option.value === values.outputLanguage)
|
|
92
|
+
? LANGUAGE_OPTIONS
|
|
93
|
+
: [{ label: `${values.outputLanguage} (missing)`, value: values.outputLanguage }, ...LANGUAGE_OPTIONS]
|
|
94
|
+
}
|
|
95
|
+
disabled={disabled}
|
|
96
|
+
onValueChange={(outputLanguage) => patch({ outputLanguage })}
|
|
97
|
+
/>
|
|
98
|
+
</SettingsCard>
|
|
99
|
+
</SettingsSection>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { SettingsCard, SettingsRow, SettingsSelect } from "@getpaseo/plugin/client/ui";
|
|
2
|
+
import type { ProviderCatalogOutput } from "../../../shared/rpc.js";
|
|
3
|
+
|
|
4
|
+
type Providers = ProviderCatalogOutput["providers"];
|
|
5
|
+
|
|
6
|
+
export interface ProviderMapCardProps {
|
|
7
|
+
title: string;
|
|
8
|
+
hint: string;
|
|
9
|
+
/** Null until the catalog has been read. */
|
|
10
|
+
providers: Providers | null;
|
|
11
|
+
/** The current map, provider id → target value. */
|
|
12
|
+
map: Readonly<Record<string, string>>;
|
|
13
|
+
/** What a provider can be mapped to. Empty disables adding. */
|
|
14
|
+
targets: readonly { label: string; value: string }[];
|
|
15
|
+
/** Shown when `targets` is empty, in place of the Add row. */
|
|
16
|
+
emptyTargetsLabel: string;
|
|
17
|
+
/** Row-level error for one mapping, or null. */
|
|
18
|
+
errorFor?(providerId: string, target: string): string | null;
|
|
19
|
+
disabled: boolean;
|
|
20
|
+
onSet(providerId: string, target: string): void;
|
|
21
|
+
onRemove(providerId: string): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const NONE = "";
|
|
25
|
+
const REMOVE = "__remove__";
|
|
26
|
+
|
|
27
|
+
/** Per-provider map: one row per mapped provider plus an Add row, not the whole catalog. */
|
|
28
|
+
export function ProviderMapCard({
|
|
29
|
+
title,
|
|
30
|
+
hint,
|
|
31
|
+
providers,
|
|
32
|
+
map,
|
|
33
|
+
targets,
|
|
34
|
+
emptyTargetsLabel,
|
|
35
|
+
errorFor,
|
|
36
|
+
disabled,
|
|
37
|
+
onSet,
|
|
38
|
+
onRemove,
|
|
39
|
+
}: ProviderMapCardProps) {
|
|
40
|
+
const labelOf = (id: string) => providers?.find((entry) => entry.provider === id)?.label ?? id;
|
|
41
|
+
const mapped = Object.entries(map);
|
|
42
|
+
const unmapped = (providers ?? []).filter((entry) => !(entry.provider in map));
|
|
43
|
+
const firstTarget = targets[0]?.value;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<SettingsCard>
|
|
47
|
+
<SettingsRow label={title} hint={hint} />
|
|
48
|
+
{mapped.map(([providerId, target]) => (
|
|
49
|
+
<SettingsSelect
|
|
50
|
+
key={providerId}
|
|
51
|
+
label={labelOf(providerId)}
|
|
52
|
+
hint={providerId}
|
|
53
|
+
error={errorFor?.(providerId, target) ?? null}
|
|
54
|
+
value={target}
|
|
55
|
+
options={[
|
|
56
|
+
...(targets.some((option) => option.value === target)
|
|
57
|
+
? []
|
|
58
|
+
: [{ label: `${target} (missing)`, value: target }]),
|
|
59
|
+
...targets,
|
|
60
|
+
{ label: "Remove mapping", value: REMOVE },
|
|
61
|
+
]}
|
|
62
|
+
disabled={disabled}
|
|
63
|
+
onValueChange={(next) => (next === REMOVE ? onRemove(providerId) : onSet(providerId, next))}
|
|
64
|
+
/>
|
|
65
|
+
))}
|
|
66
|
+
{providers === null ? (
|
|
67
|
+
<SettingsRow label="Loading providers…" />
|
|
68
|
+
) : firstTarget === undefined ? (
|
|
69
|
+
<SettingsRow label={emptyTargetsLabel} />
|
|
70
|
+
) : unmapped.length === 0 ? (
|
|
71
|
+
<SettingsRow label="Every provider is mapped." />
|
|
72
|
+
) : (
|
|
73
|
+
<SettingsSelect
|
|
74
|
+
label={mapped.length === 0 ? "No provider is mapped" : "Add another provider"}
|
|
75
|
+
hint="Pick a provider; its row appears above with the first target, which you can then change."
|
|
76
|
+
value={NONE}
|
|
77
|
+
options={[
|
|
78
|
+
{ label: "Add mapping…", value: NONE },
|
|
79
|
+
...unmapped.map((entry) => ({ label: `${entry.label} · ${entry.provider}`, value: entry.provider })),
|
|
80
|
+
]}
|
|
81
|
+
disabled={disabled}
|
|
82
|
+
onValueChange={(providerId) => {
|
|
83
|
+
if (providerId !== NONE) onSet(providerId, firstTarget);
|
|
84
|
+
}}
|
|
85
|
+
/>
|
|
86
|
+
)}
|
|
87
|
+
</SettingsCard>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { SettingsAction, SettingsInput } from "@getpaseo/plugin/client/ui";
|
|
3
|
+
|
|
4
|
+
export type KeyStatus = { status: "ok"; stored: boolean } | { status: "error"; message: string };
|
|
5
|
+
export type KeyWriteResult = { status: "ok" } | { status: "error"; message: string };
|
|
6
|
+
|
|
7
|
+
export interface StoredKeyRowsProps {
|
|
8
|
+
/** What the value is, e.g. "API key" or "Account ID"; names the rows. */
|
|
9
|
+
subject: string;
|
|
10
|
+
/** The secrets.json entry name. */
|
|
11
|
+
name: string;
|
|
12
|
+
secretsDir: string | null;
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
status(input: { secretsDir: string | null; name: string }): Promise<KeyStatus>;
|
|
15
|
+
write(input: { secretsDir: string | null; name: string; value: string | null }): Promise<KeyWriteResult>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Stored = { kind: "unknown" } | { kind: "known"; stored: boolean } | { kind: "error"; message: string };
|
|
19
|
+
|
|
20
|
+
/** Write-only entry of one secrets.json key: the value is sent once and never shown again. */
|
|
21
|
+
export function StoredKeyRows({ subject, name, secretsDir, disabled, status, write }: StoredKeyRowsProps) {
|
|
22
|
+
const [stored, setStored] = useState<Stored>({ kind: "unknown" });
|
|
23
|
+
const [busy, setBusy] = useState(false);
|
|
24
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
25
|
+
const [typed, setTyped] = useState(false);
|
|
26
|
+
const [inputKey, setInputKey] = useState(0);
|
|
27
|
+
const value = useRef("");
|
|
28
|
+
const trimmedName = name.trim();
|
|
29
|
+
|
|
30
|
+
const refresh = useCallback(async () => {
|
|
31
|
+
if (trimmedName === "") return setStored({ kind: "unknown" });
|
|
32
|
+
try {
|
|
33
|
+
const result = await status({ secretsDir, name: trimmedName });
|
|
34
|
+
setStored(result.status === "ok" ? { kind: "known", stored: result.stored } : { kind: "error", message: result.message });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
setStored({ kind: "error", message: error instanceof Error ? error.message : String(error) });
|
|
37
|
+
}
|
|
38
|
+
}, [secretsDir, status, trimmedName]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
void refresh();
|
|
42
|
+
}, [refresh]);
|
|
43
|
+
|
|
44
|
+
const run = async (next: string | null) => {
|
|
45
|
+
setBusy(true);
|
|
46
|
+
setMessage(null);
|
|
47
|
+
try {
|
|
48
|
+
const result = await write({ secretsDir, name: trimmedName, value: next });
|
|
49
|
+
if (result.status === "error") return setMessage(result.message);
|
|
50
|
+
value.current = "";
|
|
51
|
+
setTyped(false);
|
|
52
|
+
setInputKey((key) => key + 1);
|
|
53
|
+
setMessage(next === null ? `Removed ${trimmedName} from secrets.json.` : `Saved ${trimmedName} to secrets.json.`);
|
|
54
|
+
await refresh();
|
|
55
|
+
} catch (error) {
|
|
56
|
+
setMessage(error instanceof Error ? error.message : String(error));
|
|
57
|
+
} finally {
|
|
58
|
+
setBusy(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const state =
|
|
63
|
+
stored.kind === "known"
|
|
64
|
+
? stored.stored
|
|
65
|
+
? `secrets.json holds a value for ${trimmedName}.`
|
|
66
|
+
: `secrets.json has no value for ${trimmedName} yet.`
|
|
67
|
+
: stored.kind === "error"
|
|
68
|
+
? stored.message
|
|
69
|
+
: "";
|
|
70
|
+
const canWrite = !disabled && !busy && trimmedName !== "";
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<>
|
|
74
|
+
<SettingsInput
|
|
75
|
+
key={`${subject}-${inputKey}`}
|
|
76
|
+
label={subject}
|
|
77
|
+
hint={`Convenience only: the value travels once from this screen to the daemon and is written to secrets.json. Prefer an environment variable or editing secrets.json on the daemon's machine. ${state}`.trim()}
|
|
78
|
+
error={message !== null && !message.startsWith("Saved") && !message.startsWith("Removed") ? message : null}
|
|
79
|
+
initialValue=""
|
|
80
|
+
placeholder={`Paste the ${subject.toLowerCase()}, then press Save`}
|
|
81
|
+
secureTextEntry={subject === "API key"}
|
|
82
|
+
disabled={!canWrite}
|
|
83
|
+
onChangeText={(text) => {
|
|
84
|
+
value.current = text;
|
|
85
|
+
setTyped(text.trim() !== "");
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
<SettingsAction
|
|
89
|
+
label={`Store ${subject}`}
|
|
90
|
+
hint={message !== null && (message.startsWith("Saved") || message.startsWith("Removed")) ? message : "Write-only: the value is never shown or read back."}
|
|
91
|
+
actionLabel={busy ? "Saving…" : "Save"}
|
|
92
|
+
disabled={!canWrite || !typed}
|
|
93
|
+
onPress={() => void run(value.current)}
|
|
94
|
+
/>
|
|
95
|
+
{stored.kind === "known" && stored.stored ? (
|
|
96
|
+
<SettingsAction
|
|
97
|
+
label={`Remove stored ${subject}`}
|
|
98
|
+
hint={`Deletes ${trimmedName} from secrets.json. Other entries are kept.`}
|
|
99
|
+
actionLabel="Remove"
|
|
100
|
+
disabled={!canWrite}
|
|
101
|
+
onPress={() => void run(null)}
|
|
102
|
+
/>
|
|
103
|
+
) : null}
|
|
104
|
+
</>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ProviderCatalogOutput } from "../../shared/rpc.js";
|
|
2
|
+
import type { PromptKitSettings } from "../../shared/settings.js";
|
|
3
|
+
|
|
4
|
+
type Providers = ProviderCatalogOutput["providers"];
|
|
5
|
+
|
|
6
|
+
/** Null when the selection may run; otherwise the user-facing reason. The API path ignores the CLI fields. */
|
|
7
|
+
export function validateDedicatedSelection(
|
|
8
|
+
settings: PromptKitSettings,
|
|
9
|
+
providers: Providers,
|
|
10
|
+
): string | null {
|
|
11
|
+
if (settings.transport === "api") return validateApiSelection(settings);
|
|
12
|
+
if (settings.modelMode !== "dedicated") return null;
|
|
13
|
+
const provider = settings.dedicatedProvider;
|
|
14
|
+
const model = settings.dedicatedModel;
|
|
15
|
+
if (provider === null || model === null) {
|
|
16
|
+
return "Select a dedicated provider and model.";
|
|
17
|
+
}
|
|
18
|
+
const entry = providers.find((candidate) => candidate.provider === provider);
|
|
19
|
+
if (!entry || !entry.available) return `Provider is unavailable: ${provider}`;
|
|
20
|
+
const selected = entry.models.find((candidate) => candidate.id === model);
|
|
21
|
+
if (!selected) return `Model is unavailable: ${provider}/${model}`;
|
|
22
|
+
const thinking = settings.dedicatedThinkingOptionId;
|
|
23
|
+
if (thinking !== null && !selected.thinkingOptions.some((option) => option.id === thinking)) {
|
|
24
|
+
return `Thinking option is unavailable: ${thinking}`;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** API path, no network call. Mappings alone may run; a selected endpoint must be complete. */
|
|
30
|
+
function validateApiSelection(settings: PromptKitSettings): string | null {
|
|
31
|
+
const mapped = Object.keys(settings.apiEndpointByProvider).length > 0;
|
|
32
|
+
const endpointId = settings.apiEndpointId;
|
|
33
|
+
if (endpointId === null) {
|
|
34
|
+
return mapped
|
|
35
|
+
? null
|
|
36
|
+
: "Add an API endpoint and select it before rewriting over the API.";
|
|
37
|
+
}
|
|
38
|
+
const endpoint = settings.apiEndpoints.find((candidate) => candidate.id === endpointId);
|
|
39
|
+
if (endpoint === undefined) return `No API endpoint is configured with the id "${endpointId}".`;
|
|
40
|
+
const model = settings.apiModel;
|
|
41
|
+
if (model === null) return "Select an API model.";
|
|
42
|
+
if (endpoint.models.length > 0 && !endpoint.models.includes(model)) {
|
|
43
|
+
return `Model is unavailable on endpoint "${endpoint.id}": ${model}`;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tells code outside the React tree that this client saved the settings. The
|
|
3
|
+
* host gives plugins no settings-change event outside `useSettings`, so the
|
|
4
|
+
* Composer pills listen here to follow a Save without a reload.
|
|
5
|
+
*/
|
|
6
|
+
const listeners = new Set<() => void>();
|
|
7
|
+
|
|
8
|
+
export function onSettingsSaved(listener: () => void): () => void {
|
|
9
|
+
listeners.add(listener);
|
|
10
|
+
return () => {
|
|
11
|
+
listeners.delete(listener);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function announceSettingsSaved(): void {
|
|
16
|
+
for (const listener of [...listeners]) listener();
|
|
17
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { Text, View } from "react-native";
|
|
3
|
+
import { useRpc, useSettings, type PluginSurfaceProps, type SettingsState } from "@getpaseo/plugin/client";
|
|
4
|
+
import { SettingsAction, SettingsCard, SettingsSection } from "@getpaseo/plugin/client/ui";
|
|
5
|
+
import {
|
|
6
|
+
actionsListRpc,
|
|
7
|
+
apiTestRpc,
|
|
8
|
+
providerCatalogRpc,
|
|
9
|
+
secretsStatusRpc,
|
|
10
|
+
secretsWriteRpc,
|
|
11
|
+
type ActionsListOutput,
|
|
12
|
+
type ProviderCatalogOutput,
|
|
13
|
+
} from "../../shared/rpc.js";
|
|
14
|
+
import type { ActionPack } from "../../shared/action-registry/schema.js";
|
|
15
|
+
import { promptKitSettings } from "../../shared/settings.js";
|
|
16
|
+
import { describeEnabledLimit, enabledActions } from "../actions/enabled.js";
|
|
17
|
+
import { useSettingsDraft } from "./draft.js";
|
|
18
|
+
import { describeReadiness } from "./readiness.js";
|
|
19
|
+
import { ActionsSection } from "./sections/actions-section.js";
|
|
20
|
+
import { AdvancedSection } from "./sections/advanced-section.js";
|
|
21
|
+
import { CustomActionsSection } from "./sections/custom-actions-section.js";
|
|
22
|
+
import { ApiEndpointSection } from "./sections/api-endpoint-section.js";
|
|
23
|
+
import { DedicatedModelSection } from "./sections/dedicated-model-section.js";
|
|
24
|
+
import { EngineSection } from "./sections/engine-section.js";
|
|
25
|
+
import { StatusBar } from "./ui/status-bar.js";
|
|
26
|
+
import { SPACE, textStyles } from "./ui/tokens.js";
|
|
27
|
+
|
|
28
|
+
type Providers = ProviderCatalogOutput["providers"];
|
|
29
|
+
type ReadySettings = Extract<SettingsState<typeof promptKitSettings.schema>, { status: "ready" }>;
|
|
30
|
+
|
|
31
|
+
function message(error: unknown): string {
|
|
32
|
+
return error instanceof Error ? error.message : String(error);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Provider catalog read once, action list re-read when the custom actions change; both retry. */
|
|
36
|
+
function useCatalogs(customActions: readonly ActionPack[]) {
|
|
37
|
+
const listProviders = useRpc(providerCatalogRpc);
|
|
38
|
+
const listActions = useRpc(actionsListRpc);
|
|
39
|
+
const [providers, setProviders] = useState<Providers | null>(null);
|
|
40
|
+
const [providersError, setProvidersError] = useState<string | null>(null);
|
|
41
|
+
const [registry, setRegistry] = useState<ActionsListOutput | null>(null);
|
|
42
|
+
const [actionsError, setActionsError] = useState<string | null>(null);
|
|
43
|
+
|
|
44
|
+
const reloadProviders = useCallback(() => {
|
|
45
|
+
setProvidersError(null);
|
|
46
|
+
void listProviders({})
|
|
47
|
+
.then((output) => setProviders(output.providers))
|
|
48
|
+
.catch((error: unknown) => setProvidersError(message(error)));
|
|
49
|
+
}, [listProviders]);
|
|
50
|
+
|
|
51
|
+
const customKey = JSON.stringify(customActions);
|
|
52
|
+
const reloadActions = useCallback(() => {
|
|
53
|
+
setActionsError(null);
|
|
54
|
+
void listActions({ customActions: JSON.parse(customKey) as ActionPack[] })
|
|
55
|
+
.then((output) => setRegistry(output))
|
|
56
|
+
.catch((error: unknown) => setActionsError(message(error)));
|
|
57
|
+
}, [listActions, customKey]);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
reloadProviders();
|
|
61
|
+
}, [reloadProviders]);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
reloadActions();
|
|
65
|
+
}, [reloadActions]);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
providers,
|
|
69
|
+
providersError,
|
|
70
|
+
reloadProviders,
|
|
71
|
+
actions: registry?.actions ?? null,
|
|
72
|
+
rejected: registry?.rejected ?? [],
|
|
73
|
+
actionsError,
|
|
74
|
+
reloadActions,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Status bar, then sections in setup order; irrelevant sections are not rendered. */
|
|
79
|
+
function ReadyScreen({ settings, theme, layout }: { settings: ReadySettings } & PluginSurfaceProps) {
|
|
80
|
+
const settingsDraft = useSettingsDraft(settings);
|
|
81
|
+
const catalogs = useCatalogs(settingsDraft.values.customActions);
|
|
82
|
+
// The enabled limit needs the registry, which the draft itself cannot read.
|
|
83
|
+
const limitProblem =
|
|
84
|
+
catalogs.actions === null ? null : describeEnabledLimit(catalogs.actions, settingsDraft.values);
|
|
85
|
+
const draft = { ...settingsDraft, problem: settingsDraft.problem ?? limitProblem };
|
|
86
|
+
const testEndpoint = useRpc(apiTestRpc);
|
|
87
|
+
const keyStatus = useRpc(secretsStatusRpc);
|
|
88
|
+
const writeKey = useRpc(secretsWriteRpc);
|
|
89
|
+
const { values } = draft;
|
|
90
|
+
const disabled = draft.saving;
|
|
91
|
+
|
|
92
|
+
const enabledCount =
|
|
93
|
+
catalogs.actions === null ? null : enabledActions(catalogs.actions, values).length;
|
|
94
|
+
const readiness = describeReadiness({ values, providers: catalogs.providers, enabledActionCount: enabledCount });
|
|
95
|
+
const actionsChanged = useMemo(
|
|
96
|
+
() =>
|
|
97
|
+
JSON.stringify([values.actionEnabled, values.customActions]) !==
|
|
98
|
+
JSON.stringify([settings.values.actionEnabled, settings.values.customActions]),
|
|
99
|
+
[values.actionEnabled, values.customActions, settings.values.actionEnabled, settings.values.customActions],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<View style={{ gap: SPACE.lg }}>
|
|
104
|
+
<StatusBar
|
|
105
|
+
theme={theme}
|
|
106
|
+
readiness={readiness}
|
|
107
|
+
draft={draft}
|
|
108
|
+
actionsChanged={actionsChanged}
|
|
109
|
+
compact={layout.compact}
|
|
110
|
+
/>
|
|
111
|
+
|
|
112
|
+
<ActionsSection
|
|
113
|
+
actions={catalogs.actions}
|
|
114
|
+
rejected={catalogs.rejected}
|
|
115
|
+
error={catalogs.actionsError}
|
|
116
|
+
values={values}
|
|
117
|
+
disabled={disabled}
|
|
118
|
+
patch={draft.patch}
|
|
119
|
+
reload={catalogs.reloadActions}
|
|
120
|
+
/>
|
|
121
|
+
|
|
122
|
+
<CustomActionsSection
|
|
123
|
+
theme={theme}
|
|
124
|
+
actions={catalogs.actions}
|
|
125
|
+
values={values}
|
|
126
|
+
disabled={disabled}
|
|
127
|
+
patch={draft.patch}
|
|
128
|
+
/>
|
|
129
|
+
|
|
130
|
+
<EngineSection theme={theme} compact={layout.compact} values={values} disabled={disabled} patch={draft.patch} />
|
|
131
|
+
|
|
132
|
+
{values.transport === "cli" && values.modelMode === "dedicated" ? (
|
|
133
|
+
<DedicatedModelSection
|
|
134
|
+
values={values}
|
|
135
|
+
providers={catalogs.providers}
|
|
136
|
+
providersError={catalogs.providersError}
|
|
137
|
+
disabled={disabled}
|
|
138
|
+
patch={draft.patch}
|
|
139
|
+
reloadProviders={catalogs.reloadProviders}
|
|
140
|
+
/>
|
|
141
|
+
) : null}
|
|
142
|
+
|
|
143
|
+
{values.transport === "api" ? (
|
|
144
|
+
<ApiEndpointSection
|
|
145
|
+
values={values}
|
|
146
|
+
disabled={disabled}
|
|
147
|
+
epoch={draft.epoch}
|
|
148
|
+
patch={draft.patch}
|
|
149
|
+
test={(endpoint, secretsDir) => testEndpoint({ endpoint, secretsDir })}
|
|
150
|
+
keyStatus={keyStatus}
|
|
151
|
+
writeKey={writeKey}
|
|
152
|
+
/>
|
|
153
|
+
) : null}
|
|
154
|
+
|
|
155
|
+
<AdvancedSection
|
|
156
|
+
theme={theme}
|
|
157
|
+
values={values}
|
|
158
|
+
providers={catalogs.providers}
|
|
159
|
+
disabled={disabled}
|
|
160
|
+
epoch={draft.epoch}
|
|
161
|
+
patch={draft.patch}
|
|
162
|
+
/>
|
|
163
|
+
</View>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function PromptKitSettingsScreen(props: PluginSurfaceProps) {
|
|
168
|
+
const settings = useSettings(promptKitSettings);
|
|
169
|
+
const text = useMemo(() => textStyles(props.theme), [props.theme]);
|
|
170
|
+
|
|
171
|
+
if (settings.status === "loading") {
|
|
172
|
+
return <Text style={text.muted}>Loading PromptKit settings…</Text>;
|
|
173
|
+
}
|
|
174
|
+
if (settings.status !== "ready") {
|
|
175
|
+
return (
|
|
176
|
+
<SettingsSection title="PromptKit settings could not be read">
|
|
177
|
+
<SettingsCard>
|
|
178
|
+
<SettingsAction
|
|
179
|
+
label="Try again"
|
|
180
|
+
error={settings.error}
|
|
181
|
+
actionLabel="Reload"
|
|
182
|
+
onPress={() => void settings.reload()}
|
|
183
|
+
/>
|
|
184
|
+
{settings.status === "invalid" ? (
|
|
185
|
+
<SettingsAction
|
|
186
|
+
label="Restore default settings"
|
|
187
|
+
hint="Replaces the stored document with the defaults. Endpoints and overrides are lost."
|
|
188
|
+
actionLabel="Reset"
|
|
189
|
+
onPress={() => void settings.reset()}
|
|
190
|
+
/>
|
|
191
|
+
) : null}
|
|
192
|
+
</SettingsCard>
|
|
193
|
+
</SettingsSection>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return <ReadyScreen {...props} settings={settings} />;
|
|
197
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { Pressable, Text } from "react-native";
|
|
3
|
+
import type { PluginTheme } from "@getpaseo/plugin";
|
|
4
|
+
import { FONT, RADIUS, SPACE } from "./tokens.js";
|
|
5
|
+
|
|
6
|
+
export interface ButtonProps {
|
|
7
|
+
theme: PluginTheme;
|
|
8
|
+
label: string;
|
|
9
|
+
onPress(): void;
|
|
10
|
+
/** `primary` is the one action the screen wants pressed; `ghost` has no chrome. */
|
|
11
|
+
variant?: "primary" | "outline" | "ghost";
|
|
12
|
+
/** Stretch to share the row equally with siblings. */
|
|
13
|
+
fill?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
testID?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Button for use outside a SettingsRow; mirrors the host button's size and radius. */
|
|
19
|
+
export function Button({ theme, label, onPress, variant = "outline", fill, disabled, testID }: ButtonProps) {
|
|
20
|
+
const primary = variant === "primary";
|
|
21
|
+
const ghost = variant === "ghost";
|
|
22
|
+
const container = useMemo(
|
|
23
|
+
() => ({
|
|
24
|
+
paddingVertical: SPACE.xs + 2,
|
|
25
|
+
paddingHorizontal: SPACE.md,
|
|
26
|
+
borderRadius: RADIUS.md,
|
|
27
|
+
borderWidth: ghost ? 0 : 1,
|
|
28
|
+
borderColor: primary ? theme.colors.accent : theme.colors.border,
|
|
29
|
+
backgroundColor: primary ? theme.colors.accent : "transparent",
|
|
30
|
+
opacity: disabled ? 0.5 : 1,
|
|
31
|
+
alignItems: "center" as const,
|
|
32
|
+
...(fill ? { flex: 1 } : {}),
|
|
33
|
+
}),
|
|
34
|
+
[theme, primary, ghost, fill, disabled],
|
|
35
|
+
);
|
|
36
|
+
const text = useMemo(
|
|
37
|
+
() => ({
|
|
38
|
+
color: primary ? theme.colors.accentForeground : theme.colors.foreground,
|
|
39
|
+
fontSize: FONT.base,
|
|
40
|
+
fontWeight: "500" as const,
|
|
41
|
+
}),
|
|
42
|
+
[theme, primary],
|
|
43
|
+
);
|
|
44
|
+
return (
|
|
45
|
+
<Pressable
|
|
46
|
+
accessibilityRole="button"
|
|
47
|
+
accessibilityLabel={label}
|
|
48
|
+
disabled={disabled}
|
|
49
|
+
onPress={onPress}
|
|
50
|
+
style={container}
|
|
51
|
+
testID={testID}
|
|
52
|
+
>
|
|
53
|
+
<Text style={text}>{label}</Text>
|
|
54
|
+
</Pressable>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useMemo, type ReactNode } from "react";
|
|
2
|
+
import { Text, View } from "react-native";
|
|
3
|
+
import type { PluginTheme } from "@getpaseo/plugin";
|
|
4
|
+
import { RADIUS, SPACE, textStyles, toneColor, type Tone } from "./tokens.js";
|
|
5
|
+
|
|
6
|
+
export interface NoticeProps {
|
|
7
|
+
theme: PluginTheme;
|
|
8
|
+
tone: Tone;
|
|
9
|
+
title: string;
|
|
10
|
+
/** One or two short lines under the title. */
|
|
11
|
+
lines?: readonly string[];
|
|
12
|
+
/** Controls on the right, or below in a compact layout. */
|
|
13
|
+
trailing?: ReactNode;
|
|
14
|
+
compact?: boolean;
|
|
15
|
+
testID?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Bordered card with a status dot, title and short lines. */
|
|
19
|
+
export function Notice({ theme, tone, title, lines = [], trailing, compact, testID }: NoticeProps) {
|
|
20
|
+
const text = useMemo(() => textStyles(theme), [theme]);
|
|
21
|
+
const color = toneColor(theme, tone);
|
|
22
|
+
const card = useMemo(
|
|
23
|
+
() => ({
|
|
24
|
+
flexDirection: compact ? ("column" as const) : ("row" as const),
|
|
25
|
+
alignItems: compact ? ("stretch" as const) : ("center" as const),
|
|
26
|
+
gap: SPACE.md,
|
|
27
|
+
padding: SPACE.lg,
|
|
28
|
+
borderRadius: RADIUS.lg,
|
|
29
|
+
borderWidth: 1,
|
|
30
|
+
borderColor: theme.colors.border,
|
|
31
|
+
backgroundColor: theme.colors.surface1,
|
|
32
|
+
}),
|
|
33
|
+
[theme, compact],
|
|
34
|
+
);
|
|
35
|
+
const dot = useMemo(
|
|
36
|
+
() => ({ width: 8, height: 8, borderRadius: 4, backgroundColor: color, marginTop: 6 }),
|
|
37
|
+
[color],
|
|
38
|
+
);
|
|
39
|
+
return (
|
|
40
|
+
<View style={card} testID={testID}>
|
|
41
|
+
<View style={{ flexDirection: "row", gap: SPACE.sm, flex: 1, minWidth: 0 }}>
|
|
42
|
+
<View style={dot} />
|
|
43
|
+
<View style={{ flex: 1, gap: SPACE.xs }}>
|
|
44
|
+
<Text style={text.strong} testID={testID ? `${testID}-title` : undefined}>
|
|
45
|
+
{title}
|
|
46
|
+
</Text>
|
|
47
|
+
{lines.map((line) => (
|
|
48
|
+
<Text key={line} style={text.muted}>
|
|
49
|
+
{line}
|
|
50
|
+
</Text>
|
|
51
|
+
))}
|
|
52
|
+
</View>
|
|
53
|
+
</View>
|
|
54
|
+
{trailing ? (
|
|
55
|
+
<View style={{ flexDirection: "row", gap: SPACE.sm, justifyContent: "flex-end" }}>
|
|
56
|
+
{trailing}
|
|
57
|
+
</View>
|
|
58
|
+
) : null}
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
}
|