From 7187c3d06765c9d3a7b1de40430fe1567b174131 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:17:42 -0600 Subject: [PATCH 1/6] TUI: guard against overflow width crashes (#1686) Co-authored-by: Mohammad Jafari --- CHANGELOG.md | 1 + src/tui/components/filterable-select-list.ts | 2 +- src/tui/components/searchable-select-list.ts | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e39c291d2..480767383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Status: unreleased. - 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. +- TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein. ## 2026.1.24-3 diff --git a/src/tui/components/filterable-select-list.ts b/src/tui/components/filterable-select-list.ts index 67361bcf1..a7b197bf5 100644 --- a/src/tui/components/filterable-select-list.ts +++ b/src/tui/components/filterable-select-list.ts @@ -69,7 +69,7 @@ export class FilterableSelectList implements Component { lines.push(filterLabel + inputText); // Separator - lines.push(chalk.dim("─".repeat(width))); + lines.push(chalk.dim("─".repeat(Math.max(0, width)))); // Select list const listLines = this.selectList.render(width); diff --git a/src/tui/components/searchable-select-list.ts b/src/tui/components/searchable-select-list.ts index f8e07e790..54fc34918 100644 --- a/src/tui/components/searchable-select-list.ts +++ b/src/tui/components/searchable-select-list.ts @@ -214,7 +214,8 @@ export class SearchableSelectList implements Component { const maxValueWidth = Math.min(30, width - prefixWidth - 4); const truncatedValue = truncateToWidth(displayValue, maxValueWidth, ""); const valueText = this.highlightMatch(truncatedValue, query); - const spacing = " ".repeat(Math.max(1, 32 - visibleWidth(valueText))); + const spacingWidth = Math.max(1, 32 - visibleWidth(valueText)); + const spacing = " ".repeat(spacingWidth); const descriptionStart = prefixWidth + visibleWidth(valueText) + spacing.length; const remainingWidth = width - descriptionStart - 2; if (remainingWidth > 10) { From 7f6422c8977ce782f15809fda26ae67a1d4c7aa9 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:20:39 -0600 Subject: [PATCH 2/6] Telegram: preserve topic IDs in restart notifications (#1807) Co-authored-by: hsrvc --- CHANGELOG.md | 1 + src/agents/tools/sessions-send-helpers.ts | 24 +++++++++++++++++++++-- src/gateway/server-restart-sentinel.ts | 16 ++++++++++----- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 480767383..dc46291fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Status: unreleased. - Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla. - Routing: precompile session key regexes. (#1697) Thanks @Ray0907. - TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein. +- Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc. ## 2026.1.24-3 diff --git a/src/agents/tools/sessions-send-helpers.ts b/src/agents/tools/sessions-send-helpers.ts index 5e758d426..c9940de0f 100644 --- a/src/agents/tools/sessions-send-helpers.ts +++ b/src/agents/tools/sessions-send-helpers.ts @@ -14,6 +14,7 @@ export type AnnounceTarget = { channel: string; to: string; accountId?: string; + threadId?: string; // Forum topic/thread ID }; export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget | null { @@ -22,7 +23,22 @@ export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget if (parts.length < 3) return null; const [channelRaw, kind, ...rest] = parts; if (kind !== "group" && kind !== "channel") return null; - const id = rest.join(":").trim(); + + // Extract topic/thread ID from rest (supports both :topic: and :thread:) + // Telegram uses :topic:, other platforms use :thread: + let threadId: string | undefined; + const restJoined = rest.join(":"); + const topicMatch = restJoined.match(/:topic:(\d+)$/); + const threadMatch = restJoined.match(/:thread:(\d+)$/); + const match = topicMatch || threadMatch; + + if (match) { + threadId = match[1]; // Keep as string to match AgentCommandOpts.threadId + } + + // Remove :topic:N or :thread:N suffix from ID for target + const id = match ? restJoined.replace(/:(topic|thread):\d+$/, "") : restJoined.trim(); + if (!id) return null; if (!channelRaw) return null; const normalizedChannel = normalizeAnyChannelId(channelRaw) ?? normalizeChatChannelId(channelRaw); @@ -37,7 +53,11 @@ export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget const normalized = normalizedChannel ? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.(kindTarget) : undefined; - return { channel, to: normalized ?? kindTarget }; + return { + channel, + to: normalized ?? kindTarget, + threadId, + }; } export function buildAgentToAgentMessageContext(params: { diff --git a/src/gateway/server-restart-sentinel.ts b/src/gateway/server-restart-sentinel.ts index fa33b7c21..28719290e 100644 --- a/src/gateway/server-restart-sentinel.ts +++ b/src/gateway/server-restart-sentinel.ts @@ -28,11 +28,16 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) { return; } - const threadMarker = ":thread:"; - const threadIndex = sessionKey.lastIndexOf(threadMarker); - const baseSessionKey = threadIndex === -1 ? sessionKey : sessionKey.slice(0, threadIndex); + // Extract topic/thread ID from sessionKey (supports both :topic: and :thread:) + // Telegram uses :topic:, other platforms use :thread: + const topicIndex = sessionKey.lastIndexOf(":topic:"); + const threadIndex = sessionKey.lastIndexOf(":thread:"); + const markerIndex = Math.max(topicIndex, threadIndex); + const marker = topicIndex > threadIndex ? ":topic:" : ":thread:"; + + const baseSessionKey = markerIndex === -1 ? sessionKey : sessionKey.slice(0, markerIndex); const threadIdRaw = - threadIndex === -1 ? undefined : sessionKey.slice(threadIndex + threadMarker.length); + markerIndex === -1 ? undefined : sessionKey.slice(markerIndex + marker.length); const sessionThreadId = threadIdRaw?.trim() || undefined; const { cfg, entry } = loadSessionEntry(sessionKey); @@ -42,7 +47,7 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) { // Handles race condition where store wasn't flushed before restart const sentinelContext = payload.deliveryContext; let sessionDeliveryContext = deliveryContextFromSession(entry); - if (!sessionDeliveryContext && threadIndex !== -1 && baseSessionKey) { + if (!sessionDeliveryContext && markerIndex !== -1 && baseSessionKey) { const { entry: baseEntry } = loadSessionEntry(baseSessionKey); sessionDeliveryContext = deliveryContextFromSession(baseEntry); } @@ -74,6 +79,7 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) { const threadId = payload.threadId ?? + parsedTarget?.threadId ?? // From resolveAnnounceTargetFromKey (extracts :topic:N) sessionThreadId ?? (origin?.threadId != null ? String(origin.threadId) : undefined); From 1b598ad70923e6f6c3f6f7bc12ffc75f06e07004 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:22:25 -0600 Subject: [PATCH 3/6] Config: apply config.env before substitution (#1813) Co-authored-by: SPANISH FLU --- CHANGELOG.md | 1 + src/config/io.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc46291fb..6aacd64aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Status: unreleased. - Routing: precompile session key regexes. (#1697) Thanks @Ray0907. - TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein. - Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc. +- Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918. ## 2026.1.24-3 diff --git a/src/config/io.ts b/src/config/io.ts index da3a7fb23..9078ef2a2 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -211,6 +211,11 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { parseJson: (raw) => deps.json5.parse(raw), }); + // Apply config.env to process.env BEFORE substitution so ${VAR} can reference config-defined vars + if (resolved && typeof resolved === "object" && "env" in resolved) { + applyConfigEnv(resolved as ClawdbotConfig, deps.env); + } + // Substitute ${VAR} env var references const substituted = resolveConfigEnvVars(resolved, deps.env); @@ -365,6 +370,11 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { }; } + // Apply config.env to process.env BEFORE substitution so ${VAR} can reference config-defined vars + if (resolved && typeof resolved === "object" && "env" in resolved) { + applyConfigEnv(resolved as ClawdbotConfig, deps.env); + } + // Substitute ${VAR} env var references let substituted: unknown; try { From 678ad9e3aed137e7ab65736185aacdc468f8e707 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:23:18 -0600 Subject: [PATCH 4/6] CI: expand web-ui label globs --- .github/labeler.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/labeler.yml b/.github/labeler.yml index 5b34c41e0..5d2837a6c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -115,6 +115,8 @@ - "ui/**" - "src/gateway/control-ui.ts" - "src/gateway/control-ui-shared.ts" + - "src/gateway/protocol/**" + - "src/gateway/server-methods/chat.ts" - "src/infra/control-ui-assets.ts" "gateway": From 7e4e24445e21d0727895a2667ef6eac515e6904c Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:28:46 -0600 Subject: [PATCH 5/6] Slack: clear ack reaction after streaming replies (#2044) Co-authored-by: Shaurya Pratap Singh --- CHANGELOG.md | 1 + src/slack/monitor/message-handler/dispatch.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aacd64aa..2cd23f0f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Status: unreleased. - TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein. - Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc. - Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918. +- Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999. ## 2026.1.24-3 diff --git a/src/slack/monitor/message-handler/dispatch.ts b/src/slack/monitor/message-handler/dispatch.ts index d31885cfa..38b69f049 100644 --- a/src/slack/monitor/message-handler/dispatch.ts +++ b/src/slack/monitor/message-handler/dispatch.ts @@ -141,7 +141,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag }); markDispatchIdle(); - if (!queuedFinal) { + const anyReplyDelivered = queuedFinal || (counts.block ?? 0) > 0 || (counts.final ?? 0) > 0; + + if (!anyReplyDelivered) { if (prepared.isRoomish) { clearHistoryEntriesIfEnabled({ historyMap: ctx.channelHistories, From 8b91ceb7c96f5eb15e3cda39d6fa6a769dddcfad Mon Sep 17 00:00:00 2001 From: Shadow Date: Sun, 25 Jan 2026 21:46:15 -0600 Subject: [PATCH 6/6] macOS: preserve custom SSH usernames (#2046) Co-authored-by: Alexis Gallagher --- CHANGELOG.md | 1 + apps/macos/Sources/Clawdbot/AppState.swift | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd23f0f1..5e3ab78da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Status: unreleased. - Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc. - Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918. - Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999. +- macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal. ## 2026.1.24-3 diff --git a/apps/macos/Sources/Clawdbot/AppState.swift b/apps/macos/Sources/Clawdbot/AppState.swift index eeaf034d0..6ccb83369 100644 --- a/apps/macos/Sources/Clawdbot/AppState.swift +++ b/apps/macos/Sources/Clawdbot/AppState.swift @@ -413,10 +413,17 @@ final class AppState { } private func updateRemoteTarget(host: String) { - let parsed = CommandResolver.parseSSHTarget(self.remoteTarget) - let user = parsed?.user ?? NSUserName() - let port = parsed?.port ?? 22 - let assembled = port == 22 ? "\(user)@\(host)" : "\(user)@\(host):\(port)" + let trimmed = self.remoteTarget.trimmingCharacters(in: .whitespacesAndNewlines) + guard let parsed = CommandResolver.parseSSHTarget(trimmed) else { return } + let trimmedUser = parsed.user?.trimmingCharacters(in: .whitespacesAndNewlines) + let user = (trimmedUser?.isEmpty ?? true) ? nil : trimmedUser + let port = parsed.port + let assembled: String + if let user { + assembled = port == 22 ? "\(user)@\(host)" : "\(user)@\(host):\(port)" + } else { + assembled = port == 22 ? host : "\(host):\(port)" + } if assembled != self.remoteTarget { self.remoteTarget = assembled }