feat(acp): replace raw config and secret methods (#9000)
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
This commit is contained in:
@@ -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 =
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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.",
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user