feat(acp): replace raw config and secret methods (#9000)

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
This commit is contained in:
Kalvin C
2026-05-04 18:24:28 -07:00
committed by GitHub
parent e82c4fdcb7
commit 2fe4c3d0bb
39 changed files with 1481 additions and 855 deletions
+8
View File
@@ -169,6 +169,14 @@ The skills → sources migration in [#8675](https://github.com/block/goose/pull/
For a minimal frontend `api/` wrapper using the typed shape, see `ui/goose2/src/features/providers/api/inventory.ts` — ~30 lines, typed SDK calls, thin adapter. For a fully worked end-to-end feature including OS-keychain handling and progress streaming, see the voice dictation feature ([#8609](https://github.com/block/goose/pull/8609)) and `ui/goose2/src/shared/api/dictation.ts`.
### Typed ACP config contracts
Build goose2 config flows around typed ACP methods whose contract matches the domain: provider config, preferences, defaults, dictation secrets, or extension config.
Keep raw Goose storage keys and secret-handling decisions behind backend-owned ACP methods. This lets Goose validate inputs, apply provider metadata, invalidate caches, refresh dependent state, and keep generated SDK types aligned with supported behavior.
For reference, define contracts in `crates/goose-sdk/src/custom_requests.rs`, implement them in `crates/goose/src/acp/server/`, regenerate `ui/sdk/src/generated/`, then call the generated `client.goose.*` methods from a feature or shared `api/` wrapper.
### When `invoke()` is still appropriate
Tauri commands (`invoke()` from `@tauri-apps/api/core`) are reserved for things that genuinely belong to the desktop shell, not to `goose` core. Provider config or secret mutations that affect the Goose runtime must flow through React → SDK → ACP → goose core so core can validate provider metadata, invalidate secret caches, refresh inventory, and apply provider changes consistently. In practice, Tauri is limited to:
@@ -2,7 +2,6 @@ import { act, renderHook, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
AUTO_COMPACT_PREFERENCES_EVENT,
AUTO_COMPACT_THRESHOLD_CONFIG_KEY,
DEFAULT_AUTO_COMPACT_THRESHOLD,
} from "../../lib/autoCompact";
@@ -26,8 +25,10 @@ describe("useAutoCompactPreferences", () => {
it("hydrates from the stored threshold value", async () => {
mockGetClient.mockResolvedValue({
goose: {
GooseConfigRead: vi.fn().mockResolvedValue({ value: 0.65 }),
GooseConfigUpsert: vi.fn().mockResolvedValue({}),
GoosePreferencesRead: vi.fn().mockResolvedValue({
values: [{ key: "autoCompactThreshold", value: 0.65 }],
}),
GoosePreferencesSave: vi.fn().mockResolvedValue({}),
},
});
@@ -42,13 +43,17 @@ describe("useAutoCompactPreferences", () => {
const upsert = vi.fn().mockResolvedValue({});
const read = vi
.fn()
.mockResolvedValueOnce({ value: null })
.mockResolvedValue({ value: 0.9 });
.mockResolvedValueOnce({
values: [{ key: "autoCompactThreshold", value: null }],
})
.mockResolvedValue({
values: [{ key: "autoCompactThreshold", value: 0.9 }],
});
mockGetClient.mockResolvedValue({
goose: {
GooseConfigRead: read,
GooseConfigUpsert: upsert,
GoosePreferencesRead: read,
GoosePreferencesSave: upsert,
},
});
@@ -64,8 +69,7 @@ describe("useAutoCompactPreferences", () => {
});
expect(upsert).toHaveBeenCalledWith({
key: AUTO_COMPACT_THRESHOLD_CONFIG_KEY,
value: 0.9,
values: [{ key: "autoCompactThreshold", value: 0.9 }],
});
expect(eventListener).toHaveBeenCalledTimes(1);
expect(result.current.autoCompactThreshold).toBe(0.9);
@@ -73,13 +77,17 @@ describe("useAutoCompactPreferences", () => {
window.removeEventListener(AUTO_COMPACT_PREFERENCES_EVENT, eventListener);
});
it("marks the preferences hydrated even when the initial read fails", async () => {
it("does not mark preferences hydrated when the initial read fails", async () => {
mockGetClient.mockRejectedValue(new Error("ACP not ready"));
const { result } = renderHook(() => useAutoCompactPreferences());
await waitFor(() => expect(result.current.isHydrated).toBe(true));
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(result.current.isHydrated).toBe(false);
expect(result.current.autoCompactThreshold).toBe(
DEFAULT_AUTO_COMPACT_THRESHOLD,
);
@@ -90,12 +98,14 @@ describe("useAutoCompactPreferences", () => {
const read = vi
.fn()
.mockRejectedValueOnce(new Error("ACP not ready"))
.mockResolvedValueOnce({ value: 0.65 });
.mockResolvedValueOnce({
values: [{ key: "autoCompactThreshold", value: 0.65 }],
});
mockGetClient.mockResolvedValue({
goose: {
GooseConfigRead: read,
GooseConfigUpsert: vi.fn().mockResolvedValue({}),
GoosePreferencesRead: read,
GoosePreferencesSave: vi.fn().mockResolvedValue({}),
},
});
@@ -106,7 +116,7 @@ describe("useAutoCompactPreferences", () => {
await Promise.resolve();
});
expect(result.current.isHydrated).toBe(true);
expect(result.current.isHydrated).toBe(false);
expect(result.current.autoCompactThreshold).toBe(
DEFAULT_AUTO_COMPACT_THRESHOLD,
);
@@ -116,6 +126,47 @@ describe("useAutoCompactPreferences", () => {
});
expect(result.current.autoCompactThreshold).toBe(0.65);
expect(result.current.isHydrated).toBe(true);
expect(read).toHaveBeenCalledTimes(2);
});
it("backs off repeated hydration retries", async () => {
vi.useFakeTimers();
const read = vi.fn().mockRejectedValue(new Error("ACP not ready"));
mockGetClient.mockResolvedValue({
goose: {
GoosePreferencesRead: read,
GoosePreferencesSave: vi.fn().mockResolvedValue({}),
},
});
renderHook(() => useAutoCompactPreferences());
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(read).toHaveBeenCalledTimes(1);
await act(async () => {
await vi.advanceTimersByTimeAsync(999);
});
expect(read).toHaveBeenCalledTimes(1);
await act(async () => {
await vi.advanceTimersByTimeAsync(1);
});
expect(read).toHaveBeenCalledTimes(2);
await act(async () => {
await vi.advanceTimersByTimeAsync(1999);
});
expect(read).toHaveBeenCalledTimes(2);
await act(async () => {
await vi.advanceTimersByTimeAsync(1);
});
expect(read).toHaveBeenCalledTimes(3);
});
});
@@ -9,7 +9,7 @@ const mockAcpPrepareSession = vi.fn();
const mockAcpSetModel = vi.fn();
const mockSetSelectedProvider = vi.fn();
const mockResolveSessionCwd = vi.fn();
const mockGooseConfigRead = vi.fn();
const mockGooseDefaultsRead = vi.fn();
const mockUseProviderInventory = vi.fn();
const mockPickerState = {
pickerAgents: [{ id: "goose", label: "Goose" }],
@@ -31,7 +31,7 @@ vi.mock("@/shared/api/acp", () => ({
vi.mock("@/shared/api/acpConnection", () => ({
getClient: async () => ({
goose: {
GooseConfigRead: (...args: unknown[]) => mockGooseConfigRead(...args),
GooseDefaultsRead: (...args: unknown[]) => mockGooseDefaultsRead(...args),
},
}),
}));
@@ -116,7 +116,10 @@ describe("useChatSessionController", () => {
mockAcpPrepareSession.mockResolvedValue(undefined);
mockAcpSetModel.mockResolvedValue(undefined);
mockResolveSessionCwd.mockResolvedValue("/tmp/project");
mockGooseConfigRead.mockResolvedValue({ value: null });
mockGooseDefaultsRead.mockResolvedValue({
providerId: null,
modelId: null,
});
mockUseProviderInventory.mockReturnValue({
getEntry: () => undefined,
});
@@ -283,17 +286,10 @@ describe("useChatSessionController", () => {
it("falls back to the configured goose default model when no explicit model is stored", async () => {
useAgentStore.setState({ selectedProvider: "goose" });
mockGooseConfigRead.mockImplementation(
async ({ key }: { key: string }): Promise<{ value: string | null }> => {
if (key === "GOOSE_PROVIDER") {
return { value: "databricks" };
}
if (key === "GOOSE_MODEL") {
return { value: "goose-claude-4-6-opus" };
}
return { value: null };
},
);
mockGooseDefaultsRead.mockResolvedValue({
providerId: "databricks",
modelId: "goose-claude-4-6-opus",
});
mockPickerState.availableModels = [
{
id: "goose-claude-4-6-opus",
@@ -26,7 +26,10 @@ describe("useResolvedAgentModelPicker", () => {
mockGetClient.mockResolvedValue({
goose: {
GooseConfigRead: vi.fn().mockResolvedValue({ value: null }),
GooseDefaultsRead: vi.fn().mockResolvedValue({
providerId: null,
modelId: null,
}),
},
});
@@ -27,17 +27,20 @@ describe("useVoiceInputPreferences", () => {
mockGetClient.mockResolvedValue({
goose: {
GooseConfigRead: vi.fn().mockImplementation(({ key }) => {
if (key === "VOICE_DICTATION_PROVIDER") {
if (shouldFailProviderRead) {
return Promise.reject(new Error("temporary acp failure"));
}
return Promise.resolve({ value: "groq" });
GoosePreferencesRead: vi.fn().mockImplementation(() => {
if (shouldFailProviderRead) {
return Promise.reject(new Error("temporary acp failure"));
}
return Promise.resolve({ value: null });
return Promise.resolve({
values: [
{ key: "voiceAutoSubmitPhrases", value: null },
{ key: "voiceDictationProvider", value: "groq" },
{ key: "voiceDictationPreferredMic", value: null },
],
});
}),
GooseConfigUpsert: vi.fn().mockResolvedValue({}),
GooseConfigRemove: vi.fn().mockResolvedValue({}),
GoosePreferencesSave: vi.fn().mockResolvedValue({}),
GoosePreferencesRemove: vi.fn().mockResolvedValue({}),
},
});
@@ -61,21 +64,27 @@ describe("useVoiceInputPreferences", () => {
it("broadcasts preference changes only after config persistence settles", async () => {
const upsert = vi.fn();
const providerRead = deferred<{ value?: unknown }>();
const providerRead = deferred<{
values: Array<{ key: string; value: unknown }>;
}>();
const pendingWrite = deferred<void>();
mockGetClient.mockResolvedValue({
goose: {
GooseConfigRead: vi
GoosePreferencesRead: vi
.fn()
.mockResolvedValueOnce({ value: null })
.mockResolvedValueOnce({ value: null })
.mockResolvedValueOnce({ value: null })
.mockResolvedValueOnce({
values: [
{ key: "voiceAutoSubmitPhrases", value: null },
{ key: "voiceDictationProvider", value: null },
{ key: "voiceDictationPreferredMic", value: null },
],
})
.mockImplementation(() => providerRead.promise),
GooseConfigUpsert: upsert.mockImplementation(
GoosePreferencesSave: upsert.mockImplementation(
() => pendingWrite.promise,
),
GooseConfigRemove: vi.fn().mockResolvedValue({}),
GoosePreferencesRemove: vi.fn().mockResolvedValue({}),
},
});
@@ -100,7 +109,114 @@ describe("useVoiceInputPreferences", () => {
await waitFor(() => expect(eventListener).toHaveBeenCalledTimes(1));
providerRead.resolve({ value: "openai" });
providerRead.resolve({
values: [
{ key: "voiceAutoSubmitPhrases", value: null },
{ key: "voiceDictationProvider", value: "openai" },
{ key: "voiceDictationPreferredMic", value: null },
],
});
window.removeEventListener("goose:voice-input-preferences", eventListener);
});
it("does not broadcast failed preference writes and re-syncs stored state", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const save = vi.fn().mockRejectedValue(new Error("write failed"));
const read = vi.fn().mockResolvedValue({
values: [
{ key: "voiceAutoSubmitPhrases", value: null },
{ key: "voiceDictationProvider", value: "groq" },
{ key: "voiceDictationPreferredMic", value: null },
],
});
mockGetClient.mockResolvedValue({
goose: {
GoosePreferencesRead: read,
GoosePreferencesSave: save,
GoosePreferencesRemove: vi.fn().mockResolvedValue({}),
},
});
const eventListener = vi.fn();
window.addEventListener("goose:voice-input-preferences", eventListener);
const { result } = renderHook(() => useVoiceInputPreferences());
await waitFor(() => expect(result.current.isHydrated).toBe(true));
act(() => {
result.current.setSelectedProvider("openai");
});
expect(result.current.selectedProvider).toBe("openai");
await waitFor(() => expect(read).toHaveBeenCalledTimes(2));
expect(eventListener).not.toHaveBeenCalled();
expect(result.current.selectedProvider).toBe("groq");
window.removeEventListener("goose:voice-input-preferences", eventListener);
warn.mockRestore();
});
it("stores the disabled sentinel when provider is set to null", async () => {
const save = vi.fn().mockResolvedValue({});
mockGetClient.mockResolvedValue({
goose: {
GoosePreferencesRead: vi.fn().mockResolvedValue({
values: [
{ key: "voiceAutoSubmitPhrases", value: null },
{ key: "voiceDictationProvider", value: "groq" },
{ key: "voiceDictationPreferredMic", value: null },
],
}),
GoosePreferencesSave: save,
GoosePreferencesRemove: vi.fn().mockResolvedValue({}),
},
});
const { result } = renderHook(() => useVoiceInputPreferences());
await waitFor(() => expect(result.current.isHydrated).toBe(true));
act(() => {
result.current.setSelectedProvider(null);
});
await waitFor(() => {
expect(save).toHaveBeenCalledWith({
values: [{ key: "voiceDictationProvider", value: "__disabled__" }],
});
});
});
it("clearing selected provider removes only the provider preference", async () => {
const remove = vi.fn().mockResolvedValue({});
mockGetClient.mockResolvedValue({
goose: {
GoosePreferencesRead: vi.fn().mockResolvedValue({
values: [
{ key: "voiceAutoSubmitPhrases", value: "submit" },
{ key: "voiceDictationProvider", value: "groq" },
{ key: "voiceDictationPreferredMic", value: "mic-1" },
],
}),
GoosePreferencesSave: vi.fn().mockResolvedValue({}),
GoosePreferencesRemove: remove,
},
});
const { result } = renderHook(() => useVoiceInputPreferences());
await waitFor(() => expect(result.current.isHydrated).toBe(true));
act(() => {
result.current.clearSelectedProvider();
});
await waitFor(() => {
expect(remove).toHaveBeenCalledWith({
keys: ["voiceDictationProvider"],
});
});
});
});
@@ -1,13 +1,14 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { getClient } from "@/shared/api/acpConnection";
import {
AUTO_COMPACT_PREFERENCES_EVENT,
AUTO_COMPACT_THRESHOLD_CONFIG_KEY,
DEFAULT_AUTO_COMPACT_THRESHOLD,
normalizeAutoCompactThreshold,
} from "../lib/autoCompact";
const AUTO_COMPACT_RETRY_DELAY_MS = 1000;
const AUTO_COMPACT_INITIAL_RETRY_DELAY_MS = 1000;
const AUTO_COMPACT_MAX_RETRY_DELAY_MS = 30000;
const AUTO_COMPACT_THRESHOLD_PREFERENCE_KEY = "autoCompactThreshold";
type ConfigReadResult =
| {
@@ -18,22 +19,29 @@ type ConfigReadResult =
ok: false;
};
async function readConfigValue(key: string): Promise<ConfigReadResult> {
async function readAutoCompactThreshold(): Promise<ConfigReadResult> {
try {
const client = await getClient();
const response = await client.goose.GooseConfigRead({ key });
const response = await client.goose.GoosePreferencesRead({
keys: [AUTO_COMPACT_THRESHOLD_PREFERENCE_KEY],
});
const preference = response.values.find(
(value) => value.key === AUTO_COMPACT_THRESHOLD_PREFERENCE_KEY,
);
return {
ok: true,
value: response.value ?? null,
value: preference?.value ?? null,
};
} catch {
return { ok: false };
}
}
async function writeConfigValue(key: string, value: number): Promise<void> {
async function writeAutoCompactThreshold(value: number): Promise<void> {
const client = await getClient();
await client.goose.GooseConfigUpsert({ key, value });
await client.goose.GoosePreferencesSave({
values: [{ key: AUTO_COMPACT_THRESHOLD_PREFERENCE_KEY, value }],
});
}
export function useAutoCompactPreferences() {
@@ -41,31 +49,10 @@ export function useAutoCompactPreferences() {
DEFAULT_AUTO_COMPACT_THRESHOLD,
);
const [isHydrated, setIsHydrated] = useState(false);
const [syncVersion, setSyncVersion] = useState(0);
const retryDelayMsRef = useRef(AUTO_COMPACT_INITIAL_RETRY_DELAY_MS);
const requestSyncFromConfig = useCallback(() => {
setSyncVersion((current) => current + 1);
}, []);
useEffect(() => {
const handler = () => {
requestSyncFromConfig();
};
window.addEventListener(
AUTO_COMPACT_PREFERENCES_EVENT,
handler as EventListener,
);
return () => {
window.removeEventListener(
AUTO_COMPACT_PREFERENCES_EVENT,
handler as EventListener,
);
};
}, [requestSyncFromConfig]);
const syncFromConfig = useCallback(async (_syncVersion: number) => {
void _syncVersion;
const result = await readConfigValue(AUTO_COMPACT_THRESHOLD_CONFIG_KEY);
const syncFromConfig = useCallback(async () => {
const result = await readAutoCompactThreshold();
return result;
}, []);
@@ -73,8 +60,16 @@ export function useAutoCompactPreferences() {
let cancelled = false;
let retryTimer: number | null = null;
const clearRetryTimer = () => {
if (retryTimer !== null) {
window.clearTimeout(retryTimer);
retryTimer = null;
}
};
const applyConfig = async () => {
const result = await syncFromConfig(syncVersion);
clearRetryTimer();
const result = await syncFromConfig();
if (cancelled) {
return;
}
@@ -83,24 +78,40 @@ export function useAutoCompactPreferences() {
setAutoCompactThresholdState(
normalizeAutoCompactThreshold(result.value),
);
setIsHydrated(true);
retryDelayMsRef.current = AUTO_COMPACT_INITIAL_RETRY_DELAY_MS;
} else {
retryTimer = window.setTimeout(
requestSyncFromConfig,
AUTO_COMPACT_RETRY_DELAY_MS,
const delayMs = retryDelayMsRef.current;
retryDelayMsRef.current = Math.min(
delayMs * 2,
AUTO_COMPACT_MAX_RETRY_DELAY_MS,
);
retryTimer = window.setTimeout(() => {
void applyConfig();
}, delayMs);
}
setIsHydrated(true);
};
const handler = () => {
retryDelayMsRef.current = AUTO_COMPACT_INITIAL_RETRY_DELAY_MS;
void applyConfig();
};
window.addEventListener(
AUTO_COMPACT_PREFERENCES_EVENT,
handler as EventListener,
);
void applyConfig();
return () => {
cancelled = true;
if (retryTimer !== null) {
window.clearTimeout(retryTimer);
}
clearRetryTimer();
window.removeEventListener(
AUTO_COMPACT_PREFERENCES_EVENT,
handler as EventListener,
);
};
}, [requestSyncFromConfig, syncFromConfig, syncVersion]);
}, [syncFromConfig]);
const dispatchPreferencesEvent = useCallback(() => {
window.dispatchEvent(new Event(AUTO_COMPACT_PREFERENCES_EVENT));
@@ -109,7 +120,7 @@ export function useAutoCompactPreferences() {
const setAutoCompactThreshold = useCallback(
async (value: number) => {
const normalized = normalizeAutoCompactThreshold(value);
await writeConfigValue(AUTO_COMPACT_THRESHOLD_CONFIG_KEY, normalized);
await writeAutoCompactThreshold(normalized);
setAutoCompactThresholdState(normalized);
setIsHydrated(true);
dispatchPreferencesEvent();
@@ -15,8 +15,6 @@ import {
setStoredModelPreference,
} from "../lib/modelPreferences";
const GOOSE_PROVIDER_CONFIG_KEY = "GOOSE_PROVIDER";
const GOOSE_MODEL_CONFIG_KEY = "GOOSE_MODEL";
const MODEL_ALIAS_IDS = new Set(["current", "default"]);
export type PreferredModelSelection = {
@@ -134,23 +132,14 @@ export function useResolvedAgentModelPicker({
const loadGooseDefaultSelection = async () => {
try {
const client = await getClient();
const [providerResponse, modelResponse] = await Promise.all([
client.goose.GooseConfigRead({ key: GOOSE_PROVIDER_CONFIG_KEY }),
client.goose.GooseConfigRead({ key: GOOSE_MODEL_CONFIG_KEY }),
]);
const defaults = await client.goose.GooseDefaultsRead({});
if (cancelled) {
return;
}
const providerId =
typeof providerResponse.value === "string"
? providerResponse.value
: undefined;
const modelId =
typeof modelResponse.value === "string"
? modelResponse.value
: undefined;
const providerId = defaults.providerId ?? undefined;
const modelId = defaults.modelId ?? undefined;
if (!modelId) {
setGooseDefaultSelection(null);
@@ -3,47 +3,64 @@ import { getClient } from "@/shared/api/acpConnection";
import {
DEFAULT_AUTO_SUBMIT_PHRASES_RAW,
DISABLED_DICTATION_PROVIDER_CONFIG_VALUE,
VOICE_AUTO_SUBMIT_PHRASES_CONFIG_KEY,
VOICE_DICTATION_PREFERRED_MIC_CONFIG_KEY,
VOICE_DICTATION_PROVIDER_CONFIG_KEY,
normalizeDictationProvider,
parseAutoSubmitPhrases,
} from "../lib/voiceInput";
import type { DictationProvider } from "@/shared/types/dictation";
const VOICE_INPUT_PREFERENCES_EVENT = "goose:voice-input-preferences";
const VOICE_AUTO_SUBMIT_PHRASES_PREFERENCE_KEY = "voiceAutoSubmitPhrases";
const VOICE_DICTATION_PROVIDER_PREFERENCE_KEY = "voiceDictationProvider";
const VOICE_DICTATION_PREFERRED_MIC_PREFERENCE_KEY =
"voiceDictationPreferredMic";
type VoicePreferenceKey =
| typeof VOICE_AUTO_SUBMIT_PHRASES_PREFERENCE_KEY
| typeof VOICE_DICTATION_PROVIDER_PREFERENCE_KEY
| typeof VOICE_DICTATION_PREFERRED_MIC_PREFERENCE_KEY;
type ConfigReadResult = { ok: true; value: string | null } | { ok: false };
async function readConfigString(key: string): Promise<ConfigReadResult> {
async function readPreferenceStrings(
keys: VoicePreferenceKey[],
): Promise<Record<VoicePreferenceKey, ConfigReadResult>> {
const unavailable = Object.fromEntries(
keys.map((key) => [key, { ok: false }]),
) as Record<VoicePreferenceKey, ConfigReadResult>;
try {
const client = await getClient();
const response = await client.goose.GooseConfigRead({ key });
return {
ok: true,
value: typeof response.value === "string" ? response.value : null,
};
const response = await client.goose.GoosePreferencesRead({ keys });
const values = new Map(
response.values.map((entry) => [entry.key, entry.value]),
);
return Object.fromEntries(
keys.map((key) => {
const value = values.get(key);
return [
key,
{
ok: true,
value: typeof value === "string" ? value : null,
},
];
}),
) as Record<VoicePreferenceKey, ConfigReadResult>;
} catch {
return { ok: false };
return unavailable;
}
}
async function writeConfigString(key: string, value: string): Promise<void> {
try {
const client = await getClient();
await client.goose.GooseConfigUpsert({ key, value });
} catch {
// goose config may be unavailable
}
async function writePreferenceString(
key: VoicePreferenceKey,
value: string,
): Promise<void> {
const client = await getClient();
await client.goose.GoosePreferencesSave({ values: [{ key, value }] });
}
async function removeConfigKey(key: string): Promise<void> {
try {
const client = await getClient();
await client.goose.GooseConfigRemove({ key });
} catch {
// goose config may be unavailable
}
async function removePreferenceKey(key: VoicePreferenceKey): Promise<void> {
const client = await getClient();
await client.goose.GoosePreferencesRemove({ keys: [key] });
}
export function useVoiceInputPreferences() {
@@ -65,11 +82,14 @@ export function useVoiceInputPreferences() {
const [isHydrated, setIsHydrated] = useState(false);
const syncFromConfig = useCallback(async () => {
const [phrasesResult, providerResult, micResult] = await Promise.all([
readConfigString(VOICE_AUTO_SUBMIT_PHRASES_CONFIG_KEY),
readConfigString(VOICE_DICTATION_PROVIDER_CONFIG_KEY),
readConfigString(VOICE_DICTATION_PREFERRED_MIC_CONFIG_KEY),
const results = await readPreferenceStrings([
VOICE_AUTO_SUBMIT_PHRASES_PREFERENCE_KEY,
VOICE_DICTATION_PROVIDER_PREFERENCE_KEY,
VOICE_DICTATION_PREFERRED_MIC_PREFERENCE_KEY,
]);
const phrasesResult = results[VOICE_AUTO_SUBMIT_PHRASES_PREFERENCE_KEY];
const providerResult = results[VOICE_DICTATION_PROVIDER_PREFERENCE_KEY];
const micResult = results[VOICE_DICTATION_PREFERRED_MIC_PREFERENCE_KEY];
if (phrasesResult.ok) {
setRawAutoSubmitPhrasesState(
@@ -99,7 +119,9 @@ export function useVoiceInputPreferences() {
// through to the default cleanly.
setSelectedProviderState(null);
setHasStoredProviderPreferenceState(false);
void removeConfigKey(VOICE_DICTATION_PROVIDER_CONFIG_KEY);
void removePreferenceKey(VOICE_DICTATION_PROVIDER_PREFERENCE_KEY).catch(
() => undefined,
);
}
} else {
setSelectedProviderState(null);
@@ -135,18 +157,23 @@ export function useVoiceInputPreferences() {
const persistAndBroadcast = useCallback(
(operation: Promise<void>) => {
void operation.finally(() => {
dispatchPreferencesEvent();
});
void operation
.then(() => {
dispatchPreferencesEvent();
})
.catch((error: unknown) => {
console.warn("Failed to persist voice input preferences", error);
void syncFromConfig();
});
},
[dispatchPreferencesEvent],
[dispatchPreferencesEvent, syncFromConfig],
);
const setRawAutoSubmitPhrases = useCallback(
(value: string) => {
setRawAutoSubmitPhrasesState(value);
persistAndBroadcast(
writeConfigString(VOICE_AUTO_SUBMIT_PHRASES_CONFIG_KEY, value),
writePreferenceString(VOICE_AUTO_SUBMIT_PHRASES_PREFERENCE_KEY, value),
);
},
[persistAndBroadcast],
@@ -157,8 +184,8 @@ export function useVoiceInputPreferences() {
setSelectedProviderState(value);
setHasStoredProviderPreferenceState(true);
persistAndBroadcast(
writeConfigString(
VOICE_DICTATION_PROVIDER_CONFIG_KEY,
writePreferenceString(
VOICE_DICTATION_PROVIDER_PREFERENCE_KEY,
value ?? DISABLED_DICTATION_PROVIDER_CONFIG_VALUE,
),
);
@@ -172,7 +199,9 @@ export function useVoiceInputPreferences() {
const clearSelectedProvider = useCallback(() => {
setSelectedProviderState(null);
setHasStoredProviderPreferenceState(false);
persistAndBroadcast(removeConfigKey(VOICE_DICTATION_PROVIDER_CONFIG_KEY));
persistAndBroadcast(
removePreferenceKey(VOICE_DICTATION_PROVIDER_PREFERENCE_KEY),
);
}, [persistAndBroadcast]);
const setPreferredMicrophoneId = useCallback(
@@ -180,11 +209,14 @@ export function useVoiceInputPreferences() {
setPreferredMicrophoneIdState(value);
if (value) {
persistAndBroadcast(
writeConfigString(VOICE_DICTATION_PREFERRED_MIC_CONFIG_KEY, value),
writePreferenceString(
VOICE_DICTATION_PREFERRED_MIC_PREFERENCE_KEY,
value,
),
);
} else {
persistAndBroadcast(
removeConfigKey(VOICE_DICTATION_PREFERRED_MIC_CONFIG_KEY),
removePreferenceKey(VOICE_DICTATION_PREFERRED_MIC_PREFERENCE_KEY),
);
}
},
@@ -3,8 +3,7 @@ import type {
DictationProviderStatus,
} from "@/shared/types/dictation";
// goose config keys — stored in the user's goose config.yaml via the
// _goose/config/{read,upsert,remove} ACP methods, not localStorage.
// Stored in the user's goose config.yaml via typed ACP preference methods, not localStorage.
export const VOICE_AUTO_SUBMIT_PHRASES_CONFIG_KEY = "VOICE_AUTO_SUBMIT_PHRASES";
export const VOICE_DICTATION_PROVIDER_CONFIG_KEY = "VOICE_DICTATION_PROVIDER";
export const VOICE_DICTATION_PREFERRED_MIC_CONFIG_KEY =
+12 -13
View File
@@ -3,7 +3,6 @@ import {
type AppRendererProps,
type McpUiHostContext,
} from "@mcp-ui/client";
import type { GooseToolCallResponse } from "@aaif/goose-sdk";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import packageJson from "../../../../package.json";
@@ -31,18 +30,13 @@ interface McpAppViewProps {
const DEFAULT_APP_HEIGHT = 240;
// Goose2 currently only implements inline display mode.
type HostContextDisplayMode = NonNullable<
const GOOSE2_DISPLAY_MODE = "inline" satisfies NonNullable<
McpUiHostContext["displayMode"]
>;
const AVAILABLE_DISPLAY_MODES: NonNullable<
McpUiHostContext["availableDisplayModes"]
>[number];
type AvailableDisplayMode = Extract<HostContextDisplayMode, "inline">;
const AVAILABLE_DISPLAY_MODES = [
"inline",
] satisfies readonly AvailableDisplayMode[];
> = [GOOSE2_DISPLAY_MODE];
const GOOSE2_USER_AGENT = `${packageJson.name}/${packageJson.version}`;
const GOOSE2_HOST_INFO = {
name: packageJson.name,
version: packageJson.version,
} satisfies NonNullable<AppRendererProps["hostInfo"]>;
const DESKTOP_SAFE_AREA_INSETS = {
top: 0,
right: 0,
@@ -59,6 +53,12 @@ type CallToolResult = Awaited<
type ReadResourceResult = Awaited<
ReturnType<NonNullable<AppRendererProps["onReadResource"]>>
>;
type GooseToolCallResponse = {
content?: unknown[];
structuredContent?: unknown;
isError: boolean;
_meta?: unknown;
};
type HostContextToolInfo = NonNullable<McpUiHostContext["toolInfo"]>;
type HostContextTool = HostContextToolInfo["tool"];
@@ -272,7 +272,7 @@ export function McpAppView({
const hostContext = useMemo<McpUiHostContext>(
() => ({
theme: resolvedTheme,
displayMode: "inline",
displayMode: GOOSE2_DISPLAY_MODE,
availableDisplayModes: [...AVAILABLE_DISPLAY_MODES],
containerDimensions:
containerWidth !== null
@@ -426,7 +426,6 @@ export function McpAppView({
toolResourceUri={renderableDocument.resourceUri}
html={renderableDocument.html}
sandbox={sandbox}
hostInfo={GOOSE2_HOST_INFO}
toolInput={currentToolInput}
toolResult={currentToolResult}
hostContext={hostContext}
@@ -260,7 +260,7 @@ describe("McpAppView nested tool calls", () => {
expect(borderlessChrome?.className).not.toContain("overflow-hidden");
});
it("passes Goose2 package identity as host info", async () => {
it("passes Goose2 package identity in host context", async () => {
render(
<McpAppView
payload={createPayload()}
@@ -272,10 +272,9 @@ describe("McpAppView nested tool calls", () => {
expect(screen.getByTestId("mock-app-renderer")).toBeInTheDocument();
});
expect(getLatestAppRendererProps().hostInfo).toEqual({
name: packageJson.name,
version: packageJson.version,
});
expect(getLatestAppRendererProps().hostContext?.userAgent).toBe(
`${packageJson.name}/${packageJson.version}`,
);
});
it("does not install a fallback handler for non-standard app requests", async () => {
@@ -79,6 +79,12 @@ describe("useCredentials", () => {
});
it("saves secret fields through the credential API and syncs inventory without requiring restart", async () => {
const voiceConfigListener = vi.fn();
window.addEventListener(
"goose:voice-dictation-config",
voiceConfigListener,
);
const { result } = renderHook(() => useCredentials());
await waitFor(() => expect(result.current.loading).toBe(false));
@@ -110,8 +116,14 @@ describe("useCredentials", () => {
initialRefresh: saveResponse.refresh,
}),
);
expect(voiceConfigListener).toHaveBeenCalledTimes(1);
expect(result.current).not.toHaveProperty("needsRestart");
expect(result.current).not.toHaveProperty("restart");
window.removeEventListener(
"goose:voice-dictation-config",
voiceConfigListener,
);
});
it("records refresh failure as a provider warning without rejecting the save", async () => {
@@ -140,6 +152,12 @@ describe("useCredentials", () => {
});
it("suppresses stale refresh errors after deleting provider config", async () => {
const voiceConfigListener = vi.fn();
window.addEventListener(
"goose:voice-dictation-config",
voiceConfigListener,
);
mocks.syncProviderInventory.mockResolvedValueOnce({
entries: [
{
@@ -162,7 +180,13 @@ describe("useCredentials", () => {
await waitFor(() =>
expect(result.current.syncingProviderIds.has("anthropic")).toBe(false),
);
expect(voiceConfigListener).toHaveBeenCalledTimes(1);
expect(result.current.inventoryWarnings.has("anthropic")).toBe(false);
window.removeEventListener(
"goose:voice-dictation-config",
voiceConfigListener,
);
});
it("invalidates native OAuth secrets before refreshing provider status", async () => {
@@ -6,6 +6,7 @@ import {
type ProviderStatus,
checkAllProviderStatus,
} from "@/features/providers/api/credentials";
import { notifyVoiceDictationConfigChanged } from "@/features/chat/lib/voiceInput";
import {
syncProviderInventory,
type SyncProviderInventoryResult,
@@ -208,6 +209,7 @@ export function useCredentials(): UseCredentialsReturn {
fields.map(({ key, value }) => ({ key, value })),
);
updateProviderStatus(result.status);
notifyVoiceDictationConfigChanged();
startInventorySync(providerId, result.refresh);
} finally {
setProviderSaving(providerId, false);
@@ -222,6 +224,7 @@ export function useCredentials(): UseCredentialsReturn {
try {
const result = await deleteProviderConfig(providerId);
updateProviderStatus(result.status);
notifyVoiceDictationConfigChanged();
startInventorySync(providerId, result.refresh);
} finally {
setProviderSaving(providerId, false);
@@ -241,6 +244,7 @@ export function useCredentials(): UseCredentialsReturn {
setProviderInventoryWarning(providerId, errorMessage(error));
}
await refreshStatuses();
notifyVoiceDictationConfigChanged();
startInventorySync(providerId, initialRefresh);
},
[refreshStatuses, setProviderInventoryWarning, startInventorySync],
@@ -58,6 +58,10 @@ export function SettingsModal({
return () => clearTimeout(timer);
}, []);
useEffect(() => {
setActiveSection(initialSection);
}, [initialSection]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
@@ -11,6 +11,7 @@ import {
getDefaultDictationProvider,
} from "@/features/chat/lib/voiceInput";
import { useVoiceInputPreferences } from "@/features/chat/hooks/useVoiceInputPreferences";
import { requestOpenSettings } from "@/features/settings/lib/settingsEvents";
import type {
DictationProvider,
DictationProviderStatus,
@@ -145,11 +146,7 @@ export function VoiceInputSettings() {
setError(null);
try {
await saveDictationProviderSecret(
selectedProvider,
apiKeyInput,
selectedStatus?.configKey ?? undefined,
);
await saveDictationProviderSecret(selectedProvider, apiKeyInput);
setApiKeyInput("");
setIsEditingApiKey(false);
await refreshConfig();
@@ -161,7 +158,7 @@ export function VoiceInputSettings() {
: t("general.voiceInput.saveError"),
);
}
}, [apiKeyInput, refreshConfig, selectedProvider, selectedStatus, t]);
}, [apiKeyInput, refreshConfig, selectedProvider, t]);
const removeApiKey = useCallback(async () => {
if (!selectedProvider) {
@@ -170,10 +167,7 @@ export function VoiceInputSettings() {
setError(null);
try {
await deleteDictationProviderSecret(
selectedProvider,
selectedStatus?.configKey ?? undefined,
);
await deleteDictationProviderSecret(selectedProvider);
setApiKeyInput("");
setIsEditingApiKey(false);
await refreshConfig();
@@ -185,7 +179,7 @@ export function VoiceInputSettings() {
: t("general.voiceInput.deleteError"),
);
}
}, [refreshConfig, selectedProvider, selectedStatus, t]);
}, [refreshConfig, selectedProvider, t]);
const handleModelChange = useCallback(
async (modelId: string) => {
@@ -333,6 +327,27 @@ export function VoiceInputSettings() {
{selectedStatus ? (
<>
{selectedStatus.usesProviderConfig ? (
<div className="space-y-3 rounded-lg border border-border px-3 py-3">
<div>
<p className="text-xs font-medium text-foreground">
{t("general.voiceInput.providerConfigLabel")}
</p>
<p className="mt-1 text-xs text-muted-foreground">
{t("general.voiceInput.providerConfigDescription")}
</p>
</div>
<Button
type="button"
size="sm"
variant="outline-flat"
onClick={() => requestOpenSettings("providers")}
>
{t("general.voiceInput.openProviders")}
</Button>
</div>
) : null}
{!selectedStatus.usesProviderConfig &&
selectedProvider !== "local" ? (
<div className="space-y-3 rounded-lg border border-border px-3 py-3">
@@ -0,0 +1,98 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { VoiceInputSettings } from "../VoiceInputSettings";
const mockGetDictationConfig = vi.fn();
const mockUseVoiceInputPreferences = vi.fn();
vi.mock("@/shared/api/dictation", () => ({
getDictationConfig: () => mockGetDictationConfig(),
saveDictationModelSelection: vi.fn(),
saveDictationProviderSecret: vi.fn(),
deleteDictationProviderSecret: vi.fn(),
}));
vi.mock("@/features/chat/hooks/useVoiceInputPreferences", () => ({
useVoiceInputPreferences: () => mockUseVoiceInputPreferences(),
}));
vi.mock("@/shared/ui/ai-elements/mic-selector", () => ({
useAudioDevices: () => ({
devices: [],
error: null,
hasPermission: false,
loadDevices: vi.fn(),
loading: false,
}),
}));
vi.mock("../LocalWhisperModels", () => ({
LocalWhisperModels: () => <div />,
}));
describe("VoiceInputSettings", () => {
beforeEach(() => {
mockGetDictationConfig.mockReset();
mockUseVoiceInputPreferences.mockReset();
mockUseVoiceInputPreferences.mockReturnValue({
clearSelectedProvider: vi.fn(),
hasStoredProviderPreference: true,
isHydrated: true,
preferredMicrophoneId: null,
rawAutoSubmitPhrases: "submit",
selectedProvider: "openai",
setPreferredMicrophoneId: vi.fn(),
setRawAutoSubmitPhrases: vi.fn(),
setSelectedProvider: vi.fn(),
});
mockGetDictationConfig.mockResolvedValue({
openai: {
configured: false,
description: "Uses OpenAI Whisper API for high-quality transcription.",
usesProviderConfig: true,
settingsPath: "Settings > Models",
configKey: null,
modelConfigKey: "OPENAI_TRANSCRIPTION_MODEL",
defaultModel: "whisper-1",
selectedModel: null,
availableModels: [
{
id: "whisper-1",
label: "Whisper-1",
description: "OpenAI's hosted Whisper transcription model.",
},
],
},
});
});
it("points OpenAI Whisper setup to provider settings", async () => {
const user = userEvent.setup();
const openSettingsListener = vi.fn();
window.addEventListener("goose:open-settings", openSettingsListener);
render(<VoiceInputSettings />);
await waitFor(() =>
expect(screen.getByText("Provider credentials")).toBeInTheDocument(),
);
expect(
screen.getByText(
"This transcription provider uses the credentials from its model provider setup.",
),
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Add API key" }),
).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Open Providers" }));
expect(openSettingsListener).toHaveBeenCalledTimes(1);
expect(openSettingsListener.mock.calls[0][0]).toMatchObject({
detail: { section: "providers" },
});
window.removeEventListener("goose:open-settings", openSettingsListener);
});
});
@@ -1,161 +0,0 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import {
cancelDictationLocalModelDownload,
deleteDictationLocalModel,
deleteDictationProviderSecret,
downloadDictationLocalModel,
getDictationConfig,
getDictationLocalModelDownloadProgress,
listDictationLocalModels,
saveDictationModelSelection,
saveDictationProviderSecret,
transcribeDictation,
} from "../dictation";
import { getClient } from "../acpConnection";
vi.mock("../acpConnection", () => ({
getClient: vi.fn(),
}));
describe("dictation SDK wiring", () => {
let client: { goose: Record<string, ReturnType<typeof vi.fn>> };
beforeEach(() => {
client = {
goose: {
GooseDictationConfig: vi.fn().mockResolvedValue({
providers: {
openai: {
configured: true,
description: "OpenAI transcription",
usesProviderConfig: true,
availableModels: [],
},
},
}),
GooseDictationTranscribe: vi.fn().mockResolvedValue({ text: "hello" }),
},
};
vi.mocked(getClient).mockResolvedValue(
client as unknown as Awaited<ReturnType<typeof getClient>>,
);
});
it("getDictationConfig calls GooseDictationConfig and returns providers map", async () => {
const result = await getDictationConfig();
expect(client.goose.GooseDictationConfig).toHaveBeenCalledWith({});
expect(result.openai.configured).toBe(true);
});
it("transcribeDictation forwards audio + mimeType + provider", async () => {
const result = await transcribeDictation({
audio: "base64==",
mimeType: "audio/webm",
provider: "openai",
});
expect(client.goose.GooseDictationTranscribe).toHaveBeenCalledWith({
audio: "base64==",
mimeType: "audio/webm",
provider: "openai",
});
expect(result.text).toBe("hello");
});
it("saveDictationModelSelection calls GooseDictationModelSelect", async () => {
client.goose.GooseDictationModelSelect = vi.fn().mockResolvedValue({});
await saveDictationModelSelection("local", "tiny");
expect(client.goose.GooseDictationModelSelect).toHaveBeenCalledWith({
provider: "local",
modelId: "tiny",
});
});
it("saveDictationProviderSecret calls GooseSecretUpsert", async () => {
client.goose.GooseSecretUpsert = vi.fn().mockResolvedValue({});
await saveDictationProviderSecret("groq", "gsk-test", "GROQ_API_KEY");
expect(client.goose.GooseSecretUpsert).toHaveBeenCalledWith({
key: "GROQ_API_KEY",
value: "gsk-test",
});
});
it("deleteDictationProviderSecret calls GooseSecretRemove", async () => {
client.goose.GooseSecretRemove = vi.fn().mockResolvedValue({});
await deleteDictationProviderSecret("groq", "GROQ_API_KEY");
expect(client.goose.GooseSecretRemove).toHaveBeenCalledWith({
key: "GROQ_API_KEY",
});
});
it("listDictationLocalModels returns the models array", async () => {
client.goose.GooseDictationModelsList = vi.fn().mockResolvedValue({
models: [
{
id: "tiny",
description: "Tiny",
sizeMb: 75,
downloaded: true,
downloadInProgress: false,
},
],
});
const result = await listDictationLocalModels();
expect(client.goose.GooseDictationModelsList).toHaveBeenCalledWith({});
expect(result).toHaveLength(1);
expect(result[0].id).toBe("tiny");
});
it("downloadDictationLocalModel forwards modelId", async () => {
client.goose.GooseDictationModelsDownload = vi.fn().mockResolvedValue({});
await downloadDictationLocalModel("tiny");
expect(client.goose.GooseDictationModelsDownload).toHaveBeenCalledWith({
modelId: "tiny",
});
});
it("getDictationLocalModelDownloadProgress returns progress or null", async () => {
client.goose.GooseDictationModelsDownloadProgress = vi
.fn()
.mockResolvedValue({
progress: {
bytesDownloaded: 100,
totalBytes: 1000,
progressPercent: 10,
status: "downloading",
error: null,
},
});
const result = await getDictationLocalModelDownloadProgress("tiny");
expect(result?.bytesDownloaded).toBe(100);
expect(
client.goose.GooseDictationModelsDownloadProgress,
).toHaveBeenCalledWith({
modelId: "tiny",
});
});
it("getDictationLocalModelDownloadProgress returns null when no download", async () => {
client.goose.GooseDictationModelsDownloadProgress = vi
.fn()
.mockResolvedValue({
progress: undefined,
});
const result = await getDictationLocalModelDownloadProgress("tiny");
expect(result).toBeNull();
});
it("cancelDictationLocalModelDownload forwards modelId", async () => {
client.goose.GooseDictationModelsCancel = vi.fn().mockResolvedValue({});
await cancelDictationLocalModelDownload("tiny");
expect(client.goose.GooseDictationModelsCancel).toHaveBeenCalledWith({
modelId: "tiny",
});
});
it("deleteDictationLocalModel forwards modelId", async () => {
client.goose.GooseDictationModelsDelete = vi.fn().mockResolvedValue({});
await deleteDictationLocalModel("tiny");
expect(client.goose.GooseDictationModelsDelete).toHaveBeenCalledWith({
modelId: "tiny",
});
});
});
+4 -12
View File
@@ -40,26 +40,18 @@ export async function saveDictationModelSelection(
}
export async function saveDictationProviderSecret(
_provider: DictationProvider,
provider: DictationProvider,
value: string,
configKey?: string,
): Promise<void> {
if (!configKey) {
throw new Error("No config key for this provider");
}
const client = await getClient();
await client.goose.GooseSecretUpsert({ key: configKey, value });
await client.goose.GooseDictationSecretSave({ provider, value });
}
export async function deleteDictationProviderSecret(
_provider: DictationProvider,
configKey?: string,
provider: DictationProvider,
): Promise<void> {
if (!configKey) {
throw new Error("Cannot delete secrets for this provider");
}
const client = await getClient();
await client.goose.GooseSecretRemove({ key: configKey });
await client.goose.GooseDictationSecretDelete({ provider });
}
export async function listDictationLocalModels(): Promise<
@@ -176,6 +176,9 @@
"addApiKey": "Add API key",
"updateApiKey": "Update API key",
"removeApiKey": "Remove API key",
"providerConfigLabel": "Provider credentials",
"providerConfigDescription": "This transcription provider uses the credentials from its model provider setup.",
"openProviders": "Open Providers",
"localModelLabel": "Local Whisper Model",
"localModelDescription": "Download a Whisper model to run transcription locally. Selecting a model sets it as your active local transcription model.",
"noLocalModels": "No local Whisper models available.",
@@ -176,6 +176,9 @@
"addApiKey": "Agregar clave API",
"updateApiKey": "Actualizar clave API",
"removeApiKey": "Eliminar clave API",
"providerConfigLabel": "Credenciales del proveedor",
"providerConfigDescription": "Este proveedor de transcripción usa las credenciales de la configuración de su proveedor de modelos.",
"openProviders": "Abrir proveedores",
"localModelLabel": "Modelo Whisper local",
"localModelDescription": "Descarga un modelo Whisper para transcribir localmente. Seleccionar un modelo lo establece como tu modelo de transcripción local activo.",
"noLocalModels": "No hay modelos Whisper locales disponibles.",
+38 -32
View File
@@ -11,8 +11,6 @@ import type {
AddConfigExtensionRequest,
AddExtensionRequest,
ArchiveSessionRequest,
CheckSecretRequest,
CheckSecretResponse,
CreateSourceRequest,
CreateSourceResponse,
CustomProviderCreateRequest,
@@ -23,6 +21,8 @@ import type {
CustomProviderReadResponse,
CustomProviderUpdateRequest,
CustomProviderUpdateResponse,
DefaultsReadRequest,
DefaultsReadResponse,
DeleteSessionRequest,
DeleteSourceRequest,
DictationConfigRequest,
@@ -35,6 +35,8 @@ import type {
DictationModelSelectRequest,
DictationModelsListRequest,
DictationModelsListResponse,
DictationSecretDeleteRequest,
DictationSecretSaveRequest,
DictationTranscribeRequest,
DictationTranscribeResponse,
ExportSessionRequest,
@@ -57,6 +59,10 @@ import type {
ListProvidersResponse,
ListSourcesRequest,
ListSourcesResponse,
PreferencesReadRequest,
PreferencesReadResponse,
PreferencesRemoveRequest,
PreferencesSaveRequest,
ProviderCatalogListRequest,
ProviderCatalogListResponse,
ProviderCatalogTemplateRequest,
@@ -68,16 +74,12 @@ import type {
ProviderConfigSaveRequest,
ProviderConfigStatusRequest,
ProviderConfigStatusResponse,
ReadConfigRequest,
ReadConfigResponse,
ReadResourceRequest,
ReadResourceResponse,
RefreshProviderInventoryRequest,
RefreshProviderInventoryResponse,
RemoveConfigExtensionRequest,
RemoveConfigRequest,
RemoveExtensionRequest,
RemoveSecretRequest,
RenameSessionRequest,
ToggleConfigExtensionRequest,
UnarchiveSessionRequest,
@@ -85,16 +87,14 @@ import type {
UpdateSourceRequest,
UpdateSourceResponse,
UpdateWorkingDirRequest,
UpsertConfigRequest,
UpsertSecretRequest,
} from './types.gen.js';
import {
zCheckSecretResponse,
zCreateSourceResponse,
zCustomProviderCreateResponse,
zCustomProviderDeleteResponse,
zCustomProviderReadResponse,
zCustomProviderUpdateResponse,
zDefaultsReadResponse,
zDictationConfigResponse,
zDictationModelDownloadProgressResponse,
zDictationModelsListResponse,
@@ -109,12 +109,12 @@ import {
zImportSourcesResponse,
zListProvidersResponse,
zListSourcesResponse,
zPreferencesReadResponse,
zProviderCatalogListResponse,
zProviderCatalogTemplateResponse,
zProviderConfigChangeResponse,
zProviderConfigReadResponse,
zProviderConfigStatusResponse,
zReadConfigResponse,
zReadResourceResponse,
zRefreshProviderInventoryResponse,
zUpdateSourceResponse,
@@ -327,34 +327,28 @@ export class GooseExtClient {
) as ProviderConfigChangeResponse;
}
async GooseConfigRead(
params: ReadConfigRequest,
): Promise<ReadConfigResponse> {
const raw = await this.conn.extMethod("_goose/config/read", params);
return zReadConfigResponse.parse(raw) as ReadConfigResponse;
async GoosePreferencesRead(
params: PreferencesReadRequest,
): Promise<PreferencesReadResponse> {
const raw = await this.conn.extMethod("_goose/preferences/read", params);
return zPreferencesReadResponse.parse(raw) as PreferencesReadResponse;
}
async GooseConfigUpsert(params: UpsertConfigRequest): Promise<void> {
await this.conn.extMethod("_goose/config/upsert", params);
async GoosePreferencesSave(params: PreferencesSaveRequest): Promise<void> {
await this.conn.extMethod("_goose/preferences/save", params);
}
async GooseConfigRemove(params: RemoveConfigRequest): Promise<void> {
await this.conn.extMethod("_goose/config/remove", params);
async GoosePreferencesRemove(
params: PreferencesRemoveRequest,
): Promise<void> {
await this.conn.extMethod("_goose/preferences/remove", params);
}
async GooseSecretCheck(
params: CheckSecretRequest,
): Promise<CheckSecretResponse> {
const raw = await this.conn.extMethod("_goose/secret/check", params);
return zCheckSecretResponse.parse(raw) as CheckSecretResponse;
}
async GooseSecretUpsert(params: UpsertSecretRequest): Promise<void> {
await this.conn.extMethod("_goose/secret/upsert", params);
}
async GooseSecretRemove(params: RemoveSecretRequest): Promise<void> {
await this.conn.extMethod("_goose/secret/remove", params);
async GooseDefaultsRead(
params: DefaultsReadRequest,
): Promise<DefaultsReadResponse> {
const raw = await this.conn.extMethod("_goose/defaults/read", params);
return zDefaultsReadResponse.parse(raw) as DefaultsReadResponse;
}
async GooseSessionExport(
@@ -447,6 +441,18 @@ export class GooseExtClient {
return zDictationConfigResponse.parse(raw) as DictationConfigResponse;
}
async GooseDictationSecretSave(
params: DictationSecretSaveRequest,
): Promise<void> {
await this.conn.extMethod("_goose/dictation/secret/save", params);
}
async GooseDictationSecretDelete(
params: DictationSecretDeleteRequest,
): Promise<void> {
await this.conn.extMethod("_goose/dictation/secret/delete", params);
}
async GooseDictationModelsList(
params: DictationModelsListRequest,
): Promise<DictationModelsListResponse> {
+21 -21
View File
@@ -1,6 +1,6 @@
// This file is auto-generated by @hey-api/openapi-ts
export type { AddConfigExtensionRequest, AddExtensionRequest, ArchiveSessionRequest, CheckSecretRequest, CheckSecretResponse, CreateSourceRequest, CreateSourceResponse, CustomProviderConfigDto, CustomProviderCreateRequest, CustomProviderCreateResponse, CustomProviderDeleteRequest, CustomProviderDeleteResponse, CustomProviderReadRequest, CustomProviderReadResponse, CustomProviderUpdateRequest, CustomProviderUpdateResponse, DeleteSessionRequest, DeleteSourceRequest, DictationConfigRequest, DictationConfigResponse, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest, DictationModelDeleteRequest, DictationModelDownloadProgressRequest, DictationModelDownloadProgressResponse, DictationModelDownloadRequest, DictationModelOption, DictationModelSelectRequest, DictationModelsListRequest, DictationModelsListResponse, DictationProviderStatusEntry, DictationTranscribeRequest, DictationTranscribeResponse, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExportSourceRequest, ExportSourceResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetSessionExtensionsRequest, GetSessionExtensionsResponse, GetToolsRequest, GetToolsResponse, GooseToolCallRequest, GooseToolCallResponse, ImportSessionRequest, ImportSessionResponse, ImportSourcesRequest, ImportSourcesResponse, ListProvidersRequest, ListProvidersResponse, ListSourcesRequest, ListSourcesResponse, ProviderCatalogEntryDto, ProviderCatalogListRequest, ProviderCatalogListResponse, ProviderCatalogTemplateRequest, ProviderCatalogTemplateResponse, ProviderConfigChangeResponse, ProviderConfigDeleteRequest, ProviderConfigFieldUpdate, ProviderConfigFieldValueDto, ProviderConfigKey, ProviderConfigReadRequest, ProviderConfigReadResponse, ProviderConfigSaveRequest, ProviderConfigStatusDto, ProviderConfigStatusRequest, ProviderConfigStatusResponse, ProviderInventoryEntryDto, ProviderInventoryModelDto, ProviderTemplateCapabilitiesDto, ProviderTemplateDto, ProviderTemplateModelDto, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, ReadResourceResponse, RefreshProviderInventoryRequest, RefreshProviderInventoryResponse, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest, RemoveConfigRequest, RemoveExtensionRequest, RemoveSecretRequest, RenameSessionRequest, SourceEntry, SourceType, ToggleConfigExtensionRequest, UnarchiveSessionRequest, UpdateSessionProjectRequest, UpdateSourceRequest, UpdateSourceResponse, UpdateWorkingDirRequest, UpsertConfigRequest, UpsertSecretRequest } from './types.gen.js';
export type { AddConfigExtensionRequest, AddExtensionRequest, ArchiveSessionRequest, CreateSourceRequest, CreateSourceResponse, CustomProviderConfigDto, CustomProviderCreateRequest, CustomProviderCreateResponse, CustomProviderDeleteRequest, CustomProviderDeleteResponse, CustomProviderReadRequest, CustomProviderReadResponse, CustomProviderUpdateRequest, CustomProviderUpdateResponse, DefaultsReadRequest, DefaultsReadResponse, DeleteSessionRequest, DeleteSourceRequest, DictationConfigRequest, DictationConfigResponse, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest, DictationModelDeleteRequest, DictationModelDownloadProgressRequest, DictationModelDownloadProgressResponse, DictationModelDownloadRequest, DictationModelOption, DictationModelSelectRequest, DictationModelsListRequest, DictationModelsListResponse, DictationProviderStatusEntry, DictationSecretDeleteRequest, DictationSecretSaveRequest, DictationTranscribeRequest, DictationTranscribeResponse, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExportSourceRequest, ExportSourceResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetSessionExtensionsRequest, GetSessionExtensionsResponse, GetToolsRequest, GetToolsResponse, GooseToolCallRequest, GooseToolCallResponse, ImportSessionRequest, ImportSessionResponse, ImportSourcesRequest, ImportSourcesResponse, ListProvidersRequest, ListProvidersResponse, ListSourcesRequest, ListSourcesResponse, PreferenceKey, PreferencesReadRequest, PreferencesReadResponse, PreferencesRemoveRequest, PreferencesSaveRequest, PreferenceValue, ProviderCatalogEntryDto, ProviderCatalogListRequest, ProviderCatalogListResponse, ProviderCatalogTemplateRequest, ProviderCatalogTemplateResponse, ProviderConfigChangeResponse, ProviderConfigDeleteRequest, ProviderConfigFieldUpdate, ProviderConfigFieldValueDto, ProviderConfigKey, ProviderConfigReadRequest, ProviderConfigReadResponse, ProviderConfigSaveRequest, ProviderConfigStatusDto, ProviderConfigStatusRequest, ProviderConfigStatusResponse, ProviderInventoryEntryDto, ProviderInventoryModelDto, ProviderTemplateCapabilitiesDto, ProviderTemplateDto, ProviderTemplateModelDto, ReadResourceRequest, ReadResourceResponse, RefreshProviderInventoryRequest, RefreshProviderInventoryResponse, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest, RemoveExtensionRequest, RenameSessionRequest, SourceEntry, SourceType, ToggleConfigExtensionRequest, UnarchiveSessionRequest, UpdateSessionProjectRequest, UpdateSourceRequest, UpdateSourceResponse, UpdateWorkingDirRequest } from './types.gen.js';
export const GOOSE_EXT_METHODS = [
{
@@ -124,34 +124,24 @@ export const GOOSE_EXT_METHODS = [
responseType: "ProviderConfigChangeResponse",
},
{
method: "_goose/config/read",
requestType: "ReadConfigRequest",
responseType: "ReadConfigResponse",
method: "_goose/preferences/read",
requestType: "PreferencesReadRequest",
responseType: "PreferencesReadResponse",
},
{
method: "_goose/config/upsert",
requestType: "UpsertConfigRequest",
method: "_goose/preferences/save",
requestType: "PreferencesSaveRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/config/remove",
requestType: "RemoveConfigRequest",
method: "_goose/preferences/remove",
requestType: "PreferencesRemoveRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/secret/check",
requestType: "CheckSecretRequest",
responseType: "CheckSecretResponse",
},
{
method: "_goose/secret/upsert",
requestType: "UpsertSecretRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/secret/remove",
requestType: "RemoveSecretRequest",
responseType: "EmptyResponse",
method: "_goose/defaults/read",
requestType: "DefaultsReadRequest",
responseType: "DefaultsReadResponse",
},
{
method: "_goose/session/export",
@@ -223,6 +213,16 @@ export const GOOSE_EXT_METHODS = [
requestType: "DictationConfigRequest",
responseType: "DictationConfigResponse",
},
{
method: "_goose/dictation/secret/save",
requestType: "DictationSecretSaveRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/dictation/secret/delete",
requestType: "DictationSecretDeleteRequest",
responseType: "EmptyResponse",
},
{
method: "_goose/dictation/models/list",
requestType: "DictationModelsListRequest",
+40 -39
View File
@@ -520,61 +520,47 @@ export type ProviderConfigDeleteRequest = {
};
/**
* Read a single non-secret config value.
* Read allowlisted user preferences. Empty `keys` means all supported preferences.
*/
export type ReadConfigRequest = {
key: string;
export type PreferencesReadRequest = {
keys?: Array<PreferenceKey>;
};
/**
* Config read response.
*/
export type ReadConfigResponse = {
export type PreferenceKey = 'autoCompactThreshold' | 'voiceAutoSubmitPhrases' | 'voiceDictationProvider' | 'voiceDictationPreferredMic';
export type PreferencesReadResponse = {
values: Array<PreferenceValue>;
};
export type PreferenceValue = {
key: PreferenceKey;
value?: unknown;
};
/**
* Upsert a single non-secret config value.
* Save allowlisted user preferences.
*/
export type UpsertConfigRequest = {
key: string;
value: unknown;
export type PreferencesSaveRequest = {
values?: Array<PreferenceValue>;
};
/**
* Remove a single non-secret config value.
* Remove allowlisted user preferences.
*/
export type RemoveConfigRequest = {
key: string;
export type PreferencesRemoveRequest = {
keys?: Array<PreferenceKey>;
};
/**
* Check whether a secret exists. Never returns the actual value.
* Read Goose default provider and model configuration.
*/
export type CheckSecretRequest = {
key: string;
export type DefaultsReadRequest = {
[key: string]: unknown;
};
/**
* Secret check response.
*/
export type CheckSecretResponse = {
exists: boolean;
};
/**
* Set a secret value (write-only).
*/
export type UpsertSecretRequest = {
key: string;
value: unknown;
};
/**
* Remove a secret.
*/
export type RemoveSecretRequest = {
key: string;
export type DefaultsReadResponse = {
providerId?: string | null;
modelId?: string | null;
};
/**
@@ -818,6 +804,21 @@ export type DictationModelOption = {
description: string;
};
/**
* Set a dictation provider secret value.
*/
export type DictationSecretSaveRequest = {
provider: string;
value: string;
};
/**
* Remove a dictation provider secret value.
*/
export type DictationSecretDeleteRequest = {
provider: string;
};
/**
* List available local Whisper models with their download status.
*/
@@ -895,14 +896,14 @@ export type DictationModelSelectRequest = {
export type ExtRequest = {
id: string;
method: string;
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | GooseToolCallRequest | ReadResourceRequest | UpdateWorkingDirRequest | DeleteSessionRequest | GetExtensionsRequest | AddConfigExtensionRequest | RemoveConfigExtensionRequest | ToggleConfigExtensionRequest | GetSessionExtensionsRequest | ListProvidersRequest | ProviderCatalogListRequest | ProviderCatalogTemplateRequest | CustomProviderCreateRequest | CustomProviderReadRequest | CustomProviderUpdateRequest | CustomProviderDeleteRequest | RefreshProviderInventoryRequest | ProviderConfigReadRequest | ProviderConfigStatusRequest | ProviderConfigSaveRequest | ProviderConfigDeleteRequest | ReadConfigRequest | UpsertConfigRequest | RemoveConfigRequest | CheckSecretRequest | UpsertSecretRequest | RemoveSecretRequest | ExportSessionRequest | ImportSessionRequest | UpdateSessionProjectRequest | RenameSessionRequest | ArchiveSessionRequest | UnarchiveSessionRequest | CreateSourceRequest | ListSourcesRequest | UpdateSourceRequest | DeleteSourceRequest | ExportSourceRequest | ImportSourcesRequest | DictationTranscribeRequest | DictationConfigRequest | DictationModelsListRequest | DictationModelDownloadRequest | DictationModelDownloadProgressRequest | DictationModelCancelRequest | DictationModelDeleteRequest | DictationModelSelectRequest | {
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | GooseToolCallRequest | ReadResourceRequest | UpdateWorkingDirRequest | DeleteSessionRequest | GetExtensionsRequest | AddConfigExtensionRequest | RemoveConfigExtensionRequest | ToggleConfigExtensionRequest | GetSessionExtensionsRequest | ListProvidersRequest | ProviderCatalogListRequest | ProviderCatalogTemplateRequest | CustomProviderCreateRequest | CustomProviderReadRequest | CustomProviderUpdateRequest | CustomProviderDeleteRequest | RefreshProviderInventoryRequest | ProviderConfigReadRequest | ProviderConfigStatusRequest | ProviderConfigSaveRequest | ProviderConfigDeleteRequest | PreferencesReadRequest | PreferencesSaveRequest | PreferencesRemoveRequest | DefaultsReadRequest | ExportSessionRequest | ImportSessionRequest | UpdateSessionProjectRequest | RenameSessionRequest | ArchiveSessionRequest | UnarchiveSessionRequest | CreateSourceRequest | ListSourcesRequest | UpdateSourceRequest | DeleteSourceRequest | ExportSourceRequest | ImportSourcesRequest | DictationTranscribeRequest | DictationConfigRequest | DictationSecretSaveRequest | DictationSecretDeleteRequest | DictationModelsListRequest | DictationModelDownloadRequest | DictationModelDownloadProgressRequest | DictationModelCancelRequest | DictationModelDeleteRequest | DictationModelSelectRequest | {
[key: string]: unknown;
} | null;
};
export type ExtResponse = {
id: string;
result?: EmptyResponse | GetToolsResponse | GooseToolCallResponse | ReadResourceResponse | GetExtensionsResponse | GetSessionExtensionsResponse | ListProvidersResponse | ProviderCatalogListResponse | ProviderCatalogTemplateResponse | CustomProviderCreateResponse | CustomProviderReadResponse | CustomProviderUpdateResponse | CustomProviderDeleteResponse | RefreshProviderInventoryResponse | ProviderConfigReadResponse | ProviderConfigStatusResponse | ProviderConfigChangeResponse | ReadConfigResponse | CheckSecretResponse | ExportSessionResponse | ImportSessionResponse | CreateSourceResponse | ListSourcesResponse | UpdateSourceResponse | ExportSourceResponse | ImportSourcesResponse | DictationTranscribeResponse | DictationConfigResponse | DictationModelsListResponse | DictationModelDownloadProgressResponse | unknown;
result?: EmptyResponse | GetToolsResponse | GooseToolCallResponse | ReadResourceResponse | GetExtensionsResponse | GetSessionExtensionsResponse | ListProvidersResponse | ProviderCatalogListResponse | ProviderCatalogTemplateResponse | CustomProviderCreateResponse | CustomProviderReadResponse | CustomProviderUpdateResponse | CustomProviderDeleteResponse | RefreshProviderInventoryResponse | ProviderConfigReadResponse | ProviderConfigStatusResponse | ProviderConfigChangeResponse | PreferencesReadResponse | DefaultsReadResponse | ExportSessionResponse | ImportSessionResponse | CreateSourceResponse | ListSourcesResponse | UpdateSourceResponse | ExportSourceResponse | ImportSourcesResponse | DictationTranscribeResponse | DictationConfigResponse | DictationModelsListResponse | DictationModelDownloadProgressResponse | unknown;
} | {
error: {
code: number;
+55 -45
View File
@@ -486,62 +486,57 @@ export const zProviderConfigDeleteRequest = z.object({
providerId: z.string()
});
/**
* Read a single non-secret config value.
*/
export const zReadConfigRequest = z.object({
key: z.string()
});
export const zPreferenceKey = z.enum([
'autoCompactThreshold',
'voiceAutoSubmitPhrases',
'voiceDictationProvider',
'voiceDictationPreferredMic'
]);
/**
* Config read response.
* Read allowlisted user preferences. Empty `keys` means all supported preferences.
*/
export const zReadConfigResponse = z.object({
export const zPreferencesReadRequest = z.object({
keys: z.array(zPreferenceKey).optional().default([])
});
export const zPreferenceValue = z.object({
key: zPreferenceKey,
value: z.unknown().optional().default(null)
});
/**
* Upsert a single non-secret config value.
*/
export const zUpsertConfigRequest = z.object({
key: z.string(),
value: z.unknown()
export const zPreferencesReadResponse = z.object({
values: z.array(zPreferenceValue)
});
/**
* Remove a single non-secret config value.
* Save allowlisted user preferences.
*/
export const zRemoveConfigRequest = z.object({
key: z.string()
export const zPreferencesSaveRequest = z.object({
values: z.array(zPreferenceValue).optional().default([])
});
/**
* Check whether a secret exists. Never returns the actual value.
* Remove allowlisted user preferences.
*/
export const zCheckSecretRequest = z.object({
key: z.string()
export const zPreferencesRemoveRequest = z.object({
keys: z.array(zPreferenceKey).optional().default([])
});
/**
* Secret check response.
* Read Goose default provider and model configuration.
*/
export const zCheckSecretResponse = z.object({
exists: z.boolean()
});
export const zDefaultsReadRequest = z.record(z.unknown());
/**
* Set a secret value (write-only).
*/
export const zUpsertSecretRequest = z.object({
key: z.string(),
value: z.unknown()
});
/**
* Remove a secret.
*/
export const zRemoveSecretRequest = z.object({
key: z.string()
export const zDefaultsReadResponse = z.object({
providerId: z.union([
z.string(),
z.null()
]).optional(),
modelId: z.union([
z.string(),
z.null()
]).optional()
});
/**
@@ -802,6 +797,21 @@ export const zDictationConfigResponse = z.object({
providers: z.record(zDictationProviderStatusEntry)
});
/**
* Set a dictation provider secret value.
*/
export const zDictationSecretSaveRequest = z.object({
provider: z.string(),
value: z.string()
});
/**
* Remove a dictation provider secret value.
*/
export const zDictationSecretDeleteRequest = z.object({
provider: z.string()
});
/**
* List available local Whisper models with their download status.
*/
@@ -903,12 +913,10 @@ export const zExtRequest = z.object({
zProviderConfigStatusRequest,
zProviderConfigSaveRequest,
zProviderConfigDeleteRequest,
zReadConfigRequest,
zUpsertConfigRequest,
zRemoveConfigRequest,
zCheckSecretRequest,
zUpsertSecretRequest,
zRemoveSecretRequest,
zPreferencesReadRequest,
zPreferencesSaveRequest,
zPreferencesRemoveRequest,
zDefaultsReadRequest,
zExportSessionRequest,
zImportSessionRequest,
zUpdateSessionProjectRequest,
@@ -923,6 +931,8 @@ export const zExtRequest = z.object({
zImportSourcesRequest,
zDictationTranscribeRequest,
zDictationConfigRequest,
zDictationSecretSaveRequest,
zDictationSecretDeleteRequest,
zDictationModelsListRequest,
zDictationModelDownloadRequest,
zDictationModelDownloadProgressRequest,
@@ -959,8 +969,8 @@ export const zExtResponse = z.union([
zProviderConfigReadResponse,
zProviderConfigStatusResponse,
zProviderConfigChangeResponse,
zReadConfigResponse,
zCheckSecretResponse,
zPreferencesReadResponse,
zDefaultsReadResponse,
zExportSessionResponse,
zImportSessionResponse,
zCreateSourceResponse,
+8 -14
View File
@@ -359,9 +359,9 @@ export default function ConfigureScreen({
if (initialIntent === "model") {
try {
const cfg = await client.goose.GooseConfigRead({ key: "GOOSE_PROVIDER" });
const cfg = await client.goose.GooseDefaultsRead({});
if (cancelled) return;
const current = sorted.find((p) => p.providerId === cfg.value);
const current = sorted.find((p) => p.providerId === cfg.providerId);
if (current) {
setSelectedProvider(current);
setPendingConfigValues({});
@@ -395,19 +395,13 @@ export default function ConfigureScreen({
) => {
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.providerId,
await client.goose.GooseProvidersConfigSave({
providerId: provider.providerId,
fields: Object.entries(configValues).map(([key, value]) => ({
key,
value,
})),
});
await client.goose.GooseConfigUpsert({ key: "GOOSE_MODEL", value: model });
await client.setSessionConfigOption({
sessionId,
configId: "provider",
+5 -12
View File
@@ -32,10 +32,6 @@ function deriveNameFromValue(addType: AddType, value: string): string {
try { return new URL(value.trim()).hostname; } catch { return value.trim(); }
}
function keyFromName(name: string): string {
return name.replace(/[^A-Za-z0-9_-]/g, "_").toLowerCase();
}
function buildConfig(addType: AddType, value: string, name: string, description: string): ExtEntry {
if (addType === "stdio") {
const parts = value.trim().split(/\s+/);
@@ -125,15 +121,12 @@ export default function ExtensionsManager({
const saveNewExtension = useCallback((description: string) => {
const config = buildConfig(addType, addValue, addName, description);
const key = keyFromName(config.name);
withSaving(async () => {
let extMap: Record<string, unknown> = {};
try {
const raw = await client.goose.GooseConfigRead({key: "extensions"});
if (raw.value && typeof raw.value === "object") extMap = raw.value as Record<string, unknown>;
} catch { }
extMap[key] = config;
await client.goose.GooseConfigUpsert({key: "extensions", value: extMap as any});
await client.goose.GooseConfigExtensionsAdd({
name: config.name,
extensionConfig: config as any,
enabled: true,
});
await client.goose.GooseExtensionsAdd({sessionId, config: config as any});
});
}, [addType, addValue, addName, client, sessionId, withSaving]);
+6 -15
View File
@@ -559,21 +559,12 @@ export default function Onboarding({
async (provider: ProviderInventoryEntryDto, values: Record<string, string>) => {
setPhase("saving");
try {
for (const [key, value] of Object.entries(values)) {
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.providerId,
});
await client.goose.GooseConfigUpsert({
key: "GOOSE_MODEL",
value: provider.defaultModel,
await client.goose.GooseProvidersConfigSave({
providerId: provider.providerId,
fields: Object.entries(values).map(([key, value]) => ({
key,
value,
})),
});
setPhase("success");
setTimeout(onComplete, 1000);
+4 -4
View File
@@ -785,11 +785,11 @@ function App({
setStatus("checking provider…");
let hasProvider = false;
try {
const resp = await client.goose.GooseConfigRead({
key: "GOOSE_PROVIDER",
});
const resp = await client.goose.GooseDefaultsRead({});
hasProvider =
resp.value != null && resp.value !== "" && resp.value !== "null";
resp.providerId != null &&
resp.providerId !== "" &&
resp.providerId !== "null";
} catch {
hasProvider = false;
}