Format: apply oxfmt fixes
This commit is contained in:
committed by
Peter Steinberger
parent
8c1e6a82b2
commit
232c512502
@@ -2,82 +2,77 @@ import { completeSimple, getModel } from "@mariozechner/pi-ai";
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
const GEMINI_KEY = process.env.GEMINI_API_KEY ?? "";
|
const GEMINI_KEY = process.env.GEMINI_API_KEY ?? "";
|
||||||
const LIVE =
|
const LIVE = process.env.GEMINI_LIVE_TEST === "1" || process.env.LIVE === "1";
|
||||||
process.env.GEMINI_LIVE_TEST === "1" || process.env.LIVE === "1";
|
|
||||||
|
|
||||||
const describeLive = LIVE && GEMINI_KEY ? describe : describe.skip;
|
const describeLive = LIVE && GEMINI_KEY ? describe : describe.skip;
|
||||||
|
|
||||||
describeLive("gemini live switch", () => {
|
describeLive("gemini live switch", () => {
|
||||||
it(
|
it("handles unsigned tool calls from Antigravity when switching to Gemini 3", async () => {
|
||||||
"handles unsigned tool calls from Antigravity when switching to Gemini 3",
|
const now = Date.now();
|
||||||
async () => {
|
const model = getModel("google", "gemini-3-pro-preview");
|
||||||
const now = Date.now();
|
|
||||||
const model = getModel("google", "gemini-3-pro-preview");
|
|
||||||
|
|
||||||
const res = await completeSimple(
|
const res = await completeSimple(
|
||||||
model,
|
model,
|
||||||
{
|
{
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: "Reply with ok.",
|
content: "Reply with ok.",
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "toolCall",
|
type: "toolCall",
|
||||||
id: "call_1",
|
id: "call_1",
|
||||||
name: "bash",
|
name: "bash",
|
||||||
arguments: { command: "ls -la" },
|
arguments: { command: "ls -la" },
|
||||||
// No thoughtSignature: simulates Claude via Antigravity.
|
// No thoughtSignature: simulates Claude via Antigravity.
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
api: "google-gemini-cli",
|
api: "google-gemini-cli",
|
||||||
provider: "google-antigravity",
|
provider: "google-antigravity",
|
||||||
model: "claude-sonnet-4-20250514",
|
model: "claude-sonnet-4-20250514",
|
||||||
usage: {
|
usage: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
cost: {
|
||||||
input: 0,
|
input: 0,
|
||||||
output: 0,
|
output: 0,
|
||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
totalTokens: 0,
|
total: 0,
|
||||||
cost: {
|
|
||||||
input: 0,
|
|
||||||
output: 0,
|
|
||||||
cacheRead: 0,
|
|
||||||
cacheWrite: 0,
|
|
||||||
total: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stopReason: "stop",
|
|
||||||
timestamp: now,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
tools: [
|
|
||||||
{
|
|
||||||
name: "bash",
|
|
||||||
description: "Run shell command",
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
command: { type: "string" },
|
|
||||||
},
|
|
||||||
required: ["command"],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
stopReason: "stop",
|
||||||
},
|
timestamp: now,
|
||||||
{
|
},
|
||||||
apiKey: GEMINI_KEY,
|
],
|
||||||
reasoning: "low",
|
tools: [
|
||||||
maxTokens: 128,
|
{
|
||||||
},
|
name: "bash",
|
||||||
);
|
description: "Run shell command",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
command: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["command"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiKey: GEMINI_KEY,
|
||||||
|
reasoning: "low",
|
||||||
|
maxTokens: 128,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
expect(res.stopReason).not.toBe("error");
|
expect(res.stopReason).not.toBe("error");
|
||||||
},
|
}, 20000);
|
||||||
20000,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -116,7 +116,12 @@ export function resolveCommandAuthorization(params: {
|
|||||||
|
|
||||||
const ownerCandidates = allowAll ? [] : allowFromList.filter((entry) => entry !== "*");
|
const ownerCandidates = allowAll ? [] : allowFromList.filter((entry) => entry !== "*");
|
||||||
if (!allowAll && ownerCandidates.length === 0 && to) {
|
if (!allowAll && ownerCandidates.length === 0 && to) {
|
||||||
const normalizedTo = normalizeAllowFromEntry({ dock, cfg, accountId: ctx.AccountId, value: to });
|
const normalizedTo = normalizeAllowFromEntry({
|
||||||
|
dock,
|
||||||
|
cfg,
|
||||||
|
accountId: ctx.AccountId,
|
||||||
|
value: to,
|
||||||
|
});
|
||||||
if (normalizedTo) ownerCandidates.push(normalizedTo);
|
if (normalizedTo) ownerCandidates.push(normalizedTo);
|
||||||
}
|
}
|
||||||
const ownerList = ownerCandidates;
|
const ownerList = ownerCandidates;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function buildThreadingToolContext(params: {
|
|||||||
const dock = getChannelDock(provider);
|
const dock = getChannelDock(provider);
|
||||||
if (!dock?.threading?.buildToolContext) return {};
|
if (!dock?.threading?.buildToolContext) return {};
|
||||||
// WhatsApp context isolation keys off conversation id, not the bot's own number.
|
// WhatsApp context isolation keys off conversation id, not the bot's own number.
|
||||||
const threadingTo = provider === "whatsapp" ? sessionCtx.From ?? sessionCtx.To : sessionCtx.To;
|
const threadingTo = provider === "whatsapp" ? (sessionCtx.From ?? sessionCtx.To) : sessionCtx.To;
|
||||||
return (
|
return (
|
||||||
dock.threading.buildToolContext({
|
dock.threading.buildToolContext({
|
||||||
cfg: config,
|
cfg: config,
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import { initSessionState } from "./session.js";
|
|||||||
|
|
||||||
describe("initSessionState reset triggers in WhatsApp groups", () => {
|
describe("initSessionState reset triggers in WhatsApp groups", () => {
|
||||||
async function createStorePath(prefix: string): Promise<string> {
|
async function createStorePath(prefix: string): Promise<string> {
|
||||||
const root = await fs.mkdtemp(
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||||
path.join(os.tmpdir(), prefix),
|
|
||||||
);
|
|
||||||
return path.join(root, "sessions.json");
|
return path.join(root, "sessions.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,7 @@ import {
|
|||||||
} from "./message-utils.js";
|
} from "./message-utils.js";
|
||||||
import { buildDirectLabel, buildGuildLabel, resolveReplyContext } from "./reply-context.js";
|
import { buildDirectLabel, buildGuildLabel, resolveReplyContext } from "./reply-context.js";
|
||||||
import { deliverDiscordReply } from "./reply-delivery.js";
|
import { deliverDiscordReply } from "./reply-delivery.js";
|
||||||
import {
|
import { resolveDiscordAutoThreadReplyPlan, resolveDiscordThreadStarter } from "./threading.js";
|
||||||
resolveDiscordAutoThreadReplyPlan,
|
|
||||||
resolveDiscordThreadStarter,
|
|
||||||
} from "./threading.js";
|
|
||||||
import { sendTyping } from "./typing.js";
|
import { sendTyping } from "./typing.js";
|
||||||
|
|
||||||
export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) {
|
export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) {
|
||||||
@@ -191,72 +188,72 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
|||||||
peer: { kind: "channel", id: threadParentId },
|
peer: { kind: "channel", id: threadParentId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const mediaPayload = buildDiscordMediaPayload(mediaList);
|
const mediaPayload = buildDiscordMediaPayload(mediaList);
|
||||||
const threadKeys = resolveThreadSessionKeys({
|
const threadKeys = resolveThreadSessionKeys({
|
||||||
baseSessionKey,
|
baseSessionKey,
|
||||||
threadId: threadChannel ? message.channelId : undefined,
|
threadId: threadChannel ? message.channelId : undefined,
|
||||||
parentSessionKey,
|
parentSessionKey,
|
||||||
useSuffix: false,
|
useSuffix: false,
|
||||||
});
|
});
|
||||||
const replyPlan = await resolveDiscordAutoThreadReplyPlan({
|
const replyPlan = await resolveDiscordAutoThreadReplyPlan({
|
||||||
client,
|
client,
|
||||||
message,
|
message,
|
||||||
isGuildMessage,
|
isGuildMessage,
|
||||||
channelConfig,
|
channelConfig,
|
||||||
threadChannel,
|
threadChannel,
|
||||||
baseText: baseText ?? "",
|
baseText: baseText ?? "",
|
||||||
combinedBody,
|
combinedBody,
|
||||||
replyToMode,
|
replyToMode,
|
||||||
agentId: route.agentId,
|
agentId: route.agentId,
|
||||||
channel: route.channel,
|
channel: route.channel,
|
||||||
});
|
});
|
||||||
const deliverTarget = replyPlan.deliverTarget;
|
const deliverTarget = replyPlan.deliverTarget;
|
||||||
const replyTarget = replyPlan.replyTarget;
|
const replyTarget = replyPlan.replyTarget;
|
||||||
const replyReference = replyPlan.replyReference;
|
const replyReference = replyPlan.replyReference;
|
||||||
const autoThreadContext = replyPlan.autoThreadContext;
|
const autoThreadContext = replyPlan.autoThreadContext;
|
||||||
|
|
||||||
const effectiveFrom = isDirectMessage
|
const effectiveFrom = isDirectMessage
|
||||||
? `discord:${author.id}`
|
? `discord:${author.id}`
|
||||||
: (autoThreadContext?.From ?? `group:${message.channelId}`);
|
: (autoThreadContext?.From ?? `group:${message.channelId}`);
|
||||||
const effectiveTo = autoThreadContext?.To ?? replyTarget;
|
const effectiveTo = autoThreadContext?.To ?? replyTarget;
|
||||||
if (!effectiveTo) {
|
if (!effectiveTo) {
|
||||||
runtime.error?.(danger("discord: missing reply target"));
|
runtime.error?.(danger("discord: missing reply target"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctxPayload = {
|
const ctxPayload = {
|
||||||
Body: combinedBody,
|
Body: combinedBody,
|
||||||
RawBody: baseText,
|
RawBody: baseText,
|
||||||
CommandBody: baseText,
|
CommandBody: baseText,
|
||||||
From: effectiveFrom,
|
From: effectiveFrom,
|
||||||
To: effectiveTo,
|
To: effectiveTo,
|
||||||
SessionKey: autoThreadContext?.SessionKey ?? threadKeys.sessionKey,
|
SessionKey: autoThreadContext?.SessionKey ?? threadKeys.sessionKey,
|
||||||
AccountId: route.accountId,
|
AccountId: route.accountId,
|
||||||
ChatType: isDirectMessage ? "direct" : "group",
|
ChatType: isDirectMessage ? "direct" : "group",
|
||||||
SenderName: data.member?.nickname ?? author.globalName ?? author.username,
|
SenderName: data.member?.nickname ?? author.globalName ?? author.username,
|
||||||
SenderId: author.id,
|
SenderId: author.id,
|
||||||
SenderUsername: author.username,
|
SenderUsername: author.username,
|
||||||
SenderTag: formatDiscordUserTag(author),
|
SenderTag: formatDiscordUserTag(author),
|
||||||
GroupSubject: groupSubject,
|
GroupSubject: groupSubject,
|
||||||
GroupRoom: groupRoom,
|
GroupRoom: groupRoom,
|
||||||
GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined,
|
GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined,
|
||||||
GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined,
|
GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined,
|
||||||
Provider: "discord" as const,
|
Provider: "discord" as const,
|
||||||
Surface: "discord" as const,
|
Surface: "discord" as const,
|
||||||
WasMentioned: effectiveWasMentioned,
|
WasMentioned: effectiveWasMentioned,
|
||||||
MessageSid: message.id,
|
MessageSid: message.id,
|
||||||
ParentSessionKey: autoThreadContext?.ParentSessionKey ?? threadKeys.parentSessionKey,
|
ParentSessionKey: autoThreadContext?.ParentSessionKey ?? threadKeys.parentSessionKey,
|
||||||
ThreadStarterBody: threadStarterBody,
|
ThreadStarterBody: threadStarterBody,
|
||||||
ThreadLabel: threadLabel,
|
ThreadLabel: threadLabel,
|
||||||
Timestamp: resolveTimestampMs(message.timestamp),
|
Timestamp: resolveTimestampMs(message.timestamp),
|
||||||
...mediaPayload,
|
...mediaPayload,
|
||||||
CommandAuthorized: commandAuthorized,
|
CommandAuthorized: commandAuthorized,
|
||||||
CommandSource: "text" as const,
|
CommandSource: "text" as const,
|
||||||
// Originating channel for reply routing.
|
// Originating channel for reply routing.
|
||||||
OriginatingChannel: "discord" as const,
|
OriginatingChannel: "discord" as const,
|
||||||
OriginatingTo: autoThreadContext?.OriginatingTo ?? replyTarget,
|
OriginatingTo: autoThreadContext?.OriginatingTo ?? replyTarget,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isDirectMessage) {
|
if (isDirectMessage) {
|
||||||
const sessionCfg = cfg.session;
|
const sessionCfg = cfg.session;
|
||||||
@@ -272,20 +269,20 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldLogVerbose()) {
|
if (shouldLogVerbose()) {
|
||||||
const preview = truncateUtf16Safe(combinedBody, 200).replace(/\n/g, "\\n");
|
const preview = truncateUtf16Safe(combinedBody, 200).replace(/\n/g, "\\n");
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`discord inbound: channel=${message.channelId} deliver=${deliverTarget} from=${ctxPayload.From} preview="${preview}"`,
|
`discord inbound: channel=${message.channelId} deliver=${deliverTarget} from=${ctxPayload.From} preview="${preview}"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let didSendReply = false;
|
let didSendReply = false;
|
||||||
const typingChannelId = deliverTarget.startsWith("channel:")
|
const typingChannelId = deliverTarget.startsWith("channel:")
|
||||||
? deliverTarget.slice("channel:".length)
|
? deliverTarget.slice("channel:".length)
|
||||||
: message.channelId;
|
: message.channelId;
|
||||||
const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
|
const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
|
||||||
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
|
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
|
||||||
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
|
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
|
||||||
deliver: async (payload: ReplyPayload) => {
|
deliver: async (payload: ReplyPayload) => {
|
||||||
const replyToId = replyReference.use();
|
const replyToId = replyReference.use();
|
||||||
await deliverDiscordReply({
|
await deliverDiscordReply({
|
||||||
@@ -302,11 +299,11 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
|||||||
didSendReply = true;
|
didSendReply = true;
|
||||||
replyReference.markSent();
|
replyReference.markSent();
|
||||||
},
|
},
|
||||||
onError: (err, info) => {
|
onError: (err, info) => {
|
||||||
runtime.error?.(danger(`discord ${info.kind} reply failed: ${String(err)}`));
|
runtime.error?.(danger(`discord ${info.kind} reply failed: ${String(err)}`));
|
||||||
},
|
},
|
||||||
onReplyStart: () => sendTyping({ client, channelId: typingChannelId }),
|
onReplyStart: () => sendTyping({ client, channelId: typingChannelId }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { queuedFinal, counts } = await dispatchReplyFromConfig({
|
const { queuedFinal, counts } = await dispatchReplyFromConfig({
|
||||||
ctx: ctxPayload,
|
ctx: ctxPayload,
|
||||||
|
|||||||
@@ -93,9 +93,14 @@ describe("resolveDiscordAutoThreadReplyPlan", () => {
|
|||||||
} as unknown as Client;
|
} as unknown as Client;
|
||||||
const plan = await resolveDiscordAutoThreadReplyPlan({
|
const plan = await resolveDiscordAutoThreadReplyPlan({
|
||||||
client,
|
client,
|
||||||
message: { id: "m1", channelId: "parent" } as unknown as import("./listeners.js").DiscordMessageEvent["message"],
|
message: {
|
||||||
|
id: "m1",
|
||||||
|
channelId: "parent",
|
||||||
|
} as unknown as import("./listeners.js").DiscordMessageEvent["message"],
|
||||||
isGuildMessage: true,
|
isGuildMessage: true,
|
||||||
channelConfig: { autoThread: true } as unknown as import("./allow-list.js").DiscordChannelConfigResolved,
|
channelConfig: {
|
||||||
|
autoThread: true,
|
||||||
|
} as unknown as import("./allow-list.js").DiscordChannelConfigResolved,
|
||||||
threadChannel: null,
|
threadChannel: null,
|
||||||
baseText: "hello",
|
baseText: "hello",
|
||||||
combinedBody: "hello",
|
combinedBody: "hello",
|
||||||
@@ -118,9 +123,14 @@ describe("resolveDiscordAutoThreadReplyPlan", () => {
|
|||||||
const client = { rest: { post: async () => ({ id: "thread" }) } } as unknown as Client;
|
const client = { rest: { post: async () => ({ id: "thread" }) } } as unknown as Client;
|
||||||
const plan = await resolveDiscordAutoThreadReplyPlan({
|
const plan = await resolveDiscordAutoThreadReplyPlan({
|
||||||
client,
|
client,
|
||||||
message: { id: "m1", channelId: "parent" } as unknown as import("./listeners.js").DiscordMessageEvent["message"],
|
message: {
|
||||||
|
id: "m1",
|
||||||
|
channelId: "parent",
|
||||||
|
} as unknown as import("./listeners.js").DiscordMessageEvent["message"],
|
||||||
isGuildMessage: true,
|
isGuildMessage: true,
|
||||||
channelConfig: { autoThread: false } as unknown as import("./allow-list.js").DiscordChannelConfigResolved,
|
channelConfig: {
|
||||||
|
autoThread: false,
|
||||||
|
} as unknown as import("./allow-list.js").DiscordChannelConfigResolved,
|
||||||
threadChannel: null,
|
threadChannel: null,
|
||||||
baseText: "hello",
|
baseText: "hello",
|
||||||
combinedBody: "hello",
|
combinedBody: "hello",
|
||||||
|
|||||||
@@ -86,9 +86,7 @@ describe("deliverOutboundPayloads", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("chunks Signal markdown using the format-first chunker", async () => {
|
it("chunks Signal markdown using the format-first chunker", async () => {
|
||||||
const sendSignal = vi
|
const sendSignal = vi.fn().mockResolvedValue({ messageId: "s1", timestamp: 123 });
|
||||||
.fn()
|
|
||||||
.mockResolvedValue({ messageId: "s1", timestamp: 123 });
|
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
channels: { signal: { textChunkLimit: 20 } },
|
channels: { signal: { textChunkLimit: 20 } },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,13 +25,7 @@ type MarkdownToken = {
|
|||||||
attrGet?: (name: string) => string | null;
|
attrGet?: (name: string) => string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MarkdownStyle =
|
export type MarkdownStyle = "bold" | "italic" | "strikethrough" | "code" | "code_block" | "spoiler";
|
||||||
| "bold"
|
|
||||||
| "italic"
|
|
||||||
| "strikethrough"
|
|
||||||
| "code"
|
|
||||||
| "code_block"
|
|
||||||
| "spoiler";
|
|
||||||
|
|
||||||
export type MarkdownStyleSpan = {
|
export type MarkdownStyleSpan = {
|
||||||
start: number;
|
start: number;
|
||||||
@@ -414,11 +408,7 @@ function sliceStyleSpans(
|
|||||||
return mergeStyleSpans(sliced);
|
return mergeStyleSpans(sliced);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sliceLinkSpans(
|
function sliceLinkSpans(spans: MarkdownLinkSpan[], start: number, end: number): MarkdownLinkSpan[] {
|
||||||
spans: MarkdownLinkSpan[],
|
|
||||||
start: number,
|
|
||||||
end: number,
|
|
||||||
): MarkdownLinkSpan[] {
|
|
||||||
if (spans.length === 0) return [];
|
if (spans.length === 0) return [];
|
||||||
const sliced: MarkdownLinkSpan[] = [];
|
const sliced: MarkdownLinkSpan[] = [];
|
||||||
for (const span of spans) {
|
for (const span of spans) {
|
||||||
@@ -465,7 +455,8 @@ export function markdownToIR(markdown: string, options: MarkdownParseOptions = {
|
|||||||
if (span.end > codeBlockEnd) codeBlockEnd = span.end;
|
if (span.end > codeBlockEnd) codeBlockEnd = span.end;
|
||||||
}
|
}
|
||||||
const finalLength = Math.max(trimmedLength, codeBlockEnd);
|
const finalLength = Math.max(trimmedLength, codeBlockEnd);
|
||||||
const finalText = finalLength === state.text.length ? state.text : state.text.slice(0, finalLength);
|
const finalText =
|
||||||
|
finalLength === state.text.length ? state.text : state.text.slice(0, finalLength);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: finalText,
|
text: finalText,
|
||||||
|
|||||||
@@ -46,9 +46,7 @@ export function renderMarkdownWithMarkers(ir: MarkdownIR, options: RenderOptions
|
|||||||
if (!text) return "";
|
if (!text) return "";
|
||||||
|
|
||||||
const styleMarkers = options.styleMarkers;
|
const styleMarkers = options.styleMarkers;
|
||||||
const styled = sortStyleSpans(
|
const styled = sortStyleSpans(ir.styles.filter((span) => Boolean(styleMarkers[span.style])));
|
||||||
ir.styles.filter((span) => Boolean(styleMarkers[span.style])),
|
|
||||||
);
|
|
||||||
|
|
||||||
const boundaries = new Set<number>();
|
const boundaries = new Set<number>();
|
||||||
boundaries.add(0);
|
boundaries.add(0);
|
||||||
|
|||||||
@@ -16,13 +16,9 @@ describe("markdownToSignalText", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders links as label plus url when needed", () => {
|
it("renders links as label plus url when needed", () => {
|
||||||
const res = markdownToSignalText(
|
const res = markdownToSignalText("see [docs](https://example.com) and https://example.com");
|
||||||
"see [docs](https://example.com) and https://example.com",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(res.text).toBe(
|
expect(res.text).toBe("see docs (https://example.com) and https://example.com");
|
||||||
"see docs (https://example.com) and https://example.com",
|
|
||||||
);
|
|
||||||
expect(res.styles).toEqual([]);
|
expect(res.styles).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,18 +30,14 @@ describe("markdownToSignalText", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders fenced code blocks with monospaced styles", () => {
|
it("renders fenced code blocks with monospaced styles", () => {
|
||||||
const res = markdownToSignalText(
|
const res = markdownToSignalText("before\n\n```\nconst x = 1;\n```\n\nafter");
|
||||||
"before\n\n```\nconst x = 1;\n```\n\nafter",
|
|
||||||
);
|
|
||||||
|
|
||||||
const prefix = "before\n\n";
|
const prefix = "before\n\n";
|
||||||
const code = "const x = 1;\n";
|
const code = "const x = 1;\n";
|
||||||
const suffix = "\nafter";
|
const suffix = "\nafter";
|
||||||
|
|
||||||
expect(res.text).toBe(`${prefix}${code}${suffix}`);
|
expect(res.text).toBe(`${prefix}${code}${suffix}`);
|
||||||
expect(res.styles).toEqual([
|
expect(res.styles).toEqual([{ start: prefix.length, length: code.length, style: "MONOSPACE" }]);
|
||||||
{ start: prefix.length, length: code.length, style: "MONOSPACE" },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders lists without extra block markup", () => {
|
it("renders lists without extra block markup", () => {
|
||||||
@@ -60,8 +52,6 @@ describe("markdownToSignalText", () => {
|
|||||||
|
|
||||||
const prefix = "😀 ";
|
const prefix = "😀 ";
|
||||||
expect(res.text).toBe(`${prefix}bold`);
|
expect(res.text).toBe(`${prefix}bold`);
|
||||||
expect(res.styles).toEqual([
|
expect(res.styles).toEqual([{ start: prefix.length, length: 4, style: "BOLD" }]);
|
||||||
{ start: prefix.length, length: 4, style: "BOLD" },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { chunkMarkdownIR, markdownToIR, type MarkdownIR, type MarkdownStyle } from "../markdown/ir.js";
|
import {
|
||||||
|
chunkMarkdownIR,
|
||||||
|
markdownToIR,
|
||||||
|
type MarkdownIR,
|
||||||
|
type MarkdownStyle,
|
||||||
|
} from "../markdown/ir.js";
|
||||||
|
|
||||||
type SignalTextStyle =
|
type SignalTextStyle = "BOLD" | "ITALIC" | "STRIKETHROUGH" | "MONOSPACE" | "SPOILER";
|
||||||
| "BOLD"
|
|
||||||
| "ITALIC"
|
|
||||||
| "STRIKETHROUGH"
|
|
||||||
| "MONOSPACE"
|
|
||||||
| "SPOILER";
|
|
||||||
|
|
||||||
export type SignalTextStyleRange = {
|
export type SignalTextStyleRange = {
|
||||||
start: number;
|
start: number;
|
||||||
@@ -57,11 +57,7 @@ function mergeStyles(styles: SignalTextStyleRange[]): SignalTextStyleRange[] {
|
|||||||
const merged: SignalTextStyleRange[] = [];
|
const merged: SignalTextStyleRange[] = [];
|
||||||
for (const style of sorted) {
|
for (const style of sorted) {
|
||||||
const prev = merged[merged.length - 1];
|
const prev = merged[merged.length - 1];
|
||||||
if (
|
if (prev && prev.style === style.style && style.start <= prev.start + prev.length) {
|
||||||
prev &&
|
|
||||||
prev.style === style.style &&
|
|
||||||
style.start <= prev.start + prev.length
|
|
||||||
) {
|
|
||||||
const prevEnd = prev.start + prev.length;
|
const prevEnd = prev.start + prev.length;
|
||||||
const nextEnd = Math.max(prevEnd, style.start + style.length);
|
const nextEnd = Math.max(prevEnd, style.start + style.length);
|
||||||
prev.length = nextEnd - prev.start;
|
prev.length = nextEnd - prev.start;
|
||||||
@@ -73,10 +69,7 @@ function mergeStyles(styles: SignalTextStyleRange[]): SignalTextStyleRange[] {
|
|||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampStyles(
|
function clampStyles(styles: SignalTextStyleRange[], maxLength: number): SignalTextStyleRange[] {
|
||||||
styles: SignalTextStyleRange[],
|
|
||||||
maxLength: number,
|
|
||||||
): SignalTextStyleRange[] {
|
|
||||||
const clamped: SignalTextStyleRange[] = [];
|
const clamped: SignalTextStyleRange[] = [];
|
||||||
for (const style of styles) {
|
for (const style of styles) {
|
||||||
const start = Math.max(0, Math.min(style.start, maxLength));
|
const start = Math.max(0, Math.min(style.start, maxLength));
|
||||||
@@ -205,10 +198,7 @@ export function markdownToSignalText(markdown: string): SignalFormattedText {
|
|||||||
return renderSignalText(ir);
|
return renderSignalText(ir);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markdownToSignalTextChunks(
|
export function markdownToSignalTextChunks(markdown: string, limit: number): SignalFormattedText[] {
|
||||||
markdown: string,
|
|
||||||
limit: number,
|
|
||||||
): SignalFormattedText[] {
|
|
||||||
const ir = markdownToIR(markdown ?? "", {
|
const ir = markdownToIR(markdown ?? "", {
|
||||||
linkify: true,
|
linkify: true,
|
||||||
enableSpoilers: true,
|
enableSpoilers: true,
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { chunkMarkdownIR, markdownToIR, type MarkdownLinkSpan, type MarkdownIR } from "../markdown/ir.js";
|
import {
|
||||||
|
chunkMarkdownIR,
|
||||||
|
markdownToIR,
|
||||||
|
type MarkdownLinkSpan,
|
||||||
|
type MarkdownIR,
|
||||||
|
} from "../markdown/ir.js";
|
||||||
import { renderMarkdownWithMarkers } from "../markdown/render.js";
|
import { renderMarkdownWithMarkers } from "../markdown/render.js";
|
||||||
|
|
||||||
export type TelegramFormattedChunk = {
|
export type TelegramFormattedChunk = {
|
||||||
@@ -50,7 +55,10 @@ export function markdownToTelegramHtml(markdown: string): string {
|
|||||||
return renderTelegramHtml(ir);
|
return renderTelegramHtml(ir);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markdownToTelegramChunks(markdown: string, limit: number): TelegramFormattedChunk[] {
|
export function markdownToTelegramChunks(
|
||||||
|
markdown: string,
|
||||||
|
limit: number,
|
||||||
|
): TelegramFormattedChunk[] {
|
||||||
const ir = markdownToIR(markdown ?? "", {
|
const ir = markdownToIR(markdown ?? "", {
|
||||||
linkify: true,
|
linkify: true,
|
||||||
headingStyle: "none",
|
headingStyle: "none",
|
||||||
|
|||||||
@@ -432,9 +432,7 @@ export async function deleteMessageTelegram(
|
|||||||
verbose: opts.verbose,
|
verbose: opts.verbose,
|
||||||
});
|
});
|
||||||
await request(() => api.deleteMessage(chatId, messageId), "deleteMessage");
|
await request(() => api.deleteMessage(chatId, messageId), "deleteMessage");
|
||||||
logVerbose(
|
logVerbose(`[telegram] Deleted message ${messageId} from chat ${chatId}`);
|
||||||
`[telegram] Deleted message ${messageId} from chat ${chatId}`,
|
|
||||||
);
|
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import {
|
import { isWhatsAppGroupJid, isWhatsAppUserTarget, normalizeWhatsAppTarget } from "./normalize.js";
|
||||||
isWhatsAppGroupJid,
|
|
||||||
isWhatsAppUserTarget,
|
|
||||||
normalizeWhatsAppTarget,
|
|
||||||
} from "./normalize.js";
|
|
||||||
|
|
||||||
describe("normalizeWhatsAppTarget", () => {
|
describe("normalizeWhatsAppTarget", () => {
|
||||||
it("preserves group JIDs", () => {
|
it("preserves group JIDs", () => {
|
||||||
@@ -31,16 +27,10 @@ describe("normalizeWhatsAppTarget", () => {
|
|||||||
it("normalizes user JIDs with device suffix to E.164", () => {
|
it("normalizes user JIDs with device suffix to E.164", () => {
|
||||||
// This is the bug fix: JIDs like "41796666864:0@s.whatsapp.net" should
|
// This is the bug fix: JIDs like "41796666864:0@s.whatsapp.net" should
|
||||||
// normalize to "+41796666864", not "+417966668640" (extra digit from ":0")
|
// normalize to "+41796666864", not "+417966668640" (extra digit from ":0")
|
||||||
expect(normalizeWhatsAppTarget("41796666864:0@s.whatsapp.net")).toBe(
|
expect(normalizeWhatsAppTarget("41796666864:0@s.whatsapp.net")).toBe("+41796666864");
|
||||||
"+41796666864",
|
expect(normalizeWhatsAppTarget("1234567890:123@s.whatsapp.net")).toBe("+1234567890");
|
||||||
);
|
|
||||||
expect(normalizeWhatsAppTarget("1234567890:123@s.whatsapp.net")).toBe(
|
|
||||||
"+1234567890",
|
|
||||||
);
|
|
||||||
// Without device suffix still works
|
// Without device suffix still works
|
||||||
expect(normalizeWhatsAppTarget("41796666864@s.whatsapp.net")).toBe(
|
expect(normalizeWhatsAppTarget("41796666864@s.whatsapp.net")).toBe("+41796666864");
|
||||||
"+41796666864",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("normalizes LID JIDs to E.164", () => {
|
it("normalizes LID JIDs to E.164", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user