1fdeda904b
* feat: add voice mode for chat input Adds a voice input button to the chat composer with three transcription engines: in-browser Web Speech API, in-browser Whisper via @huggingface/transformers, and a server-side OpenAI Whisper fallback gated on KANDEV_VOICE_OPENAI_API_KEY. User-configurable from a new /settings/voice-mode page: - Master on/off toggle (defaults to on; opt-out, not opt-in) - Engine preference (auto / Web Speech / Whisper Web / Whisper Server) - Language hint (auto-detect or BCP-47 pinned) - Activation mode (click-to-toggle or hold-to-talk) - Auto-send after transcription - Whisper Web model size (tiny / base / small) - Editable Cmd+Shift+M keyboard shortcut Voice settings live under user_settings.voice_mode as a nested JSON object — no schema migration. Setting changes broadcast via the existing user.settings.updated WS event so any open chat tab updates live. The mic button is rendered on the right of the composer alongside the send button (round primary fill; destructive fill with pulse ring while recording). On browsers without a usable engine (e.g. iOS Chrome over HTTP) the button stays visible as a muted greyed icon — tapping it surfaces a toast with the reason (typically a missing secure context). Whisper Web runs in a Web Worker with fp16 decoder weights (avoids the MatMulNBits scale-missing crash that q4/q8 hit on WASM). Models are pulled from the onnx-community/* mirror and cached by transformers.js in the Cache Storage on first use. * fix: address voice mode PR review feedback and E2E selector clash Bot-review fixes from #1159: - Map http.MaxBytesError from c.FormFile to 413 instead of bare 400. - Strip BCP-47 region suffix before passing language hint to Whisper Web. - Recover from worker crash: terminate + null worker + reset ready flag so next init/transcribe creates a fresh worker instead of hanging. - Read live store state via storeApi.getState() in voice-mode settings savers to avoid stale-closure clobbering concurrent settings updates. - Detach Web Speech callbacks before abort() so trailing onend/onerror events don't mutate hook state after cancel(). - Spread caller init before method/body in voice-api fetch so a caller- supplied init can't override the multipart POST. - Replace t.Fatalf with t.Errorf+return inside test HTTP handler goroutine (FailNow from non-test goroutine is unsafe). - Add Enabled/AutoSend zeroing assertions in partial-update test so the bool-overwrite-on-PATCH behavior is locked in explicitly. E2E fix: - toolbar-overflow spec used button.rounded-full which now matches both the send and voice buttons (strict-mode violation). Switched to the submit-message-button testid for an unambiguous locator. * fix: scope voice settings rollback + whisper worker error to changed keys Round 2 of PR review feedback on #1159: - voice-mode settings rollback now restores only the keys patched by the failing request (not the whole snapshot), preserving concurrent edits to unrelated voiceMode fields. - Shortcut rollback diffs previous vs next and reverts only the changed keys (restore prior value or delete if absent), preserving other concurrent shortcut edits. - Whisper worker error handler captures the worker reference at attach time and only clears refs when this.worker still matches it — a stale error from a previously replaced worker no longer nukes the active one. - Update stale whisper-web-models JSDoc that still pointed at Xenova mirrors. * fix: voice mode progress scale, hold-mode race, race-aware rollback Round 3 of PR review feedback on #1159: - Normalise Whisper progress to 0–1 in ensureWhisperClient (transformers.js emits 0–100, the button's display + the `ready: 1` convention expect 0–1 — fixes the 5000% mid-download display). - Claim driverRef synchronously at the top of finishCapture so concurrent pointerup + pointerleave invocations early-return instead of racing and clobbering a freshly-started recording's ref. - Race-aware rollback: voice-mode and shortcut catch branches now skip reverting a key when latest[key] !== next[key]. A failed earlier request no longer overwrites a later successful save to the same key. - Update the Worker wire-protocol comment to show onnx-community/whisper-base instead of the stale Xenova/* example. --------- Co-authored-by: Kandev Agent <agent@kandev.dev>
666 lines
19 KiB
TypeScript
666 lines
19 KiB
TypeScript
import { fetchJson, fetchJsonWithRetry, type ApiRequestOptions } from "../client";
|
|
import { getBackendConfig } from "@/lib/config";
|
|
import type {
|
|
Agent,
|
|
ListAgentsResponse,
|
|
ListAgentDiscoveryResponse,
|
|
ListAvailableAgentsResponse,
|
|
AgentProfileMcpConfig,
|
|
ListExecutorsResponse,
|
|
ListExecutorProfilesResponse,
|
|
ExecutorProfile,
|
|
ProfileEnvVar,
|
|
NotificationProvidersResponse,
|
|
NotificationProvider,
|
|
EditorsResponse,
|
|
EditorOption,
|
|
CustomPrompt,
|
|
PromptsResponse,
|
|
SavedLayout,
|
|
SidebarViewApi,
|
|
UserSettingsResponse,
|
|
DynamicModelsResponse,
|
|
} from "@/lib/types/http";
|
|
import type { VoiceModeSettings } from "@/lib/types/http-voice";
|
|
|
|
// User settings
|
|
export async function fetchUserSettings(options?: ApiRequestOptions) {
|
|
return fetchJsonWithRetry<UserSettingsResponse>("/api/v1/user/settings", options);
|
|
}
|
|
|
|
export async function updateUserSettings(
|
|
payload: {
|
|
workspace_id?: string;
|
|
workflow_filter_id?: string;
|
|
kanban_view_mode?: string;
|
|
repository_ids?: string[];
|
|
preferred_shell?: string;
|
|
default_editor_id?: string;
|
|
enable_preview_on_click?: boolean;
|
|
chat_submit_key?: "enter" | "cmd_enter";
|
|
review_auto_mark_on_scroll?: boolean;
|
|
show_release_notification?: boolean;
|
|
release_notes_last_seen_version?: string;
|
|
lsp_auto_start_languages?: string[];
|
|
lsp_auto_install_languages?: string[];
|
|
lsp_server_configs?: Record<string, Record<string, unknown>>;
|
|
saved_layouts?: SavedLayout[];
|
|
sidebar_views?: SidebarViewApi[];
|
|
default_utility_agent_id?: string;
|
|
default_utility_model?: string;
|
|
keyboard_shortcuts?: Record<string, { key: string; modifiers?: Record<string, boolean> }>;
|
|
terminal_link_behavior?: "new_tab" | "browser_panel";
|
|
terminal_font_family?: string;
|
|
terminal_font_size?: number;
|
|
changes_panel_layout?: "flat" | "tree";
|
|
voice_mode?: VoiceModeSettings;
|
|
},
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<UserSettingsResponse>("/api/v1/user/settings", {
|
|
...options,
|
|
init: { method: "PATCH", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
// Executors
|
|
export async function listExecutors(options?: ApiRequestOptions): Promise<ListExecutorsResponse> {
|
|
return fetchJson<ListExecutorsResponse>("/api/v1/executors", options);
|
|
}
|
|
|
|
export async function fetchExecutor(
|
|
executorId: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<{ id: string; name: string; type: string; config?: Record<string, string> }> {
|
|
return fetchJson<{ id: string; name: string; type: string; config?: Record<string, string> }>(
|
|
`/api/v1/executors/${executorId}`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function createExecutor(
|
|
payload: {
|
|
name: string;
|
|
type: string;
|
|
config?: Record<string, string>;
|
|
},
|
|
options?: ApiRequestOptions,
|
|
): Promise<{ id: string; name: string; type: string; config?: Record<string, string> }> {
|
|
return fetchJson<{ id: string; name: string; type: string; config?: Record<string, string> }>(
|
|
"/api/v1/executors",
|
|
{
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function updateExecutor(
|
|
executorId: string,
|
|
payload: { name?: string; config?: Record<string, string> },
|
|
options?: ApiRequestOptions,
|
|
): Promise<void> {
|
|
await fetchJson<void>(`/api/v1/executors/${executorId}`, {
|
|
...options,
|
|
init: { method: "PATCH", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
// Executor profiles
|
|
export async function listExecutorProfiles(
|
|
executorId: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<ListExecutorProfilesResponse> {
|
|
return fetchJson<ListExecutorProfilesResponse>(
|
|
`/api/v1/executors/${executorId}/profiles`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function listAllExecutorProfiles(
|
|
options?: ApiRequestOptions,
|
|
): Promise<ListExecutorProfilesResponse> {
|
|
return fetchJson<ListExecutorProfilesResponse>("/api/v1/executor-profiles", options);
|
|
}
|
|
|
|
export type ScriptPlaceholder = {
|
|
key: string;
|
|
description: string;
|
|
example: string;
|
|
executor_types: string[];
|
|
};
|
|
|
|
export async function listScriptPlaceholders(
|
|
options?: ApiRequestOptions,
|
|
): Promise<{ placeholders: ScriptPlaceholder[] }> {
|
|
return fetchJson<{ placeholders: ScriptPlaceholder[] }>("/api/v1/script-placeholders", options);
|
|
}
|
|
|
|
export async function fetchDefaultScripts(
|
|
executorType: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<{ prepare_script: string; cleanup_script: string }> {
|
|
return fetchJson<{ prepare_script: string; cleanup_script: string }>(
|
|
`/api/v1/executor-profiles/default-script?type=${encodeURIComponent(executorType)}`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function createExecutorProfile(
|
|
executorId: string,
|
|
payload: {
|
|
name: string;
|
|
mcp_policy?: string;
|
|
config?: Record<string, string>;
|
|
prepare_script?: string;
|
|
cleanup_script?: string;
|
|
env_vars?: ProfileEnvVar[];
|
|
},
|
|
options?: ApiRequestOptions,
|
|
): Promise<ExecutorProfile> {
|
|
return fetchJson<ExecutorProfile>(`/api/v1/executors/${executorId}/profiles`, {
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function updateExecutorProfile(
|
|
executorId: string,
|
|
profileId: string,
|
|
payload: {
|
|
name?: string;
|
|
mcp_policy?: string;
|
|
config?: Record<string, string>;
|
|
prepare_script?: string;
|
|
cleanup_script?: string;
|
|
env_vars?: ProfileEnvVar[];
|
|
},
|
|
options?: ApiRequestOptions,
|
|
): Promise<ExecutorProfile> {
|
|
return fetchJson<ExecutorProfile>(`/api/v1/executors/${executorId}/profiles/${profileId}`, {
|
|
...options,
|
|
init: { method: "PATCH", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function deleteExecutorProfile(
|
|
executorId: string,
|
|
profileId: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<{ success: boolean }> {
|
|
return fetchJson<{ success: boolean }>(`/api/v1/executors/${executorId}/profiles/${profileId}`, {
|
|
...options,
|
|
init: { method: "DELETE", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
// Agents
|
|
import { normalizeAgentProfile } from "@/lib/api/domains/agent-profile-normalize";
|
|
|
|
function normalizeAgentResponse(agent: Agent): Agent {
|
|
return {
|
|
...agent,
|
|
profiles: (agent.profiles ?? []).map((profile) => normalizeAgentProfile(profile)),
|
|
};
|
|
}
|
|
|
|
export async function listAgents(options?: ApiRequestOptions): Promise<ListAgentsResponse> {
|
|
const res = await fetchJson<ListAgentsResponse>("/api/v1/agents", options);
|
|
return { ...res, agents: (res.agents ?? []).map(normalizeAgentResponse) };
|
|
}
|
|
|
|
export async function listAgentDiscovery(
|
|
options?: ApiRequestOptions,
|
|
): Promise<ListAgentDiscoveryResponse> {
|
|
return fetchJson<ListAgentDiscoveryResponse>("/api/v1/agents/discovery", options);
|
|
}
|
|
|
|
export async function listAvailableAgents(
|
|
options?: ApiRequestOptions,
|
|
): Promise<ListAvailableAgentsResponse> {
|
|
return fetchJson<ListAvailableAgentsResponse>("/api/v1/agents/available", options);
|
|
}
|
|
|
|
export async function getAgentProfileMcpConfig(
|
|
profileId: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<AgentProfileMcpConfig> {
|
|
return fetchJson<AgentProfileMcpConfig>(
|
|
`/api/v1/agent-profiles/${profileId}/mcp-config`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export type CommandPreviewRequest = {
|
|
model: string;
|
|
permission_settings: Record<string, boolean>;
|
|
cli_passthrough: boolean;
|
|
};
|
|
|
|
export type CommandPreviewResponse = {
|
|
supported: boolean;
|
|
command: string[];
|
|
command_string: string;
|
|
};
|
|
|
|
export async function previewAgentCommand(
|
|
agentName: string,
|
|
payload: CommandPreviewRequest,
|
|
options?: ApiRequestOptions,
|
|
): Promise<CommandPreviewResponse> {
|
|
return fetchJson<CommandPreviewResponse>(`/api/v1/agent-command-preview/${agentName}`, {
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export type InstallJobStatus = "queued" | "running" | "succeeded" | "failed";
|
|
|
|
export type InstallJob = {
|
|
job_id: string;
|
|
agent_name: string;
|
|
status: InstallJobStatus;
|
|
output?: string;
|
|
error?: string;
|
|
exit_code?: number;
|
|
started_at: string;
|
|
finished_at?: string;
|
|
};
|
|
|
|
/** Enqueue an install. Returns the job snapshot (status=queued or running). */
|
|
export async function installAgent(
|
|
agentName: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<InstallJob> {
|
|
return fetchJson<InstallJob>(`/api/v1/agent-install/${agentName}`, {
|
|
...options,
|
|
init: { method: "POST", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function listInstallJobs(
|
|
options?: ApiRequestOptions,
|
|
): Promise<{ jobs: InstallJob[] }> {
|
|
return fetchJson<{ jobs: InstallJob[] }>("/api/v1/agent-install/jobs", options);
|
|
}
|
|
|
|
export async function getInstallJob(
|
|
jobId: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<InstallJob> {
|
|
return fetchJson<InstallJob>(`/api/v1/agent-install/jobs/${jobId}`, options);
|
|
}
|
|
|
|
export type AgentLoginSession = {
|
|
session_id: string;
|
|
agent_id: string;
|
|
cmd: string[];
|
|
running: boolean;
|
|
started_at: string;
|
|
finished_at?: string;
|
|
exit_code?: number;
|
|
};
|
|
|
|
export async function startAgentLogin(
|
|
agentName: string,
|
|
size: { cols: number; rows: number },
|
|
options?: ApiRequestOptions,
|
|
): Promise<AgentLoginSession> {
|
|
return fetchJson<AgentLoginSession>(`/api/v1/agent-login/agents/${agentName}/start`, {
|
|
...options,
|
|
init: {
|
|
method: "POST",
|
|
body: JSON.stringify(size),
|
|
...(options?.init ?? {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function stopAgentLogin(sessionID: string): Promise<void> {
|
|
await fetchJson<{ ok: boolean }>(`/api/v1/agent-login/sessions/${sessionID}/stop`, {
|
|
init: { method: "POST" },
|
|
});
|
|
}
|
|
|
|
export async function resizeAgentLogin(
|
|
sessionID: string,
|
|
size: { cols: number; rows: number },
|
|
): Promise<void> {
|
|
await fetchJson<{ ok: boolean }>(`/api/v1/agent-login/sessions/${sessionID}/resize`, {
|
|
init: { method: "POST", body: JSON.stringify(size) },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Build the bi-directional WS URL for streaming a login session.
|
|
* Derives the host from the backend config (NOT window.location) so dev mode
|
|
* — where the browser is on :37429 and the API is on :38429 — routes to the
|
|
* Go backend, not the Next dev server.
|
|
*/
|
|
export function agentLoginStreamUrl(sessionID: string): string {
|
|
const { apiBaseUrl } = getBackendConfig();
|
|
const url = new URL(apiBaseUrl);
|
|
const proto = url.protocol === "https:" ? "wss:" : "ws:";
|
|
return `${proto}//${url.host}/api/v1/agent-login/sessions/${sessionID}/stream`;
|
|
}
|
|
|
|
/**
|
|
* Start a plain host shell PTY (spawns $SHELL, or bash/sh fallback). Reuses
|
|
* the same session manager as agent-login, so stop/resize/stream all use the
|
|
* same session-ID-based endpoints.
|
|
*/
|
|
export async function startHostShell(
|
|
size: { cols: number; rows: number },
|
|
options?: ApiRequestOptions,
|
|
): Promise<AgentLoginSession> {
|
|
return fetchJson<AgentLoginSession>("/api/v1/host-shell/start", {
|
|
...options,
|
|
init: {
|
|
method: "POST",
|
|
body: JSON.stringify(size),
|
|
...(options?.init ?? {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function createCustomTUIAgent(
|
|
payload: { display_name: string; model?: string; command: string; description?: string },
|
|
options?: ApiRequestOptions,
|
|
): Promise<Agent> {
|
|
const res = await fetchJson<Agent>("/api/v1/agents/tui", {
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
return normalizeAgentResponse(res);
|
|
}
|
|
|
|
export async function fetchDynamicModels(
|
|
agentName: string,
|
|
options?: ApiRequestOptions & { refresh?: boolean },
|
|
): Promise<DynamicModelsResponse> {
|
|
const refresh = options?.refresh ?? false;
|
|
const url = `/api/v1/agent-models/${agentName}${refresh ? "?refresh=true" : ""}`;
|
|
return fetchJson<DynamicModelsResponse>(url, options);
|
|
}
|
|
|
|
// Editors
|
|
export async function listEditors(options?: ApiRequestOptions) {
|
|
return fetchJson<EditorsResponse>("/api/v1/editors", options);
|
|
}
|
|
|
|
export async function createEditor(
|
|
payload: {
|
|
name: string;
|
|
kind: string;
|
|
config?: Record<string, unknown>;
|
|
enabled?: boolean;
|
|
},
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<EditorOption>("/api/v1/editors", {
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function updateEditor(
|
|
editorId: string,
|
|
payload: {
|
|
name?: string;
|
|
kind?: string;
|
|
config?: Record<string, unknown>;
|
|
enabled?: boolean;
|
|
},
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<EditorOption>(`/api/v1/editors/${editorId}`, {
|
|
...options,
|
|
init: { method: "PATCH", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function deleteEditor(editorId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<{ success: boolean }>(`/api/v1/editors/${editorId}`, {
|
|
...options,
|
|
init: { method: "DELETE", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
// Prompts
|
|
export async function listPrompts(options?: ApiRequestOptions) {
|
|
return fetchJson<PromptsResponse>("/api/v1/prompts", options);
|
|
}
|
|
|
|
export async function createPrompt(
|
|
payload: { name: string; content: string },
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<CustomPrompt>("/api/v1/prompts", {
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function updatePrompt(
|
|
promptId: string,
|
|
payload: { name?: string; content?: string },
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<CustomPrompt>(`/api/v1/prompts/${promptId}`, {
|
|
...options,
|
|
init: { method: "PATCH", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function deletePrompt(promptId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<{ success: boolean }>(`/api/v1/prompts/${promptId}`, {
|
|
...options,
|
|
init: { method: "DELETE", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
// Notification providers
|
|
export async function listNotificationProviders(options?: ApiRequestOptions) {
|
|
return fetchJson<NotificationProvidersResponse>("/api/v1/notification-providers", options);
|
|
}
|
|
|
|
export async function createNotificationProvider(
|
|
payload: {
|
|
name: string;
|
|
type: NotificationProvider["type"];
|
|
config?: NotificationProvider["config"];
|
|
enabled?: boolean;
|
|
events?: string[];
|
|
},
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<NotificationProvider>("/api/v1/notification-providers", {
|
|
...options,
|
|
init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function updateNotificationProvider(
|
|
providerId: string,
|
|
payload: Partial<{
|
|
name: string;
|
|
type: NotificationProvider["type"];
|
|
config: NotificationProvider["config"];
|
|
enabled: boolean;
|
|
events: string[];
|
|
}>,
|
|
options?: ApiRequestOptions,
|
|
) {
|
|
return fetchJson<NotificationProvider>(`/api/v1/notification-providers/${providerId}`, {
|
|
...options,
|
|
init: { method: "PATCH", body: JSON.stringify(payload), ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function testNotificationProvider(providerId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<void>(`/api/v1/notification-providers/${providerId}/test`, {
|
|
...options,
|
|
init: { method: "POST", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function deleteNotificationProvider(providerId: string, options?: ApiRequestOptions) {
|
|
return fetchJson<void>(`/api/v1/notification-providers/${providerId}`, {
|
|
...options,
|
|
init: { method: "DELETE", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
// Docker management
|
|
|
|
export type DockerContainer = {
|
|
id: string;
|
|
name: string;
|
|
image: string;
|
|
state: string;
|
|
status: string;
|
|
started_at?: string;
|
|
labels?: Record<string, string>;
|
|
};
|
|
|
|
export function buildDockerImageUrl(payload: {
|
|
dockerfile: string;
|
|
tag: string;
|
|
build_args?: Record<string, string>;
|
|
}): string {
|
|
// Returns the URL for SSE streaming. Caller should use EventSource or fetch with streaming.
|
|
const params = new URLSearchParams();
|
|
params.set("dockerfile", payload.dockerfile);
|
|
params.set("tag", payload.tag);
|
|
if (payload.build_args) {
|
|
params.set("build_args", JSON.stringify(payload.build_args));
|
|
}
|
|
return `/api/v1/docker/build?${params.toString()}`;
|
|
}
|
|
|
|
export async function buildDockerImage(
|
|
payload: {
|
|
dockerfile: string;
|
|
tag: string;
|
|
build_args?: Record<string, string>;
|
|
},
|
|
options?: ApiRequestOptions,
|
|
): Promise<Response> {
|
|
const baseUrl = options?.baseUrl ?? getBackendConfig().apiBaseUrl;
|
|
return fetch(`${baseUrl}/api/v1/docker/build`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
...(options?.init ?? {}),
|
|
});
|
|
}
|
|
|
|
export async function listDockerContainers(
|
|
filter?: { image?: string; labels?: Record<string, string> },
|
|
options?: ApiRequestOptions,
|
|
): Promise<{ containers: DockerContainer[] }> {
|
|
const params = new URLSearchParams();
|
|
if (filter?.image) params.set("image", filter.image);
|
|
if (filter?.labels) {
|
|
const labelPairs = Object.entries(filter.labels).map(([k, v]) => `${k}=${v}`);
|
|
params.set("labels", labelPairs.join(","));
|
|
}
|
|
const qs = params.toString();
|
|
return fetchJson<{ containers: DockerContainer[] }>(
|
|
`/api/v1/docker/containers${qs ? `?${qs}` : ""}`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function stopDockerContainer(id: string, options?: ApiRequestOptions): Promise<void> {
|
|
await fetchJson<{ success: boolean }>(`/api/v1/docker/containers/${id}/stop`, {
|
|
...options,
|
|
init: { method: "POST", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
export async function removeDockerContainer(
|
|
id: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<void> {
|
|
await fetchJson<{ success: boolean }>(`/api/v1/docker/containers/${id}`, {
|
|
...options,
|
|
init: { method: "DELETE", ...(options?.init ?? {}) },
|
|
});
|
|
}
|
|
|
|
// Remote auth methods (for remote executors like Sprites)
|
|
|
|
export type RemoteAuthMethod = {
|
|
method_id: string;
|
|
type: "env" | "files" | "gh_cli_token";
|
|
env_var?: string;
|
|
setup_hint?: string;
|
|
source_files?: string[];
|
|
target_rel_dir?: string;
|
|
label?: string;
|
|
has_local_files?: boolean;
|
|
};
|
|
|
|
export type RemoteAuthSpec = {
|
|
id: string;
|
|
display_name: string;
|
|
methods: RemoteAuthMethod[];
|
|
};
|
|
|
|
export type ListRemoteCredentialsResponse = {
|
|
auth_specs: RemoteAuthSpec[];
|
|
};
|
|
|
|
export async function listRemoteCredentials(
|
|
options?: ApiRequestOptions,
|
|
): Promise<ListRemoteCredentialsResponse> {
|
|
return fetchJson<ListRemoteCredentialsResponse>("/api/v1/remote-credentials", options);
|
|
}
|
|
|
|
export type LocalGitIdentityResponse = {
|
|
user_name: string;
|
|
user_email: string;
|
|
detected: boolean;
|
|
};
|
|
|
|
export async function fetchLocalGitIdentity(
|
|
options?: ApiRequestOptions,
|
|
): Promise<LocalGitIdentityResponse> {
|
|
return fetchJson<LocalGitIdentityResponse>("/api/v1/git/identity", options);
|
|
}
|
|
|
|
// Sprites network policies
|
|
|
|
export type NetworkPolicyRule = {
|
|
domain: string;
|
|
action: "allow" | "deny";
|
|
include?: string;
|
|
};
|
|
|
|
export type NetworkPolicy = {
|
|
rules: NetworkPolicyRule[];
|
|
};
|
|
|
|
export async function getSpritesNetworkPolicies(
|
|
secretId: string,
|
|
spriteName: string,
|
|
options?: ApiRequestOptions,
|
|
): Promise<NetworkPolicy> {
|
|
const params = new URLSearchParams({ secret_id: secretId, sprite_name: spriteName });
|
|
return fetchJson<NetworkPolicy>(`/api/v1/sprites/network-policies?${params.toString()}`, options);
|
|
}
|
|
|
|
export async function updateSpritesNetworkPolicies(
|
|
secretId: string,
|
|
spriteName: string,
|
|
policy: NetworkPolicy,
|
|
options?: ApiRequestOptions,
|
|
): Promise<void> {
|
|
const params = new URLSearchParams({ secret_id: secretId, sprite_name: spriteName });
|
|
await fetchJson<void>(`/api/v1/sprites/network-policies?${params.toString()}`, {
|
|
...options,
|
|
init: { method: "PUT", body: JSON.stringify(policy), ...(options?.init ?? {}) },
|
|
});
|
|
}
|