@2tle/pi-provider-manager 0.1.1 → 0.1.3
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/README.md +59 -2
- package/index.ts +194 -545
- package/package.json +1 -1
- package/src/config.ts +117 -0
- package/src/models.ts +85 -0
- package/src/provider.ts +60 -0
- package/src/types.ts +82 -0
- package/src/ui.ts +63 -0
package/package.json
CHANGED
package/src/config.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import type { StoredProvider, StoredSecrets, StoredState } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const CONFIG_PATH = join(homedir(), ".pi", "agent", "pi-provider-manager.json");
|
|
7
|
+
const SECRETS_PATH = join(homedir(), ".pi", "agent", "pi-provider-manager-secrets.json");
|
|
8
|
+
|
|
9
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
10
|
+
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function asNonEmptyString(value: unknown): string | undefined {
|
|
14
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function asPositiveSafeInteger(value: unknown): number | undefined {
|
|
18
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeModelOverrides(value: unknown): StoredProvider["modelOverrides"] {
|
|
22
|
+
const overrides: NonNullable<StoredProvider["modelOverrides"]> = {};
|
|
23
|
+
for (const [modelId, rawOverride] of Object.entries(asRecord(value) ?? {})) {
|
|
24
|
+
const override = asRecord(rawOverride);
|
|
25
|
+
const contextWindow = asPositiveSafeInteger(override?.contextWindow);
|
|
26
|
+
const maxTokens = asPositiveSafeInteger(override?.maxTokens);
|
|
27
|
+
if (contextWindow !== undefined || maxTokens !== undefined) {
|
|
28
|
+
overrides[modelId] = {
|
|
29
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
30
|
+
...(maxTokens !== undefined ? { maxTokens } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return Object.keys(overrides).length > 0 ? overrides : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeBaseUrl(value: string): string {
|
|
38
|
+
return value.trim().replace(/\/+$/, "");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validateProviderId(id: string): void {
|
|
42
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(id)) {
|
|
43
|
+
throw new Error("Provider ID may contain only lowercase letters, numbers, '.', '_' or '-'.");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function validateBaseUrl(value: string): string {
|
|
48
|
+
const baseUrl = normalizeBaseUrl(value);
|
|
49
|
+
let parsed: URL;
|
|
50
|
+
try {
|
|
51
|
+
parsed = new URL(baseUrl);
|
|
52
|
+
} catch {
|
|
53
|
+
throw new Error("Base URL is not a valid URL.");
|
|
54
|
+
}
|
|
55
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
56
|
+
throw new Error("Base URL must use the http or https protocol.");
|
|
57
|
+
}
|
|
58
|
+
return baseUrl;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function loadState(): Promise<StoredState> {
|
|
62
|
+
try {
|
|
63
|
+
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
64
|
+
const parsed = asRecord(JSON.parse(raw));
|
|
65
|
+
const providers = Array.isArray(parsed?.providers) ? parsed.providers : [];
|
|
66
|
+
const normalized: StoredProvider[] = [];
|
|
67
|
+
for (const item of providers) {
|
|
68
|
+
const record = asRecord(item);
|
|
69
|
+
const id = asNonEmptyString(record?.id);
|
|
70
|
+
const name = asNonEmptyString(record?.name);
|
|
71
|
+
const baseUrl = asNonEmptyString(record?.baseUrl);
|
|
72
|
+
if (!id || !name || !baseUrl) continue;
|
|
73
|
+
try {
|
|
74
|
+
validateProviderId(id);
|
|
75
|
+
const modelOverrides = normalizeModelOverrides(record?.modelOverrides);
|
|
76
|
+
normalized.push({ id, name, baseUrl: validateBaseUrl(baseUrl), ...(modelOverrides ? { modelOverrides } : {}) });
|
|
77
|
+
} catch {
|
|
78
|
+
// Ignore malformed entries so one broken provider does not prevent startup.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { providers: normalized };
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return { providers: [] };
|
|
84
|
+
throw new Error(`Unable to read provider configuration: ${error instanceof Error ? error.message : String(error)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function loadSecrets(): Promise<StoredSecrets> {
|
|
89
|
+
try {
|
|
90
|
+
const raw = await readFile(SECRETS_PATH, "utf8");
|
|
91
|
+
const parsed = asRecord(JSON.parse(raw));
|
|
92
|
+
const apiKeys: Record<string, string> = {};
|
|
93
|
+
const rawApiKeys = asRecord(parsed?.apiKeys);
|
|
94
|
+
for (const [id, value] of Object.entries(rawApiKeys ?? {})) {
|
|
95
|
+
if (typeof value === "string" && value.trim()) apiKeys[id] = value;
|
|
96
|
+
}
|
|
97
|
+
return { apiKeys };
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return { apiKeys: {} };
|
|
100
|
+
throw new Error(`Unable to read provider API keys: ${error instanceof Error ? error.message : String(error)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function saveJsonFile(path: string, value: object): Promise<void> {
|
|
105
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
106
|
+
const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
107
|
+
try {
|
|
108
|
+
await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
109
|
+
await chmod(temporaryPath, 0o600);
|
|
110
|
+
await rename(temporaryPath, path);
|
|
111
|
+
} finally {
|
|
112
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const saveState = (state: StoredState): Promise<void> => saveJsonFile(CONFIG_PATH, state);
|
|
117
|
+
export const saveSecrets = (secrets: StoredSecrets): Promise<void> => saveJsonFile(SECRETS_PATH, secrets);
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
API,
|
|
3
|
+
CODEX_API,
|
|
4
|
+
DEFAULT_CONTEXT_WINDOW,
|
|
5
|
+
DEFAULT_MAX_TOKENS,
|
|
6
|
+
PI_THINKING_LEVELS,
|
|
7
|
+
type OpenAIModelPayload,
|
|
8
|
+
type ProviderModelConfig,
|
|
9
|
+
type PiThinkingLevel,
|
|
10
|
+
type StoredProvider,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
|
|
13
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
14
|
+
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function asNonEmptyString(value: unknown): string | undefined {
|
|
18
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function asPositiveNumber(value: unknown, fallback: number): number {
|
|
22
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getReasoningEfforts(value: unknown): Set<string> {
|
|
26
|
+
if (!Array.isArray(value)) return new Set();
|
|
27
|
+
return new Set(value.flatMap((item) => {
|
|
28
|
+
if (typeof item === "string") return [item.toLowerCase()];
|
|
29
|
+
const record = asRecord(item);
|
|
30
|
+
const effort = asNonEmptyString(record?.value ?? record?.effort);
|
|
31
|
+
return effort ? [effort.toLowerCase()] : [];
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function thinkingLevelMap(efforts: Set<string>): Partial<Record<PiThinkingLevel, string | null>> | undefined {
|
|
36
|
+
if (efforts.size === 0) return undefined;
|
|
37
|
+
const map: Partial<Record<PiThinkingLevel, string | null>> = {};
|
|
38
|
+
for (const level of PI_THINKING_LEVELS) {
|
|
39
|
+
if (efforts.has(level)) map[level] = level;
|
|
40
|
+
else if (level === "max" && efforts.has("ultra")) map[level] = "ultra";
|
|
41
|
+
else map[level] = null;
|
|
42
|
+
}
|
|
43
|
+
return map;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function modelFromPayload(provider: StoredProvider, payload: OpenAIModelPayload): ProviderModelConfig | undefined {
|
|
47
|
+
const id = asNonEmptyString(payload.id);
|
|
48
|
+
if (!id) return undefined;
|
|
49
|
+
|
|
50
|
+
const override = provider.modelOverrides?.[id];
|
|
51
|
+
const capabilities = asRecord(payload.capabilities);
|
|
52
|
+
const cost = asRecord(payload.cost);
|
|
53
|
+
const inputModalities = Array.isArray(payload.input) ? payload.input : payload.input_modalities ?? capabilities?.input_modalities;
|
|
54
|
+
const input: ("text" | "image")[] = Array.isArray(inputModalities) && inputModalities.includes("image")
|
|
55
|
+
? ["text", "image"]
|
|
56
|
+
: ["text"];
|
|
57
|
+
const efforts = getReasoningEfforts(payload.reasoning_efforts ?? payload.supported_reasoning_levels ?? capabilities?.reasoning_effort);
|
|
58
|
+
const reasoning = payload.reasoning === true || payload.supports_reasoning === true ||
|
|
59
|
+
payload.supports_reasoning_effort === true || capabilities?.supports_reasoning === true || efforts.size > 0;
|
|
60
|
+
|
|
61
|
+
const isCodex = provider.baseUrl.trim().replace(/\/+$/, "").endsWith("/codex");
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
name: asNonEmptyString(payload.name) ?? id,
|
|
66
|
+
api: isCodex ? CODEX_API : API,
|
|
67
|
+
reasoning,
|
|
68
|
+
...(reasoning ? { thinkingLevelMap: thinkingLevelMap(efforts), compat: { supportsReasoningEffort: true } } : {}),
|
|
69
|
+
input,
|
|
70
|
+
cost: {
|
|
71
|
+
input: asPositiveNumber(cost?.input, 0),
|
|
72
|
+
output: asPositiveNumber(cost?.output, 0),
|
|
73
|
+
cacheRead: asPositiveNumber(cost?.cacheRead, 0),
|
|
74
|
+
cacheWrite: asPositiveNumber(cost?.cacheWrite, 0),
|
|
75
|
+
},
|
|
76
|
+
contextWindow: asPositiveNumber(override?.contextWindow, asPositiveNumber(
|
|
77
|
+
payload.context_window ?? payload.contextWindow ?? capabilities?.context_length,
|
|
78
|
+
DEFAULT_CONTEXT_WINDOW,
|
|
79
|
+
)),
|
|
80
|
+
maxTokens: asPositiveNumber(override?.maxTokens, asPositiveNumber(
|
|
81
|
+
payload.max_tokens ?? payload.maxTokens ?? capabilities?.max_output_tokens,
|
|
82
|
+
DEFAULT_MAX_TOKENS,
|
|
83
|
+
)),
|
|
84
|
+
};
|
|
85
|
+
}
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { RefreshModelsContext } from "@earendil-works/pi-ai";
|
|
2
|
+
import { modelFromPayload } from "./models.js";
|
|
3
|
+
import { CODEX_API } from "./types.js";
|
|
4
|
+
import type { ManagedProviderConfig, OpenAIModelsPayload, OpenAIModelPayload, ProviderModelConfig, StoredProvider } from "./types.js";
|
|
5
|
+
|
|
6
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
7
|
+
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeBaseUrl(value: string): string {
|
|
11
|
+
return value.trim().replace(/\/+$/, "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function modelsUrl(baseUrl: string): string {
|
|
15
|
+
return new URL("models", `${normalizeBaseUrl(baseUrl)}/`).toString();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function fetchProviderModels(
|
|
19
|
+
provider: StoredProvider,
|
|
20
|
+
context: RefreshModelsContext,
|
|
21
|
+
getApiKey: () => string | undefined,
|
|
22
|
+
): Promise<ProviderModelConfig[]> {
|
|
23
|
+
const apiKey = getApiKey();
|
|
24
|
+
if (!apiKey) throw new Error(`API key for provider '${provider.id}' is not configured.`);
|
|
25
|
+
const response = await fetch(modelsUrl(provider.baseUrl), {
|
|
26
|
+
signal: context.signal,
|
|
27
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) throw new Error(`Failed to fetch model list (${response.status} ${response.statusText})`);
|
|
30
|
+
|
|
31
|
+
const payload = (await response.json()) as OpenAIModelsPayload | unknown[];
|
|
32
|
+
const data = Array.isArray(payload) ? payload : asRecord(payload)?.data ?? asRecord(payload)?.models;
|
|
33
|
+
if (!Array.isArray(data)) throw new Error("Model list response does not contain a data/models array.");
|
|
34
|
+
return data.map((item) => {
|
|
35
|
+
const record = asRecord(item);
|
|
36
|
+
if (!record) return undefined;
|
|
37
|
+
// Codex's catalog uses slug/display_name instead of OpenAI's id/name.
|
|
38
|
+
const normalized: OpenAIModelPayload = provider.baseUrl.trim().replace(/\/+$/, "").endsWith("/codex")
|
|
39
|
+
? { ...record, id: record.id ?? record.slug, name: record.name ?? record.display_name }
|
|
40
|
+
: record;
|
|
41
|
+
return modelFromPayload(provider, normalized);
|
|
42
|
+
}).filter((model): model is ProviderModelConfig => model !== undefined);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createManagedProvider(
|
|
46
|
+
config: StoredProvider,
|
|
47
|
+
getApiKey: () => string | undefined,
|
|
48
|
+
): ManagedProviderConfig {
|
|
49
|
+
const isCodex = config.baseUrl.trim().replace(/\/+$/, "").endsWith("/codex");
|
|
50
|
+
return {
|
|
51
|
+
name: config.name,
|
|
52
|
+
baseUrl: config.baseUrl,
|
|
53
|
+
// Codex gateways expose Responses at /codex/responses; using the
|
|
54
|
+
// completions transport would POST /codex/chat/completions (405).
|
|
55
|
+
api: isCodex ? CODEX_API : "openai-completions",
|
|
56
|
+
apiKey: getApiKey() ?? "local",
|
|
57
|
+
models: [],
|
|
58
|
+
refreshModels: (context) => fetchProviderModels(config, context, getApiKey),
|
|
59
|
+
};
|
|
60
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { RefreshModelsContext } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
export const API = "openai-completions" as const;
|
|
4
|
+
export const CODEX_API = "openai-responses" as const;
|
|
5
|
+
export const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
6
|
+
export const DEFAULT_MAX_TOKENS = 16_384;
|
|
7
|
+
export const REFRESH_TIMEOUT_MS = 30_000;
|
|
8
|
+
export const PI_THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
9
|
+
|
|
10
|
+
export type PiThinkingLevel = (typeof PI_THINKING_LEVELS)[number];
|
|
11
|
+
|
|
12
|
+
export interface ModelOverride {
|
|
13
|
+
contextWindow?: number;
|
|
14
|
+
maxTokens?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface StoredProvider {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
modelOverrides?: Record<string, ModelOverride>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface StoredState {
|
|
25
|
+
providers: StoredProvider[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface StoredSecrets {
|
|
29
|
+
apiKeys: Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ProviderModelConfig {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
api: typeof API | typeof CODEX_API;
|
|
36
|
+
reasoning: boolean;
|
|
37
|
+
thinkingLevelMap?: Partial<Record<PiThinkingLevel, string | null>>;
|
|
38
|
+
compat?: {
|
|
39
|
+
supportsReasoningEffort: boolean;
|
|
40
|
+
};
|
|
41
|
+
input: ("text" | "image")[];
|
|
42
|
+
cost: {
|
|
43
|
+
input: number;
|
|
44
|
+
output: number;
|
|
45
|
+
cacheRead: number;
|
|
46
|
+
cacheWrite: number;
|
|
47
|
+
};
|
|
48
|
+
contextWindow: number;
|
|
49
|
+
maxTokens: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ManagedProviderConfig {
|
|
53
|
+
name: string;
|
|
54
|
+
baseUrl: string;
|
|
55
|
+
api: typeof API | typeof CODEX_API;
|
|
56
|
+
apiKey: string;
|
|
57
|
+
models: ProviderModelConfig[];
|
|
58
|
+
refreshModels(context: RefreshModelsContext): Promise<ProviderModelConfig[]>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface OpenAIModelPayload {
|
|
62
|
+
id?: unknown;
|
|
63
|
+
name?: unknown;
|
|
64
|
+
context_window?: unknown;
|
|
65
|
+
contextWindow?: unknown;
|
|
66
|
+
max_tokens?: unknown;
|
|
67
|
+
maxTokens?: unknown;
|
|
68
|
+
reasoning?: unknown;
|
|
69
|
+
supports_reasoning?: unknown;
|
|
70
|
+
supports_reasoning_effort?: unknown;
|
|
71
|
+
reasoning_efforts?: unknown;
|
|
72
|
+
supported_reasoning_levels?: unknown;
|
|
73
|
+
input?: unknown;
|
|
74
|
+
input_modalities?: unknown;
|
|
75
|
+
cost?: unknown;
|
|
76
|
+
capabilities?: unknown;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface OpenAIModelsPayload {
|
|
80
|
+
data?: unknown;
|
|
81
|
+
models?: unknown;
|
|
82
|
+
}
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { DynamicBorder, keyHint } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Container, CURSOR_MARKER, getKeybindings, Input, Spacer, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
export function maskInputLine(line: string): string {
|
|
5
|
+
const prompt = line.startsWith("> ") ? "> " : "";
|
|
6
|
+
let result = "";
|
|
7
|
+
for (let index = prompt.length; index < line.length;) {
|
|
8
|
+
if (line.startsWith(CURSOR_MARKER, index)) {
|
|
9
|
+
result += CURSOR_MARKER;
|
|
10
|
+
index += CURSOR_MARKER.length;
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (line[index] === "\x1b") {
|
|
14
|
+
const ansi = line.slice(index).match(/^\x1b\[[0-9;?]*[ -/]*[@-~]/)?.[0];
|
|
15
|
+
if (ansi) {
|
|
16
|
+
result += ansi;
|
|
17
|
+
index += ansi.length;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const character = line[index++];
|
|
22
|
+
result += /\s/u.test(character) ? character : "*";
|
|
23
|
+
}
|
|
24
|
+
return prompt + result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class MaskedInput extends Input {
|
|
28
|
+
override render(width: number): string[] {
|
|
29
|
+
const terminalWidth = process.stdout.columns;
|
|
30
|
+
const safeWidth = Math.max(1, Number.isFinite(terminalWidth) ? Math.min(width, terminalWidth) : width);
|
|
31
|
+
return super.render(safeWidth).map((line) => truncateToWidth(maskInputLine(line), safeWidth, "", false));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class SecretInputDialog extends Container {
|
|
36
|
+
private readonly input = new MaskedInput();
|
|
37
|
+
private _focused = false;
|
|
38
|
+
|
|
39
|
+
constructor(done: (value: string | undefined) => void, title: string, helpText: string, border: (text: string) => string) {
|
|
40
|
+
super();
|
|
41
|
+
this.addChild(new DynamicBorder(border));
|
|
42
|
+
this.addChild(new Spacer(1));
|
|
43
|
+
this.addChild(new Text(title, 1, 0));
|
|
44
|
+
this.addChild(new Spacer(1));
|
|
45
|
+
this.addChild(this.input);
|
|
46
|
+
this.addChild(new Spacer(1));
|
|
47
|
+
this.addChild(new Text(helpText, 1, 0));
|
|
48
|
+
this.addChild(new Spacer(1));
|
|
49
|
+
this.addChild(new DynamicBorder(border));
|
|
50
|
+
this.done = done;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private readonly done: (value: string | undefined) => void;
|
|
54
|
+
get focused(): boolean { return this._focused; }
|
|
55
|
+
set focused(value: boolean) { this._focused = value; this.input.focused = value; }
|
|
56
|
+
|
|
57
|
+
handleInput(data: string): void {
|
|
58
|
+
const keybindings = getKeybindings();
|
|
59
|
+
if (keybindings.matches(data, "tui.select.confirm") || data === "\n") this.done(this.input.getValue());
|
|
60
|
+
else if (keybindings.matches(data, "tui.select.cancel")) this.done(undefined);
|
|
61
|
+
else this.input.handleInput(data);
|
|
62
|
+
}
|
|
63
|
+
}
|