Merge branch 'main' into perplexity-search

This commit is contained in:
Kesku 2026-01-25 19:17:27 -08:00 committed by GitHub
commit 824eea4b50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 250 additions and 102 deletions

205
.github/labeler.yml vendored
View File

@ -1,109 +1,172 @@
"channel: bluebubbles":
- "extensions/bluebubbles/**"
- "docs/channels/bluebubbles.md"
- changed-files:
- any-glob-to-any-file:
- "extensions/bluebubbles/**"
- "docs/channels/bluebubbles.md"
"channel: discord":
- "src/discord/**"
- "extensions/discord/**"
- "docs/channels/discord.md"
- changed-files:
- any-glob-to-any-file:
- "src/discord/**"
- "extensions/discord/**"
- "docs/channels/discord.md"
"channel: googlechat":
- "extensions/googlechat/**"
- "docs/channels/googlechat.md"
- changed-files:
- any-glob-to-any-file:
- "extensions/googlechat/**"
- "docs/channels/googlechat.md"
"channel: imessage":
- "src/imessage/**"
- "extensions/imessage/**"
- "docs/channels/imessage.md"
- changed-files:
- any-glob-to-any-file:
- "src/imessage/**"
- "extensions/imessage/**"
- "docs/channels/imessage.md"
"channel: line":
- "extensions/line/**"
- changed-files:
- any-glob-to-any-file:
- "extensions/line/**"
"channel: matrix":
- "extensions/matrix/**"
- "docs/channels/matrix.md"
- changed-files:
- any-glob-to-any-file:
- "extensions/matrix/**"
- "docs/channels/matrix.md"
"channel: mattermost":
- "extensions/mattermost/**"
- "docs/channels/mattermost.md"
- changed-files:
- any-glob-to-any-file:
- "extensions/mattermost/**"
- "docs/channels/mattermost.md"
"channel: msteams":
- "extensions/msteams/**"
- "docs/channels/msteams.md"
- changed-files:
- any-glob-to-any-file:
- "extensions/msteams/**"
- "docs/channels/msteams.md"
"channel: nextcloud-talk":
- "extensions/nextcloud-talk/**"
- "docs/channels/nextcloud-talk.md"
- changed-files:
- any-glob-to-any-file:
- "extensions/nextcloud-talk/**"
- "docs/channels/nextcloud-talk.md"
"channel: nostr":
- "extensions/nostr/**"
- "docs/channels/nostr.md"
- changed-files:
- any-glob-to-any-file:
- "extensions/nostr/**"
- "docs/channels/nostr.md"
"channel: signal":
- "src/signal/**"
- "extensions/signal/**"
- "docs/channels/signal.md"
- changed-files:
- any-glob-to-any-file:
- "src/signal/**"
- "extensions/signal/**"
- "docs/channels/signal.md"
"channel: slack":
- "src/slack/**"
- "extensions/slack/**"
- "docs/channels/slack.md"
- changed-files:
- any-glob-to-any-file:
- "src/slack/**"
- "extensions/slack/**"
- "docs/channels/slack.md"
"channel: telegram":
- "src/telegram/**"
- "extensions/telegram/**"
- "docs/channels/telegram.md"
- changed-files:
- any-glob-to-any-file:
- "src/telegram/**"
- "extensions/telegram/**"
- "docs/channels/telegram.md"
"channel: tlon":
- "extensions/tlon/**"
- "docs/channels/tlon.md"
- changed-files:
- any-glob-to-any-file:
- "extensions/tlon/**"
- "docs/channels/tlon.md"
"channel: voice-call":
- "extensions/voice-call/**"
- changed-files:
- any-glob-to-any-file:
- "extensions/voice-call/**"
"channel: whatsapp-web":
- "src/web/**"
- "extensions/whatsapp/**"
- "docs/channels/whatsapp.md"
- changed-files:
- any-glob-to-any-file:
- "src/web/**"
- "extensions/whatsapp/**"
- "docs/channels/whatsapp.md"
"channel: zalo":
- "extensions/zalo/**"
- "docs/channels/zalo.md"
- changed-files:
- any-glob-to-any-file:
- "extensions/zalo/**"
- "docs/channels/zalo.md"
"channel: zalouser":
- "extensions/zalouser/**"
- "docs/channels/zalouser.md"
- changed-files:
- any-glob-to-any-file:
- "extensions/zalouser/**"
- "docs/channels/zalouser.md"
"app: android":
- "apps/android/**"
- "docs/platforms/android.md"
- changed-files:
- any-glob-to-any-file:
- "apps/android/**"
- "docs/platforms/android.md"
"app: ios":
- "apps/ios/**"
- "docs/platforms/ios.md"
- changed-files:
- any-glob-to-any-file:
- "apps/ios/**"
- "docs/platforms/ios.md"
"app: macos":
- "apps/macos/**"
- "docs/platforms/macos.md"
- "docs/platforms/mac/**"
- changed-files:
- any-glob-to-any-file:
- "apps/macos/**"
- "docs/platforms/macos.md"
- "docs/platforms/mac/**"
"app: web-ui":
- "ui/**"
- "src/gateway/control-ui.ts"
- "src/gateway/control-ui-shared.ts"
- "src/infra/control-ui-assets.ts"
"cli":
- "src/cli/**"
- "src/commands/**"
- "src/tui/**"
- changed-files:
- any-glob-to-any-file:
- "ui/**"
- "src/gateway/control-ui.ts"
- "src/gateway/control-ui-shared.ts"
- "src/infra/control-ui-assets.ts"
"gateway":
- "src/gateway/**"
- "src/daemon/**"
- "docs/gateway/**"
- changed-files:
- any-glob-to-any-file:
- "src/gateway/**"
- "src/daemon/**"
- "docs/gateway/**"
"docs":
- "docs/**"
- "docs.acp.md"
- changed-files:
- any-glob-to-any-file:
- "docs/**"
- "docs.acp.md"
"extensions: copilot-proxy":
- "extensions/copilot-proxy/**"
- changed-files:
- any-glob-to-any-file:
- "extensions/copilot-proxy/**"
"extensions: diagnostics-otel":
- "extensions/diagnostics-otel/**"
- changed-files:
- any-glob-to-any-file:
- "extensions/diagnostics-otel/**"
"extensions: google-antigravity-auth":
- "extensions/google-antigravity-auth/**"
- changed-files:
- any-glob-to-any-file:
- "extensions/google-antigravity-auth/**"
"extensions: google-gemini-cli-auth":
- "extensions/google-gemini-cli-auth/**"
- changed-files:
- any-glob-to-any-file:
- "extensions/google-gemini-cli-auth/**"
"extensions: llm-task":
- "extensions/llm-task/**"
- changed-files:
- any-glob-to-any-file:
- "extensions/llm-task/**"
"extensions: lobster":
- "extensions/lobster/**"
- changed-files:
- any-glob-to-any-file:
- "extensions/lobster/**"
"extensions: memory-core":
- "extensions/memory-core/**"
- changed-files:
- any-glob-to-any-file:
- "extensions/memory-core/**"
"extensions: memory-lancedb":
- "extensions/memory-lancedb/**"
- changed-files:
- any-glob-to-any-file:
- "extensions/memory-lancedb/**"
"extensions: open-prose":
- "extensions/open-prose/**"
- changed-files:
- any-glob-to-any-file:
- "extensions/open-prose/**"
"extensions: qwen-portal-auth":
- "extensions/qwen-portal-auth/**"
- changed-files:
- any-glob-to-any-file:
- "extensions/qwen-portal-auth/**"

View File

@ -12,6 +12,12 @@ jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/labeler@v5
with:
configuration-path: .github/labeler.yml
repo-token: ${{ steps.app-token.outputs.token }}

View File

@ -13,6 +13,12 @@ Status: unreleased.
- Docs: add Render deployment guide. (#1975) Thanks @anurag.
- CI: increase Node heap size for macOS checks. (#1890) Thanks @realZachi.
- macOS: avoid crash when rendering code blocks by bumping Textual to 0.3.1. (#2033) Thanks @garricn.
- Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev.
- Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg.
- Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra.
- Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco.
- Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla.
- Routing: precompile session key regexes. (#1697) Thanks @Ray0907.
## 2026.1.24-3

View File

@ -281,6 +281,7 @@ async function loginAntigravity(params: {
openUrl: (url: string) => Promise<void>;
prompt: (message: string) => Promise<string>;
note: (message: string, title?: string) => Promise<void>;
log: (message: string) => void;
progress: { update: (msg: string) => void; stop: (msg?: string) => void };
}): Promise<{
access: string;
@ -314,6 +315,11 @@ async function loginAntigravity(params: {
].join("\n"),
"Google Antigravity OAuth",
);
// Output raw URL below the box for easy copying (fixes #1772)
params.log("");
params.log("Copy this URL:");
params.log(authUrl);
params.log("");
}
if (!needsManual) {
@ -382,6 +388,7 @@ const antigravityPlugin = {
openUrl: ctx.openUrl,
prompt: async (message) => String(await ctx.prompter.text({ message })),
note: ctx.prompter.note,
log: (message) => ctx.runtime.log(message),
progress: spin,
});

View File

@ -369,12 +369,13 @@ export async function runAgentTurnWithFallback(params: {
// Use pipeline if available (block streaming enabled), otherwise send directly
if (params.blockStreamingEnabled && params.blockReplyPipeline) {
params.blockReplyPipeline.enqueue(blockPayload);
} else {
// Send directly when flushing before tool execution (no streaming).
} else if (params.blockStreamingEnabled) {
// Send directly when flushing before tool execution (no pipeline but streaming enabled).
// Track sent key to avoid duplicate in final payloads.
directlySentBlockKeys.add(createBlockReplyPayloadKey(blockPayload));
await params.opts?.onBlockReply?.(blockPayload);
}
// When streaming is disabled entirely, blocks are accumulated in final text instead.
}
: undefined,
onBlockReplyFlush:

View File

@ -337,12 +337,56 @@ async function pageTargetId(page: Page): Promise<string | null> {
}
}
async function findPageByTargetId(browser: Browser, targetId: string): Promise<Page | null> {
async function findPageByTargetId(
browser: Browser,
targetId: string,
cdpUrl?: string,
): Promise<Page | null> {
const pages = await getAllPages(browser);
// First, try the standard CDP session approach
for (const page of pages) {
const tid = await pageTargetId(page).catch(() => null);
if (tid && tid === targetId) return page;
}
// If CDP sessions fail (e.g., extension relay blocks Target.attachToBrowserTarget),
// fall back to URL-based matching using the /json/list endpoint
if (cdpUrl) {
try {
const baseUrl = cdpUrl
.replace(/\/+$/, "")
.replace(/^ws:/, "http:")
.replace(/\/cdp$/, "");
const response = await fetch(`${baseUrl}/json/list`);
if (response.ok) {
const targets = (await response.json()) as Array<{
id: string;
url: string;
title?: string;
}>;
const target = targets.find((t) => t.id === targetId);
if (target) {
// Try to find a page with matching URL
const urlMatch = pages.filter((p) => p.url() === target.url);
if (urlMatch.length === 1) {
return urlMatch[0];
}
// If multiple URL matches, use index-based matching as fallback
// This works when Playwright and the relay enumerate tabs in the same order
if (urlMatch.length > 1) {
const sameUrlTargets = targets.filter((t) => t.url === target.url);
if (sameUrlTargets.length === urlMatch.length) {
const idx = sameUrlTargets.findIndex((t) => t.id === targetId);
if (idx >= 0 && idx < urlMatch.length) {
return urlMatch[idx];
}
}
}
}
}
} catch {
// Ignore fetch errors and fall through to return null
}
}
return null;
}
@ -355,7 +399,7 @@ export async function getPageForTargetId(opts: {
if (!pages.length) throw new Error("No pages available in the connected browser.");
const first = pages[0];
if (!opts.targetId) return first;
const found = await findPageByTargetId(browser, opts.targetId);
const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
if (!found) {
// Extension relays can block CDP attachment APIs (e.g. Target.attachToBrowserTarget),
// which prevents us from resolving a page's targetId via newCDPSession(). If Playwright
@ -496,7 +540,7 @@ export async function closePageByTargetIdViaPlaywright(opts: {
targetId: string;
}): Promise<void> {
const { browser } = await connectBrowser(opts.cdpUrl);
const page = await findPageByTargetId(browser, opts.targetId);
const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
if (!page) {
throw new Error("tab not found");
}
@ -512,7 +556,7 @@ export async function focusPageByTargetIdViaPlaywright(opts: {
targetId: string;
}): Promise<void> {
const { browser } = await connectBrowser(opts.cdpUrl);
const page = await findPageByTargetId(browser, opts.targetId);
const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
if (!page) {
throw new Error("tab not found");
}

View File

@ -13,11 +13,9 @@ const providerId = "telegram";
function readTelegramSendParams(params: Record<string, unknown>) {
const to = readStringParam(params, "to", { required: true });
const mediaUrl = readStringParam(params, "media", { trim: false });
const content =
readStringParam(params, "message", {
required: !mediaUrl,
allowEmpty: true,
}) ?? "";
const message = readStringParam(params, "message", { required: !mediaUrl, allowEmpty: true });
const caption = readStringParam(params, "caption", { allowEmpty: true });
const content = message || caption || "";
const replyTo = readStringParam(params, "replyTo");
const threadId = readStringParam(params, "threadId");
const buttons = params.buttons;

View File

@ -129,9 +129,10 @@ export async function checkGitUpdateStatus(params: {
).catch(() => null);
const upstream = upstreamRes && upstreamRes.code === 0 ? upstreamRes.stdout.trim() : null;
const dirtyRes = await runCommandWithTimeout(["git", "-C", root, "status", "--porcelain"], {
timeoutMs,
}).catch(() => null);
const dirtyRes = await runCommandWithTimeout(
["git", "-C", root, "status", "--porcelain", "--", ":!dist/control-ui/"],
{ timeoutMs },
).catch(() => null);
const dirty = dirtyRes && dirtyRes.code === 0 ? dirtyRes.stdout.trim().length > 0 : null;
const fetchOk = params.fetch

View File

@ -44,7 +44,7 @@ describe("runGatewayUpdate", () => {
[`git -C ${tempDir} rev-parse --show-toplevel`]: { stdout: tempDir },
[`git -C ${tempDir} rev-parse HEAD`]: { stdout: "abc123" },
[`git -C ${tempDir} rev-parse --abbrev-ref HEAD`]: { stdout: "main" },
[`git -C ${tempDir} status --porcelain`]: { stdout: " M README.md" },
[`git -C ${tempDir} status --porcelain -- :!dist/control-ui/`]: { stdout: " M README.md" },
});
const result = await runGatewayUpdate({
@ -69,7 +69,7 @@ describe("runGatewayUpdate", () => {
[`git -C ${tempDir} rev-parse --show-toplevel`]: { stdout: tempDir },
[`git -C ${tempDir} rev-parse HEAD`]: { stdout: "abc123" },
[`git -C ${tempDir} rev-parse --abbrev-ref HEAD`]: { stdout: "main" },
[`git -C ${tempDir} status --porcelain`]: { stdout: "" },
[`git -C ${tempDir} status --porcelain -- :!dist/control-ui/`]: { stdout: "" },
[`git -C ${tempDir} rev-parse --abbrev-ref --symbolic-full-name @{upstream}`]: {
stdout: "origin/main",
},
@ -103,7 +103,7 @@ describe("runGatewayUpdate", () => {
const { runner, calls } = createRunner({
[`git -C ${tempDir} rev-parse --show-toplevel`]: { stdout: tempDir },
[`git -C ${tempDir} rev-parse HEAD`]: { stdout: "abc123" },
[`git -C ${tempDir} status --porcelain`]: { stdout: "" },
[`git -C ${tempDir} status --porcelain -- :!dist/control-ui/`]: { stdout: "" },
[`git -C ${tempDir} fetch --all --prune --tags`]: { stdout: "" },
[`git -C ${tempDir} tag --list v* --sort=-v:refname`]: {
stdout: `${stableTag}\n${betaTag}\n`,
@ -112,6 +112,7 @@ describe("runGatewayUpdate", () => {
"pnpm install": { stdout: "" },
"pnpm build": { stdout: "" },
"pnpm ui:build": { stdout: "" },
[`git -C ${tempDir} checkout -- dist/control-ui/`]: { stdout: "" },
"pnpm clawdbot doctor --non-interactive": { stdout: "" },
});

View File

@ -346,10 +346,14 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
const channel: UpdateChannel = opts.channel ?? "dev";
const branch = channel === "dev" ? await readBranchName(runCommand, gitRoot, timeoutMs) : null;
const needsCheckoutMain = channel === "dev" && branch !== DEV_BRANCH;
gitTotalSteps = channel === "dev" ? (needsCheckoutMain ? 10 : 9) : 8;
gitTotalSteps = channel === "dev" ? (needsCheckoutMain ? 11 : 10) : 9;
const statusCheck = await runStep(
step("clean check", ["git", "-C", gitRoot, "status", "--porcelain"], gitRoot),
step(
"clean check",
["git", "-C", gitRoot, "status", "--porcelain", "--", ":!dist/control-ui/"],
gitRoot,
),
);
steps.push(statusCheck);
const hasUncommittedChanges =
@ -654,6 +658,17 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
);
steps.push(uiBuildStep);
// Restore dist/control-ui/ to committed state to prevent dirty repo after update
// (ui:build regenerates assets with new hashes, which would block future updates)
const restoreUiStep = await runStep(
step(
"restore control-ui",
["git", "-C", gitRoot, "checkout", "--", "dist/control-ui/"],
gitRoot,
),
);
steps.push(restoreUiStep);
const doctorStep = await runStep(
step(
"clawdbot doctor",

View File

@ -11,6 +11,12 @@ export const DEFAULT_AGENT_ID = "main";
export const DEFAULT_MAIN_KEY = "main";
export const DEFAULT_ACCOUNT_ID = "default";
// Pre-compiled regex
const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
const INVALID_CHARS_RE = /[^a-z0-9_-]+/g;
const LEADING_DASH_RE = /^-+/;
const TRAILING_DASH_RE = /-+$/;
function normalizeToken(value: string | undefined | null): string {
return (value ?? "").trim().toLowerCase();
}
@ -52,14 +58,14 @@ export function normalizeAgentId(value: string | undefined | null): string {
const trimmed = (value ?? "").trim();
if (!trimmed) return DEFAULT_AGENT_ID;
// Keep it path-safe + shell-friendly.
if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) return trimmed.toLowerCase();
if (VALID_ID_RE.test(trimmed)) return trimmed.toLowerCase();
// Best-effort fallback: collapse invalid characters to "-"
return (
trimmed
.toLowerCase()
.replace(/[^a-z0-9_-]+/g, "-")
.replace(/^-+/, "")
.replace(/-+$/, "")
.replace(INVALID_CHARS_RE, "-")
.replace(LEADING_DASH_RE, "")
.replace(TRAILING_DASH_RE, "")
.slice(0, 64) || DEFAULT_AGENT_ID
);
}
@ -67,13 +73,13 @@ export function normalizeAgentId(value: string | undefined | null): string {
export function sanitizeAgentId(value: string | undefined | null): string {
const trimmed = (value ?? "").trim();
if (!trimmed) return DEFAULT_AGENT_ID;
if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) return trimmed.toLowerCase();
if (VALID_ID_RE.test(trimmed)) return trimmed.toLowerCase();
return (
trimmed
.toLowerCase()
.replace(/[^a-z0-9_-]+/gi, "-")
.replace(/^-+/, "")
.replace(/-+$/, "")
.replace(INVALID_CHARS_RE, "-")
.replace(LEADING_DASH_RE, "")
.replace(TRAILING_DASH_RE, "")
.slice(0, 64) || DEFAULT_AGENT_ID
);
}
@ -81,13 +87,13 @@ export function sanitizeAgentId(value: string | undefined | null): string {
export function normalizeAccountId(value: string | undefined | null): string {
const trimmed = (value ?? "").trim();
if (!trimmed) return DEFAULT_ACCOUNT_ID;
if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) return trimmed.toLowerCase();
if (VALID_ID_RE.test(trimmed)) return trimmed.toLowerCase();
return (
trimmed
.toLowerCase()
.replace(/[^a-z0-9_-]+/g, "-")
.replace(/^-+/, "")
.replace(/-+$/, "")
.replace(INVALID_CHARS_RE, "-")
.replace(LEADING_DASH_RE, "")
.replace(TRAILING_DASH_RE, "")
.slice(0, 64) || DEFAULT_ACCOUNT_ID
);
}