Merge branch 'main' into perplexity-search
This commit is contained in:
commit
ad06bd4a45
2
.github/labeler.yml
vendored
2
.github/labeler.yml
vendored
@ -115,6 +115,8 @@
|
|||||||
- "ui/**"
|
- "ui/**"
|
||||||
- "src/gateway/control-ui.ts"
|
- "src/gateway/control-ui.ts"
|
||||||
- "src/gateway/control-ui-shared.ts"
|
- "src/gateway/control-ui-shared.ts"
|
||||||
|
- "src/gateway/protocol/**"
|
||||||
|
- "src/gateway/server-methods/chat.ts"
|
||||||
- "src/infra/control-ui-assets.ts"
|
- "src/infra/control-ui-assets.ts"
|
||||||
|
|
||||||
"gateway":
|
"gateway":
|
||||||
|
|||||||
@ -19,6 +19,11 @@ Status: unreleased.
|
|||||||
- Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco.
|
- Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco.
|
||||||
- Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla.
|
- Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla.
|
||||||
- Routing: precompile session key regexes. (#1697) Thanks @Ray0907.
|
- 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
|
## 2026.1.24-3
|
||||||
|
|
||||||
|
|||||||
@ -413,10 +413,17 @@ final class AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateRemoteTarget(host: String) {
|
private func updateRemoteTarget(host: String) {
|
||||||
let parsed = CommandResolver.parseSSHTarget(self.remoteTarget)
|
let trimmed = self.remoteTarget.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let user = parsed?.user ?? NSUserName()
|
guard let parsed = CommandResolver.parseSSHTarget(trimmed) else { return }
|
||||||
let port = parsed?.port ?? 22
|
let trimmedUser = parsed.user?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let assembled = port == 22 ? "\(user)@\(host)" : "\(user)@\(host):\(port)"
|
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 {
|
if assembled != self.remoteTarget {
|
||||||
self.remoteTarget = assembled
|
self.remoteTarget = assembled
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export type AnnounceTarget = {
|
|||||||
channel: string;
|
channel: string;
|
||||||
to: string;
|
to: string;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
|
threadId?: string; // Forum topic/thread ID
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget | null {
|
export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget | null {
|
||||||
@ -22,7 +23,22 @@ export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget
|
|||||||
if (parts.length < 3) return null;
|
if (parts.length < 3) return null;
|
||||||
const [channelRaw, kind, ...rest] = parts;
|
const [channelRaw, kind, ...rest] = parts;
|
||||||
if (kind !== "group" && kind !== "channel") return null;
|
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 (!id) return null;
|
||||||
if (!channelRaw) return null;
|
if (!channelRaw) return null;
|
||||||
const normalizedChannel = normalizeAnyChannelId(channelRaw) ?? normalizeChatChannelId(channelRaw);
|
const normalizedChannel = normalizeAnyChannelId(channelRaw) ?? normalizeChatChannelId(channelRaw);
|
||||||
@ -37,7 +53,11 @@ export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget
|
|||||||
const normalized = normalizedChannel
|
const normalized = normalizedChannel
|
||||||
? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.(kindTarget)
|
? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.(kindTarget)
|
||||||
: undefined;
|
: undefined;
|
||||||
return { channel, to: normalized ?? kindTarget };
|
return {
|
||||||
|
channel,
|
||||||
|
to: normalized ?? kindTarget,
|
||||||
|
threadId,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildAgentToAgentMessageContext(params: {
|
export function buildAgentToAgentMessageContext(params: {
|
||||||
|
|||||||
@ -211,6 +211,11 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
|||||||
parseJson: (raw) => deps.json5.parse(raw),
|
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
|
// Substitute ${VAR} env var references
|
||||||
const substituted = resolveConfigEnvVars(resolved, deps.env);
|
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
|
// Substitute ${VAR} env var references
|
||||||
let substituted: unknown;
|
let substituted: unknown;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -28,11 +28,16 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const threadMarker = ":thread:";
|
// Extract topic/thread ID from sessionKey (supports both :topic: and :thread:)
|
||||||
const threadIndex = sessionKey.lastIndexOf(threadMarker);
|
// Telegram uses :topic:, other platforms use :thread:
|
||||||
const baseSessionKey = threadIndex === -1 ? sessionKey : sessionKey.slice(0, threadIndex);
|
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 =
|
const threadIdRaw =
|
||||||
threadIndex === -1 ? undefined : sessionKey.slice(threadIndex + threadMarker.length);
|
markerIndex === -1 ? undefined : sessionKey.slice(markerIndex + marker.length);
|
||||||
const sessionThreadId = threadIdRaw?.trim() || undefined;
|
const sessionThreadId = threadIdRaw?.trim() || undefined;
|
||||||
|
|
||||||
const { cfg, entry } = loadSessionEntry(sessionKey);
|
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
|
// Handles race condition where store wasn't flushed before restart
|
||||||
const sentinelContext = payload.deliveryContext;
|
const sentinelContext = payload.deliveryContext;
|
||||||
let sessionDeliveryContext = deliveryContextFromSession(entry);
|
let sessionDeliveryContext = deliveryContextFromSession(entry);
|
||||||
if (!sessionDeliveryContext && threadIndex !== -1 && baseSessionKey) {
|
if (!sessionDeliveryContext && markerIndex !== -1 && baseSessionKey) {
|
||||||
const { entry: baseEntry } = loadSessionEntry(baseSessionKey);
|
const { entry: baseEntry } = loadSessionEntry(baseSessionKey);
|
||||||
sessionDeliveryContext = deliveryContextFromSession(baseEntry);
|
sessionDeliveryContext = deliveryContextFromSession(baseEntry);
|
||||||
}
|
}
|
||||||
@ -74,6 +79,7 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) {
|
|||||||
|
|
||||||
const threadId =
|
const threadId =
|
||||||
payload.threadId ??
|
payload.threadId ??
|
||||||
|
parsedTarget?.threadId ?? // From resolveAnnounceTargetFromKey (extracts :topic:N)
|
||||||
sessionThreadId ??
|
sessionThreadId ??
|
||||||
(origin?.threadId != null ? String(origin.threadId) : undefined);
|
(origin?.threadId != null ? String(origin.threadId) : undefined);
|
||||||
|
|
||||||
|
|||||||
@ -141,7 +141,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
|||||||
});
|
});
|
||||||
markDispatchIdle();
|
markDispatchIdle();
|
||||||
|
|
||||||
if (!queuedFinal) {
|
const anyReplyDelivered = queuedFinal || (counts.block ?? 0) > 0 || (counts.final ?? 0) > 0;
|
||||||
|
|
||||||
|
if (!anyReplyDelivered) {
|
||||||
if (prepared.isRoomish) {
|
if (prepared.isRoomish) {
|
||||||
clearHistoryEntriesIfEnabled({
|
clearHistoryEntriesIfEnabled({
|
||||||
historyMap: ctx.channelHistories,
|
historyMap: ctx.channelHistories,
|
||||||
|
|||||||
@ -69,7 +69,7 @@ export class FilterableSelectList implements Component {
|
|||||||
lines.push(filterLabel + inputText);
|
lines.push(filterLabel + inputText);
|
||||||
|
|
||||||
// Separator
|
// Separator
|
||||||
lines.push(chalk.dim("─".repeat(width)));
|
lines.push(chalk.dim("─".repeat(Math.max(0, width))));
|
||||||
|
|
||||||
// Select list
|
// Select list
|
||||||
const listLines = this.selectList.render(width);
|
const listLines = this.selectList.render(width);
|
||||||
|
|||||||
@ -214,7 +214,8 @@ export class SearchableSelectList implements Component {
|
|||||||
const maxValueWidth = Math.min(30, width - prefixWidth - 4);
|
const maxValueWidth = Math.min(30, width - prefixWidth - 4);
|
||||||
const truncatedValue = truncateToWidth(displayValue, maxValueWidth, "");
|
const truncatedValue = truncateToWidth(displayValue, maxValueWidth, "");
|
||||||
const valueText = this.highlightMatch(truncatedValue, query);
|
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 descriptionStart = prefixWidth + visibleWidth(valueText) + spacing.length;
|
||||||
const remainingWidth = width - descriptionStart - 2;
|
const remainingWidth = width - descriptionStart - 2;
|
||||||
if (remainingWidth > 10) {
|
if (remainingWidth > 10) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user