Compare commits

..
Author SHA1 Message Date
Peter Steinberger 8df39f19c1 fix: support Bedrock auth + inline provider ids (#1286) (thanks @alauppe) 2026-01-20 07:02:52 +00:00
Andrew Lauppe 9ca5cb2db9 fix(models): attach provider to inline model definitions
When resolving models from custom provider configurations, ensure the
provider name is attached to each inline model entry. This fixes model
resolution for custom providers where the model definition exists in
the config but lacks an explicit provider field.

Without this fix, inline models from custom providers (like amazon-bedrock)
would fail to resolve because the provider context was lost during the
flatMap operation.
2026-01-20 06:51:25 +00:00
Andrew Lauppe 7f25523d89 feat(models): add bedrock-converse-stream API type
Add AWS Bedrock Converse Stream API to the list of supported model APIs,
enabling custom provider configurations for Amazon Bedrock endpoints.

This allows users to configure Bedrock models in their clawdbot.json:

  "models": {
    "providers": {
      "amazon-bedrock": {
        "baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com",
        "api": "bedrock-converse-stream",
        "models": [...]
      }
    }
  }

The underlying adapter already exists; this change exposes it as a valid
configuration option.
2026-01-20 06:51:25 +00:00
16 changed files with 203 additions and 216 deletions

No files matched your search

-1
View File
@@ -57,7 +57,6 @@
- Changelog workflow: keep latest released version at top (no `Unreleased`); after publishing, bump version and start a new top section.
- PRs should summarize scope, note testing performed, and mention any user-facing changes or new flags.
- PR review flow: when given a PR link, review via `gh pr view`/`gh pr diff` and do **not** change branches.
- Before starting a review when a GH Issue/PR is pasted: run `git pull`; if there are local changes or unpushed commits, stop and alert the user before reviewing.
- PR merge flow: create a temp branch from `main`, merge the PR branch into it (prefer squash unless commit history is important; use rebase/merge when it is). Always try to merge the PR unless it’s truly difficult, then use another approach. If we squash, add the PR author as a co-contributor. Apply fixes, add changelog entry (include PR # + thanks), run full gate before the final commit, commit, merge back to `main`, delete the temp branch, and end on `main`.
- If you review a PR and later do work on it, land via merge/squash (no direct-main commits) and always add the PR author as a co-contributor.
- When working on a PR: add a changelog entry with the PR number and thank the contributor.
+1 -2
View File
@@ -7,12 +7,11 @@ Docs: https://docs.clawd.bot
### Changes
- Android: remove legacy bridge transport code now that nodes use the gateway protocol.
- Android: send structured payloads in node events/invokes and include user-agent metadata in gateway connects.
- Docs: surface Amazon Bedrock in provider lists and clarify Bedrock auth env vars. (#1289) — thanks @steipete.
### Fixes
- Gateway: strip inbound envelope headers from chat history messages to keep clients clean.
- UI: prevent double-scroll in Control UI chat by locking chat layout to the viewport. (#1283) — thanks @bradleypriest.
- UI: centralize Control UI session routing + URL sync so chat deep links stay stable. (#1288) — thanks @bradleypriest.
- Models: allow Bedrock custom providers without API keys and preserve inline provider IDs. (#1286) — thanks @alauppe.
## 2026.1.19-2
+1 -3
View File
@@ -28,11 +28,9 @@ export AWS_REGION="us-east-1"
# Optional:
export AWS_SESSION_TOKEN="..."
export AWS_PROFILE="your-profile"
# Optional (Bedrock API key/bearer token):
export AWS_BEARER_TOKEN_BEDROCK="..."
```
2) Add a Bedrock provider and model to your config (no `apiKey` required):
2) Add a Bedrock provider and model to your config:
```json5
{
-1
View File
@@ -31,7 +31,6 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/etc.)? See [Chann
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
- [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot)
- [OpenCode Zen](/providers/opencode)
- [Amazon Bedrock](/bedrock)
- [Z.AI](/providers/zai)
- [GLM models](/providers/glm)
- [MiniMax](/providers/minimax)
+90
View File
@@ -280,4 +280,94 @@ describe("getApiKeyForModel", () => {
}
}
});
it("accepts AWS profile auth for amazon-bedrock", async () => {
const previousProfile = process.env.AWS_PROFILE;
const previousAccess = process.env.AWS_ACCESS_KEY_ID;
const previousSecret = process.env.AWS_SECRET_ACCESS_KEY;
const previousBearer = process.env.AWS_BEARER_TOKEN_BEDROCK;
try {
process.env.AWS_PROFILE = "bedrock-test";
delete process.env.AWS_ACCESS_KEY_ID;
delete process.env.AWS_SECRET_ACCESS_KEY;
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
vi.resetModules();
const { resolveApiKeyForProvider } = await import("./model-auth.js");
const resolved = await resolveApiKeyForProvider({
provider: "amazon-bedrock",
store: { version: 1, profiles: {} },
});
expect(resolved.apiKey).toBe("<authenticated>");
expect(resolved.source).toContain("AWS_PROFILE");
} finally {
if (previousProfile === undefined) {
delete process.env.AWS_PROFILE;
} else {
process.env.AWS_PROFILE = previousProfile;
}
if (previousAccess === undefined) {
delete process.env.AWS_ACCESS_KEY_ID;
} else {
process.env.AWS_ACCESS_KEY_ID = previousAccess;
}
if (previousSecret === undefined) {
delete process.env.AWS_SECRET_ACCESS_KEY;
} else {
process.env.AWS_SECRET_ACCESS_KEY = previousSecret;
}
if (previousBearer === undefined) {
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
} else {
process.env.AWS_BEARER_TOKEN_BEDROCK = previousBearer;
}
}
});
it("allows amazon-bedrock without an API key", async () => {
const previousProfile = process.env.AWS_PROFILE;
const previousAccess = process.env.AWS_ACCESS_KEY_ID;
const previousSecret = process.env.AWS_SECRET_ACCESS_KEY;
const previousBearer = process.env.AWS_BEARER_TOKEN_BEDROCK;
try {
delete process.env.AWS_PROFILE;
delete process.env.AWS_ACCESS_KEY_ID;
delete process.env.AWS_SECRET_ACCESS_KEY;
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
vi.resetModules();
const { resolveApiKeyForProvider } = await import("./model-auth.js");
const resolved = await resolveApiKeyForProvider({
provider: "amazon-bedrock",
store: { version: 1, profiles: {} },
});
expect(resolved.apiKey).toBe("<authenticated>");
expect(resolved.source).toBe("aws-sdk");
} finally {
if (previousProfile === undefined) {
delete process.env.AWS_PROFILE;
} else {
process.env.AWS_PROFILE = previousProfile;
}
if (previousAccess === undefined) {
delete process.env.AWS_ACCESS_KEY_ID;
} else {
process.env.AWS_ACCESS_KEY_ID = previousAccess;
}
if (previousSecret === undefined) {
delete process.env.AWS_SECRET_ACCESS_KEY;
} else {
process.env.AWS_SECRET_ACCESS_KEY = previousSecret;
}
if (previousBearer === undefined) {
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
} else {
process.env.AWS_BEARER_TOKEN_BEDROCK = previousBearer;
}
}
});
});
+26
View File
@@ -36,6 +36,7 @@ export async function resolveApiKeyForProvider(params: {
}): Promise<{ apiKey: string; profileId?: string; source: string }> {
const { provider, cfg, profileId, preferredProfile } = params;
const store = params.store ?? ensureAuthProfileStore(params.agentDir);
const normalized = normalizeProviderId(provider);
if (profileId) {
const resolved = await resolveApiKeyForProfile({
@@ -88,6 +89,10 @@ export async function resolveApiKeyForProvider(params: {
return { apiKey: customKey, source: "models.json" };
}
if (normalized === "amazon-bedrock") {
return { apiKey: "<authenticated>", source: "aws-sdk" };
}
if (provider === "openai") {
const hasCodex = listProfilesForProvider(store, "openai-codex").length > 0;
if (hasCodex) {
@@ -120,6 +125,12 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
const source = applied.has(envVar) ? `shell env: ${envVar}` : `env: ${envVar}`;
return { apiKey: value, source };
};
const pickPresent = (envVar: string): EnvApiKeyResult | null => {
const value = process.env[envVar]?.trim();
if (!value) return null;
const source = applied.has(envVar) ? `shell env: ${envVar}` : `env: ${envVar}`;
return { apiKey: "<authenticated>", source };
};
if (normalized === "github-copilot") {
return pick("COPILOT_GITHUB_TOKEN") ?? pick("GH_TOKEN") ?? pick("GITHUB_TOKEN");
@@ -143,6 +154,21 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
return { apiKey: envKey, source: "gcloud adc" };
}
if (normalized === "amazon-bedrock") {
return (
pickPresent("AWS_BEARER_TOKEN_BEDROCK") ??
pickPresent("AWS_PROFILE") ??
(process.env.AWS_ACCESS_KEY_ID?.trim() && process.env.AWS_SECRET_ACCESS_KEY?.trim()
? {
apiKey: "<authenticated>",
source: applied.has("AWS_ACCESS_KEY_ID")
? "shell env: AWS_ACCESS_KEY_ID"
: "env: AWS_ACCESS_KEY_ID",
}
: null)
);
}
if (normalized === "opencode") {
return pick("OPENCODE_API_KEY") ?? pick("OPENCODE_ZEN_API_KEY");
}
+4 -1
View File
@@ -143,7 +143,10 @@ export function normalizeProviders(params: {
provider: normalizedKey,
store: authStore,
});
const apiKey = fromEnv ?? fromProfiles;
const apiKey =
fromEnv ??
fromProfiles ??
(normalizedKey === "amazon-bedrock" ? "AWS_PROFILE" : undefined);
if (apiKey?.trim()) {
mutated = true;
normalizedProvider = { ...normalizedProvider, apiKey };
+3 -3
View File
@@ -40,9 +40,9 @@ export function resolveModel(
const providers = cfg?.models?.providers ?? {};
const inlineModels =
providers[provider]?.models?.map((entry) => ({ ...entry, provider })) ??
Object.values(providers)
.flatMap((entry) => entry?.models ?? [])
.map((entry) => ({ ...entry, provider }));
Object.entries(providers).flatMap(([providerId, entry]) =>
(entry?.models ?? []).map((modelEntry) => ({ ...modelEntry, provider: providerId })),
);
const inlineMatch = inlineModels.find((entry) => entry.id === modelId);
if (inlineMatch) {
const normalized = normalizeModelCompat(inlineMatch as Model<Api>);
+3 -10
View File
@@ -22,17 +22,9 @@ import {
installGatewayTestHooks();
async function waitForSignal(check: () => boolean, timeoutMs = 2000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (check()) return;
await new Promise((resolve) => setTimeout(resolve, 10));
}
throw new Error("timeout");
}
describe("gateway update.run", () => {
it("writes sentinel and schedules restart", async () => {
vi.useFakeTimers();
const sigusr1 = vi.fn();
process.on("SIGUSR1", sigusr1);
@@ -57,7 +49,7 @@ describe("gateway update.run", () => {
);
expect(res.ok).toBe(true);
await waitForSignal(() => sigusr1.mock.calls.length > 0);
await vi.advanceTimersByTimeAsync(0);
expect(sigusr1).toHaveBeenCalled();
const sentinelPath = path.join(os.homedir(), ".clawdbot", "restart-sentinel.json");
@@ -71,5 +63,6 @@ describe("gateway update.run", () => {
ws.close();
await server.close();
process.off("SIGUSR1", sigusr1);
vi.useRealTimers();
});
});
+8 -4
View File
@@ -1,10 +1,11 @@
import type { Tab } from "./navigation";
import { connectGateway } from "./app-gateway";
import {
applyStateFromLocation,
applySettingsFromUrl,
attachThemeListener,
detachThemeListener,
inferBasePath,
syncTabWithLocation,
syncThemeWithSettings,
} from "./app-settings";
import { observeTopbar, scheduleChatScroll, scheduleLogsScroll } from "./app-scroll";
@@ -32,9 +33,9 @@ type LifecycleHost = {
export function handleConnected(host: LifecycleHost) {
host.basePath = inferBasePath();
applyStateFromLocation(
host as unknown as Parameters<typeof applyStateFromLocation>[0],
{ replace: true },
syncTabWithLocation(
host as unknown as Parameters<typeof syncTabWithLocation>[0],
true,
);
syncThemeWithSettings(
host as unknown as Parameters<typeof syncThemeWithSettings>[0],
@@ -43,6 +44,9 @@ export function handleConnected(host: LifecycleHost) {
host as unknown as Parameters<typeof attachThemeListener>[0],
);
window.addEventListener("popstate", host.popStateHandler);
applySettingsFromUrl(
host as unknown as Parameters<typeof applySettingsFromUrl>[0],
);
connectGateway(host as unknown as Parameters<typeof connectGateway>[0]);
startNodesPolling(host as unknown as Parameters<typeof startNodesPolling>[0]);
if (host.tab === "logs") {
+14 -13
View File
@@ -1,5 +1,4 @@
import { html } from "lit";
import { repeat } from "lit/directives/repeat.js";
import type { AppViewState } from "./app-view-state";
import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation";
@@ -53,18 +52,22 @@ export function renderChatControls(state: AppViewState) {
?disabled=${!state.connected}
@change=${(e: Event) => {
const next = (e.target as HTMLSelectElement).value;
state.setSessionKey(next, {
source: "user",
resetChat: true,
loadHistory: true,
syncUrl: true,
replace: true,
state.sessionKey = next;
state.chatMessage = "";
state.chatStream = null;
state.chatStreamStartedAt = null;
state.chatRunId = null;
state.resetToolStream();
state.resetChatScroll();
state.applySettings({
...state.settings,
sessionKey: next,
lastActiveSessionKey: next,
});
void loadChatHistory(state);
}}
>
${repeat(
sessionOptions,
(entry) => entry.key,
${sessionOptions.map(
(entry) =>
html`<option value=${entry.key}>
${entry.displayName ?? entry.key}
@@ -116,11 +119,9 @@ function resolveSessionOptions(sessionKey: string, sessions: SessionsListResult
const seen = new Set<string>();
const options: Array<{ key: string; displayName?: string }> = [];
const resolvedCurrent = sessions?.sessions?.find((s) => s.key === sessionKey);
// Add current session key first
seen.add(sessionKey);
options.push({ key: sessionKey, displayName: resolvedCurrent?.displayName });
options.push({ key: sessionKey });
// Add sessions from the result
if (sessions?.sessions) {
+19 -13
View File
@@ -183,14 +183,14 @@ export function renderApp(state: AppViewState) {
onSettingsChange: (next) => state.applySettings(next),
onPasswordChange: (next) => (state.password = next),
onSessionKeyChange: (next) => {
state.setSessionKey(next, {
source: "settings",
resetChat: false,
loadHistory: false,
syncUrl: false,
});
state.sessionKey = next;
state.chatMessage = "";
state.resetToolStream();
state.applySettings({
...state.settings,
sessionKey: next,
lastActiveSessionKey: next,
});
},
onConnect: () => state.connect(),
onRefresh: () => state.loadOverview(),
@@ -366,14 +366,20 @@ export function renderApp(state: AppViewState) {
? renderChat({
sessionKey: state.sessionKey,
onSessionKeyChange: (next) => {
state.setSessionKey(next, {
source: "user",
resetChat: true,
loadHistory: true,
syncUrl: true,
replace: true,
clearQueue: true,
state.sessionKey = next;
state.chatMessage = "";
state.chatStream = null;
state.chatStreamStartedAt = null;
state.chatRunId = null;
state.chatQueue = [];
state.resetToolStream();
state.resetChatScroll();
state.applySettings({
...state.settings,
sessionKey: next,
lastActiveSessionKey: next,
});
void loadChatHistory(state);
},
thinkingLevel: state.chatThinkingLevel,
loading: state.chatLoading,
+33 -124
View File
@@ -1,4 +1,3 @@
import { loadChatHistory } from "./controllers/chat";
import { loadConfig, loadConfigSchema } from "./controllers/config";
import { loadCronJobs, loadCronStatus } from "./controllers/cron";
import { loadChannels } from "./controllers/channels";
@@ -17,7 +16,6 @@ import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll";
import { startLogsPolling, stopLogsPolling } from "./app-polling";
import { refreshChat } from "./app-chat";
import type { ClawdbotApp } from "./app";
import type { ChatQueueItem } from "./ui-types";
type SettingsHost = {
settings: UiSettings;
@@ -36,25 +34,6 @@ type SettingsHost = {
themeMediaHandler: ((event: MediaQueryListEvent) => void) | null;
};
export type SessionKeyOptions = {
source?: "user" | "route" | "settings" | "gateway";
syncUrl?: boolean;
replace?: boolean;
resetChat?: boolean;
loadHistory?: boolean;
clearQueue?: boolean;
};
type SessionKeyHost = SettingsHost & {
chatMessage: string;
chatStream: string | null;
chatStreamStartedAt: number | null;
chatRunId: string | null;
chatQueue: ChatQueueItem[];
resetToolStream: () => void;
resetChatScroll: () => void;
};
export function applySettings(host: SettingsHost, next: UiSettings) {
const normalized = {
...next,
@@ -76,85 +55,13 @@ export function setLastActiveSessionKey(host: SettingsHost, next: string) {
applySettings(host, { ...host.settings, lastActiveSessionKey: trimmed });
}
function updateSessionSettings(host: SettingsHost, next: string) {
const trimmed = next.trim();
if (!trimmed) return;
if (
host.settings.sessionKey === trimmed &&
host.settings.lastActiveSessionKey === trimmed
) {
return;
}
applySettings(host, {
...host.settings,
sessionKey: trimmed,
lastActiveSessionKey: trimmed,
});
}
export function setSessionKey(
host: SessionKeyHost,
next: string,
options: SessionKeyOptions = {},
) {
const trimmed = next.trim();
if (!trimmed) return;
const shouldResetChat = options.resetChat ?? false;
if (shouldResetChat) {
host.chatMessage = "";
host.chatStream = null;
host.chatStreamStartedAt = null;
host.chatRunId = null;
host.resetToolStream();
host.resetChatScroll();
if (options.clearQueue ?? true) {
host.chatQueue = [];
}
}
if (host.sessionKey !== trimmed) {
host.sessionKey = trimmed;
}
updateSessionSettings(host, trimmed);
if (options.syncUrl ?? host.tab === "chat") {
syncUrlWithTab(host, host.tab, options.replace ?? true);
}
if (options.loadHistory) {
void loadChatHistory(host as unknown as ClawdbotApp);
}
}
type LocationApplyOptions = {
replace: boolean;
};
function buildUrlForTab(host: SettingsHost, tab: Tab, baseUrl?: URL) {
const url = baseUrl ? new URL(baseUrl.toString()) : new URL(window.location.href);
const targetPath = normalizePath(pathForTab(tab, host.basePath));
url.pathname = targetPath;
if (tab === "chat" && host.sessionKey) {
url.searchParams.set("session", host.sessionKey);
} else {
url.searchParams.delete("session");
}
return url;
}
export function applyStateFromLocation(
host: SessionKeyHost,
options: LocationApplyOptions,
) {
if (typeof window === "undefined") return;
const currentHref = window.location.href;
const url = new URL(currentHref);
const params = url.searchParams;
export function applySettingsFromUrl(host: SettingsHost) {
if (!window.location.search) return;
const params = new URLSearchParams(window.location.search);
const tokenRaw = params.get("token");
const passwordRaw = params.get("password");
const sessionRaw = params.get("session");
let shouldCleanUrl = false;
if (tokenRaw != null) {
const token = tokenRaw.trim();
@@ -162,6 +69,7 @@ export function applyStateFromLocation(
applySettings(host, { ...host.settings, token });
}
params.delete("token");
shouldCleanUrl = true;
}
if (passwordRaw != null) {
@@ -170,34 +78,22 @@ export function applyStateFromLocation(
(host as { password: string }).password = password;
}
params.delete("password");
shouldCleanUrl = true;
}
const resolved = tabFromPath(url.pathname, host.basePath) ?? "chat";
const session = sessionRaw?.trim() ?? "";
if (resolved === "chat") {
if (sessionRaw != null) {
const session = sessionRaw.trim();
if (session) {
setSessionKey(host, session, {
source: "route",
syncUrl: false,
resetChat: false,
loadHistory: false,
});
} else if (sessionRaw != null) {
params.delete("session");
host.sessionKey = session;
}
} else if (sessionRaw != null) {
params.delete("session");
shouldCleanUrl = true;
}
setTabFromRoute(host, resolved);
const targetUrl = buildUrlForTab(host, resolved, url).toString();
if (targetUrl === currentHref) return;
if (options.replace) {
window.history.replaceState({}, "", targetUrl);
} else {
window.history.pushState({}, "", targetUrl);
}
if (!shouldCleanUrl) return;
const url = new URL(window.location.href);
url.search = params.toString();
window.history.replaceState({}, "", url.toString());
}
export function setTab(host: SettingsHost, next: Tab) {
@@ -318,8 +214,18 @@ export function detachThemeListener(host: SettingsHost) {
host.themeMediaHandler = null;
}
export function onPopState(host: SessionKeyHost) {
applyStateFromLocation(host, { replace: true });
export function syncTabWithLocation(host: SettingsHost, replace: boolean) {
if (typeof window === "undefined") return;
const resolved = tabFromPath(window.location.pathname, host.basePath) ?? "chat";
setTabFromRoute(host, resolved);
syncUrlWithTab(host, resolved, replace);
}
export function onPopState(host: SettingsHost) {
if (typeof window === "undefined") return;
const resolved = tabFromPath(window.location.pathname, host.basePath);
if (!resolved) return;
setTabFromRoute(host, resolved);
}
export function setTabFromRoute(host: SettingsHost, next: Tab) {
@@ -333,12 +239,15 @@ export function setTabFromRoute(host: SettingsHost, next: Tab) {
export function syncUrlWithTab(host: SettingsHost, tab: Tab, replace: boolean) {
if (typeof window === "undefined") return;
const targetUrl = buildUrlForTab(host, tab).toString();
if (targetUrl === window.location.href) return;
const targetPath = normalizePath(pathForTab(tab, host.basePath));
const currentPath = normalizePath(window.location.pathname);
if (currentPath === targetPath) return;
const url = new URL(window.location.href);
url.pathname = targetPath;
if (replace) {
window.history.replaceState({}, "", targetUrl);
window.history.replaceState({}, "", url.toString());
} else {
window.history.pushState({}, "", targetUrl);
window.history.pushState({}, "", url.toString());
}
}
+1 -2
View File
@@ -1,6 +1,5 @@
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway";
import type { Tab } from "./navigation";
import type { SessionKeyOptions } from "./app-settings";
import type { UiSettings } from "./storage";
import type { ThemeMode } from "./theme";
import type { ThemeTransitionContext } from "./theme-transition";
@@ -161,7 +160,7 @@ export type AppViewState = {
handleDebugCall: () => Promise<void>;
handleRunUpdate: () => Promise<void>;
setPassword: (next: string) => void;
setSessionKey: (next: string, options?: SessionKeyOptions) => void;
setSessionKey: (next: string) => void;
setChatMessage: (next: string) => void;
handleChatSend: () => Promise<void>;
handleChatAbort: () => Promise<void>;
-10
View File
@@ -50,10 +50,8 @@ import {
applySettings as applySettingsInternal,
loadCron as loadCronInternal,
loadOverview as loadOverviewInternal,
setSessionKey as setSessionKeyInternal,
setTab as setTabInternal,
setTheme as setThemeInternal,
type SessionKeyOptions,
onPopState as onPopStateInternal,
} from "./app-settings";
import {
@@ -299,14 +297,6 @@ export class ClawdbotApp extends LitElement {
setTabInternal(this as unknown as Parameters<typeof setTabInternal>[0], next);
}
setSessionKey(next: string, options?: SessionKeyOptions) {
setSessionKeyInternal(
this as unknown as Parameters<typeof setSessionKeyInternal>[0],
next,
options,
);
}
setTheme(next: ThemeMode, context?: Parameters<typeof setThemeInternal>[2]) {
setThemeInternal(
this as unknown as Parameters<typeof setThemeInternal>[0],
-29
View File
@@ -167,35 +167,6 @@ describe("control UI routing", () => {
expect(window.location.search).toBe("");
});
it("hydrates session from chat URL params", async () => {
const app = mountApp("/chat?session=agent:main");
await app.updateComplete;
expect(app.sessionKey).toBe("agent:main");
expect(window.location.pathname).toBe("/chat");
expect(new URLSearchParams(window.location.search).get("session")).toBe(
"agent:main",
);
});
it("adds session param for chat routes when missing", async () => {
const app = mountApp("/chat");
await app.updateComplete;
expect(app.sessionKey).toBe("main");
expect(window.location.pathname).toBe("/chat");
expect(new URLSearchParams(window.location.search).get("session")).toBe("main");
});
it("strips session params from non-chat routes", async () => {
const app = mountApp("/sessions?session=agent:main");
await app.updateComplete;
expect(app.tab).toBe("sessions");
expect(window.location.pathname).toBe("/sessions");
expect(new URLSearchParams(window.location.search).get("session")).toBeNull();
});
it("hydrates token from URL params even when settings already set", async () => {
localStorage.setItem(
"clawdbot.control.settings.v1",