feature: provider & model config (#8515)
This commit is contained in:
@@ -35,7 +35,7 @@ export const Header = React.memo(function Header({
|
||||
<Box width={leftSideWidth}>
|
||||
<Text color={TEXT_PRIMARY} bold>goose</Text>
|
||||
<Text color={RULE_COLOR}> · </Text>
|
||||
<Box width={Math.max(leftSideWidth - 10, 5)}>
|
||||
<Box flexShrink={1}>
|
||||
<Text color={statusColor} wrap="truncate-end">{status}</Text>
|
||||
</Box>
|
||||
{loading && !hasPendingPermission && (
|
||||
@@ -48,7 +48,7 @@ export const Header = React.memo(function Header({
|
||||
{turnInfo.current}/{turnInfo.total}{" "}
|
||||
</Text>
|
||||
)}
|
||||
<Text color={TEXT_DIM}>^C exit</Text>
|
||||
<Text color={TEXT_DIM}>^G configure · ^C exit</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Rule width={constrainedWidth} />
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Box, Text, useInput, useStdout } from "ink";
|
||||
import type { GooseClient, ProviderDetailEntry } from "@aaif/goose-acp";
|
||||
import {
|
||||
TEAL,
|
||||
GOLD,
|
||||
TEXT_PRIMARY,
|
||||
TEXT_DIM,
|
||||
RULE_COLOR,
|
||||
} from "./colors.js";
|
||||
import { Spinner, SPINNER_FRAMES } from "./components/Spinner.js";
|
||||
import { ErrorScreen } from "./components/ErrorScreen.js";
|
||||
import { ProviderSelector, ProviderConfigurator } from "./onboarding.js";
|
||||
|
||||
const LOAD_MODELS_TIMEOUT_MS = 30000;
|
||||
|
||||
type Phase =
|
||||
| "loading"
|
||||
| "select_provider"
|
||||
| "configure"
|
||||
| "loading_models"
|
||||
| "select_model"
|
||||
| "saving"
|
||||
| "error";
|
||||
|
||||
interface ConfigureProps {
|
||||
client: GooseClient;
|
||||
sessionId: string;
|
||||
width: number;
|
||||
height: number;
|
||||
onComplete: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
interface ModelSelectorProps {
|
||||
client: GooseClient;
|
||||
provider: ProviderDetailEntry;
|
||||
height: number;
|
||||
onSelect: (model: string) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const ModelSelector = React.memo(function ModelSelector({
|
||||
client,
|
||||
provider,
|
||||
height,
|
||||
onSelect,
|
||||
onBack,
|
||||
}: ModelSelectorProps) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [models, setModels] = useState<string[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedIdx, setSelectedIdx] = useState(0);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [manualEntry, setManualEntry] = useState(false);
|
||||
const { stdout } = useStdout();
|
||||
const columns = stdout?.columns ?? 80;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!cancelled) {
|
||||
setError("Request timed out. The provider may be slow to respond.");
|
||||
setLoading(false);
|
||||
}
|
||||
}, LOAD_MODELS_TIMEOUT_MS);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const resp = await client.goose.GooseProvidersModels({
|
||||
providerName: provider.name,
|
||||
});
|
||||
if (!cancelled) {
|
||||
setModels(resp.models);
|
||||
const defaultIdx = resp.models.findIndex((m) => m === provider.defaultModel);
|
||||
setSelectedIdx(defaultIdx >= 0 ? defaultIdx : 0);
|
||||
setLoading(false);
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (!cancelled) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
setLoading(false);
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [client, provider.name, provider.defaultModel]);
|
||||
|
||||
const filtered = (() => {
|
||||
if (!searchQuery) return models;
|
||||
const q = searchQuery.toLowerCase();
|
||||
return models.filter((m) => m.toLowerCase().includes(q));
|
||||
})();
|
||||
|
||||
const maxWidth = Math.min(columns - 4, 80);
|
||||
const HEADER_HEIGHT = 2;
|
||||
const SEARCH_BOX_HEIGHT = 3;
|
||||
const FOOTER_HEIGHT = 3;
|
||||
const CHROME_HEIGHT = HEADER_HEIGHT + SEARCH_BOX_HEIGHT + FOOTER_HEIGHT + 4;
|
||||
const listHeight = Math.max(height - CHROME_HEIGHT, 3);
|
||||
const [scrollOffset, setScrollOffset] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIdx < scrollOffset) {
|
||||
setScrollOffset(selectedIdx);
|
||||
} else if (selectedIdx >= scrollOffset + listHeight) {
|
||||
setScrollOffset(selectedIdx - listHeight + 1);
|
||||
}
|
||||
}, [selectedIdx, scrollOffset, listHeight]);
|
||||
|
||||
useInput((ch, key) => {
|
||||
if (key.escape) {
|
||||
if (manualEntry) {
|
||||
setManualEntry(false);
|
||||
setSearchQuery("");
|
||||
return;
|
||||
}
|
||||
if (searchQuery) {
|
||||
setSearchQuery("");
|
||||
setSelectedIdx(0);
|
||||
setScrollOffset(0);
|
||||
return;
|
||||
}
|
||||
onBack();
|
||||
return;
|
||||
}
|
||||
if (manualEntry) {
|
||||
if (key.return) {
|
||||
if (searchQuery.trim()) {
|
||||
onSelect(searchQuery.trim());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.backspace || key.delete) {
|
||||
setSearchQuery((q) => q.slice(0, -1));
|
||||
return;
|
||||
}
|
||||
if (ch && ch.length === 1 && !key.ctrl && !key.meta) {
|
||||
setSearchQuery((q) => q + ch);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.upArrow) {
|
||||
setSelectedIdx((i) => Math.max(i - 1, 0));
|
||||
return;
|
||||
}
|
||||
if (key.downArrow) {
|
||||
setSelectedIdx((i) => Math.min(i + 1, filtered.length - 1));
|
||||
return;
|
||||
}
|
||||
if (key.return) {
|
||||
const m = filtered[selectedIdx];
|
||||
if (m) onSelect(m);
|
||||
return;
|
||||
}
|
||||
if (key.backspace || key.delete) {
|
||||
setSearchQuery((q) => q.slice(0, -1));
|
||||
setSelectedIdx(0);
|
||||
setScrollOffset(0);
|
||||
return;
|
||||
}
|
||||
if (ch === "m" && !searchQuery) {
|
||||
setManualEntry(true);
|
||||
return;
|
||||
}
|
||||
if (ch && ch.length === 1 && !key.ctrl && !key.meta) {
|
||||
setSearchQuery((q) => q + ch);
|
||||
setSelectedIdx(0);
|
||||
setScrollOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box flexDirection="column" justifyContent="center" alignItems="center" width={columns} height={height}>
|
||||
<Spinner idx={0} />
|
||||
<Box marginTop={1}>
|
||||
<Text color={TEXT_DIM}>loading models…</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Box flexDirection="column" justifyContent="center" alignItems="center" width={columns} height={height}>
|
||||
<Box flexDirection="column" alignItems="center" width={maxWidth}>
|
||||
<Text color={GOLD}>⚠ Failed to load models</Text>
|
||||
<Box marginTop={1} width={maxWidth}>
|
||||
<Text color={TEXT_DIM} wrap="wrap">{error}</Text>
|
||||
</Box>
|
||||
<Box marginTop={2}>
|
||||
<Text color={TEXT_DIM}>m manual entry · esc back</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" justifyContent="center" alignItems="center" height={height} width={columns}>
|
||||
<Box flexDirection="column" width={maxWidth} paddingX={2}>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
Enter model name manually
|
||||
</Text>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={GOLD}
|
||||
paddingX={2}
|
||||
width={inputWidth}
|
||||
>
|
||||
<Text color={GOLD} bold>{"❯ "}</Text>
|
||||
<Text color={searchQuery ? TEXT_PRIMARY : TEXT_DIM}>
|
||||
{truncatedText}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={2}>
|
||||
<Text color={TEXT_DIM}>
|
||||
enter confirm · esc cancel
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const visible = filtered.slice(scrollOffset, scrollOffset + listHeight);
|
||||
const searchBoxWidth = Math.min(60, maxWidth - 4);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" justifyContent="center" alignItems="center" height={height} width={columns}>
|
||||
<Box flexDirection="column" width={maxWidth} paddingX={2}>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
Select model for {provider.displayName}
|
||||
</Text>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={RULE_COLOR}
|
||||
paddingX={2}
|
||||
width={searchBoxWidth}
|
||||
>
|
||||
<Text color={GOLD} bold>{"❯ "}</Text>
|
||||
<Box width={searchBoxWidth - 8}>
|
||||
<Text color={searchQuery ? TEXT_PRIMARY : TEXT_DIM} wrap="truncate">
|
||||
{searchQuery || "search models…"}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1} flexDirection="column" height={listHeight}>
|
||||
{filtered.length === 0 ? (
|
||||
<Text color={TEXT_DIM}>No matching models</Text>
|
||||
) : (
|
||||
<>
|
||||
{scrollOffset > 0 && (
|
||||
<Text color={TEXT_DIM}>▲ {scrollOffset} more above</Text>
|
||||
)}
|
||||
{visible.map((model, vi) => {
|
||||
const idx = vi + scrollOffset;
|
||||
const active = idx === selectedIdx;
|
||||
const isDefault = model === provider.defaultModel;
|
||||
const modelWidth = maxWidth - 8;
|
||||
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}>
|
||||
{truncatedModel}
|
||||
</Text>
|
||||
{isDefault && <Text color={TEAL}> (default)</Text>}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{scrollOffset + listHeight < filtered.length && (
|
||||
<Text color={TEXT_DIM}>
|
||||
▼ {filtered.length - scrollOffset - listHeight} more below
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={TEXT_DIM}>
|
||||
↑↓ navigate · enter select · m manual · esc back
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
export default function ConfigureScreen({
|
||||
client,
|
||||
sessionId,
|
||||
width,
|
||||
height,
|
||||
onComplete,
|
||||
onCancel,
|
||||
}: ConfigureProps) {
|
||||
const [phase, setPhase] = useState<Phase>("loading");
|
||||
const [providers, setProviders] = useState<ProviderDetailEntry[]>([]);
|
||||
const [selectedProvider, setSelectedProvider] = useState<ProviderDetailEntry | null>(null);
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
const [spinIdx, setSpinIdx] = useState(0);
|
||||
const [fetchKey, setFetchKey] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(
|
||||
() => setSpinIdx((i) => (i + 1) % SPINNER_FRAMES.length),
|
||||
300,
|
||||
);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const resp = await client.goose.GooseProvidersDetails({});
|
||||
if (!cancelled) {
|
||||
const sorted = [...resp.providers].sort((a, b) => {
|
||||
const aP = a.providerType === "Preferred" ? 0 : 1;
|
||||
const bP = b.providerType === "Preferred" ? 0 : 1;
|
||||
if (aP !== bP) return aP - bP;
|
||||
return a.displayName.localeCompare(b.displayName);
|
||||
});
|
||||
setProviders(sorted);
|
||||
setPhase("select_provider");
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (!cancelled) {
|
||||
setErrorMsg(e instanceof Error ? e.message : String(e));
|
||||
setPhase("error");
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [client, fetchKey]);
|
||||
|
||||
const applyProviderModel = useCallback(
|
||||
async (provider: ProviderDetailEntry, model: string, configValues: Record<string, string>) => {
|
||||
setPhase("saving");
|
||||
try {
|
||||
for (const [key, value] of Object.entries(configValues)) {
|
||||
const configKey = provider.configKeys.find((k) => k.name === key);
|
||||
if (configKey?.secret) {
|
||||
await client.goose.GooseSecretUpsert({ key, value });
|
||||
} else {
|
||||
await client.goose.GooseConfigUpsert({ key, value });
|
||||
}
|
||||
}
|
||||
await client.goose.GooseConfigUpsert({ key: "GOOSE_PROVIDER", value: provider.name });
|
||||
await client.goose.GooseConfigUpsert({ key: "GOOSE_MODEL", value: model });
|
||||
await client.goose.GooseSessionProviderUpdate({
|
||||
sessionId,
|
||||
provider: provider.name,
|
||||
model,
|
||||
});
|
||||
onComplete();
|
||||
} catch (e: unknown) {
|
||||
setErrorMsg(e instanceof Error ? e.message : String(e));
|
||||
setPhase("error");
|
||||
}
|
||||
},
|
||||
[client, sessionId, onComplete],
|
||||
);
|
||||
|
||||
const [pendingConfigValues, setPendingConfigValues] = useState<Record<string, string>>({});
|
||||
|
||||
const handleProviderSelected = useCallback(
|
||||
(provider: ProviderDetailEntry) => {
|
||||
const keys = provider.configKeys.filter(
|
||||
(k) => k.required && !k.oauthFlow && !k.deviceCodeFlow,
|
||||
);
|
||||
setSelectedProvider(provider);
|
||||
if (keys.length > 0 && !provider.isConfigured) {
|
||||
setPhase("configure");
|
||||
} else {
|
||||
setPendingConfigValues({});
|
||||
setPhase("select_model");
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleConfigComplete = useCallback(
|
||||
(values: Record<string, string>) => {
|
||||
if (!selectedProvider) return;
|
||||
setPendingConfigValues(values);
|
||||
setPhase("select_model");
|
||||
},
|
||||
[selectedProvider],
|
||||
);
|
||||
|
||||
const handleModelSelected = useCallback(
|
||||
(model: string) => {
|
||||
if (!selectedProvider) return;
|
||||
applyProviderModel(selectedProvider, model, pendingConfigValues);
|
||||
},
|
||||
[selectedProvider, pendingConfigValues, applyProviderModel],
|
||||
);
|
||||
|
||||
const handleRetry = useCallback(() => {
|
||||
setErrorMsg("");
|
||||
setFetchKey((k) => k + 1);
|
||||
setPhase("loading");
|
||||
}, []);
|
||||
|
||||
if (phase === "loading" || phase === "loading_models" || phase === "saving") {
|
||||
const label =
|
||||
phase === "loading" ? "loading providers…" :
|
||||
phase === "loading_models" ? "loading models…" :
|
||||
"applying changes…";
|
||||
return (
|
||||
<Box flexDirection="column" justifyContent="center" alignItems="center" width={width} height={height}>
|
||||
<Spinner idx={spinIdx} />
|
||||
<Box marginTop={1}>
|
||||
<Text color={TEXT_DIM}>{label}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (phase === "error") {
|
||||
return (
|
||||
<Box flexDirection="column" height={height} alignItems="center" width={width}>
|
||||
<ErrorScreen errorMsg={errorMsg} onRetry={handleRetry} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (phase === "configure" && selectedProvider) {
|
||||
return (
|
||||
<ProviderConfigurator
|
||||
provider={selectedProvider}
|
||||
height={height}
|
||||
onComplete={handleConfigComplete}
|
||||
onBack={() => {
|
||||
setSelectedProvider(null);
|
||||
setPhase("select_provider");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (phase === "select_model" && selectedProvider) {
|
||||
return (
|
||||
<ModelSelector
|
||||
client={client}
|
||||
provider={selectedProvider}
|
||||
height={height}
|
||||
onSelect={handleModelSelected}
|
||||
onBack={() => {
|
||||
setPhase("select_provider");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ProviderSelector
|
||||
providers={providers}
|
||||
height={height}
|
||||
onSelect={handleProviderSelected}
|
||||
title="◆ Configure provider ◆"
|
||||
subtitle="Select a provider and model for this session"
|
||||
onBack={onCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -29,13 +29,16 @@ interface OnboardingProps {
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
interface ProviderSelectorProps {
|
||||
export interface ProviderSelectorProps {
|
||||
providers: ProviderDetailEntry[];
|
||||
height: number;
|
||||
onSelect: (provider: ProviderDetailEntry) => void;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
const ProviderSelector = React.memo(function ProviderSelector({ providers, height, onSelect }: 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();
|
||||
@@ -89,6 +92,10 @@ const ProviderSelector = React.memo(function ProviderSelector({ providers, heigh
|
||||
setScrollRow(0);
|
||||
return;
|
||||
}
|
||||
if (onBack) {
|
||||
onBack();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (filtered.length === 0) {
|
||||
// Only allow typing/backspace when no results match; skip navigation
|
||||
@@ -231,12 +238,12 @@ const ProviderSelector = React.memo(function ProviderSelector({ providers, heigh
|
||||
<Box marginTop={1} />
|
||||
<Box justifyContent="center" marginBottom={1}>
|
||||
<Text color={TEXT_PRIMARY} bold>
|
||||
◆ Welcome to goose ◆
|
||||
{title ?? "◆ Welcome to goose ◆"}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box justifyContent="center" marginBottom={2}>
|
||||
<Text color={TEXT_DIM}>
|
||||
Connect an AI model provider to get started
|
||||
{subtitle ?? "Connect an AI model provider to get started"}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -291,21 +298,21 @@ const ProviderSelector = React.memo(function ProviderSelector({ providers, heigh
|
||||
{/* Footer */}
|
||||
<Box justifyContent="center" marginTop={2}>
|
||||
<Text color={TEXT_DIM}>
|
||||
↑↓←→ navigate · enter select · type to search · esc clear
|
||||
↑↓←→ navigate · enter select · type to search{onBack ? " · esc back" : " · esc clear"}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
interface ProviderConfiguratorProps {
|
||||
export interface ProviderConfiguratorProps {
|
||||
provider: ProviderDetailEntry;
|
||||
height: number;
|
||||
onComplete: (values: Record<string, string>) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
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>>({});
|
||||
|
||||
+30
-1
@@ -20,6 +20,7 @@ import type {
|
||||
import { ndJsonStream } from "@agentclientprotocol/sdk";
|
||||
import { GooseClient } from "@aaif/goose-acp";
|
||||
import Onboarding from "./onboarding.js";
|
||||
import ConfigureScreen from "./configure.js";
|
||||
import type { PendingPermission, ResponseItem, Turn } from "./types.js";
|
||||
import {
|
||||
emptyLine,
|
||||
@@ -482,6 +483,7 @@ function App({
|
||||
const [scrollOffset, setScrollOffset] = useState(0);
|
||||
const [pastedFull, setPastedFull] = useState<string | null>(null);
|
||||
const [needsOnboarding, setNeedsOnboarding] = useState(false);
|
||||
const [configuring, setConfiguring] = useState(false);
|
||||
|
||||
const clientRef = useRef<GooseClient | null>(null);
|
||||
const sessionIdRef = useRef<string | null>(null);
|
||||
@@ -803,6 +805,11 @@ function App({
|
||||
exit();
|
||||
}
|
||||
|
||||
if (ch === "g" && key.ctrl && !loading && !pendingPermission && sessionIdRef.current) {
|
||||
setConfiguring(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingPermission) {
|
||||
const opts = pendingPermission.options;
|
||||
if (key.upArrow) { setPermissionIdx((i) => (i - 1 + opts.length) % opts.length); return; }
|
||||
@@ -868,7 +875,7 @@ function App({
|
||||
});
|
||||
return;
|
||||
}
|
||||
}, { isActive: !needsOnboarding });
|
||||
}, { isActive: !needsOnboarding && !configuring });
|
||||
|
||||
const PAD_X = 2;
|
||||
const PAD_Y = 1;
|
||||
@@ -928,6 +935,28 @@ function App({
|
||||
);
|
||||
}
|
||||
|
||||
if (configuring && clientRef.current && sessionIdRef.current) {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
width={safeTermWidth}
|
||||
height={safeTermHeight}
|
||||
>
|
||||
<ConfigureScreen
|
||||
client={clientRef.current}
|
||||
sessionId={sessionIdRef.current}
|
||||
width={safeTermWidth}
|
||||
height={safeTermHeight}
|
||||
onComplete={() => {
|
||||
setConfiguring(false);
|
||||
setStatus("ready");
|
||||
}}
|
||||
onCancel={() => setConfiguring(false)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
|
||||
Reference in New Issue
Block a user