Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4e30d559b |
No files matched your search
@@ -57,7 +57,6 @@
|
|||||||
- Changelog workflow: keep latest released version at top (no `Unreleased`); after publishing, bump version and start a new top section.
|
- 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.
|
- 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.
|
- 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`.
|
- 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.
|
- 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.
|
- When working on a PR: add a changelog entry with the PR number and thank the contributor.
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ Docs: https://docs.clawd.bot
|
|||||||
### Changes
|
### Changes
|
||||||
- Android: remove legacy bridge transport code now that nodes use the gateway protocol.
|
- 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.
|
- 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
|
### Fixes
|
||||||
- Gateway: strip inbound envelope headers from chat history messages to keep clients clean.
|
- 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: 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.
|
|
||||||
|
|
||||||
## 2026.1.19-2
|
## 2026.1.19-2
|
||||||
|
|
||||||
|
|||||||
+1
-3
@@ -28,11 +28,9 @@ export AWS_REGION="us-east-1"
|
|||||||
# Optional:
|
# Optional:
|
||||||
export AWS_SESSION_TOKEN="..."
|
export AWS_SESSION_TOKEN="..."
|
||||||
export AWS_PROFILE="your-profile"
|
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
|
```json5
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/etc.)? See [Chann
|
|||||||
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
||||||
- [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot)
|
- [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot)
|
||||||
- [OpenCode Zen](/providers/opencode)
|
- [OpenCode Zen](/providers/opencode)
|
||||||
- [Amazon Bedrock](/bedrock)
|
|
||||||
- [Z.AI](/providers/zai)
|
- [Z.AI](/providers/zai)
|
||||||
- [GLM models](/providers/glm)
|
- [GLM models](/providers/glm)
|
||||||
- [MiniMax](/providers/minimax)
|
- [MiniMax](/providers/minimax)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function resolveModel(
|
|||||||
if (!model) {
|
if (!model) {
|
||||||
const providers = cfg?.models?.providers ?? {};
|
const providers = cfg?.models?.providers ?? {};
|
||||||
const inlineModels =
|
const inlineModels =
|
||||||
providers[provider]?.models?.map((entry) => ({ ...entry, provider })) ??
|
providers[provider]?.models ??
|
||||||
Object.values(providers)
|
Object.values(providers)
|
||||||
.flatMap((entry) => entry?.models ?? [])
|
.flatMap((entry) => entry?.models ?? [])
|
||||||
.map((entry) => ({ ...entry, provider }));
|
.map((entry) => ({ ...entry, provider }));
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export async function checkGatewayHealth(params: {
|
|||||||
typeof params.timeoutMs === "number" && params.timeoutMs > 0 ? params.timeoutMs : 10_000;
|
typeof params.timeoutMs === "number" && params.timeoutMs > 0 ? params.timeoutMs : 10_000;
|
||||||
let healthOk = false;
|
let healthOk = false;
|
||||||
try {
|
try {
|
||||||
await healthCommand({ json: false, timeoutMs, config: params.cfg }, params.runtime);
|
await healthCommand({ json: false, timeoutMs }, params.runtime);
|
||||||
healthOk = true;
|
healthOk = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = String(err);
|
const message = String(err);
|
||||||
|
|||||||
+1
-10
@@ -10,7 +10,7 @@ import {
|
|||||||
resolveHooksGmailModel,
|
resolveHooksGmailModel,
|
||||||
} from "../agents/model-selection.js";
|
} from "../agents/model-selection.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { CONFIG_PATH_CLAWDBOT, readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
|
import { CONFIG_PATH_CLAWDBOT, writeConfigFile } from "../config/config.js";
|
||||||
import { resolveGatewayService } from "../daemon/service.js";
|
import { resolveGatewayService } from "../daemon/service.js";
|
||||||
import { buildGatewayConnectionDetails } from "../gateway/call.js";
|
import { buildGatewayConnectionDetails } from "../gateway/call.js";
|
||||||
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js";
|
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js";
|
||||||
@@ -269,14 +269,5 @@ export async function doctorCommand(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalSnapshot = await readConfigFileSnapshot();
|
|
||||||
if (finalSnapshot.exists && !finalSnapshot.valid) {
|
|
||||||
runtime.error("Invalid config:");
|
|
||||||
for (const issue of finalSnapshot.issues) {
|
|
||||||
const path = issue.path || "<root>";
|
|
||||||
runtime.error(`- ${path}: ${issue.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
outro("Doctor complete.");
|
outro("Doctor complete.");
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,6 @@ import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
|||||||
import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
|
import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
|
||||||
import type { ChannelAccountSnapshot } from "../channels/plugins/types.js";
|
import type { ChannelAccountSnapshot } from "../channels/plugins/types.js";
|
||||||
import { withProgress } from "../cli/progress.js";
|
import { withProgress } from "../cli/progress.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
|
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
|
||||||
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
||||||
@@ -502,10 +501,9 @@ export async function getHealthSnapshot(params?: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function healthCommand(
|
export async function healthCommand(
|
||||||
opts: { json?: boolean; timeoutMs?: number; verbose?: boolean; config?: ClawdbotConfig },
|
opts: { json?: boolean; timeoutMs?: number; verbose?: boolean },
|
||||||
runtime: RuntimeEnv,
|
runtime: RuntimeEnv,
|
||||||
) {
|
) {
|
||||||
const cfg = opts.config ?? loadConfig();
|
|
||||||
// Always query the running gateway; do not open a direct Baileys socket here.
|
// Always query the running gateway; do not open a direct Baileys socket here.
|
||||||
const summary = await withProgress(
|
const summary = await withProgress(
|
||||||
{
|
{
|
||||||
@@ -518,7 +516,6 @@ export async function healthCommand(
|
|||||||
method: "health",
|
method: "health",
|
||||||
params: opts.verbose ? { probe: true } : undefined,
|
params: opts.verbose ? { probe: true } : undefined,
|
||||||
timeoutMs: opts.timeoutMs,
|
timeoutMs: opts.timeoutMs,
|
||||||
config: cfg,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
// Gateway reachability defines success; channel issues are reported but not fatal here.
|
// Gateway reachability defines success; channel issues are reported but not fatal here.
|
||||||
@@ -529,12 +526,13 @@ export async function healthCommand(
|
|||||||
} else {
|
} else {
|
||||||
const debugEnabled = isTruthyEnvValue(process.env.CLAWDBOT_DEBUG_HEALTH);
|
const debugEnabled = isTruthyEnvValue(process.env.CLAWDBOT_DEBUG_HEALTH);
|
||||||
if (opts.verbose) {
|
if (opts.verbose) {
|
||||||
const details = buildGatewayConnectionDetails({ config: cfg });
|
const details = buildGatewayConnectionDetails();
|
||||||
runtime.log(info("Gateway connection:"));
|
runtime.log(info("Gateway connection:"));
|
||||||
for (const line of details.message.split("\n")) {
|
for (const line of details.message.split("\n")) {
|
||||||
runtime.log(` ${line}`);
|
runtime.log(` ${line}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const cfg = loadConfig();
|
||||||
const localAgents = resolveAgentOrder(cfg);
|
const localAgents = resolveAgentOrder(cfg);
|
||||||
const defaultAgentId = summary.defaultAgentId ?? localAgents.defaultAgentId;
|
const defaultAgentId = summary.defaultAgentId ?? localAgents.defaultAgentId;
|
||||||
const agents = Array.isArray(summary.agents) ? summary.agents : [];
|
const agents = Array.isArray(summary.agents) ? summary.agents : [];
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ export type ModelApi =
|
|||||||
| "openai-responses"
|
| "openai-responses"
|
||||||
| "anthropic-messages"
|
| "anthropic-messages"
|
||||||
| "google-generative-ai"
|
| "google-generative-ai"
|
||||||
| "github-copilot"
|
| "github-copilot";
|
||||||
| "bedrock-converse-stream";
|
|
||||||
|
|
||||||
export type ModelCompatConfig = {
|
export type ModelCompatConfig = {
|
||||||
supportsStore?: boolean;
|
supportsStore?: boolean;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export const ModelApiSchema = z.union([
|
|||||||
z.literal("anthropic-messages"),
|
z.literal("anthropic-messages"),
|
||||||
z.literal("google-generative-ai"),
|
z.literal("google-generative-ai"),
|
||||||
z.literal("github-copilot"),
|
z.literal("github-copilot"),
|
||||||
z.literal("bedrock-converse-stream"),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const ModelCompatSchema = z
|
export const ModelCompatSchema = z
|
||||||
|
|||||||
+1
-2
@@ -22,7 +22,6 @@ export type CallGatewayOptions = {
|
|||||||
url?: string;
|
url?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
config?: ClawdbotConfig;
|
|
||||||
method: string;
|
method: string;
|
||||||
params?: unknown;
|
params?: unknown;
|
||||||
expectFinal?: boolean;
|
expectFinal?: boolean;
|
||||||
@@ -110,7 +109,7 @@ export function buildGatewayConnectionDetails(
|
|||||||
|
|
||||||
export async function callGateway<T = unknown>(opts: CallGatewayOptions): Promise<T> {
|
export async function callGateway<T = unknown>(opts: CallGatewayOptions): Promise<T> {
|
||||||
const timeoutMs = opts.timeoutMs ?? 10_000;
|
const timeoutMs = opts.timeoutMs ?? 10_000;
|
||||||
const config = opts.config ?? loadConfig();
|
const config = loadConfig();
|
||||||
const isRemoteMode = config.gateway?.mode === "remote";
|
const isRemoteMode = config.gateway?.mode === "remote";
|
||||||
const remote = isRemoteMode ? config.gateway?.remote : undefined;
|
const remote = isRemoteMode ? config.gateway?.remote : undefined;
|
||||||
const urlOverride =
|
const urlOverride =
|
||||||
|
|||||||
@@ -22,17 +22,9 @@ import {
|
|||||||
|
|
||||||
installGatewayTestHooks();
|
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", () => {
|
describe("gateway update.run", () => {
|
||||||
it("writes sentinel and schedules restart", async () => {
|
it("writes sentinel and schedules restart", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
const sigusr1 = vi.fn();
|
const sigusr1 = vi.fn();
|
||||||
process.on("SIGUSR1", sigusr1);
|
process.on("SIGUSR1", sigusr1);
|
||||||
|
|
||||||
@@ -57,7 +49,7 @@ describe("gateway update.run", () => {
|
|||||||
);
|
);
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
|
|
||||||
await waitForSignal(() => sigusr1.mock.calls.length > 0);
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
expect(sigusr1).toHaveBeenCalled();
|
expect(sigusr1).toHaveBeenCalled();
|
||||||
|
|
||||||
const sentinelPath = path.join(os.homedir(), ".clawdbot", "restart-sentinel.json");
|
const sentinelPath = path.join(os.homedir(), ".clawdbot", "restart-sentinel.json");
|
||||||
@@ -71,5 +63,6 @@ describe("gateway update.run", () => {
|
|||||||
ws.close();
|
ws.close();
|
||||||
await server.close();
|
await server.close();
|
||||||
process.off("SIGUSR1", sigusr1);
|
process.off("SIGUSR1", sigusr1);
|
||||||
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { Tab } from "./navigation";
|
import type { Tab } from "./navigation";
|
||||||
import { connectGateway } from "./app-gateway";
|
import { connectGateway } from "./app-gateway";
|
||||||
import {
|
import {
|
||||||
applyStateFromLocation,
|
applySettingsFromUrl,
|
||||||
attachThemeListener,
|
attachThemeListener,
|
||||||
detachThemeListener,
|
detachThemeListener,
|
||||||
inferBasePath,
|
inferBasePath,
|
||||||
|
syncTabWithLocation,
|
||||||
syncThemeWithSettings,
|
syncThemeWithSettings,
|
||||||
} from "./app-settings";
|
} from "./app-settings";
|
||||||
import { observeTopbar, scheduleChatScroll, scheduleLogsScroll } from "./app-scroll";
|
import { observeTopbar, scheduleChatScroll, scheduleLogsScroll } from "./app-scroll";
|
||||||
@@ -32,9 +33,9 @@ type LifecycleHost = {
|
|||||||
|
|
||||||
export function handleConnected(host: LifecycleHost) {
|
export function handleConnected(host: LifecycleHost) {
|
||||||
host.basePath = inferBasePath();
|
host.basePath = inferBasePath();
|
||||||
applyStateFromLocation(
|
syncTabWithLocation(
|
||||||
host as unknown as Parameters<typeof applyStateFromLocation>[0],
|
host as unknown as Parameters<typeof syncTabWithLocation>[0],
|
||||||
{ replace: true },
|
true,
|
||||||
);
|
);
|
||||||
syncThemeWithSettings(
|
syncThemeWithSettings(
|
||||||
host as unknown as Parameters<typeof syncThemeWithSettings>[0],
|
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],
|
host as unknown as Parameters<typeof attachThemeListener>[0],
|
||||||
);
|
);
|
||||||
window.addEventListener("popstate", host.popStateHandler);
|
window.addEventListener("popstate", host.popStateHandler);
|
||||||
|
applySettingsFromUrl(
|
||||||
|
host as unknown as Parameters<typeof applySettingsFromUrl>[0],
|
||||||
|
);
|
||||||
connectGateway(host as unknown as Parameters<typeof connectGateway>[0]);
|
connectGateway(host as unknown as Parameters<typeof connectGateway>[0]);
|
||||||
startNodesPolling(host as unknown as Parameters<typeof startNodesPolling>[0]);
|
startNodesPolling(host as unknown as Parameters<typeof startNodesPolling>[0]);
|
||||||
if (host.tab === "logs") {
|
if (host.tab === "logs") {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
import { repeat } from "lit/directives/repeat.js";
|
|
||||||
|
|
||||||
import type { AppViewState } from "./app-view-state";
|
import type { AppViewState } from "./app-view-state";
|
||||||
import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation";
|
import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation";
|
||||||
@@ -53,18 +52,22 @@ export function renderChatControls(state: AppViewState) {
|
|||||||
?disabled=${!state.connected}
|
?disabled=${!state.connected}
|
||||||
@change=${(e: Event) => {
|
@change=${(e: Event) => {
|
||||||
const next = (e.target as HTMLSelectElement).value;
|
const next = (e.target as HTMLSelectElement).value;
|
||||||
state.setSessionKey(next, {
|
state.sessionKey = next;
|
||||||
source: "user",
|
state.chatMessage = "";
|
||||||
resetChat: true,
|
state.chatStream = null;
|
||||||
loadHistory: true,
|
state.chatStreamStartedAt = null;
|
||||||
syncUrl: true,
|
state.chatRunId = null;
|
||||||
replace: true,
|
state.resetToolStream();
|
||||||
|
state.resetChatScroll();
|
||||||
|
state.applySettings({
|
||||||
|
...state.settings,
|
||||||
|
sessionKey: next,
|
||||||
|
lastActiveSessionKey: next,
|
||||||
});
|
});
|
||||||
|
void loadChatHistory(state);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${repeat(
|
${sessionOptions.map(
|
||||||
sessionOptions,
|
|
||||||
(entry) => entry.key,
|
|
||||||
(entry) =>
|
(entry) =>
|
||||||
html`<option value=${entry.key}>
|
html`<option value=${entry.key}>
|
||||||
${entry.displayName ?? entry.key}
|
${entry.displayName ?? entry.key}
|
||||||
@@ -116,11 +119,9 @@ function resolveSessionOptions(sessionKey: string, sessions: SessionsListResult
|
|||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const options: Array<{ key: string; displayName?: string }> = [];
|
const options: Array<{ key: string; displayName?: string }> = [];
|
||||||
|
|
||||||
const resolvedCurrent = sessions?.sessions?.find((s) => s.key === sessionKey);
|
|
||||||
|
|
||||||
// Add current session key first
|
// Add current session key first
|
||||||
seen.add(sessionKey);
|
seen.add(sessionKey);
|
||||||
options.push({ key: sessionKey, displayName: resolvedCurrent?.displayName });
|
options.push({ key: sessionKey });
|
||||||
|
|
||||||
// Add sessions from the result
|
// Add sessions from the result
|
||||||
if (sessions?.sessions) {
|
if (sessions?.sessions) {
|
||||||
|
|||||||
+19
-13
@@ -183,14 +183,14 @@ export function renderApp(state: AppViewState) {
|
|||||||
onSettingsChange: (next) => state.applySettings(next),
|
onSettingsChange: (next) => state.applySettings(next),
|
||||||
onPasswordChange: (next) => (state.password = next),
|
onPasswordChange: (next) => (state.password = next),
|
||||||
onSessionKeyChange: (next) => {
|
onSessionKeyChange: (next) => {
|
||||||
state.setSessionKey(next, {
|
state.sessionKey = next;
|
||||||
source: "settings",
|
|
||||||
resetChat: false,
|
|
||||||
loadHistory: false,
|
|
||||||
syncUrl: false,
|
|
||||||
});
|
|
||||||
state.chatMessage = "";
|
state.chatMessage = "";
|
||||||
state.resetToolStream();
|
state.resetToolStream();
|
||||||
|
state.applySettings({
|
||||||
|
...state.settings,
|
||||||
|
sessionKey: next,
|
||||||
|
lastActiveSessionKey: next,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onConnect: () => state.connect(),
|
onConnect: () => state.connect(),
|
||||||
onRefresh: () => state.loadOverview(),
|
onRefresh: () => state.loadOverview(),
|
||||||
@@ -366,14 +366,20 @@ export function renderApp(state: AppViewState) {
|
|||||||
? renderChat({
|
? renderChat({
|
||||||
sessionKey: state.sessionKey,
|
sessionKey: state.sessionKey,
|
||||||
onSessionKeyChange: (next) => {
|
onSessionKeyChange: (next) => {
|
||||||
state.setSessionKey(next, {
|
state.sessionKey = next;
|
||||||
source: "user",
|
state.chatMessage = "";
|
||||||
resetChat: true,
|
state.chatStream = null;
|
||||||
loadHistory: true,
|
state.chatStreamStartedAt = null;
|
||||||
syncUrl: true,
|
state.chatRunId = null;
|
||||||
replace: true,
|
state.chatQueue = [];
|
||||||
clearQueue: true,
|
state.resetToolStream();
|
||||||
|
state.resetChatScroll();
|
||||||
|
state.applySettings({
|
||||||
|
...state.settings,
|
||||||
|
sessionKey: next,
|
||||||
|
lastActiveSessionKey: next,
|
||||||
});
|
});
|
||||||
|
void loadChatHistory(state);
|
||||||
},
|
},
|
||||||
thinkingLevel: state.chatThinkingLevel,
|
thinkingLevel: state.chatThinkingLevel,
|
||||||
loading: state.chatLoading,
|
loading: state.chatLoading,
|
||||||
|
|||||||
+33
-124
@@ -1,4 +1,3 @@
|
|||||||
import { loadChatHistory } from "./controllers/chat";
|
|
||||||
import { loadConfig, loadConfigSchema } from "./controllers/config";
|
import { loadConfig, loadConfigSchema } from "./controllers/config";
|
||||||
import { loadCronJobs, loadCronStatus } from "./controllers/cron";
|
import { loadCronJobs, loadCronStatus } from "./controllers/cron";
|
||||||
import { loadChannels } from "./controllers/channels";
|
import { loadChannels } from "./controllers/channels";
|
||||||
@@ -17,7 +16,6 @@ import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll";
|
|||||||
import { startLogsPolling, stopLogsPolling } from "./app-polling";
|
import { startLogsPolling, stopLogsPolling } from "./app-polling";
|
||||||
import { refreshChat } from "./app-chat";
|
import { refreshChat } from "./app-chat";
|
||||||
import type { ClawdbotApp } from "./app";
|
import type { ClawdbotApp } from "./app";
|
||||||
import type { ChatQueueItem } from "./ui-types";
|
|
||||||
|
|
||||||
type SettingsHost = {
|
type SettingsHost = {
|
||||||
settings: UiSettings;
|
settings: UiSettings;
|
||||||
@@ -36,25 +34,6 @@ type SettingsHost = {
|
|||||||
themeMediaHandler: ((event: MediaQueryListEvent) => void) | null;
|
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) {
|
export function applySettings(host: SettingsHost, next: UiSettings) {
|
||||||
const normalized = {
|
const normalized = {
|
||||||
...next,
|
...next,
|
||||||
@@ -76,85 +55,13 @@ export function setLastActiveSessionKey(host: SettingsHost, next: string) {
|
|||||||
applySettings(host, { ...host.settings, lastActiveSessionKey: trimmed });
|
applySettings(host, { ...host.settings, lastActiveSessionKey: trimmed });
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSessionSettings(host: SettingsHost, next: string) {
|
export function applySettingsFromUrl(host: SettingsHost) {
|
||||||
const trimmed = next.trim();
|
if (!window.location.search) return;
|
||||||
if (!trimmed) return;
|
const params = new URLSearchParams(window.location.search);
|
||||||
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;
|
|
||||||
|
|
||||||
const tokenRaw = params.get("token");
|
const tokenRaw = params.get("token");
|
||||||
const passwordRaw = params.get("password");
|
const passwordRaw = params.get("password");
|
||||||
const sessionRaw = params.get("session");
|
const sessionRaw = params.get("session");
|
||||||
|
let shouldCleanUrl = false;
|
||||||
|
|
||||||
if (tokenRaw != null) {
|
if (tokenRaw != null) {
|
||||||
const token = tokenRaw.trim();
|
const token = tokenRaw.trim();
|
||||||
@@ -162,6 +69,7 @@ export function applyStateFromLocation(
|
|||||||
applySettings(host, { ...host.settings, token });
|
applySettings(host, { ...host.settings, token });
|
||||||
}
|
}
|
||||||
params.delete("token");
|
params.delete("token");
|
||||||
|
shouldCleanUrl = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (passwordRaw != null) {
|
if (passwordRaw != null) {
|
||||||
@@ -170,34 +78,22 @@ export function applyStateFromLocation(
|
|||||||
(host as { password: string }).password = password;
|
(host as { password: string }).password = password;
|
||||||
}
|
}
|
||||||
params.delete("password");
|
params.delete("password");
|
||||||
|
shouldCleanUrl = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolved = tabFromPath(url.pathname, host.basePath) ?? "chat";
|
if (sessionRaw != null) {
|
||||||
const session = sessionRaw?.trim() ?? "";
|
const session = sessionRaw.trim();
|
||||||
if (resolved === "chat") {
|
|
||||||
if (session) {
|
if (session) {
|
||||||
setSessionKey(host, session, {
|
host.sessionKey = session;
|
||||||
source: "route",
|
|
||||||
syncUrl: false,
|
|
||||||
resetChat: false,
|
|
||||||
loadHistory: false,
|
|
||||||
});
|
|
||||||
} else if (sessionRaw != null) {
|
|
||||||
params.delete("session");
|
|
||||||
}
|
}
|
||||||
} else if (sessionRaw != null) {
|
|
||||||
params.delete("session");
|
params.delete("session");
|
||||||
|
shouldCleanUrl = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTabFromRoute(host, resolved);
|
if (!shouldCleanUrl) return;
|
||||||
|
const url = new URL(window.location.href);
|
||||||
const targetUrl = buildUrlForTab(host, resolved, url).toString();
|
url.search = params.toString();
|
||||||
if (targetUrl === currentHref) return;
|
window.history.replaceState({}, "", url.toString());
|
||||||
if (options.replace) {
|
|
||||||
window.history.replaceState({}, "", targetUrl);
|
|
||||||
} else {
|
|
||||||
window.history.pushState({}, "", targetUrl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTab(host: SettingsHost, next: Tab) {
|
export function setTab(host: SettingsHost, next: Tab) {
|
||||||
@@ -318,8 +214,18 @@ export function detachThemeListener(host: SettingsHost) {
|
|||||||
host.themeMediaHandler = null;
|
host.themeMediaHandler = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onPopState(host: SessionKeyHost) {
|
export function syncTabWithLocation(host: SettingsHost, replace: boolean) {
|
||||||
applyStateFromLocation(host, { replace: true });
|
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) {
|
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) {
|
export function syncUrlWithTab(host: SettingsHost, tab: Tab, replace: boolean) {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
const targetUrl = buildUrlForTab(host, tab).toString();
|
const targetPath = normalizePath(pathForTab(tab, host.basePath));
|
||||||
if (targetUrl === window.location.href) return;
|
const currentPath = normalizePath(window.location.pathname);
|
||||||
|
if (currentPath === targetPath) return;
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.pathname = targetPath;
|
||||||
if (replace) {
|
if (replace) {
|
||||||
window.history.replaceState({}, "", targetUrl);
|
window.history.replaceState({}, "", url.toString());
|
||||||
} else {
|
} else {
|
||||||
window.history.pushState({}, "", targetUrl);
|
window.history.pushState({}, "", url.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway";
|
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway";
|
||||||
import type { Tab } from "./navigation";
|
import type { Tab } from "./navigation";
|
||||||
import type { SessionKeyOptions } from "./app-settings";
|
|
||||||
import type { UiSettings } from "./storage";
|
import type { UiSettings } from "./storage";
|
||||||
import type { ThemeMode } from "./theme";
|
import type { ThemeMode } from "./theme";
|
||||||
import type { ThemeTransitionContext } from "./theme-transition";
|
import type { ThemeTransitionContext } from "./theme-transition";
|
||||||
@@ -161,7 +160,7 @@ export type AppViewState = {
|
|||||||
handleDebugCall: () => Promise<void>;
|
handleDebugCall: () => Promise<void>;
|
||||||
handleRunUpdate: () => Promise<void>;
|
handleRunUpdate: () => Promise<void>;
|
||||||
setPassword: (next: string) => void;
|
setPassword: (next: string) => void;
|
||||||
setSessionKey: (next: string, options?: SessionKeyOptions) => void;
|
setSessionKey: (next: string) => void;
|
||||||
setChatMessage: (next: string) => void;
|
setChatMessage: (next: string) => void;
|
||||||
handleChatSend: () => Promise<void>;
|
handleChatSend: () => Promise<void>;
|
||||||
handleChatAbort: () => Promise<void>;
|
handleChatAbort: () => Promise<void>;
|
||||||
|
|||||||
@@ -50,10 +50,8 @@ import {
|
|||||||
applySettings as applySettingsInternal,
|
applySettings as applySettingsInternal,
|
||||||
loadCron as loadCronInternal,
|
loadCron as loadCronInternal,
|
||||||
loadOverview as loadOverviewInternal,
|
loadOverview as loadOverviewInternal,
|
||||||
setSessionKey as setSessionKeyInternal,
|
|
||||||
setTab as setTabInternal,
|
setTab as setTabInternal,
|
||||||
setTheme as setThemeInternal,
|
setTheme as setThemeInternal,
|
||||||
type SessionKeyOptions,
|
|
||||||
onPopState as onPopStateInternal,
|
onPopState as onPopStateInternal,
|
||||||
} from "./app-settings";
|
} from "./app-settings";
|
||||||
import {
|
import {
|
||||||
@@ -299,14 +297,6 @@ export class ClawdbotApp extends LitElement {
|
|||||||
setTabInternal(this as unknown as Parameters<typeof setTabInternal>[0], next);
|
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]) {
|
setTheme(next: ThemeMode, context?: Parameters<typeof setThemeInternal>[2]) {
|
||||||
setThemeInternal(
|
setThemeInternal(
|
||||||
this as unknown as Parameters<typeof setThemeInternal>[0],
|
this as unknown as Parameters<typeof setThemeInternal>[0],
|
||||||
|
|||||||
@@ -167,35 +167,6 @@ describe("control UI routing", () => {
|
|||||||
expect(window.location.search).toBe("");
|
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 () => {
|
it("hydrates token from URL params even when settings already set", async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"clawdbot.control.settings.v1",
|
"clawdbot.control.settings.v1",
|
||||||
|
|||||||
Reference in new issue
Block a user