feat: add chunking mode for outbound messages
- Introduced `chunkMode` option in various account configurations to allow splitting messages by "length" or "newline". - Updated message processing to handle chunking based on the selected mode. - Added tests for new chunking functionality, ensuring correct behavior for both modes.
This commit is contained in:
parent
6375ee836f
commit
4e4ff506da
@ -38,6 +38,7 @@ const bluebubblesAccountSchema = z.object({
|
|||||||
historyLimit: z.number().int().min(0).optional(),
|
historyLimit: z.number().int().min(0).optional(),
|
||||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||||
textChunkLimit: z.number().int().positive().optional(),
|
textChunkLimit: z.number().int().positive().optional(),
|
||||||
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||||
mediaMaxMb: z.number().int().positive().optional(),
|
mediaMaxMb: z.number().int().positive().optional(),
|
||||||
sendReadReceipts: z.boolean().optional(),
|
sendReadReceipts: z.boolean().optional(),
|
||||||
blockStreaming: z.boolean().optional(),
|
blockStreaming: z.boolean().optional(),
|
||||||
|
|||||||
@ -1851,16 +1851,25 @@ async function processMessage(
|
|||||||
account.config.textChunkLimit && account.config.textChunkLimit > 0
|
account.config.textChunkLimit && account.config.textChunkLimit > 0
|
||||||
? account.config.textChunkLimit
|
? account.config.textChunkLimit
|
||||||
: DEFAULT_TEXT_LIMIT;
|
: DEFAULT_TEXT_LIMIT;
|
||||||
|
const chunkMode = core.channel.text.resolveChunkMode(
|
||||||
|
config,
|
||||||
|
"bluebubbles",
|
||||||
|
account.accountId,
|
||||||
|
);
|
||||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||||
cfg: config,
|
cfg: config,
|
||||||
channel: "bluebubbles",
|
channel: "bluebubbles",
|
||||||
accountId: account.accountId,
|
accountId: account.accountId,
|
||||||
});
|
});
|
||||||
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
||||||
const chunks = core.channel.text.chunkMarkdownText(text, textLimit);
|
const chunks =
|
||||||
|
chunkMode === "newline"
|
||||||
|
? core.channel.text.chunkTextWithMode(text, textLimit, chunkMode)
|
||||||
|
: core.channel.text.chunkMarkdownText(text, textLimit);
|
||||||
if (!chunks.length && text) chunks.push(text);
|
if (!chunks.length && text) chunks.push(text);
|
||||||
if (!chunks.length) return;
|
if (!chunks.length) return;
|
||||||
for (const chunk of chunks) {
|
for (let i = 0; i < chunks.length; i++) {
|
||||||
|
const chunk = chunks[i];
|
||||||
const result = await sendMessageBlueBubbles(outboundTarget, chunk, {
|
const result = await sendMessageBlueBubbles(outboundTarget, chunk, {
|
||||||
cfg: config,
|
cfg: config,
|
||||||
accountId: account.accountId,
|
accountId: account.accountId,
|
||||||
@ -1869,6 +1878,15 @@ async function processMessage(
|
|||||||
maybeEnqueueOutboundMessageId(result.messageId, chunk);
|
maybeEnqueueOutboundMessageId(result.messageId, chunk);
|
||||||
sentMessage = true;
|
sentMessage = true;
|
||||||
statusSink?.({ lastOutboundAt: Date.now() });
|
statusSink?.({ lastOutboundAt: Date.now() });
|
||||||
|
// In newline mode, restart typing after each chunk if more chunks remain
|
||||||
|
if (chunkMode === "newline" && i < chunks.length - 1 && chatGuidForActions) {
|
||||||
|
sendBlueBubblesTyping(chatGuidForActions, true, {
|
||||||
|
cfg: config,
|
||||||
|
accountId: account.accountId,
|
||||||
|
}).catch(() => {
|
||||||
|
// Ignore typing errors
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onReplyStart: async () => {
|
onReplyStart: async () => {
|
||||||
|
|||||||
@ -38,6 +38,8 @@ export type BlueBubblesAccountConfig = {
|
|||||||
dms?: Record<string, unknown>;
|
dms?: Record<string, unknown>;
|
||||||
/** Outbound text chunk size (chars). Default: 4000. */
|
/** Outbound text chunk size (chars). Default: 4000. */
|
||||||
textChunkLimit?: number;
|
textChunkLimit?: number;
|
||||||
|
/** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */
|
||||||
|
chunkMode?: "length" | "newline";
|
||||||
blockStreaming?: boolean;
|
blockStreaming?: boolean;
|
||||||
/** Merge streamed block replies before sending. */
|
/** Merge streamed block replies before sending. */
|
||||||
blockStreamingCoalesce?: Record<string, unknown>;
|
blockStreamingCoalesce?: Record<string, unknown>;
|
||||||
|
|||||||
@ -50,6 +50,7 @@ export const MatrixConfigSchema = z.object({
|
|||||||
replyToMode: z.enum(["off", "first", "all"]).optional(),
|
replyToMode: z.enum(["off", "first", "all"]).optional(),
|
||||||
threadReplies: z.enum(["off", "inbound", "always"]).optional(),
|
threadReplies: z.enum(["off", "inbound", "always"]).optional(),
|
||||||
textChunkLimit: z.number().optional(),
|
textChunkLimit: z.number().optional(),
|
||||||
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||||
mediaMaxMb: z.number().optional(),
|
mediaMaxMb: z.number().optional(),
|
||||||
autoJoin: z.enum(["always", "allowlist", "off"]).optional(),
|
autoJoin: z.enum(["always", "allowlist", "off"]).optional(),
|
||||||
autoJoinAllowlist: z.array(allowFromEntry).optional(),
|
autoJoinAllowlist: z.array(allowFromEntry).optional(),
|
||||||
|
|||||||
@ -69,6 +69,8 @@ export type MatrixConfig = {
|
|||||||
threadReplies?: "off" | "inbound" | "always";
|
threadReplies?: "off" | "inbound" | "always";
|
||||||
/** Outbound text chunk size (chars). Default: 4000. */
|
/** Outbound text chunk size (chars). Default: 4000. */
|
||||||
textChunkLimit?: number;
|
textChunkLimit?: number;
|
||||||
|
/** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */
|
||||||
|
chunkMode?: "length" | "newline";
|
||||||
/** Max outbound media size in MB. */
|
/** Max outbound media size in MB. */
|
||||||
mediaMaxMb?: number;
|
mediaMaxMb?: number;
|
||||||
/** Auto-join invites (always|allowlist|off). Default: always. */
|
/** Auto-join invites (always|allowlist|off). Default: always. */
|
||||||
|
|||||||
@ -25,6 +25,7 @@ const MattermostAccountSchemaBase = z
|
|||||||
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
||||||
textChunkLimit: z.number().int().positive().optional(),
|
textChunkLimit: z.number().int().positive().optional(),
|
||||||
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||||
blockStreaming: z.boolean().optional(),
|
blockStreaming: z.boolean().optional(),
|
||||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -36,6 +36,8 @@ export type MattermostAccountConfig = {
|
|||||||
groupPolicy?: GroupPolicy;
|
groupPolicy?: GroupPolicy;
|
||||||
/** Outbound text chunk size (chars). Default: 4000. */
|
/** Outbound text chunk size (chars). Default: 4000. */
|
||||||
textChunkLimit?: number;
|
textChunkLimit?: number;
|
||||||
|
/** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */
|
||||||
|
chunkMode?: "length" | "newline";
|
||||||
/** Disable block streaming for this account. */
|
/** Disable block streaming for this account. */
|
||||||
blockStreaming?: boolean;
|
blockStreaming?: boolean;
|
||||||
/** Merge streamed block replies before sending. */
|
/** Merge streamed block replies before sending. */
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export const NextcloudTalkAccountSchemaBase = z
|
|||||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||||
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
||||||
textChunkLimit: z.number().int().positive().optional(),
|
textChunkLimit: z.number().int().positive().optional(),
|
||||||
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||||
blockStreaming: z.boolean().optional(),
|
blockStreaming: z.boolean().optional(),
|
||||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||||
mediaMaxMb: z.number().positive().optional(),
|
mediaMaxMb: z.number().positive().optional(),
|
||||||
|
|||||||
@ -62,6 +62,8 @@ export type NextcloudTalkAccountConfig = {
|
|||||||
dms?: Record<string, DmConfig>;
|
dms?: Record<string, DmConfig>;
|
||||||
/** Outbound text chunk size (chars). Default: 4000. */
|
/** Outbound text chunk size (chars). Default: 4000. */
|
||||||
textChunkLimit?: number;
|
textChunkLimit?: number;
|
||||||
|
/** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */
|
||||||
|
chunkMode?: "length" | "newline";
|
||||||
/** Disable block streaming for this account. */
|
/** Disable block streaming for this account. */
|
||||||
blockStreaming?: boolean;
|
blockStreaming?: boolean;
|
||||||
/** Merge streamed block replies before sending. */
|
/** Merge streamed block replies before sending. */
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { chunkMarkdownText, chunkText, resolveTextChunkLimit } from "./chunk.js";
|
import {
|
||||||
|
chunkByNewline,
|
||||||
|
chunkMarkdownText,
|
||||||
|
chunkText,
|
||||||
|
chunkTextWithMode,
|
||||||
|
resolveChunkMode,
|
||||||
|
resolveTextChunkLimit,
|
||||||
|
} from "./chunk.js";
|
||||||
|
|
||||||
function expectFencesBalanced(chunks: string[]) {
|
function expectFencesBalanced(chunks: string[]) {
|
||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
@ -231,3 +238,89 @@ describe("chunkMarkdownText", () => {
|
|||||||
expect(chunks.join("")).toBe(text);
|
expect(chunks.join("")).toBe(text);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("chunkByNewline", () => {
|
||||||
|
it("splits text on newlines", () => {
|
||||||
|
const text = "Line one\nLine two\nLine three";
|
||||||
|
const chunks = chunkByNewline(text, 1000);
|
||||||
|
expect(chunks).toEqual(["Line one", "Line two", "Line three"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters empty lines", () => {
|
||||||
|
const text = "Line one\n\n\nLine two\n\nLine three";
|
||||||
|
const chunks = chunkByNewline(text, 1000);
|
||||||
|
expect(chunks).toEqual(["Line one", "Line two", "Line three"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("trims whitespace from lines", () => {
|
||||||
|
const text = " Line one \n Line two ";
|
||||||
|
const chunks = chunkByNewline(text, 1000);
|
||||||
|
expect(chunks).toEqual(["Line one", "Line two"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to length-based for long lines", () => {
|
||||||
|
const text = "Short line\n" + "a".repeat(50) + "\nAnother short";
|
||||||
|
const chunks = chunkByNewline(text, 20);
|
||||||
|
expect(chunks[0]).toBe("Short line");
|
||||||
|
// Long line gets split into multiple chunks
|
||||||
|
expect(chunks[1].length).toBe(20);
|
||||||
|
expect(chunks[2].length).toBe(20);
|
||||||
|
expect(chunks[3].length).toBe(10);
|
||||||
|
expect(chunks[4]).toBe("Another short");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty array for empty input", () => {
|
||||||
|
expect(chunkByNewline("", 100)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty array for whitespace-only input", () => {
|
||||||
|
expect(chunkByNewline(" \n\n ", 100)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("chunkTextWithMode", () => {
|
||||||
|
it("uses length-based chunking for length mode", () => {
|
||||||
|
const text = "Line one\nLine two";
|
||||||
|
const chunks = chunkTextWithMode(text, 1000, "length");
|
||||||
|
expect(chunks).toEqual(["Line one\nLine two"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses newline-based chunking for newline mode", () => {
|
||||||
|
const text = "Line one\nLine two";
|
||||||
|
const chunks = chunkTextWithMode(text, 1000, "newline");
|
||||||
|
expect(chunks).toEqual(["Line one", "Line two"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveChunkMode", () => {
|
||||||
|
it("returns length as default", () => {
|
||||||
|
expect(resolveChunkMode(undefined, "telegram")).toBe("length");
|
||||||
|
expect(resolveChunkMode({}, "discord")).toBe("length");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns length for internal channel", () => {
|
||||||
|
const cfg = { channels: { telegram: { chunkMode: "newline" as const } } };
|
||||||
|
expect(resolveChunkMode(cfg, "__internal__")).toBe("length");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports provider-level overrides", () => {
|
||||||
|
const cfg = { channels: { telegram: { chunkMode: "newline" as const } } };
|
||||||
|
expect(resolveChunkMode(cfg, "telegram")).toBe("newline");
|
||||||
|
expect(resolveChunkMode(cfg, "discord")).toBe("length");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports account-level overrides", () => {
|
||||||
|
const cfg = {
|
||||||
|
channels: {
|
||||||
|
telegram: {
|
||||||
|
chunkMode: "length" as const,
|
||||||
|
accounts: {
|
||||||
|
primary: { chunkMode: "newline" as const },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(resolveChunkMode(cfg, "telegram", "primary")).toBe("newline");
|
||||||
|
expect(resolveChunkMode(cfg, "telegram", "other")).toBe("length");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -10,11 +10,20 @@ import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
|
|||||||
|
|
||||||
export type TextChunkProvider = ChannelId | typeof INTERNAL_MESSAGE_CHANNEL;
|
export type TextChunkProvider = ChannelId | typeof INTERNAL_MESSAGE_CHANNEL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chunking mode for outbound messages:
|
||||||
|
* - "length": Split only when exceeding textChunkLimit (default)
|
||||||
|
* - "newline": Split on every newline, with fallback to length-based for long lines
|
||||||
|
*/
|
||||||
|
export type ChunkMode = "length" | "newline";
|
||||||
|
|
||||||
const DEFAULT_CHUNK_LIMIT = 4000;
|
const DEFAULT_CHUNK_LIMIT = 4000;
|
||||||
|
const DEFAULT_CHUNK_MODE: ChunkMode = "length";
|
||||||
|
|
||||||
type ProviderChunkConfig = {
|
type ProviderChunkConfig = {
|
||||||
textChunkLimit?: number;
|
textChunkLimit?: number;
|
||||||
accounts?: Record<string, { textChunkLimit?: number }>;
|
chunkMode?: ChunkMode;
|
||||||
|
accounts?: Record<string, { textChunkLimit?: number; chunkMode?: ChunkMode }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveChunkLimitForProvider(
|
function resolveChunkLimitForProvider(
|
||||||
@ -63,6 +72,77 @@ export function resolveTextChunkLimit(
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveChunkModeForProvider(
|
||||||
|
cfgSection: ProviderChunkConfig | undefined,
|
||||||
|
accountId?: string | null,
|
||||||
|
): ChunkMode | undefined {
|
||||||
|
if (!cfgSection) return undefined;
|
||||||
|
const normalizedAccountId = normalizeAccountId(accountId);
|
||||||
|
const accounts = cfgSection.accounts;
|
||||||
|
if (accounts && typeof accounts === "object") {
|
||||||
|
const direct = accounts[normalizedAccountId];
|
||||||
|
if (direct?.chunkMode) {
|
||||||
|
return direct.chunkMode;
|
||||||
|
}
|
||||||
|
const matchKey = Object.keys(accounts).find(
|
||||||
|
(key) => key.toLowerCase() === normalizedAccountId.toLowerCase(),
|
||||||
|
);
|
||||||
|
const match = matchKey ? accounts[matchKey] : undefined;
|
||||||
|
if (match?.chunkMode) {
|
||||||
|
return match.chunkMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cfgSection.chunkMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveChunkMode(
|
||||||
|
cfg: ClawdbotConfig | undefined,
|
||||||
|
provider?: TextChunkProvider,
|
||||||
|
accountId?: string | null,
|
||||||
|
): ChunkMode {
|
||||||
|
if (!provider || provider === INTERNAL_MESSAGE_CHANNEL) return DEFAULT_CHUNK_MODE;
|
||||||
|
const channelsConfig = cfg?.channels as Record<string, unknown> | undefined;
|
||||||
|
const providerConfig = (channelsConfig?.[provider] ??
|
||||||
|
(cfg as Record<string, unknown> | undefined)?.[provider]) as ProviderChunkConfig | undefined;
|
||||||
|
const mode = resolveChunkModeForProvider(providerConfig, accountId);
|
||||||
|
return mode ?? DEFAULT_CHUNK_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split text on newlines, filtering empty lines.
|
||||||
|
* Lines exceeding maxLineLength are further split using length-based chunking.
|
||||||
|
*/
|
||||||
|
export function chunkByNewline(text: string, maxLineLength: number): string[] {
|
||||||
|
if (!text) return [];
|
||||||
|
const lines = text.split("\n");
|
||||||
|
const chunks: string[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue; // skip empty lines
|
||||||
|
|
||||||
|
if (trimmed.length <= maxLineLength) {
|
||||||
|
chunks.push(trimmed);
|
||||||
|
} else {
|
||||||
|
// Long line: fall back to length-based chunking
|
||||||
|
const subChunks = chunkText(trimmed, maxLineLength);
|
||||||
|
chunks.push(...subChunks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified chunking function that dispatches based on mode.
|
||||||
|
*/
|
||||||
|
export function chunkTextWithMode(text: string, limit: number, mode: ChunkMode): string[] {
|
||||||
|
if (mode === "newline") {
|
||||||
|
return chunkByNewline(text, limit);
|
||||||
|
}
|
||||||
|
return chunkText(text, limit);
|
||||||
|
}
|
||||||
|
|
||||||
export function chunkText(text: string, limit: number): string[] {
|
export function chunkText(text: string, limit: number): string[] {
|
||||||
if (!text) return [];
|
if (!text) return [];
|
||||||
if (limit <= 0) return [text];
|
if (limit <= 0) return [text];
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
INTERNAL_MESSAGE_CHANNEL,
|
INTERNAL_MESSAGE_CHANNEL,
|
||||||
listDeliverableMessageChannels,
|
listDeliverableMessageChannels,
|
||||||
} from "../../utils/message-channel.js";
|
} from "../../utils/message-channel.js";
|
||||||
import { resolveTextChunkLimit, type TextChunkProvider } from "../chunk.js";
|
import { resolveChunkMode, resolveTextChunkLimit, type TextChunkProvider } from "../chunk.js";
|
||||||
|
|
||||||
const DEFAULT_BLOCK_STREAM_MIN = 800;
|
const DEFAULT_BLOCK_STREAM_MIN = 800;
|
||||||
const DEFAULT_BLOCK_STREAM_MAX = 1200;
|
const DEFAULT_BLOCK_STREAM_MAX = 1200;
|
||||||
@ -68,6 +68,17 @@ export function resolveBlockStreamingChunking(
|
|||||||
fallbackLimit: providerChunkLimit,
|
fallbackLimit: providerChunkLimit,
|
||||||
});
|
});
|
||||||
const chunkCfg = cfg?.agents?.defaults?.blockStreamingChunk;
|
const chunkCfg = cfg?.agents?.defaults?.blockStreamingChunk;
|
||||||
|
|
||||||
|
// Check if channel has chunkMode: "newline" - if so, use newline-based streaming
|
||||||
|
const channelChunkMode = resolveChunkMode(cfg, providerKey, accountId);
|
||||||
|
if (channelChunkMode === "newline") {
|
||||||
|
// For newline mode: use very low minChars to flush quickly on newlines
|
||||||
|
const minChars = Math.max(1, Math.floor(chunkCfg?.minChars ?? 1));
|
||||||
|
const maxRequested = Math.max(1, Math.floor(chunkCfg?.maxChars ?? textLimit));
|
||||||
|
const maxChars = Math.max(1, Math.min(maxRequested, textLimit));
|
||||||
|
return { minChars, maxChars, breakPreference: "newline" };
|
||||||
|
}
|
||||||
|
|
||||||
const maxRequested = Math.max(1, Math.floor(chunkCfg?.maxChars ?? DEFAULT_BLOCK_STREAM_MAX));
|
const maxRequested = Math.max(1, Math.floor(chunkCfg?.maxChars ?? DEFAULT_BLOCK_STREAM_MAX));
|
||||||
const maxChars = Math.max(1, Math.min(maxRequested, textLimit));
|
const maxChars = Math.max(1, Math.min(maxRequested, textLimit));
|
||||||
const minFallback = DEFAULT_BLOCK_STREAM_MIN;
|
const minFallback = DEFAULT_BLOCK_STREAM_MIN;
|
||||||
@ -91,6 +102,13 @@ export function resolveBlockStreamingCoalescing(
|
|||||||
},
|
},
|
||||||
): BlockStreamingCoalescing | undefined {
|
): BlockStreamingCoalescing | undefined {
|
||||||
const providerKey = normalizeChunkProvider(provider);
|
const providerKey = normalizeChunkProvider(provider);
|
||||||
|
|
||||||
|
// When chunkMode is "newline", disable coalescing entirely to send each line immediately
|
||||||
|
const channelChunkMode = resolveChunkMode(cfg, providerKey, accountId);
|
||||||
|
if (channelChunkMode === "newline") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const providerId = providerKey ? normalizeChannelId(providerKey) : null;
|
const providerId = providerKey ? normalizeChannelId(providerKey) : null;
|
||||||
const providerChunkLimit = providerId
|
const providerChunkLimit = providerId
|
||||||
? getChannelDock(providerId)?.outbound?.textChunkLimit
|
? getChannelDock(providerId)?.outbound?.textChunkLimit
|
||||||
|
|||||||
@ -108,6 +108,8 @@ export type DiscordAccountConfig = {
|
|||||||
groupPolicy?: GroupPolicy;
|
groupPolicy?: GroupPolicy;
|
||||||
/** Outbound text chunk size (chars). Default: 2000. */
|
/** Outbound text chunk size (chars). Default: 2000. */
|
||||||
textChunkLimit?: number;
|
textChunkLimit?: number;
|
||||||
|
/** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */
|
||||||
|
chunkMode?: "length" | "newline";
|
||||||
/** Disable block streaming for this account. */
|
/** Disable block streaming for this account. */
|
||||||
blockStreaming?: boolean;
|
blockStreaming?: boolean;
|
||||||
/** Merge streamed block replies before sending. */
|
/** Merge streamed block replies before sending. */
|
||||||
|
|||||||
@ -54,6 +54,8 @@ export type IMessageAccountConfig = {
|
|||||||
mediaMaxMb?: number;
|
mediaMaxMb?: number;
|
||||||
/** Outbound text chunk size (chars). Default: 4000. */
|
/** Outbound text chunk size (chars). Default: 4000. */
|
||||||
textChunkLimit?: number;
|
textChunkLimit?: number;
|
||||||
|
/** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */
|
||||||
|
chunkMode?: "length" | "newline";
|
||||||
blockStreaming?: boolean;
|
blockStreaming?: boolean;
|
||||||
/** Merge streamed block replies before sending. */
|
/** Merge streamed block replies before sending. */
|
||||||
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
||||||
|
|||||||
@ -72,6 +72,8 @@ export type MSTeamsConfig = {
|
|||||||
groupPolicy?: GroupPolicy;
|
groupPolicy?: GroupPolicy;
|
||||||
/** Outbound text chunk size (chars). Default: 4000. */
|
/** Outbound text chunk size (chars). Default: 4000. */
|
||||||
textChunkLimit?: number;
|
textChunkLimit?: number;
|
||||||
|
/** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */
|
||||||
|
chunkMode?: "length" | "newline";
|
||||||
/** Merge streamed block replies before sending. */
|
/** Merge streamed block replies before sending. */
|
||||||
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -56,6 +56,8 @@ export type SignalAccountConfig = {
|
|||||||
dms?: Record<string, DmConfig>;
|
dms?: Record<string, DmConfig>;
|
||||||
/** Outbound text chunk size (chars). Default: 4000. */
|
/** Outbound text chunk size (chars). Default: 4000. */
|
||||||
textChunkLimit?: number;
|
textChunkLimit?: number;
|
||||||
|
/** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */
|
||||||
|
chunkMode?: "length" | "newline";
|
||||||
blockStreaming?: boolean;
|
blockStreaming?: boolean;
|
||||||
/** Merge streamed block replies before sending. */
|
/** Merge streamed block replies before sending. */
|
||||||
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
||||||
|
|||||||
@ -116,6 +116,8 @@ export type SlackAccountConfig = {
|
|||||||
/** Per-DM config overrides keyed by user ID. */
|
/** Per-DM config overrides keyed by user ID. */
|
||||||
dms?: Record<string, DmConfig>;
|
dms?: Record<string, DmConfig>;
|
||||||
textChunkLimit?: number;
|
textChunkLimit?: number;
|
||||||
|
/** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */
|
||||||
|
chunkMode?: "length" | "newline";
|
||||||
blockStreaming?: boolean;
|
blockStreaming?: boolean;
|
||||||
/** Merge streamed block replies before sending. */
|
/** Merge streamed block replies before sending. */
|
||||||
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
||||||
|
|||||||
@ -80,6 +80,8 @@ export type TelegramAccountConfig = {
|
|||||||
dms?: Record<string, DmConfig>;
|
dms?: Record<string, DmConfig>;
|
||||||
/** Outbound text chunk size (chars). Default: 4000. */
|
/** Outbound text chunk size (chars). Default: 4000. */
|
||||||
textChunkLimit?: number;
|
textChunkLimit?: number;
|
||||||
|
/** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */
|
||||||
|
chunkMode?: "length" | "newline";
|
||||||
/** Disable block streaming for this account. */
|
/** Disable block streaming for this account. */
|
||||||
blockStreaming?: boolean;
|
blockStreaming?: boolean;
|
||||||
/** Chunking config for draft streaming in `streamMode: "block"`. */
|
/** Chunking config for draft streaming in `streamMode: "block"`. */
|
||||||
|
|||||||
@ -55,6 +55,8 @@ export type WhatsAppConfig = {
|
|||||||
dms?: Record<string, DmConfig>;
|
dms?: Record<string, DmConfig>;
|
||||||
/** Outbound text chunk size (chars). Default: 4000. */
|
/** Outbound text chunk size (chars). Default: 4000. */
|
||||||
textChunkLimit?: number;
|
textChunkLimit?: number;
|
||||||
|
/** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */
|
||||||
|
chunkMode?: "length" | "newline";
|
||||||
/** Maximum media file size in MB. Default: 50. */
|
/** Maximum media file size in MB. Default: 50. */
|
||||||
mediaMaxMb?: number;
|
mediaMaxMb?: number;
|
||||||
/** Disable block streaming for this account. */
|
/** Disable block streaming for this account. */
|
||||||
@ -122,6 +124,8 @@ export type WhatsAppAccountConfig = {
|
|||||||
/** Per-DM config overrides keyed by user ID. */
|
/** Per-DM config overrides keyed by user ID. */
|
||||||
dms?: Record<string, DmConfig>;
|
dms?: Record<string, DmConfig>;
|
||||||
textChunkLimit?: number;
|
textChunkLimit?: number;
|
||||||
|
/** Chunking mode: "length" (default) splits by size, "newline" splits on every newline. */
|
||||||
|
chunkMode?: "length" | "newline";
|
||||||
mediaMaxMb?: number;
|
mediaMaxMb?: number;
|
||||||
blockStreaming?: boolean;
|
blockStreaming?: boolean;
|
||||||
/** Merge streamed block replies before sending. */
|
/** Merge streamed block replies before sending. */
|
||||||
|
|||||||
@ -102,6 +102,7 @@ export const TelegramAccountSchemaBase = z
|
|||||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||||
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
||||||
textChunkLimit: z.number().int().positive().optional(),
|
textChunkLimit: z.number().int().positive().optional(),
|
||||||
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||||
blockStreaming: z.boolean().optional(),
|
blockStreaming: z.boolean().optional(),
|
||||||
draftChunk: BlockStreamingChunkSchema.optional(),
|
draftChunk: BlockStreamingChunkSchema.optional(),
|
||||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||||
@ -212,6 +213,7 @@ export const DiscordAccountSchema = z
|
|||||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||||
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
||||||
textChunkLimit: z.number().int().positive().optional(),
|
textChunkLimit: z.number().int().positive().optional(),
|
||||||
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||||
blockStreaming: z.boolean().optional(),
|
blockStreaming: z.boolean().optional(),
|
||||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||||
maxLinesPerMessage: z.number().int().positive().optional(),
|
maxLinesPerMessage: z.number().int().positive().optional(),
|
||||||
@ -401,6 +403,7 @@ export const SlackAccountSchema = z
|
|||||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||||
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
||||||
textChunkLimit: z.number().int().positive().optional(),
|
textChunkLimit: z.number().int().positive().optional(),
|
||||||
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||||
blockStreaming: z.boolean().optional(),
|
blockStreaming: z.boolean().optional(),
|
||||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||||
mediaMaxMb: z.number().positive().optional(),
|
mediaMaxMb: z.number().positive().optional(),
|
||||||
@ -494,6 +497,7 @@ export const SignalAccountSchemaBase = z
|
|||||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||||
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
||||||
textChunkLimit: z.number().int().positive().optional(),
|
textChunkLimit: z.number().int().positive().optional(),
|
||||||
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||||
blockStreaming: z.boolean().optional(),
|
blockStreaming: z.boolean().optional(),
|
||||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||||
mediaMaxMb: z.number().int().positive().optional(),
|
mediaMaxMb: z.number().int().positive().optional(),
|
||||||
@ -547,6 +551,7 @@ export const IMessageAccountSchemaBase = z
|
|||||||
includeAttachments: z.boolean().optional(),
|
includeAttachments: z.boolean().optional(),
|
||||||
mediaMaxMb: z.number().int().positive().optional(),
|
mediaMaxMb: z.number().int().positive().optional(),
|
||||||
textChunkLimit: z.number().int().positive().optional(),
|
textChunkLimit: z.number().int().positive().optional(),
|
||||||
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||||
blockStreaming: z.boolean().optional(),
|
blockStreaming: z.boolean().optional(),
|
||||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||||
groups: z
|
groups: z
|
||||||
@ -633,6 +638,7 @@ export const BlueBubblesAccountSchemaBase = z
|
|||||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||||
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
||||||
textChunkLimit: z.number().int().positive().optional(),
|
textChunkLimit: z.number().int().positive().optional(),
|
||||||
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||||
mediaMaxMb: z.number().int().positive().optional(),
|
mediaMaxMb: z.number().int().positive().optional(),
|
||||||
sendReadReceipts: z.boolean().optional(),
|
sendReadReceipts: z.boolean().optional(),
|
||||||
blockStreaming: z.boolean().optional(),
|
blockStreaming: z.boolean().optional(),
|
||||||
@ -704,6 +710,7 @@ export const MSTeamsConfigSchema = z
|
|||||||
groupAllowFrom: z.array(z.string()).optional(),
|
groupAllowFrom: z.array(z.string()).optional(),
|
||||||
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
||||||
textChunkLimit: z.number().int().positive().optional(),
|
textChunkLimit: z.number().int().positive().optional(),
|
||||||
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||||
mediaAllowHosts: z.array(z.string()).optional(),
|
mediaAllowHosts: z.array(z.string()).optional(),
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
|
|||||||
@ -30,6 +30,7 @@ export const WhatsAppAccountSchema = z
|
|||||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||||
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
||||||
textChunkLimit: z.number().int().positive().optional(),
|
textChunkLimit: z.number().int().positive().optional(),
|
||||||
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||||
mediaMaxMb: z.number().int().positive().optional(),
|
mediaMaxMb: z.number().int().positive().optional(),
|
||||||
blockStreaming: z.boolean().optional(),
|
blockStreaming: z.boolean().optional(),
|
||||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||||
@ -85,6 +86,7 @@ export const WhatsAppConfigSchema = z
|
|||||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||||
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
||||||
textChunkLimit: z.number().int().positive().optional(),
|
textChunkLimit: z.number().int().positive().optional(),
|
||||||
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||||
mediaMaxMb: z.number().int().positive().optional().default(50),
|
mediaMaxMb: z.number().int().positive().optional().default(50),
|
||||||
blockStreaming: z.boolean().optional(),
|
blockStreaming: z.boolean().optional(),
|
||||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
|
import {
|
||||||
|
chunkTextWithMode,
|
||||||
|
resolveChunkMode,
|
||||||
|
resolveTextChunkLimit,
|
||||||
|
} from "../../auto-reply/chunk.js";
|
||||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||||
import { resolveChannelMediaMaxBytes } from "../../channels/plugins/media-limits.js";
|
import { resolveChannelMediaMaxBytes } from "../../channels/plugins/media-limits.js";
|
||||||
import { loadChannelOutboundAdapter } from "../../channels/plugins/outbound/load.js";
|
import { loadChannelOutboundAdapter } from "../../channels/plugins/outbound/load.js";
|
||||||
@ -192,6 +196,7 @@ export async function deliverOutboundPayloads(params: {
|
|||||||
fallbackLimit: handler.textChunkLimit,
|
fallbackLimit: handler.textChunkLimit,
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const chunkMode = resolveChunkMode(cfg, channel, accountId);
|
||||||
const isSignalChannel = channel === "signal";
|
const isSignalChannel = channel === "signal";
|
||||||
const signalTableMode = isSignalChannel
|
const signalTableMode = isSignalChannel
|
||||||
? resolveMarkdownTableMode({ cfg, channel: "signal", accountId })
|
? resolveMarkdownTableMode({ cfg, channel: "signal", accountId })
|
||||||
@ -212,7 +217,12 @@ export async function deliverOutboundPayloads(params: {
|
|||||||
results.push(await handler.sendText(text));
|
results.push(await handler.sendText(text));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const chunk of handler.chunker(text, textLimit)) {
|
// Use newline chunking if explicitly configured, otherwise use the adapter's chunker
|
||||||
|
const chunks =
|
||||||
|
chunkMode === "newline"
|
||||||
|
? chunkTextWithMode(text, textLimit, chunkMode)
|
||||||
|
: handler.chunker(text, textLimit);
|
||||||
|
for (const chunk of chunks) {
|
||||||
throwIfAborted(abortSignal);
|
throwIfAborted(abortSignal);
|
||||||
results.push(await handler.sendText(chunk));
|
results.push(await handler.sendText(chunk));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,6 +112,7 @@ export type { WizardPrompter } from "../wizard/prompts.js";
|
|||||||
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||||
export { resolveAckReaction } from "../agents/identity.js";
|
export { resolveAckReaction } from "../agents/identity.js";
|
||||||
export type { ReplyPayload } from "../auto-reply/types.js";
|
export type { ReplyPayload } from "../auto-reply/types.js";
|
||||||
|
export type { ChunkMode } from "../auto-reply/chunk.js";
|
||||||
export { SILENT_REPLY_TOKEN, isSilentReplyText } from "../auto-reply/tokens.js";
|
export { SILENT_REPLY_TOKEN, isSilentReplyText } from "../auto-reply/tokens.js";
|
||||||
export {
|
export {
|
||||||
buildPendingHistoryContextFromMap,
|
buildPendingHistoryContextFromMap,
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
import { chunkMarkdownText, chunkText, resolveTextChunkLimit } from "../../auto-reply/chunk.js";
|
import {
|
||||||
|
chunkByNewline,
|
||||||
|
chunkMarkdownText,
|
||||||
|
chunkText,
|
||||||
|
chunkTextWithMode,
|
||||||
|
resolveChunkMode,
|
||||||
|
resolveTextChunkLimit,
|
||||||
|
} from "../../auto-reply/chunk.js";
|
||||||
import {
|
import {
|
||||||
hasControlCommand,
|
hasControlCommand,
|
||||||
isControlCommandMessage,
|
isControlCommandMessage,
|
||||||
@ -160,8 +167,11 @@ export function createPluginRuntime(): PluginRuntime {
|
|||||||
},
|
},
|
||||||
channel: {
|
channel: {
|
||||||
text: {
|
text: {
|
||||||
|
chunkByNewline,
|
||||||
chunkMarkdownText,
|
chunkMarkdownText,
|
||||||
chunkText,
|
chunkText,
|
||||||
|
chunkTextWithMode,
|
||||||
|
resolveChunkMode,
|
||||||
resolveTextChunkLimit,
|
resolveTextChunkLimit,
|
||||||
hasControlCommand,
|
hasControlCommand,
|
||||||
resolveMarkdownTableMode,
|
resolveMarkdownTableMode,
|
||||||
|
|||||||
@ -35,8 +35,11 @@ type ResolveInboundDebounceMs =
|
|||||||
type ResolveCommandAuthorizedFromAuthorizers =
|
type ResolveCommandAuthorizedFromAuthorizers =
|
||||||
typeof import("../../channels/command-gating.js").resolveCommandAuthorizedFromAuthorizers;
|
typeof import("../../channels/command-gating.js").resolveCommandAuthorizedFromAuthorizers;
|
||||||
type ResolveTextChunkLimit = typeof import("../../auto-reply/chunk.js").resolveTextChunkLimit;
|
type ResolveTextChunkLimit = typeof import("../../auto-reply/chunk.js").resolveTextChunkLimit;
|
||||||
|
type ResolveChunkMode = typeof import("../../auto-reply/chunk.js").resolveChunkMode;
|
||||||
type ChunkMarkdownText = typeof import("../../auto-reply/chunk.js").chunkMarkdownText;
|
type ChunkMarkdownText = typeof import("../../auto-reply/chunk.js").chunkMarkdownText;
|
||||||
type ChunkText = typeof import("../../auto-reply/chunk.js").chunkText;
|
type ChunkText = typeof import("../../auto-reply/chunk.js").chunkText;
|
||||||
|
type ChunkTextWithMode = typeof import("../../auto-reply/chunk.js").chunkTextWithMode;
|
||||||
|
type ChunkByNewline = typeof import("../../auto-reply/chunk.js").chunkByNewline;
|
||||||
type ResolveMarkdownTableMode =
|
type ResolveMarkdownTableMode =
|
||||||
typeof import("../../config/markdown-tables.js").resolveMarkdownTableMode;
|
typeof import("../../config/markdown-tables.js").resolveMarkdownTableMode;
|
||||||
type ConvertMarkdownTables = typeof import("../../markdown/tables.js").convertMarkdownTables;
|
type ConvertMarkdownTables = typeof import("../../markdown/tables.js").convertMarkdownTables;
|
||||||
@ -173,8 +176,11 @@ export type PluginRuntime = {
|
|||||||
};
|
};
|
||||||
channel: {
|
channel: {
|
||||||
text: {
|
text: {
|
||||||
|
chunkByNewline: ChunkByNewline;
|
||||||
chunkMarkdownText: ChunkMarkdownText;
|
chunkMarkdownText: ChunkMarkdownText;
|
||||||
chunkText: ChunkText;
|
chunkText: ChunkText;
|
||||||
|
chunkTextWithMode: ChunkTextWithMode;
|
||||||
|
resolveChunkMode: ResolveChunkMode;
|
||||||
resolveTextChunkLimit: ResolveTextChunkLimit;
|
resolveTextChunkLimit: ResolveTextChunkLimit;
|
||||||
hasControlCommand: HasControlCommand;
|
hasControlCommand: HasControlCommand;
|
||||||
resolveMarkdownTableMode: ResolveMarkdownTableMode;
|
resolveMarkdownTableMode: ResolveMarkdownTableMode;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user