protocol cleanup (#9147)
This commit is contained in:
+66
-28
@@ -60,7 +60,9 @@ const ModelSelector = React.memo(function ModelSelector({
|
||||
useEffect(() => {
|
||||
const availableModels = provider.models.map((model) => model.id);
|
||||
setModels(availableModels);
|
||||
const defaultIdx = availableModels.findIndex((model) => model === provider.defaultModel);
|
||||
const defaultIdx = availableModels.findIndex(
|
||||
(model) => model === provider.defaultModel,
|
||||
);
|
||||
setSelectedIdx(defaultIdx >= 0 ? defaultIdx : 0);
|
||||
setLoading(false);
|
||||
}, [provider.models, provider.defaultModel]);
|
||||
@@ -154,10 +156,14 @@ const ModelSelector = React.memo(function ModelSelector({
|
||||
<Box flexDirection="column" height={height} width={columns} paddingX={2}>
|
||||
<Box marginTop={1} />
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color={TEXT_PRIMARY} bold>◆ Select model ◆</Text>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
◆ Select model ◆
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center" marginBottom={2}>
|
||||
<Text color={TEXT_DIM}>Loading models for {provider.providerName}…</Text>
|
||||
<Text color={TEXT_DIM}>
|
||||
Loading models for {provider.providerName}…
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center" flexGrow={1} alignItems="center">
|
||||
<Spinner idx={0} />
|
||||
@@ -171,7 +177,9 @@ const ModelSelector = React.memo(function ModelSelector({
|
||||
<Box flexDirection="column" height={height} width={columns} paddingX={2}>
|
||||
<Box marginTop={1} />
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color={TEXT_PRIMARY} bold>◆ Select model ◆</Text>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
◆ Select model ◆
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center" marginBottom={2}>
|
||||
<Text color={GOLD}>⚠ No models available</Text>
|
||||
@@ -193,18 +201,23 @@ const ModelSelector = React.memo(function ModelSelector({
|
||||
if (manualEntry) {
|
||||
const inputWidth = Math.min(60, maxWidth - 4);
|
||||
const displayText = searchQuery || "type model name…";
|
||||
const truncatedText = displayText.length > inputWidth - 6
|
||||
? displayText.slice(0, inputWidth - 9) + "…"
|
||||
: displayText;
|
||||
const truncatedText =
|
||||
displayText.length > inputWidth - 6
|
||||
? displayText.slice(0, inputWidth - 9) + "…"
|
||||
: displayText;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" height={height} width={columns} paddingX={2}>
|
||||
<Box marginTop={1} />
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color={TEXT_PRIMARY} bold>◆ Enter model name ◆</Text>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
◆ Enter model name ◆
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center" marginBottom={2}>
|
||||
<Text color={TEXT_DIM}>Type a model identifier for {provider.providerName}</Text>
|
||||
<Text color={TEXT_DIM}>
|
||||
Type a model identifier for {provider.providerName}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box justifyContent="center">
|
||||
@@ -214,7 +227,9 @@ const ModelSelector = React.memo(function ModelSelector({
|
||||
paddingX={2}
|
||||
width={inputWidth}
|
||||
>
|
||||
<Text color={GOLD} bold>{"❯ "}</Text>
|
||||
<Text color={GOLD} bold>
|
||||
{"❯ "}
|
||||
</Text>
|
||||
<Text color={searchQuery ? TEXT_PRIMARY : TEXT_DIM}>
|
||||
{truncatedText}
|
||||
</Text>
|
||||
@@ -236,7 +251,9 @@ const ModelSelector = React.memo(function ModelSelector({
|
||||
{/* Header */}
|
||||
<Box marginTop={1} />
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color={TEXT_PRIMARY} bold>◆ Select model ◆</Text>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
◆ Select model ◆
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center" marginBottom={2}>
|
||||
<Text color={TEXT_DIM}>Choose a model for {provider.providerName}</Text>
|
||||
@@ -250,7 +267,9 @@ const ModelSelector = React.memo(function ModelSelector({
|
||||
paddingX={2}
|
||||
width={searchBoxWidth}
|
||||
>
|
||||
<Text color={CRANBERRY} bold>{"❯ "}</Text>
|
||||
<Text color={CRANBERRY} bold>
|
||||
{"❯ "}
|
||||
</Text>
|
||||
<Box width={searchBoxWidth - 8}>
|
||||
<Text color={searchQuery ? TEXT_PRIMARY : TEXT_DIM} wrap="truncate">
|
||||
{searchQuery || "search models…"}
|
||||
@@ -262,7 +281,11 @@ const ModelSelector = React.memo(function ModelSelector({
|
||||
{/* Model List */}
|
||||
<Box flexDirection="column" flexGrow={1} justifyContent="flex-start">
|
||||
{filtered.length === 0 ? (
|
||||
<Box justifyContent="center" alignItems="center" height={Math.max(listHeight, 1)}>
|
||||
<Box
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
height={Math.max(listHeight, 1)}
|
||||
>
|
||||
<Text color={TEXT_DIM}>No matching models</Text>
|
||||
</Box>
|
||||
) : (
|
||||
@@ -279,16 +302,20 @@ const ModelSelector = React.memo(function ModelSelector({
|
||||
const active = idx === selectedIdx;
|
||||
const isDefault = model === provider.defaultModel;
|
||||
const modelWidth = maxWidth - 8;
|
||||
const truncatedModel = model.length > modelWidth
|
||||
? model.slice(0, modelWidth - 1) + "…"
|
||||
: model;
|
||||
const truncatedModel =
|
||||
model.length > modelWidth
|
||||
? model.slice(0, modelWidth - 1) + "…"
|
||||
: model;
|
||||
|
||||
return (
|
||||
<Box key={model}>
|
||||
<Text color={active ? GOLD : TEXT_DIM}>
|
||||
{active ? "▸ " : " "}
|
||||
</Text>
|
||||
<Text color={active ? TEXT_PRIMARY : TEXT_DIM} bold={active}>
|
||||
<Text
|
||||
color={active ? TEXT_PRIMARY : TEXT_DIM}
|
||||
bold={active}
|
||||
>
|
||||
{truncatedModel}
|
||||
</Text>
|
||||
{isDefault && <Text color={TEAL}> (default)</Text>}
|
||||
@@ -329,7 +356,8 @@ export default function ConfigureScreen({
|
||||
}: ConfigureProps) {
|
||||
const [phase, setPhase] = useState<Phase>("loading");
|
||||
const [providers, setProviders] = useState<ProviderInventoryEntryDto[]>([]);
|
||||
const [selectedProvider, setSelectedProvider] = useState<ProviderInventoryEntryDto | null>(null);
|
||||
const [selectedProvider, setSelectedProvider] =
|
||||
useState<ProviderInventoryEntryDto | null>(null);
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
const [spinIdx, setSpinIdx] = useState(0);
|
||||
const [fetchKey, setFetchKey] = useState(0);
|
||||
@@ -347,7 +375,9 @@ export default function ConfigureScreen({
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const resp = await client.goose.GooseProvidersList({ providerIds: [] });
|
||||
const resp = await client.goose.providersList_unstable({
|
||||
providerIds: [],
|
||||
});
|
||||
if (cancelled) return;
|
||||
const sorted = [...resp.entries].sort((a, b) => {
|
||||
const aP = a.providerType === "Preferred" ? 0 : 1;
|
||||
@@ -359,7 +389,7 @@ export default function ConfigureScreen({
|
||||
|
||||
if (initialIntent === "model") {
|
||||
try {
|
||||
const cfg = await client.goose.GooseDefaultsRead({});
|
||||
const cfg = await client.goose.defaultsRead_unstable({});
|
||||
if (cancelled) return;
|
||||
const current = sorted.find((p) => p.providerId === cfg.providerId);
|
||||
if (current) {
|
||||
@@ -395,7 +425,7 @@ export default function ConfigureScreen({
|
||||
) => {
|
||||
setPhase("saving");
|
||||
try {
|
||||
await client.goose.GooseProvidersConfigSave({
|
||||
await client.goose.providersConfigSave_unstable({
|
||||
providerId: provider.providerId,
|
||||
fields: Object.entries(configValues).map(([key, value]) => ({
|
||||
key,
|
||||
@@ -421,7 +451,9 @@ export default function ConfigureScreen({
|
||||
[client, sessionId, onComplete],
|
||||
);
|
||||
|
||||
const [pendingConfigValues, setPendingConfigValues] = useState<Record<string, string>>({});
|
||||
const [pendingConfigValues, setPendingConfigValues] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
|
||||
const handleProviderSelected = useCallback(
|
||||
(provider: ProviderInventoryEntryDto) => {
|
||||
@@ -463,15 +495,19 @@ export default function ConfigureScreen({
|
||||
}, []);
|
||||
|
||||
if (phase === "loading" || phase === "loading_models" || phase === "saving") {
|
||||
const label =
|
||||
phase === "loading" ? "Loading providers…" :
|
||||
phase === "loading_models" ? "Loading models…" :
|
||||
"Applying changes…";
|
||||
const label =
|
||||
phase === "loading"
|
||||
? "Loading providers…"
|
||||
: phase === "loading_models"
|
||||
? "Loading models…"
|
||||
: "Applying changes…";
|
||||
return (
|
||||
<Box flexDirection="column" height={height} width={width} paddingX={2}>
|
||||
<Box marginTop={1} />
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color={TEXT_PRIMARY} bold>◆ Configure provider ◆</Text>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
◆ Configure provider ◆
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center" marginBottom={2}>
|
||||
<Text color={TEXT_DIM}>{label}</Text>
|
||||
@@ -488,7 +524,9 @@ export default function ConfigureScreen({
|
||||
<Box flexDirection="column" height={height} width={width} paddingX={2}>
|
||||
<Box marginTop={1} />
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color={TEXT_PRIMARY} bold>◆ Configure provider ◆</Text>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
◆ Configure provider ◆
|
||||
</Text>
|
||||
</Box>
|
||||
<ErrorScreen errorMsg={errorMsg} onRetry={handleRetry} />
|
||||
</Box>
|
||||
|
||||
+296
-95
@@ -1,10 +1,17 @@
|
||||
import React, {useCallback, useEffect, useState} from "react";
|
||||
import {Box, Text, useInput, useStdout} from "ink";
|
||||
import {TextInput} from "@inkjs/ui";
|
||||
import type {GooseClient} from "@aaif/goose-sdk";
|
||||
import {CRANBERRY, GOLD, RULE_COLOR, TEAL, TEXT_DIM, TEXT_PRIMARY} from "./colors.js";
|
||||
import {Spinner, SPINNER_FRAMES} from "./components/Spinner.js";
|
||||
import {ErrorScreen} from "./components/ErrorScreen.js";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Box, Text, useInput, useStdout } from "ink";
|
||||
import { TextInput } from "@inkjs/ui";
|
||||
import type { GooseClient } from "@aaif/goose-sdk";
|
||||
import {
|
||||
CRANBERRY,
|
||||
GOLD,
|
||||
RULE_COLOR,
|
||||
TEAL,
|
||||
TEXT_DIM,
|
||||
TEXT_PRIMARY,
|
||||
} from "./colors.js";
|
||||
import { Spinner, SPINNER_FRAMES } from "./components/Spinner.js";
|
||||
import { ErrorScreen } from "./components/ErrorScreen.js";
|
||||
|
||||
type ExtEntry = {
|
||||
enabled: boolean;
|
||||
@@ -15,29 +22,65 @@ type ExtEntry = {
|
||||
};
|
||||
|
||||
function isExtEntry(v: unknown): v is ExtEntry {
|
||||
return !!v && typeof v === "object" && "enabled" in v && "type" in v && "name" in v
|
||||
&& typeof (v as ExtEntry).enabled === "boolean"
|
||||
&& typeof (v as ExtEntry).type === "string"
|
||||
&& typeof (v as ExtEntry).name === "string";
|
||||
return (
|
||||
!!v &&
|
||||
typeof v === "object" &&
|
||||
"enabled" in v &&
|
||||
"type" in v &&
|
||||
"name" in v &&
|
||||
typeof (v as ExtEntry).enabled === "boolean" &&
|
||||
typeof (v as ExtEntry).type === "string" &&
|
||||
typeof (v as ExtEntry).name === "string"
|
||||
);
|
||||
}
|
||||
|
||||
type AddType = "stdio" | "streamable_http";
|
||||
type Phase = "loading" | "list" | "add_type" | "add_value" | "add_name" | "add_desc" | "saving" | "error";
|
||||
type Phase =
|
||||
| "loading"
|
||||
| "list"
|
||||
| "add_type"
|
||||
| "add_value"
|
||||
| "add_name"
|
||||
| "add_desc"
|
||||
| "saving"
|
||||
| "error";
|
||||
|
||||
function deriveNameFromValue(addType: AddType, value: string): string {
|
||||
if (addType === "stdio") {
|
||||
const cmd = value.trim().split(/\s+/)[0] ?? "";
|
||||
return cmd.split("/").pop() ?? cmd;
|
||||
}
|
||||
try { return new URL(value.trim()).hostname; } catch { return value.trim(); }
|
||||
try {
|
||||
return new URL(value.trim()).hostname;
|
||||
} catch {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
function buildConfig(addType: AddType, value: string, name: string, description: string): ExtEntry {
|
||||
function buildConfig(
|
||||
addType: AddType,
|
||||
value: string,
|
||||
name: string,
|
||||
description: string,
|
||||
): ExtEntry {
|
||||
if (addType === "stdio") {
|
||||
const parts = value.trim().split(/\s+/);
|
||||
return {type: "stdio", enabled: true, name, description, cmd: parts[0] ?? "", args: parts.slice(1)};
|
||||
return {
|
||||
type: "stdio",
|
||||
enabled: true,
|
||||
name,
|
||||
description,
|
||||
cmd: parts[0] ?? "",
|
||||
args: parts.slice(1),
|
||||
};
|
||||
}
|
||||
return {type: "streamable_http", enabled: true, name, description, uri: value.trim()};
|
||||
return {
|
||||
type: "streamable_http",
|
||||
enabled: true,
|
||||
name,
|
||||
description,
|
||||
uri: value.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
export default function ExtensionsManager({
|
||||
@@ -51,7 +94,7 @@ export default function ExtensionsManager({
|
||||
height: number;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const {stdout} = useStdout();
|
||||
const { stdout } = useStdout();
|
||||
const columns = stdout?.columns ?? 80;
|
||||
|
||||
const [phase, setPhase] = useState<Phase>("loading");
|
||||
@@ -68,7 +111,10 @@ export default function ExtensionsManager({
|
||||
const [inputKey, setInputKey] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setSpinIdx(i => (i + 1) % SPINNER_FRAMES.length), 300);
|
||||
const t = setInterval(
|
||||
() => setSpinIdx((i) => (i + 1) % SPINNER_FRAMES.length),
|
||||
300,
|
||||
);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
@@ -76,16 +122,23 @@ export default function ExtensionsManager({
|
||||
setPhase("loading");
|
||||
try {
|
||||
const [configResp, sessionResp] = await Promise.all([
|
||||
client.goose.GooseConfigExtensions({}),
|
||||
client.goose.GooseSessionExtensions({sessionId}),
|
||||
client.goose.configExtensionsList_unstable({}),
|
||||
client.goose.sessionExtensionsList_unstable({ sessionId }),
|
||||
]);
|
||||
|
||||
const allExtensions = (configResp.extensions as unknown[]).filter(isExtEntry);
|
||||
const allExtensions = (configResp.extensions as unknown[]).filter(
|
||||
isExtEntry,
|
||||
);
|
||||
const activeNames = new Set(
|
||||
(sessionResp.extensions as Array<{name?: string}>).map(e => e.name),
|
||||
(sessionResp.extensions as Array<{ name?: string }>).map((e) => e.name),
|
||||
);
|
||||
|
||||
setEntries(allExtensions.map(ext => ({...ext, enabled: activeNames.has(ext.name)})));
|
||||
setEntries(
|
||||
allExtensions.map((ext) => ({
|
||||
...ext,
|
||||
enabled: activeNames.has(ext.name),
|
||||
})),
|
||||
);
|
||||
setWarnings(configResp.warnings ?? []);
|
||||
setPhase("list");
|
||||
} catch (e: unknown) {
|
||||
@@ -94,60 +147,115 @@ export default function ExtensionsManager({
|
||||
}
|
||||
}, [client, sessionId]);
|
||||
|
||||
useEffect(() => { reload(); }, [reload]);
|
||||
|
||||
const withSaving = useCallback(async (fn: () => Promise<void>) => {
|
||||
setPhase("saving");
|
||||
try {
|
||||
await fn();
|
||||
await reload();
|
||||
} catch (e: unknown) {
|
||||
setErrorMsg(e instanceof Error ? e.message : String(e));
|
||||
setPhase("error");
|
||||
}
|
||||
useEffect(() => {
|
||||
reload();
|
||||
}, [reload]);
|
||||
|
||||
const withSaving = useCallback(
|
||||
async (fn: () => Promise<void>) => {
|
||||
setPhase("saving");
|
||||
try {
|
||||
await fn();
|
||||
await reload();
|
||||
} catch (e: unknown) {
|
||||
setErrorMsg(e instanceof Error ? e.message : String(e));
|
||||
setPhase("error");
|
||||
}
|
||||
},
|
||||
[reload],
|
||||
);
|
||||
|
||||
const toggleSelected = useCallback(() => {
|
||||
const sel = entries[selectedIdx];
|
||||
if (!sel) return;
|
||||
withSaving(async () => {
|
||||
if (sel.enabled) {
|
||||
await client.goose.GooseExtensionsRemove({sessionId, name: sel.name});
|
||||
await client.goose.sessionExtensionsRemove_unstable({
|
||||
sessionId,
|
||||
name: sel.name,
|
||||
});
|
||||
} else {
|
||||
await client.goose.GooseExtensionsAdd({sessionId, config: sel as any});
|
||||
await client.goose.sessionExtensionsAdd_unstable({
|
||||
sessionId,
|
||||
config: sel as any,
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [entries, selectedIdx, client, sessionId, withSaving]);
|
||||
|
||||
const saveNewExtension = useCallback((description: string) => {
|
||||
const config = buildConfig(addType, addValue, addName, description);
|
||||
withSaving(async () => {
|
||||
await client.goose.GooseConfigExtensionsAdd({
|
||||
name: config.name,
|
||||
extensionConfig: config as any,
|
||||
enabled: true,
|
||||
const saveNewExtension = useCallback(
|
||||
(description: string) => {
|
||||
const config = buildConfig(addType, addValue, addName, description);
|
||||
withSaving(async () => {
|
||||
await client.goose.configExtensionsAdd_unstable({
|
||||
name: config.name,
|
||||
extensionConfig: config as any,
|
||||
enabled: true,
|
||||
});
|
||||
await client.goose.sessionExtensionsAdd_unstable({
|
||||
sessionId,
|
||||
config: config as any,
|
||||
});
|
||||
});
|
||||
await client.goose.GooseExtensionsAdd({sessionId, config: config as any});
|
||||
});
|
||||
}, [addType, addValue, addName, client, sessionId, withSaving]);
|
||||
},
|
||||
[addType, addValue, addName, client, sessionId, withSaving],
|
||||
);
|
||||
|
||||
useInput((ch, key) => {
|
||||
if (phase === "list") {
|
||||
if (key.escape) { onClose(); return; }
|
||||
if (key.upArrow) { setSelectedIdx(i => Math.max(i - 1, 0)); return; }
|
||||
if (key.downArrow) { setSelectedIdx(i => Math.min(i + 1, entries.length - 1)); return; }
|
||||
if (ch === " " || key.return) { toggleSelected(); return; }
|
||||
if (ch === "a") { setAddType("stdio"); setPhase("add_type"); return; }
|
||||
if (key.escape) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
if (key.upArrow) {
|
||||
setSelectedIdx((i) => Math.max(i - 1, 0));
|
||||
return;
|
||||
}
|
||||
if (key.downArrow) {
|
||||
setSelectedIdx((i) => Math.min(i + 1, entries.length - 1));
|
||||
return;
|
||||
}
|
||||
if (ch === " " || key.return) {
|
||||
toggleSelected();
|
||||
return;
|
||||
}
|
||||
if (ch === "a") {
|
||||
setAddType("stdio");
|
||||
setPhase("add_type");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (phase === "add_type") {
|
||||
if (key.escape) { setPhase("list"); return; }
|
||||
if (key.upArrow || key.downArrow) { setAddType(t => t === "stdio" ? "streamable_http" : "stdio"); return; }
|
||||
if (key.return) { setAddValue(""); setInputKey(k => k + 1); setPhase("add_value"); return; }
|
||||
if (key.escape) {
|
||||
setPhase("list");
|
||||
return;
|
||||
}
|
||||
if (key.upArrow || key.downArrow) {
|
||||
setAddType((t) => (t === "stdio" ? "streamable_http" : "stdio"));
|
||||
return;
|
||||
}
|
||||
if (key.return) {
|
||||
setAddValue("");
|
||||
setInputKey((k) => k + 1);
|
||||
setPhase("add_value");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (key.escape) {
|
||||
if (phase === "add_value") { setPhase("add_type"); return; }
|
||||
if (phase === "add_name") { setInputKey(k => k + 1); setPhase("add_value"); return; }
|
||||
if (phase === "add_desc") { setInputKey(k => k + 1); setPhase("add_name"); return; }
|
||||
if (phase === "add_value") {
|
||||
setPhase("add_type");
|
||||
return;
|
||||
}
|
||||
if (phase === "add_name") {
|
||||
setInputKey((k) => k + 1);
|
||||
setPhase("add_value");
|
||||
return;
|
||||
}
|
||||
if (phase === "add_desc") {
|
||||
setInputKey((k) => k + 1);
|
||||
setPhase("add_name");
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -156,10 +264,14 @@ export default function ExtensionsManager({
|
||||
<Box flexDirection="column" height={height} width={columns} paddingX={2}>
|
||||
<Box marginTop={1} />
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color={TEXT_PRIMARY} bold>◆ Manage extensions ◆</Text>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
◆ Manage extensions ◆
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center" marginBottom={2}>
|
||||
<Text color={TEXT_DIM}>{phase === "loading" ? "Loading extensions…" : "Saving…"}</Text>
|
||||
<Text color={TEXT_DIM}>
|
||||
{phase === "loading" ? "Loading extensions…" : "Saving…"}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center" flexGrow={1} alignItems="center">
|
||||
<Spinner idx={spinIdx} />
|
||||
@@ -173,7 +285,9 @@ export default function ExtensionsManager({
|
||||
<Box flexDirection="column" height={height} width={columns} paddingX={2}>
|
||||
<Box marginTop={1} />
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color={TEXT_PRIMARY} bold>◆ Manage extensions ◆</Text>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
◆ Manage extensions ◆
|
||||
</Text>
|
||||
</Box>
|
||||
<ErrorScreen errorMsg={errorMsg} onRetry={() => reload()} />
|
||||
</Box>
|
||||
@@ -184,28 +298,38 @@ export default function ExtensionsManager({
|
||||
const inputW = Math.min(maxW - 10, 70);
|
||||
|
||||
if (phase === "add_type") {
|
||||
const types: {value: AddType; label: string; hint: string}[] = [
|
||||
{value: "stdio", label: "Command (stdio)", hint: "run a local command"},
|
||||
{value: "streamable_http", label: "Endpoint (HTTP)", hint: "connect to a remote server"},
|
||||
const types: { value: AddType; label: string; hint: string }[] = [
|
||||
{ value: "stdio", label: "Command (stdio)", hint: "run a local command" },
|
||||
{
|
||||
value: "streamable_http",
|
||||
label: "Endpoint (HTTP)",
|
||||
hint: "connect to a remote server",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Box flexDirection="column" width={columns} height={height} paddingX={2}>
|
||||
<Box marginTop={1} />
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color={TEXT_PRIMARY} bold>◆ Add extension ◆</Text>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
◆ Add extension ◆
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center" marginBottom={2}>
|
||||
<Text color={TEXT_DIM}>Choose a connection type</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center">
|
||||
<Box flexDirection="column">
|
||||
{types.map(t => {
|
||||
{types.map((t) => {
|
||||
const active = addType === t.value;
|
||||
return (
|
||||
<Box key={t.value}>
|
||||
<Text color={active ? GOLD : TEXT_DIM}>{active ? "▸ " : " "}</Text>
|
||||
<Text color={active ? TEXT_PRIMARY : TEXT_DIM} bold={active}>{t.label}</Text>
|
||||
<Text color={TEXT_DIM}> {t.hint}</Text>
|
||||
<Text color={active ? GOLD : TEXT_DIM}>
|
||||
{active ? "▸ " : " "}
|
||||
</Text>
|
||||
<Text color={active ? TEXT_PRIMARY : TEXT_DIM} bold={active}>
|
||||
{t.label}
|
||||
</Text>
|
||||
<Text color={TEXT_DIM}> {t.hint}</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
@@ -227,14 +351,27 @@ export default function ExtensionsManager({
|
||||
<Box flexDirection="column" width={columns} height={height} paddingX={2}>
|
||||
<Box marginTop={1} />
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color={TEXT_PRIMARY} bold>◆ {isStdio ? "Enter command" : "Enter endpoint URL"} ◆</Text>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
◆ {isStdio ? "Enter command" : "Enter endpoint URL"} ◆
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center" marginBottom={2}>
|
||||
<Text color={TEXT_DIM}>{isStdio ? "The command to launch the extension" : "URL of the remote MCP server"}</Text>
|
||||
<Text color={TEXT_DIM}>
|
||||
{isStdio
|
||||
? "The command to launch the extension"
|
||||
: "URL of the remote MCP server"}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center">
|
||||
<Box borderStyle="round" borderColor={RULE_COLOR} paddingX={2} width={inputW}>
|
||||
<Text color={CRANBERRY} bold>{"❯ "}</Text>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={RULE_COLOR}
|
||||
paddingX={2}
|
||||
width={inputW}
|
||||
>
|
||||
<Text color={CRANBERRY} bold>
|
||||
{"❯ "}
|
||||
</Text>
|
||||
<TextInput
|
||||
key={`value-${inputKey}`}
|
||||
placeholder={placeholder}
|
||||
@@ -243,7 +380,7 @@ export default function ExtensionsManager({
|
||||
if (!v.trim()) return;
|
||||
setAddValue(v);
|
||||
setAddName(deriveNameFromValue(addType, v));
|
||||
setInputKey(k => k + 1);
|
||||
setInputKey((k) => k + 1);
|
||||
setPhase("add_name");
|
||||
}}
|
||||
/>
|
||||
@@ -261,14 +398,23 @@ export default function ExtensionsManager({
|
||||
<Box flexDirection="column" width={columns} height={height} paddingX={2}>
|
||||
<Box marginTop={1} />
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color={TEXT_PRIMARY} bold>◆ Name this extension ◆</Text>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
◆ Name this extension ◆
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center" marginBottom={2}>
|
||||
<Text color={TEXT_DIM}>A short name to identify this extension</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center">
|
||||
<Box borderStyle="round" borderColor={RULE_COLOR} paddingX={2} width={inputW}>
|
||||
<Text color={CRANBERRY} bold>{"❯ "}</Text>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={RULE_COLOR}
|
||||
paddingX={2}
|
||||
width={inputW}
|
||||
>
|
||||
<Text color={CRANBERRY} bold>
|
||||
{"❯ "}
|
||||
</Text>
|
||||
<TextInput
|
||||
key={`name-${inputKey}`}
|
||||
defaultValue={addName}
|
||||
@@ -278,7 +424,7 @@ export default function ExtensionsManager({
|
||||
if (!v.trim()) return;
|
||||
setAddName(v.trim());
|
||||
setAddDesc("");
|
||||
setInputKey(k => k + 1);
|
||||
setInputKey((k) => k + 1);
|
||||
setPhase("add_desc");
|
||||
}}
|
||||
/>
|
||||
@@ -296,14 +442,23 @@ export default function ExtensionsManager({
|
||||
<Box flexDirection="column" width={columns} height={height} paddingX={2}>
|
||||
<Box marginTop={1} />
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color={TEXT_PRIMARY} bold>◆ Description ◆</Text>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
◆ Description ◆
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center" marginBottom={2}>
|
||||
<Text color={TEXT_DIM}>What does this extension do? (optional)</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center">
|
||||
<Box borderStyle="round" borderColor={RULE_COLOR} paddingX={2} width={inputW}>
|
||||
<Text color={CRANBERRY} bold>{"❯ "}</Text>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={RULE_COLOR}
|
||||
paddingX={2}
|
||||
width={inputW}
|
||||
>
|
||||
<Text color={CRANBERRY} bold>
|
||||
{"❯ "}
|
||||
</Text>
|
||||
<TextInput
|
||||
key={`desc-${inputKey}`}
|
||||
placeholder="what does this extension do?"
|
||||
@@ -313,7 +468,9 @@ export default function ExtensionsManager({
|
||||
</Box>
|
||||
</Box>
|
||||
<Box justifyContent="center" marginTop={2}>
|
||||
<Text color={TEXT_DIM}>enter save (leave empty to skip) · esc back</Text>
|
||||
<Text color={TEXT_DIM}>
|
||||
enter save (leave empty to skip) · esc back
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -322,12 +479,15 @@ export default function ExtensionsManager({
|
||||
const layoutW = maxW;
|
||||
const GUTTER = 2;
|
||||
const STATUS_W = 10;
|
||||
const nameW = Math.max(16, Math.floor(layoutW * 0.30));
|
||||
const nameW = Math.max(16, Math.floor(layoutW * 0.3));
|
||||
const descW = Math.max(8, layoutW - 2 - STATUS_W - nameW - 2 * GUTTER);
|
||||
|
||||
const rows = Math.max(height - 9, 4);
|
||||
const maxStart = Math.max(0, entries.length - rows);
|
||||
const start = Math.min(maxStart, Math.max(0, selectedIdx - Math.floor(rows / 2)));
|
||||
const start = Math.min(
|
||||
maxStart,
|
||||
Math.max(0, selectedIdx - Math.floor(rows / 2)),
|
||||
);
|
||||
const end = Math.min(entries.length, start + rows);
|
||||
const windowed = entries.slice(start, end);
|
||||
|
||||
@@ -336,17 +496,27 @@ export default function ExtensionsManager({
|
||||
{/* Header */}
|
||||
<Box marginTop={1} />
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color={TEXT_PRIMARY} bold>◆ Manage extensions ◆</Text>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
◆ Manage extensions ◆
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center" marginBottom={2}>
|
||||
<Text color={TEXT_DIM}>Toggle, add, or remove extensions for this session</Text>
|
||||
<Text color={TEXT_DIM}>
|
||||
Toggle, add, or remove extensions for this session
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Extension List */}
|
||||
<Box flexDirection="column" flexGrow={1} justifyContent="flex-start">
|
||||
{entries.length === 0 ? (
|
||||
<Box justifyContent="center" alignItems="center" height={Math.max(rows - 1, 1)}>
|
||||
<Text color={TEXT_DIM}>No extensions configured — press a to add one</Text>
|
||||
<Box
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
height={Math.max(rows - 1, 1)}
|
||||
>
|
||||
<Text color={TEXT_DIM}>
|
||||
No extensions configured — press a to add one
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
@@ -362,12 +532,37 @@ export default function ExtensionsManager({
|
||||
const active = globalIdx === selectedIdx;
|
||||
return (
|
||||
<Box key={`${ext.type}:${ext.name}`} width={layoutW}>
|
||||
<Text color={active ? GOLD : TEXT_DIM}>{active ? "▸ " : " "}</Text>
|
||||
<Box width={nameW}><Text color={active ? TEXT_PRIMARY : TEXT_DIM} bold={active} wrap="truncate">{ext.name}</Text></Box>
|
||||
<Box width={GUTTER}><Text>{" ".repeat(GUTTER)}</Text></Box>
|
||||
<Box width={descW}><Text color={TEXT_DIM} wrap="truncate">{ext.description || ""}</Text></Box>
|
||||
<Box width={GUTTER}><Text>{" ".repeat(GUTTER)}</Text></Box>
|
||||
<Box width={STATUS_W}><Text color={ext.enabled ? TEAL : TEXT_DIM} wrap="truncate">{ext.enabled ? "enabled" : "disabled"}</Text></Box>
|
||||
<Text color={active ? GOLD : TEXT_DIM}>
|
||||
{active ? "▸ " : " "}
|
||||
</Text>
|
||||
<Box width={nameW}>
|
||||
<Text
|
||||
color={active ? TEXT_PRIMARY : TEXT_DIM}
|
||||
bold={active}
|
||||
wrap="truncate"
|
||||
>
|
||||
{ext.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={GUTTER}>
|
||||
<Text>{" ".repeat(GUTTER)}</Text>
|
||||
</Box>
|
||||
<Box width={descW}>
|
||||
<Text color={TEXT_DIM} wrap="truncate">
|
||||
{ext.description || ""}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={GUTTER}>
|
||||
<Text>{" ".repeat(GUTTER)}</Text>
|
||||
</Box>
|
||||
<Box width={STATUS_W}>
|
||||
<Text
|
||||
color={ext.enabled ? TEAL : TEXT_DIM}
|
||||
wrap="truncate"
|
||||
>
|
||||
{ext.enabled ? "enabled" : "disabled"}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
@@ -375,7 +570,9 @@ export default function ExtensionsManager({
|
||||
</Box>
|
||||
{end < entries.length && (
|
||||
<Box justifyContent="center" marginTop={1}>
|
||||
<Text color={TEXT_DIM}>▼ {entries.length - end} more below</Text>
|
||||
<Text color={TEXT_DIM}>
|
||||
▼ {entries.length - end} more below
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
@@ -387,7 +584,11 @@ export default function ExtensionsManager({
|
||||
<Box width={layoutW} flexDirection="column">
|
||||
<Text color={GOLD}>Warnings</Text>
|
||||
{warnings.map((w, i) => (
|
||||
<Box key={i} width={layoutW}><Text color={TEXT_DIM} wrap="truncate">• {w}</Text></Box>
|
||||
<Box key={i} width={layoutW}>
|
||||
<Text color={TEXT_DIM} wrap="truncate">
|
||||
• {w}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
+117
-65
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Box, Text, useInput, useStdout } from "ink";
|
||||
import { TextInput, PasswordInput } from '@inkjs/ui';
|
||||
import { TextInput, PasswordInput } from "@inkjs/ui";
|
||||
import type { GooseClient, ProviderInventoryEntryDto } from "@aaif/goose-sdk";
|
||||
import {
|
||||
CRANBERRY,
|
||||
@@ -38,7 +38,14 @@ export interface ProviderSelectorProps {
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
export const ProviderSelector = React.memo(function ProviderSelector({ providers, height, onSelect, title, subtitle, onBack }: ProviderSelectorProps) {
|
||||
export const ProviderSelector = React.memo(function ProviderSelector({
|
||||
providers,
|
||||
height,
|
||||
onSelect,
|
||||
title,
|
||||
subtitle,
|
||||
onBack,
|
||||
}: ProviderSelectorProps) {
|
||||
const [selectedIdx, setSelectedIdx] = useState(0);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const { stdout } = useStdout();
|
||||
@@ -55,27 +62,38 @@ export const ProviderSelector = React.memo(function ProviderSelector({ providers
|
||||
})();
|
||||
|
||||
// Calculate grid dimensions based on terminal size
|
||||
const cardWidth = 36; // Width of each provider card
|
||||
const cardHeight = 8; // Height of each provider card
|
||||
const minSpacing = 2; // Minimum spacing between cards
|
||||
|
||||
const cardWidth = 36; // Width of each provider card
|
||||
const cardHeight = 8; // Height of each provider card
|
||||
const minSpacing = 2; // Minimum spacing between cards
|
||||
|
||||
const availableWidth = columns - 4; // Leave margins
|
||||
// Header: marginTop(1) + title+mb(2) + subtitle+mb(3) + searchbar+mb(5) = 11
|
||||
// Footer: mt(2) + text(1) = 3, plus potential scroll indicators(2)
|
||||
const availableHeight = height - 16;
|
||||
|
||||
const cardsPerRow = Math.max(1, Math.floor(availableWidth / (cardWidth + minSpacing)));
|
||||
|
||||
const cardsPerRow = Math.max(
|
||||
1,
|
||||
Math.floor(availableWidth / (cardWidth + minSpacing)),
|
||||
);
|
||||
// Cap horizontal gap so it doesn't grow unbounded on wide terminals
|
||||
const columnSpacing = Math.min(minSpacing, Math.floor((availableWidth - (cardsPerRow * cardWidth)) / Math.max(1, cardsPerRow - 1)));
|
||||
const columnSpacing = Math.min(
|
||||
minSpacing,
|
||||
Math.floor(
|
||||
(availableWidth - cardsPerRow * cardWidth) / Math.max(1, cardsPerRow - 1),
|
||||
),
|
||||
);
|
||||
// Terminal chars are ~2× taller than wide, so 1 row ≈ 2 columns visually
|
||||
const rowSpacing = 1;
|
||||
const rowsVisible = Math.max(1, Math.floor((availableHeight + rowSpacing) / (cardHeight + rowSpacing)));
|
||||
|
||||
const rowsVisible = Math.max(
|
||||
1,
|
||||
Math.floor((availableHeight + rowSpacing) / (cardHeight + rowSpacing)),
|
||||
);
|
||||
|
||||
const totalRows = Math.ceil(filtered.length / cardsPerRow);
|
||||
const selectedRow = Math.floor(selectedIdx / cardsPerRow);
|
||||
// Calculate scroll offset for rows
|
||||
const [scrollRow, setScrollRow] = useState(0);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRow < scrollRow) {
|
||||
setScrollRow(selectedRow);
|
||||
@@ -159,7 +177,7 @@ export const ProviderSelector = React.memo(function ProviderSelector({ providers
|
||||
const cardBorder = isSelected ? "double" : "single";
|
||||
const cardBorderColor = isSelected ? GOLD : RULE_COLOR;
|
||||
const textColor = isSelected ? TEXT_PRIMARY : TEXT_SECONDARY;
|
||||
|
||||
|
||||
// Calculate actual content width: cardWidth - borders (2) - paddingX (2)
|
||||
const contentWidth = cardWidth - 4;
|
||||
// Width for title (leave space for icons: 2-3 chars)
|
||||
@@ -167,7 +185,7 @@ export const ProviderSelector = React.memo(function ProviderSelector({ providers
|
||||
// Available lines for description: cardHeight - borders (2) - title (1) - margin (1) - name (1) - margin (1)
|
||||
const descriptionMaxLines = Math.max(1, cardHeight - 6);
|
||||
const descriptionMaxChars = descriptionMaxLines * contentWidth;
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={provider.providerId}
|
||||
@@ -189,12 +207,10 @@ export const ProviderSelector = React.memo(function ProviderSelector({ providers
|
||||
{provider.providerType === "Preferred" && (
|
||||
<Text color={TEAL}>★</Text>
|
||||
)}
|
||||
{provider.configured && (
|
||||
<Text color={TEAL}>✓</Text>
|
||||
)}
|
||||
{provider.configured && <Text color={TEAL}>✓</Text>}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
<Box marginTop={1} flexDirection="column" flexGrow={1}>
|
||||
<Box width={contentWidth}>
|
||||
<Text color={TEXT_DIM} wrap="truncate">
|
||||
@@ -216,22 +232,33 @@ export const ProviderSelector = React.memo(function ProviderSelector({ providers
|
||||
};
|
||||
|
||||
const visibleRows = [];
|
||||
for (let row = scrollRow; row < Math.min(scrollRow + rowsVisible, totalRows); row++) {
|
||||
for (
|
||||
let row = scrollRow;
|
||||
row < Math.min(scrollRow + rowsVisible, totalRows);
|
||||
row++
|
||||
) {
|
||||
const rowProviders = [];
|
||||
for (let col = 0; col < cardsPerRow; col++) {
|
||||
const index = row * cardsPerRow + col;
|
||||
if (index < filtered.length) {
|
||||
const isSelected = index === selectedIdx;
|
||||
rowProviders.push(renderProviderCard(filtered[index], index, isSelected));
|
||||
rowProviders.push(
|
||||
renderProviderCard(filtered[index], index, isSelected),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (rowProviders.length > 0) {
|
||||
const isLastVisibleRow = row === Math.min(scrollRow + rowsVisible, totalRows) - 1;
|
||||
const isLastVisibleRow =
|
||||
row === Math.min(scrollRow + rowsVisible, totalRows) - 1;
|
||||
visibleRows.push(
|
||||
<Box key={row} gap={columnSpacing} marginBottom={isLastVisibleRow ? 0 : rowSpacing}>
|
||||
<Box
|
||||
key={row}
|
||||
gap={columnSpacing}
|
||||
marginBottom={isLastVisibleRow ? 0 : rowSpacing}
|
||||
>
|
||||
{rowProviders}
|
||||
</Box>
|
||||
</Box>,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -250,7 +277,7 @@ export const ProviderSelector = React.memo(function ProviderSelector({ providers
|
||||
{subtitle ?? "Connect an AI model provider to get started"}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
|
||||
{/* Search Bar */}
|
||||
<Box justifyContent="center" marginBottom={2}>
|
||||
<Box
|
||||
@@ -278,20 +305,21 @@ export const ProviderSelector = React.memo(function ProviderSelector({ providers
|
||||
<>
|
||||
{scrollRow > 0 && (
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color={TEXT_DIM}>▲ {scrollRow * cardsPerRow} more above</Text>
|
||||
<Text color={TEXT_DIM}>
|
||||
▲ {scrollRow * cardsPerRow} more above
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
<Box justifyContent="center">
|
||||
<Box flexDirection="column">
|
||||
{visibleRows}
|
||||
</Box>
|
||||
<Box flexDirection="column">{visibleRows}</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
{scrollRow + rowsVisible < totalRows && (
|
||||
<Box justifyContent="center" marginTop={1}>
|
||||
<Text color={TEXT_DIM}>
|
||||
▼ {filtered.length - (scrollRow + rowsVisible) * cardsPerRow} more below
|
||||
▼ {filtered.length - (scrollRow + rowsVisible) * cardsPerRow}{" "}
|
||||
more below
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
@@ -302,7 +330,8 @@ export const ProviderSelector = React.memo(function ProviderSelector({ providers
|
||||
{/* Footer */}
|
||||
<Box justifyContent="center" marginTop={2}>
|
||||
<Text color={TEXT_DIM}>
|
||||
↑↓←→ navigate · enter select · type to search{onBack ? " · esc back" : " · esc clear"}
|
||||
↑↓←→ navigate · enter select · type to search
|
||||
{onBack ? " · esc back" : " · esc clear"}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -316,7 +345,12 @@ export interface ProviderConfiguratorProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const ProviderConfigurator = React.memo(function ProviderConfigurator({ provider, height, onComplete, onBack }: ProviderConfiguratorProps) {
|
||||
export const ProviderConfigurator = React.memo(function ProviderConfigurator({
|
||||
provider,
|
||||
height,
|
||||
onComplete,
|
||||
onBack,
|
||||
}: ProviderConfiguratorProps) {
|
||||
const [keyValues, setKeyValues] = useState<Record<string, string>>({});
|
||||
const [activeKeyIdx, setActiveKeyIdx] = useState(0);
|
||||
const [showMasked, setShowMasked] = useState<Record<string, boolean>>({});
|
||||
@@ -354,7 +388,7 @@ export const ProviderConfigurator = React.memo(function ProviderConfigurator({ p
|
||||
if (activeKeyIdx < keys.length - 1) {
|
||||
setActiveKeyIdx(activeKeyIdx + 1);
|
||||
setShowMasked({});
|
||||
setInputKey(prev => prev + 1); // Force new input component
|
||||
setInputKey((prev) => prev + 1); // Force new input component
|
||||
} else {
|
||||
onComplete(newValues);
|
||||
}
|
||||
@@ -376,12 +410,20 @@ export const ProviderConfigurator = React.memo(function ProviderConfigurator({ p
|
||||
const headerHeight = 1 + (provider.description ? 2 : 0) + 1; // title + description + spacer
|
||||
const keysHeight = keys.length; // one line per key
|
||||
const inputHeight = currentKey ? 3 : 0; // input + help text + spacing
|
||||
const setupStepsHeight = provider.setupSteps?.length ? provider.setupSteps.length + 1 : 0;
|
||||
const contentHeight = headerHeight + keysHeight + inputHeight + setupStepsHeight;
|
||||
const setupStepsHeight = provider.setupSteps?.length
|
||||
? provider.setupSteps.length + 1
|
||||
: 0;
|
||||
const contentHeight =
|
||||
headerHeight + keysHeight + inputHeight + setupStepsHeight;
|
||||
const topPad = Math.max(0, Math.floor((height - contentHeight) / 2));
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" height={height} alignItems="center" width={columns}>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
height={height}
|
||||
alignItems="center"
|
||||
width={columns}
|
||||
>
|
||||
{topPad > 0 && <Box height={topPad} />}
|
||||
<Box flexDirection="column" width={maxWidth} paddingX={2}>
|
||||
{/* Header */}
|
||||
@@ -393,7 +435,9 @@ export const ProviderConfigurator = React.memo(function ProviderConfigurator({ p
|
||||
{provider.description && (
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Box width={maxWidth - 4}>
|
||||
<Text color={TEXT_DIM} wrap="wrap">{provider.description}</Text>
|
||||
<Text color={TEXT_DIM} wrap="wrap">
|
||||
{provider.description}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
@@ -411,9 +455,7 @@ export const ProviderConfigurator = React.memo(function ProviderConfigurator({ p
|
||||
>
|
||||
{k.name}
|
||||
</Text>
|
||||
{i < activeKeyIdx && (
|
||||
<Text color={TEAL}> ••••••</Text>
|
||||
)}
|
||||
{i < activeKeyIdx && <Text color={TEAL}> ••••••</Text>}
|
||||
</Box>
|
||||
))}
|
||||
|
||||
@@ -458,19 +500,18 @@ export const ProviderConfigurator = React.memo(function ProviderConfigurator({ p
|
||||
)}
|
||||
|
||||
{/* Setup Steps */}
|
||||
{provider.setupSteps &&
|
||||
provider.setupSteps.length > 0 && (
|
||||
<Box marginTop={2} flexDirection="column">
|
||||
<Text color={TEXT_DIM}>Setup steps:</Text>
|
||||
{provider.setupSteps.map((step, i) => (
|
||||
<Box key={i} width={maxWidth - 4} marginTop={1}>
|
||||
<Text color={TEXT_DIM} wrap="wrap">
|
||||
{i + 1}. {step}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
{provider.setupSteps && provider.setupSteps.length > 0 && (
|
||||
<Box marginTop={2} flexDirection="column">
|
||||
<Text color={TEXT_DIM}>Setup steps:</Text>
|
||||
{provider.setupSteps.map((step, i) => (
|
||||
<Box key={i} width={maxWidth - 4} marginTop={1}>
|
||||
<Text color={TEXT_DIM} wrap="wrap">
|
||||
{i + 1}. {step}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -481,7 +522,10 @@ interface SuccessScreenProps {
|
||||
height: number;
|
||||
}
|
||||
|
||||
const SuccessScreen = React.memo(function SuccessScreen({ provider, height }: SuccessScreenProps) {
|
||||
const SuccessScreen = React.memo(function SuccessScreen({
|
||||
provider,
|
||||
height,
|
||||
}: SuccessScreenProps) {
|
||||
const { stdout } = useStdout();
|
||||
const columns = stdout?.columns ?? 80;
|
||||
|
||||
@@ -539,7 +583,9 @@ export default function Onboarding({
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const resp = await client.goose.GooseProvidersList({ providerIds: [] });
|
||||
const resp = await client.goose.providersList_unstable({
|
||||
providerIds: [],
|
||||
});
|
||||
const sorted = [...resp.entries].sort((a, b) => {
|
||||
const aP = a.providerType === "Preferred" ? 0 : 1;
|
||||
const bP = b.providerType === "Preferred" ? 0 : 1;
|
||||
@@ -556,10 +602,13 @@ export default function Onboarding({
|
||||
}, [client, fetchKey]);
|
||||
|
||||
const saveProvider = useCallback(
|
||||
async (provider: ProviderInventoryEntryDto, values: Record<string, string>) => {
|
||||
async (
|
||||
provider: ProviderInventoryEntryDto,
|
||||
values: Record<string, string>,
|
||||
) => {
|
||||
setPhase("saving");
|
||||
try {
|
||||
await client.goose.GooseProvidersConfigSave({
|
||||
await client.goose.providersConfigSave_unstable({
|
||||
providerId: provider.providerId,
|
||||
fields: Object.entries(values).map(([key, value]) => ({
|
||||
key,
|
||||
@@ -600,7 +649,7 @@ export default function Onboarding({
|
||||
if (phase === "loading") {
|
||||
const contentHeight = 3; // spinner + text + spacing
|
||||
const topPad = Math.max(0, Math.floor((height - contentHeight) / 2));
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
@@ -622,7 +671,12 @@ export default function Onboarding({
|
||||
|
||||
if (phase === "error") {
|
||||
return (
|
||||
<Box flexDirection="column" height={height} alignItems="center" width={width}>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
height={height}
|
||||
alignItems="center"
|
||||
width={width}
|
||||
>
|
||||
<ErrorScreen errorMsg={errorMsg} onRetry={handleRetry} />
|
||||
</Box>
|
||||
);
|
||||
@@ -631,7 +685,7 @@ export default function Onboarding({
|
||||
if (phase === "saving") {
|
||||
const contentHeight = 3; // spinner + text + spacing
|
||||
const topPad = Math.max(0, Math.floor((height - contentHeight) / 2));
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
@@ -652,9 +706,7 @@ export default function Onboarding({
|
||||
}
|
||||
|
||||
if (phase === "success") {
|
||||
return (
|
||||
<SuccessScreen provider={selectedProvider} height={height} />
|
||||
);
|
||||
return <SuccessScreen provider={selectedProvider} height={height} />;
|
||||
}
|
||||
|
||||
if (phase === "configure" && selectedProvider) {
|
||||
|
||||
+36
-29
@@ -1,5 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { Box, Text, render, useApp, useInput, useStdout } from "ink";
|
||||
import { MultilineInput } from "ink-multiline-input";
|
||||
import meow from "meow";
|
||||
@@ -172,7 +178,9 @@ const InputBar = React.memo(function InputBar({
|
||||
</Text>
|
||||
</Box>
|
||||
{scrollHint && (
|
||||
<Text color={TEXT_DIM}>↑↓ scroll · ⌥↑↓ fast · shift+↑↓ history</Text>
|
||||
<Text color={TEXT_DIM}>
|
||||
↑↓ scroll · ⌥↑↓ fast · shift+↑↓ history
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
@@ -200,7 +208,9 @@ const InputBar = React.memo(function InputBar({
|
||||
}}
|
||||
/>
|
||||
{scrollHint && (
|
||||
<Text color={TEXT_DIM}>↑↓ scroll · ⌥↑↓ fast · shift+↑↓ history</Text>
|
||||
<Text color={TEXT_DIM}>
|
||||
↑↓ scroll · ⌥↑↓ fast · shift+↑↓ history
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
@@ -792,7 +802,7 @@ function App({
|
||||
setStatus("checking provider…");
|
||||
let hasProvider = false;
|
||||
try {
|
||||
const resp = await client.goose.GooseDefaultsRead({});
|
||||
const resp = await client.goose.defaultsRead_unstable({});
|
||||
hasProvider =
|
||||
resp.providerId != null &&
|
||||
resp.providerId !== "" &&
|
||||
@@ -831,31 +841,28 @@ function App({
|
||||
exit,
|
||||
]);
|
||||
|
||||
const addLocalTurn = useCallback(
|
||||
(userText: string, message?: string) => {
|
||||
setTurns((prev) => [
|
||||
...prev,
|
||||
{
|
||||
userText,
|
||||
responseItems: message
|
||||
? [
|
||||
{
|
||||
itemType: "content_chunk",
|
||||
content: { type: "text", text: message },
|
||||
},
|
||||
]
|
||||
: [],
|
||||
toolCallsById: new Map(),
|
||||
},
|
||||
]);
|
||||
setViewTurnIdx(-1);
|
||||
setSelectedToolCallIdx(null);
|
||||
setToolCallExpanded(false);
|
||||
setToolCallExpandedScroll(0);
|
||||
setScrollOffset(0);
|
||||
},
|
||||
[],
|
||||
);
|
||||
const addLocalTurn = useCallback((userText: string, message?: string) => {
|
||||
setTurns((prev) => [
|
||||
...prev,
|
||||
{
|
||||
userText,
|
||||
responseItems: message
|
||||
? [
|
||||
{
|
||||
itemType: "content_chunk",
|
||||
content: { type: "text", text: message },
|
||||
},
|
||||
]
|
||||
: [],
|
||||
toolCallsById: new Map(),
|
||||
},
|
||||
]);
|
||||
setViewTurnIdx(-1);
|
||||
setSelectedToolCallIdx(null);
|
||||
setToolCallExpanded(false);
|
||||
setToolCallExpandedScroll(0);
|
||||
setScrollOffset(0);
|
||||
}, []);
|
||||
|
||||
const runSlashCommand = useCallback(
|
||||
(raw: string): boolean => {
|
||||
|
||||
Reference in New Issue
Block a user