diff --git a/.github/labeler.yml b/.github/labeler.yml index 0c3d863cf..5b34c41e0 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -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/**" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 6ec73a1a3..8d078774b 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -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 }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 19cea8844..e39c291d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/extensions/google-antigravity-auth/index.ts b/extensions/google-antigravity-auth/index.ts index d6902bffe..f349ada6a 100644 --- a/extensions/google-antigravity-auth/index.ts +++ b/extensions/google-antigravity-auth/index.ts @@ -281,6 +281,7 @@ async function loginAntigravity(params: { openUrl: (url: string) => Promise; prompt: (message: string) => Promise; note: (message: string, title?: string) => Promise; + 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, }); diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 47c45b09d..939fa92f0 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -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: diff --git a/src/browser/pw-session.ts b/src/browser/pw-session.ts index 0c7fa9f48..e1dbcf7a1 100644 --- a/src/browser/pw-session.ts +++ b/src/browser/pw-session.ts @@ -337,12 +337,56 @@ async function pageTargetId(page: Page): Promise { } } -async function findPageByTargetId(browser: Browser, targetId: string): Promise { +async function findPageByTargetId( + browser: Browser, + targetId: string, + cdpUrl?: string, +): Promise { 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 { 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 { 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"); } diff --git a/src/channels/plugins/actions/telegram.ts b/src/channels/plugins/actions/telegram.ts index 18a11c797..fe4e41307 100644 --- a/src/channels/plugins/actions/telegram.ts +++ b/src/channels/plugins/actions/telegram.ts @@ -13,11 +13,9 @@ const providerId = "telegram"; function readTelegramSendParams(params: Record) { 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; diff --git a/src/infra/update-check.ts b/src/infra/update-check.ts index 2e020ff8d..518da3c28 100644 --- a/src/infra/update-check.ts +++ b/src/infra/update-check.ts @@ -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 diff --git a/src/infra/update-runner.test.ts b/src/infra/update-runner.test.ts index e33159326..6bf450d83 100644 --- a/src/infra/update-runner.test.ts +++ b/src/infra/update-runner.test.ts @@ -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: "" }, }); diff --git a/src/infra/update-runner.ts b/src/infra/update-runner.ts index 0a5196fd7..c73c3a7e7 100644 --- a/src/infra/update-runner.ts +++ b/src/infra/update-runner.ts @@ -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", diff --git a/src/routing/session-key.ts b/src/routing/session-key.ts index 028e657cb..7f9f209ed 100644 --- a/src/routing/session-key.ts +++ b/src/routing/session-key.ts @@ -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 ); }