Merge branch 'main' into perplexity-search

This commit is contained in:
Kesku 2026-01-25 20:13:29 -08:00 committed by GitHub
commit ad06bd4a45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 67 additions and 14 deletions

2
.github/labeler.yml vendored
View File

@ -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":

View File

@ -19,6 +19,11 @@ 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.
- 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

View File

@ -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
}

View File

@ -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: {

View File

@ -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 {

View File

@ -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);

View File

@ -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,

View File

@ -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);

View File

@ -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) {