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,79 @@
|
|
|
1
|
+
import { actionPackSchema, type ActionPack } from "../../shared/action-registry/schema.js";
|
|
2
|
+
import type { ActionSummary } from "../../shared/rpc.js";
|
|
3
|
+
import type { PromptKitSettings } from "../../shared/settings.js";
|
|
4
|
+
import { MAX_ENABLED_ACTIONS, enabledActions } from "../actions/enabled.js";
|
|
5
|
+
|
|
6
|
+
export type ParsedAction = { ok: true; pack: ActionPack } | { ok: false; error: string };
|
|
7
|
+
|
|
8
|
+
/** JSON as the editor shows it. */
|
|
9
|
+
export function formatPack(pack: ActionPack): string {
|
|
10
|
+
return JSON.stringify(pack, null, 2);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** `base`, or `base-2`, `base-3`… so a sample never collides with a loaded action. */
|
|
14
|
+
export function freeActionId(base: string, taken: ReadonlySet<string>): string {
|
|
15
|
+
if (!taken.has(base)) return base;
|
|
16
|
+
for (let n = 2; ; n += 1) {
|
|
17
|
+
const id = `${base}-${n}`;
|
|
18
|
+
if (!taken.has(id)) return id;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Editor text → a pack that can be stored, or the reason it cannot. `takenIds`
|
|
24
|
+
* holds every loaded action id except the one being edited.
|
|
25
|
+
*/
|
|
26
|
+
export function parseCustomAction(text: string, takenIds: ReadonlySet<string>): ParsedAction {
|
|
27
|
+
let json: unknown;
|
|
28
|
+
try {
|
|
29
|
+
json = JSON.parse(text);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return { ok: false, error: `Not valid JSON: ${error instanceof Error ? error.message : String(error)}` };
|
|
32
|
+
}
|
|
33
|
+
const parsed = actionPackSchema.safeParse(json);
|
|
34
|
+
if (!parsed.success) {
|
|
35
|
+
const issue = parsed.error.issues[0];
|
|
36
|
+
const path = issue?.path.join(".") ?? "";
|
|
37
|
+
return { ok: false, error: `${path === "" ? "The pack" : `"${path}"`}: ${issue?.message ?? "invalid"}` };
|
|
38
|
+
}
|
|
39
|
+
if (takenIds.has(parsed.data.id)) {
|
|
40
|
+
return { ok: false, error: `Another action already uses the id "${parsed.data.id}".` };
|
|
41
|
+
}
|
|
42
|
+
return { ok: true, pack: parsed.data };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The settings change that stores `pack` in place of `replacingId` (null adds it).
|
|
47
|
+
* A renamed action keeps its switch; a new one starts off when the enabled set is full.
|
|
48
|
+
*/
|
|
49
|
+
export function storeCustomAction(
|
|
50
|
+
current: PromptKitSettings,
|
|
51
|
+
actions: readonly ActionSummary[],
|
|
52
|
+
pack: ActionPack,
|
|
53
|
+
replacingId: string | null,
|
|
54
|
+
): Partial<PromptKitSettings> {
|
|
55
|
+
const customActions =
|
|
56
|
+
replacingId === null
|
|
57
|
+
? [...current.customActions, pack]
|
|
58
|
+
: current.customActions.map((existing) => (existing.id === replacingId ? pack : existing));
|
|
59
|
+
|
|
60
|
+
const actionEnabled = { ...current.actionEnabled };
|
|
61
|
+
if (replacingId !== null && replacingId !== pack.id && replacingId in actionEnabled) {
|
|
62
|
+
actionEnabled[pack.id] = actionEnabled[replacingId]!;
|
|
63
|
+
delete actionEnabled[replacingId];
|
|
64
|
+
}
|
|
65
|
+
if (replacingId === null) {
|
|
66
|
+
const full = enabledActions(actions, current).length >= MAX_ENABLED_ACTIONS;
|
|
67
|
+
if (full && pack.enabledByDefault) actionEnabled[pack.id] = false;
|
|
68
|
+
}
|
|
69
|
+
return { customActions, actionEnabled };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The settings change that deletes a custom action and its switch. */
|
|
73
|
+
export function removeCustomAction(current: PromptKitSettings, id: string): Partial<PromptKitSettings> {
|
|
74
|
+
const { [id]: _removed, ...actionEnabled } = current.actionEnabled;
|
|
75
|
+
return {
|
|
76
|
+
customActions: current.customActions.filter((pack) => pack.id !== id),
|
|
77
|
+
actionEnabled,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import type { SettingsState } from "@getpaseo/plugin/client";
|
|
3
|
+
import type { promptKitSettings, PromptKitSettings } from "../../shared/settings.js";
|
|
4
|
+
import { announceSettingsSaved } from "./settings-saved.js";
|
|
5
|
+
import { findSaveProblem } from "./validation.js";
|
|
6
|
+
|
|
7
|
+
type ReadySettings = Extract<SettingsState<typeof promptKitSettings.schema>, { status: "ready" }>;
|
|
8
|
+
|
|
9
|
+
export type SettingsPatch =
|
|
10
|
+
| Partial<PromptKitSettings>
|
|
11
|
+
| ((current: PromptKitSettings) => Partial<PromptKitSettings>);
|
|
12
|
+
|
|
13
|
+
export interface SettingsDraft {
|
|
14
|
+
/** What the screen shows: the draft when one exists, else the host document. */
|
|
15
|
+
readonly values: PromptKitSettings;
|
|
16
|
+
readonly dirty: boolean;
|
|
17
|
+
readonly saving: boolean;
|
|
18
|
+
/** The host's last save failure, cleared by the next edit or save. */
|
|
19
|
+
readonly saveError: string | null;
|
|
20
|
+
/** Why the draft cannot be saved as it stands; null when Save is allowed. */
|
|
21
|
+
readonly problem: string | null;
|
|
22
|
+
/** True right after a successful save, until the next edit. */
|
|
23
|
+
readonly justSaved: boolean;
|
|
24
|
+
/** Bumps on discard/save; uncontrolled inputs key on it. */
|
|
25
|
+
readonly epoch: number;
|
|
26
|
+
patch(update: SettingsPatch): void;
|
|
27
|
+
discard(): void;
|
|
28
|
+
save(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Draft over the host document; functional patches; revision pinned at first edit. */
|
|
32
|
+
export function useSettingsDraft(settings: ReadySettings): SettingsDraft {
|
|
33
|
+
const [draft, setDraft] = useState<{ values: PromptKitSettings; revision: string } | null>(null);
|
|
34
|
+
const [justSaved, setJustSaved] = useState(false);
|
|
35
|
+
const [epoch, setEpoch] = useState(0);
|
|
36
|
+
|
|
37
|
+
const values = draft?.values ?? settings.values;
|
|
38
|
+
const revision = draft?.revision ?? settings.revision;
|
|
39
|
+
const problem = useMemo(() => findSaveProblem(values), [values]);
|
|
40
|
+
|
|
41
|
+
const patch = useCallback(
|
|
42
|
+
(update: SettingsPatch) => {
|
|
43
|
+
setJustSaved(false);
|
|
44
|
+
setDraft((previous) => {
|
|
45
|
+
const base = previous ?? { values: settings.values, revision: settings.revision };
|
|
46
|
+
const changes = typeof update === "function" ? update(base.values) : update;
|
|
47
|
+
return { ...base, values: { ...base.values, ...changes } };
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
[settings.values, settings.revision],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const discard = useCallback(() => {
|
|
54
|
+
setDraft(null);
|
|
55
|
+
setJustSaved(false);
|
|
56
|
+
setEpoch((current) => current + 1);
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const save = useCallback(async () => {
|
|
60
|
+
if (draft === null || problem !== null) return;
|
|
61
|
+
const ok = await settings.save(draft.values, revision);
|
|
62
|
+
if (ok) {
|
|
63
|
+
announceSettingsSaved();
|
|
64
|
+
setDraft(null);
|
|
65
|
+
setJustSaved(true);
|
|
66
|
+
setEpoch((current) => current + 1);
|
|
67
|
+
}
|
|
68
|
+
}, [draft, problem, revision, settings]);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
values,
|
|
72
|
+
dirty: draft !== null,
|
|
73
|
+
saving: settings.saving,
|
|
74
|
+
saveError: settings.saveError,
|
|
75
|
+
problem,
|
|
76
|
+
justSaved,
|
|
77
|
+
epoch,
|
|
78
|
+
patch,
|
|
79
|
+
discard,
|
|
80
|
+
save,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface ModelOption {
|
|
2
|
+
readonly label: string;
|
|
3
|
+
readonly value: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Lists at or below this length get no filter row. */
|
|
7
|
+
export const MODEL_FILTER_THRESHOLD = 8;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pure: options whose name or id contains the query, case-insensitive. The option equal to
|
|
11
|
+
* `keep` (the current selection) stays, so the host dropdown still shows it.
|
|
12
|
+
*/
|
|
13
|
+
export function filterModels(
|
|
14
|
+
options: readonly ModelOption[],
|
|
15
|
+
query: string,
|
|
16
|
+
keep: string | null = null,
|
|
17
|
+
): readonly ModelOption[] {
|
|
18
|
+
const needle = query.trim().toLowerCase();
|
|
19
|
+
if (needle === "") return options;
|
|
20
|
+
return options.filter(
|
|
21
|
+
(option) =>
|
|
22
|
+
option.value === keep ||
|
|
23
|
+
option.label.toLowerCase().includes(needle) ||
|
|
24
|
+
option.value.toLowerCase().includes(needle),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Hint for the filter row: how many of the models the dropdown now lists. */
|
|
29
|
+
export function describeFilter(total: number, shown: number, query: string): string {
|
|
30
|
+
return query.trim() === ""
|
|
31
|
+
? `Type part of a name or id to shorten the Model list below (${total} models).`
|
|
32
|
+
: `${shown} of ${total} models match.`;
|
|
33
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { settingsRpc } from "@getpaseo/plugin";
|
|
2
|
+
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
|
3
|
+
import {
|
|
4
|
+
promptKitSettings,
|
|
5
|
+
promptKitSettingsSchema,
|
|
6
|
+
type PromptKitSettings,
|
|
7
|
+
} from "../../shared/settings.js";
|
|
8
|
+
|
|
9
|
+
export type SettingsRead =
|
|
10
|
+
| { status: "ready"; values: PromptKitSettings }
|
|
11
|
+
| { status: "invalid"; error: string };
|
|
12
|
+
|
|
13
|
+
type Rpc = PluginClientContext["rpc"];
|
|
14
|
+
|
|
15
|
+
function message(error: unknown): string {
|
|
16
|
+
return error instanceof Error ? error.message : String(error);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reads the persisted settings through the host settings API. Every failure —
|
|
21
|
+
* unreadable document or schema mismatch — is reported as `invalid` so callers
|
|
22
|
+
* refuse instead of falling back to defaults the user never chose.
|
|
23
|
+
*
|
|
24
|
+
* Whether a *dedicated* selection is usable depends on the live provider
|
|
25
|
+
* catalog, so that check belongs to the rewrite path
|
|
26
|
+
* (`validateDedicatedSelection`), not here. Reporting an incomplete selection as
|
|
27
|
+
* an unreadable document would also hide the pill, because the pill's enabled
|
|
28
|
+
* set cannot be computed from a document the reader refuses.
|
|
29
|
+
*/
|
|
30
|
+
export function createSettingsReader(rpc: Rpc): () => Promise<SettingsRead> {
|
|
31
|
+
const read = settingsRpc(promptKitSettings.id).read;
|
|
32
|
+
return async () => {
|
|
33
|
+
try {
|
|
34
|
+
const result = await rpc(read, {});
|
|
35
|
+
if (result.status !== "ready") return { status: "invalid", error: result.error };
|
|
36
|
+
const parsed = promptKitSettingsSchema.safeParse(result.values);
|
|
37
|
+
if (!parsed.success) {
|
|
38
|
+
return { status: "invalid", error: "PromptKit settings are invalid." };
|
|
39
|
+
}
|
|
40
|
+
return { status: "ready", values: parsed.data };
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return { status: "invalid", error: message(error) };
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ProviderCatalogOutput } from "../../shared/rpc.js";
|
|
2
|
+
import { resolveLanguage } from "../../shared/language-registry/registry.js";
|
|
3
|
+
import type { PromptKitSettings } from "../../shared/settings.js";
|
|
4
|
+
import { validateDedicatedSelection } from "./selection.js";
|
|
5
|
+
|
|
6
|
+
type Providers = ProviderCatalogOutput["providers"];
|
|
7
|
+
|
|
8
|
+
/** Status line: would a rewrite run, over which path, and if not why. Computed from the draft. */
|
|
9
|
+
export type Readiness =
|
|
10
|
+
| { kind: "ready"; path: string; detail: string }
|
|
11
|
+
| { kind: "blocked"; path: string; reason: string }
|
|
12
|
+
| { kind: "checking"; path: string; detail: string };
|
|
13
|
+
|
|
14
|
+
export interface ReadinessInput {
|
|
15
|
+
values: PromptKitSettings;
|
|
16
|
+
/** Null while the catalog has not been read yet. */
|
|
17
|
+
providers: Providers | null;
|
|
18
|
+
/** Null while the action list has not been read yet. */
|
|
19
|
+
enabledActionCount: number | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function pathLabel(values: PromptKitSettings): string {
|
|
23
|
+
const transport = values.transport === "api" ? "Direct API" : "Provider CLI";
|
|
24
|
+
if (values.transport === "api") {
|
|
25
|
+
const mapped = Object.keys(values.apiEndpointByProvider).length;
|
|
26
|
+
const viaMapping = `agent model via ${mapped} mapped provider${mapped === 1 ? "" : "s"}`;
|
|
27
|
+
if (values.apiEndpointId === null) return `${transport} · ${mapped > 0 ? viaMapping : "no endpoint"}`;
|
|
28
|
+
const selected = `${transport} · ${values.apiEndpointId} · ${values.apiModel ?? "no model"}`;
|
|
29
|
+
return mapped > 0 ? `${selected} · ${viaMapping}` : selected;
|
|
30
|
+
}
|
|
31
|
+
if (values.modelMode === "dedicated") {
|
|
32
|
+
const provider = values.dedicatedProvider ?? "no provider";
|
|
33
|
+
const model = values.dedicatedModel ?? "no model";
|
|
34
|
+
return `${transport} · ${provider} · ${model}`;
|
|
35
|
+
}
|
|
36
|
+
return `${transport} · current agent model`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function describeReadiness(input: ReadinessInput): Readiness {
|
|
40
|
+
const { values, providers, enabledActionCount } = input;
|
|
41
|
+
const path = pathLabel(values);
|
|
42
|
+
|
|
43
|
+
if (enabledActionCount === 0) {
|
|
44
|
+
return {
|
|
45
|
+
kind: "blocked",
|
|
46
|
+
path,
|
|
47
|
+
reason: "No action is enabled, so the Composer shows no PromptKit pill.",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (resolveLanguage(values.outputLanguage) === undefined) {
|
|
52
|
+
return {
|
|
53
|
+
kind: "blocked",
|
|
54
|
+
path,
|
|
55
|
+
reason: `No output language is loaded with the id "${values.outputLanguage}".`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (values.transport === "cli" && values.modelMode === "dedicated" && providers === null) {
|
|
60
|
+
return { kind: "checking", path, detail: "Reading the provider catalog…" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const problem = validateDedicatedSelection(values, providers ?? []);
|
|
64
|
+
if (problem !== null) return { kind: "blocked", path, reason: problem };
|
|
65
|
+
|
|
66
|
+
if (values.transport === "api") {
|
|
67
|
+
return {
|
|
68
|
+
kind: "ready",
|
|
69
|
+
path,
|
|
70
|
+
detail:
|
|
71
|
+
values.apiEndpointId !== null
|
|
72
|
+
? "One HTTP request to the selected endpoint. No CLI is started."
|
|
73
|
+
: "Each mapped provider's agent model is sent to its endpoint; other providers are refused.",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
kind: "ready",
|
|
78
|
+
path,
|
|
79
|
+
detail:
|
|
80
|
+
values.modelMode === "dedicated"
|
|
81
|
+
? "The dedicated provider's CLI runs headlessly in a scratch directory."
|
|
82
|
+
: "The agent's own provider CLI runs the model the Composer shows.",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { SettingsAction, SettingsCard, SettingsRow, SettingsSection, SettingsSwitch } from "@getpaseo/plugin/client/ui";
|
|
2
|
+
import type { ActionSummary, ActionsListOutput } from "../../../shared/rpc.js";
|
|
3
|
+
import type { PromptKitSettings } from "../../../shared/settings.js";
|
|
4
|
+
import { MAX_ENABLED_ACTIONS, enabledActions } from "../../actions/enabled.js";
|
|
5
|
+
import type { SettingsPatch } from "../draft.js";
|
|
6
|
+
|
|
7
|
+
export interface ActionsSectionProps {
|
|
8
|
+
/** Null while the registry has not answered yet. */
|
|
9
|
+
actions: readonly ActionSummary[] | null;
|
|
10
|
+
/** Packs the registry refused, e.g. a custom action reusing a bundled id. */
|
|
11
|
+
rejected: ActionsListOutput["rejected"];
|
|
12
|
+
error: string | null;
|
|
13
|
+
values: PromptKitSettings;
|
|
14
|
+
disabled: boolean;
|
|
15
|
+
patch(update: SettingsPatch): void;
|
|
16
|
+
reload(): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hintFor(action: ActionSummary): string {
|
|
20
|
+
return action.custom ? `Custom · ${action.description}` : action.description;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Per-action enable switches, at most `MAX_ENABLED_ACTIONS` on; a lone action has no switch. */
|
|
24
|
+
export function ActionsSection({ actions, rejected, error, values, disabled, patch, reload }: ActionsSectionProps) {
|
|
25
|
+
const enabledCount = actions === null ? 0 : enabledActions(actions, values).length;
|
|
26
|
+
return (
|
|
27
|
+
<SettingsSection
|
|
28
|
+
title="Actions"
|
|
29
|
+
info={`One enabled action makes the pill rewrite at once; two or more make it a menu to choose from. Up to ${MAX_ENABLED_ACTIONS} can be on. Add your own under Custom actions.`}
|
|
30
|
+
>
|
|
31
|
+
<SettingsCard>
|
|
32
|
+
{error !== null ? (
|
|
33
|
+
<SettingsAction
|
|
34
|
+
label="Could not load actions"
|
|
35
|
+
error={error}
|
|
36
|
+
actionLabel="Retry"
|
|
37
|
+
disabled={disabled}
|
|
38
|
+
onPress={reload}
|
|
39
|
+
/>
|
|
40
|
+
) : actions === null ? (
|
|
41
|
+
<SettingsRow label="Loading actions…" />
|
|
42
|
+
) : actions.length === 0 ? (
|
|
43
|
+
<SettingsRow
|
|
44
|
+
label="No action pack is loaded"
|
|
45
|
+
hint="Add one under Custom actions below."
|
|
46
|
+
/>
|
|
47
|
+
) : actions.length === 1 && enabledCount === 1 ? (
|
|
48
|
+
<SettingsRow label={actions[0]!.title} hint={`Always on · ${hintFor(actions[0]!)}`} />
|
|
49
|
+
) : (
|
|
50
|
+
actions.map((action) => {
|
|
51
|
+
const on = enabledActions([action], values).length === 1;
|
|
52
|
+
const full = !on && enabledCount >= MAX_ENABLED_ACTIONS;
|
|
53
|
+
return (
|
|
54
|
+
<SettingsSwitch
|
|
55
|
+
key={action.id}
|
|
56
|
+
label={action.title}
|
|
57
|
+
hint={full ? `${MAX_ENABLED_ACTIONS} actions are on; turn one off first.` : hintFor(action)}
|
|
58
|
+
value={on}
|
|
59
|
+
disabled={disabled || full}
|
|
60
|
+
onValueChange={(next) =>
|
|
61
|
+
patch((current) => ({
|
|
62
|
+
actionEnabled: { ...current.actionEnabled, [action.id]: next },
|
|
63
|
+
}))
|
|
64
|
+
}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
})
|
|
68
|
+
)}
|
|
69
|
+
{rejected.map((entry) => (
|
|
70
|
+
<SettingsRow key={`rejected-${entry.source}-${entry.reason}`} label={`Not loaded: ${entry.source}`} error={entry.reason} />
|
|
71
|
+
))}
|
|
72
|
+
</SettingsCard>
|
|
73
|
+
</SettingsSection>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { SettingsCard, SettingsInput, SettingsRow, SettingsSection } from "@getpaseo/plugin/client/ui";
|
|
3
|
+
import type { PluginTheme } from "@getpaseo/plugin";
|
|
4
|
+
import { CLI_FAMILY_IDS, isCliFamilyId } from "../../../shared/cli-families.js";
|
|
5
|
+
import type { ProviderCatalogOutput } from "../../../shared/rpc.js";
|
|
6
|
+
import { TIMEOUT_MS, type PromptKitSettings } from "../../../shared/settings.js";
|
|
7
|
+
import type { SettingsPatch } from "../draft.js";
|
|
8
|
+
import { Button } from "../ui/button.js";
|
|
9
|
+
import { ProviderMapCard } from "./provider-map-card.js";
|
|
10
|
+
import { describeTimeout } from "../validation.js";
|
|
11
|
+
|
|
12
|
+
type Providers = ProviderCatalogOutput["providers"];
|
|
13
|
+
|
|
14
|
+
export interface AdvancedSectionProps {
|
|
15
|
+
theme: PluginTheme;
|
|
16
|
+
values: PromptKitSettings;
|
|
17
|
+
providers: Providers | null;
|
|
18
|
+
disabled: boolean;
|
|
19
|
+
epoch: number;
|
|
20
|
+
patch(update: SettingsPatch): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const NONE = "";
|
|
24
|
+
|
|
25
|
+
/** Timeout, secrets dir, per-provider maps. Collapsed by default. */
|
|
26
|
+
export function AdvancedSection({ theme, values, providers, disabled, epoch, patch }: AdvancedSectionProps) {
|
|
27
|
+
const [open, setOpen] = useState(false);
|
|
28
|
+
const timeout = describeTimeout(values.timeoutMs);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<SettingsSection
|
|
32
|
+
title="Advanced"
|
|
33
|
+
info="Timeout, where secrets.json lives, and per-provider overrides. Most setups never need these."
|
|
34
|
+
trailing={
|
|
35
|
+
<Button
|
|
36
|
+
theme={theme}
|
|
37
|
+
label={open ? "Hide" : "Show"}
|
|
38
|
+
onPress={() => setOpen((current) => !current)}
|
|
39
|
+
testID="prompt-kit-advanced-toggle"
|
|
40
|
+
/>
|
|
41
|
+
}
|
|
42
|
+
>
|
|
43
|
+
{open ? (
|
|
44
|
+
<>
|
|
45
|
+
<SettingsCard>
|
|
46
|
+
<SettingsInput
|
|
47
|
+
key={`${epoch}-timeout`}
|
|
48
|
+
label="Timeout (ms)"
|
|
49
|
+
hint={timeout.note ?? `How long one rewrite may run. Default ${TIMEOUT_MS.default.toLocaleString()} ms.`}
|
|
50
|
+
error={timeout.error}
|
|
51
|
+
initialValue={String(values.timeoutMs)}
|
|
52
|
+
placeholder={String(TIMEOUT_MS.default)}
|
|
53
|
+
disabled={disabled}
|
|
54
|
+
onChangeText={(text) => {
|
|
55
|
+
const parsed = Number(text.trim());
|
|
56
|
+
patch({ timeoutMs: Number.isFinite(parsed) ? parsed : Number.NaN });
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
</SettingsCard>
|
|
60
|
+
|
|
61
|
+
{values.transport === "cli" ? (
|
|
62
|
+
<ProviderMapCard
|
|
63
|
+
title="CLI per provider"
|
|
64
|
+
hint="Paseo names most providers after their CLI, so the family is read from the id and no entry is needed. Map only a provider whose id does not say which CLI runs it; an unresolved provider is refused, never guessed."
|
|
65
|
+
providers={providers}
|
|
66
|
+
map={values.providerCli}
|
|
67
|
+
targets={CLI_FAMILY_IDS.map((family) => ({ label: family, value: family }))}
|
|
68
|
+
emptyTargetsLabel="No CLI family is available."
|
|
69
|
+
errorFor={(providerId, family) =>
|
|
70
|
+
isCliFamilyId(family) ? null : `"${family}" is not a supported CLI.`
|
|
71
|
+
}
|
|
72
|
+
disabled={disabled}
|
|
73
|
+
onSet={(providerId, family) =>
|
|
74
|
+
patch((current) => ({
|
|
75
|
+
providerCli: isCliFamilyId(family)
|
|
76
|
+
? { ...current.providerCli, [providerId]: family }
|
|
77
|
+
: current.providerCli,
|
|
78
|
+
}))
|
|
79
|
+
}
|
|
80
|
+
onRemove={(providerId) =>
|
|
81
|
+
patch((current) => {
|
|
82
|
+
const next = { ...current.providerCli };
|
|
83
|
+
delete next[providerId];
|
|
84
|
+
return { providerCli: next };
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
/>
|
|
88
|
+
) : (
|
|
89
|
+
<ProviderMapCard
|
|
90
|
+
title="Endpoint per provider"
|
|
91
|
+
hint="When you press the pill in an agent of a mapped provider, that agent's own model is sent to the endpoint over HTTP instead of through its CLI. A mapped provider ignores the Model chosen under API endpoint."
|
|
92
|
+
providers={providers}
|
|
93
|
+
map={values.apiEndpointByProvider}
|
|
94
|
+
targets={values.apiEndpoints.map((endpoint) => ({ label: endpoint.label, value: endpoint.id }))}
|
|
95
|
+
emptyTargetsLabel="Add an endpoint above first."
|
|
96
|
+
disabled={disabled}
|
|
97
|
+
onSet={(providerId, endpointId) =>
|
|
98
|
+
patch((current) => ({
|
|
99
|
+
apiEndpointByProvider: { ...current.apiEndpointByProvider, [providerId]: endpointId },
|
|
100
|
+
}))
|
|
101
|
+
}
|
|
102
|
+
onRemove={(providerId) =>
|
|
103
|
+
patch((current) => {
|
|
104
|
+
const next = { ...current.apiEndpointByProvider };
|
|
105
|
+
delete next[providerId];
|
|
106
|
+
return { apiEndpointByProvider: next };
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
/>
|
|
110
|
+
)}
|
|
111
|
+
</>
|
|
112
|
+
) : (
|
|
113
|
+
<SettingsCard>
|
|
114
|
+
<SettingsRow
|
|
115
|
+
label={`Timeout ${values.timeoutMs.toLocaleString()} ms · ${
|
|
116
|
+
values.transport === "cli"
|
|
117
|
+
? `${Object.keys(values.providerCli).length} CLI override${Object.keys(values.providerCli).length === 1 ? "" : "s"}`
|
|
118
|
+
: `${Object.keys(values.apiEndpointByProvider).length} mapped provider${Object.keys(values.apiEndpointByProvider).length === 1 ? "" : "s"}`
|
|
119
|
+
}`}
|
|
120
|
+
hint="Press Show to change these."
|
|
121
|
+
error={timeout.error}
|
|
122
|
+
/>
|
|
123
|
+
</SettingsCard>
|
|
124
|
+
)}
|
|
125
|
+
</SettingsSection>
|
|
126
|
+
);
|
|
127
|
+
}
|