pikiclaw 0.2.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,115 @@
1
+ /**
2
+ * bot-handler.ts — channel-agnostic message handling pipeline.
3
+ *
4
+ * Defines the `MessagePipeline` interface that each IM implements to plug in
5
+ * its own placeholder, live preview, final reply, and MCP file-send logic.
6
+ *
7
+ * The generic `handleIncomingMessage()` orchestrates the shared flow:
8
+ * resolve session → create placeholder → start live preview → run stream →
9
+ * settle preview → send final reply → cleanup
10
+ *
11
+ * File return is handled in real-time by the MCP bridge during the stream,
12
+ * not as a post-stream batch.
13
+ */
14
+ import { buildPrompt } from './bot.js';
15
+ import { stageSessionFiles } from './code-agent.js';
16
+ export async function stageFilesIntoSession(bot, session, files) {
17
+ const staged = stageSessionFiles({
18
+ agent: session.agent,
19
+ workdir: bot.workdir,
20
+ files,
21
+ sessionId: session.sessionId,
22
+ title: files[0],
23
+ });
24
+ session.workspacePath = staged.workspacePath;
25
+ return {
26
+ ok: staged.importedFiles.length > 0,
27
+ sessionId: staged.sessionId,
28
+ workspacePath: staged.workspacePath,
29
+ importedCount: staged.importedFiles.length,
30
+ };
31
+ }
32
+ /**
33
+ * Generic message handling orchestration. Call this from your IM-specific bot.
34
+ *
35
+ * This handles the full lifecycle: session resolution → task registration →
36
+ * placeholder → live preview → stream (with MCP bridge) → final reply → cleanup.
37
+ */
38
+ export async function handleIncomingMessage(opts) {
39
+ const { bot, pipeline, ctx, files, systemPrompt } = opts;
40
+ const text = opts.text.trim();
41
+ if (!text && !files.length)
42
+ return;
43
+ const chatId = pipeline.getChatId(ctx);
44
+ const messageId = pipeline.getMessageId(ctx);
45
+ const session = pipeline.resolveSession(ctx, text, files);
46
+ const cs = bot.chat(chatId);
47
+ // Apply session selection to chat state
48
+ cs.activeSessionKey = session.key;
49
+ cs.agent = session.agent;
50
+ cs.sessionId = session.sessionId;
51
+ cs.workspacePath = session.workspacePath;
52
+ cs.codexCumulative = session.codexCumulative;
53
+ cs.modelId = session.modelId ?? null;
54
+ // File-only message: stage files without running prompt
55
+ if (!text && files.length) {
56
+ const hadPendingWork = bot['sessionHasPendingWork']?.(session) ?? false;
57
+ const stageTask = opts.queueSessionTask(session, async () => {
58
+ const result = await stageFilesIntoSession(bot, session, files);
59
+ opts.syncSelectedChats(session);
60
+ if (!result.ok)
61
+ throw new Error('no files persisted');
62
+ opts.log(`[handleMessage] staged files session=${result.sessionId} files=${result.importedCount}`);
63
+ });
64
+ if (hadPendingWork) {
65
+ void stageTask.catch(e => opts.log(`[handleMessage] stage queue failed: ${e}`));
66
+ }
67
+ else {
68
+ await stageTask.catch(e => opts.log(`[handleMessage] stage queue failed: ${e}`));
69
+ }
70
+ return;
71
+ }
72
+ const prompt = buildPrompt(text, files);
73
+ const start = Date.now();
74
+ opts.log(`[handleMessage] queued chat=${chatId} agent=${session.agent} session=${session.sessionId || '(new)'} prompt="${prompt.slice(0, 100)}" files=${files.length}`);
75
+ const placeholder = await pipeline.createPlaceholder(ctx, session);
76
+ const taskId = opts.createTaskId(session);
77
+ opts.beginTask({
78
+ taskId,
79
+ chatId,
80
+ agent: session.agent,
81
+ sessionKey: session.key,
82
+ prompt,
83
+ startedAt: start,
84
+ sourceMessageId: messageId,
85
+ });
86
+ // Create MCP sendFile callback bound to this chat context
87
+ const mcpSendFile = pipeline.createMcpSendFile(ctx, session);
88
+ void opts.queueSessionTask(session, async () => {
89
+ let preview = null;
90
+ try {
91
+ if (placeholder) {
92
+ preview = pipeline.createLivePreview(ctx, placeholder, session);
93
+ preview?.start();
94
+ }
95
+ const result = await bot.runStream(prompt, session, files, (nextText, nextThinking, nextActivity = '', meta, plan) => {
96
+ preview?.update(nextText, nextThinking, nextActivity, meta, plan);
97
+ }, systemPrompt, mcpSendFile);
98
+ await preview?.settle();
99
+ opts.log(`[handleMessage] done agent=${session.agent} ok=${result.ok} elapsed=${result.elapsedS.toFixed(1)}s`);
100
+ await pipeline.sendFinalReply(ctx, placeholder, session, result);
101
+ }
102
+ catch (e) {
103
+ opts.log(`[handleMessage] task failed chat=${chatId} error=${e?.message || e}`);
104
+ await pipeline.onError(ctx, placeholder, session, e instanceof Error ? e : new Error(String(e)));
105
+ }
106
+ finally {
107
+ preview?.dispose();
108
+ opts.finishTask(taskId);
109
+ opts.syncSelectedChats(session);
110
+ }
111
+ }).catch(e => {
112
+ opts.log(`[handleMessage] queue execution failed: ${e}`);
113
+ opts.finishTask(taskId);
114
+ });
115
+ }
@@ -0,0 +1,44 @@
1
+ export const SKILL_CMD_PREFIX = 'sk_';
2
+ export function buildWelcomeIntro(version) {
3
+ return {
4
+ title: "Hi, I'm pikiclaw",
5
+ subtitle: 'Send me a message to get started.',
6
+ version,
7
+ };
8
+ }
9
+ export function buildSkillCommandName(skillName) {
10
+ const normalized = skillName.trim().toLowerCase().replace(/[^a-z0-9_]/g, '_');
11
+ if (!normalized)
12
+ return null;
13
+ const cmdName = `${SKILL_CMD_PREFIX}${normalized}`;
14
+ if (cmdName.length > 32)
15
+ return null;
16
+ return cmdName;
17
+ }
18
+ export function indexSkillsByCommand(skills) {
19
+ const indexed = new Map();
20
+ for (const skill of skills) {
21
+ const cmdName = buildSkillCommandName(skill.name);
22
+ if (!cmdName || indexed.has(cmdName))
23
+ continue;
24
+ indexed.set(cmdName, skill);
25
+ }
26
+ return indexed;
27
+ }
28
+ export function buildDefaultMenuCommands(agentCount, skills = []) {
29
+ const commands = [
30
+ { command: 'sessions', description: 'Switch sessions' },
31
+ ];
32
+ if (agentCount > 1) {
33
+ commands.push({ command: 'agents', description: 'Switch agents' });
34
+ }
35
+ commands.push({ command: 'switch', description: 'Change workdir' }, { command: 'models', description: 'Switch models' }, { command: 'status', description: 'Show status' }, { command: 'host', description: 'Host info' });
36
+ if (skills.length) {
37
+ commands.push({ command: 'skills', description: 'Browse skills' });
38
+ }
39
+ if (agentCount === 1) {
40
+ commands.push({ command: 'agents', description: 'Switch agents' });
41
+ }
42
+ commands.push({ command: 'restart', description: 'Restart bot' });
43
+ return commands;
44
+ }
@@ -0,0 +1,165 @@
1
+ const INJECTED_PROMPT_MARKERS = [
2
+ '\n[Session Workspace]',
3
+ '\n[Telegram Artifact Return]',
4
+ '\n[Artifact Return]',
5
+ ];
6
+ export function stripInjectedPrompts(text) {
7
+ for (const marker of INJECTED_PROMPT_MARKERS) {
8
+ const idx = text.indexOf(marker);
9
+ if (idx >= 0)
10
+ return text.slice(0, idx).trim();
11
+ }
12
+ return text.trim();
13
+ }
14
+ export function summarizePromptForStatus(prompt, maxLen = 50) {
15
+ const clean = stripInjectedPrompts(prompt).replace(/\s+/g, ' ').trim();
16
+ if (!clean)
17
+ return '';
18
+ if (clean.length <= maxLen)
19
+ return clean;
20
+ return clean.slice(0, Math.max(0, maxLen - 3)).trimEnd() + '...';
21
+ }
22
+ function parseClaudeShellActivity(line) {
23
+ const prefix = 'Run shell: ';
24
+ if (!line.startsWith(prefix))
25
+ return null;
26
+ const detail = line.slice(prefix.length).trim();
27
+ if (!detail)
28
+ return { key: prefix.trim(), status: 'active' };
29
+ const doneIdx = detail.indexOf(' -> ');
30
+ if (doneIdx > 0) {
31
+ return {
32
+ key: detail.slice(0, doneIdx).trim(),
33
+ status: 'done',
34
+ };
35
+ }
36
+ const failed = detail.match(/^(.*)\sfailed(?::.*)?$/);
37
+ if (failed?.[1]?.trim()) {
38
+ return {
39
+ key: failed[1].trim(),
40
+ status: 'failed',
41
+ };
42
+ }
43
+ if (detail.endsWith(' done')) {
44
+ const key = detail.slice(0, -' done'.length).trim();
45
+ return { key: key || detail, status: 'done' };
46
+ }
47
+ return { key: detail, status: 'active' };
48
+ }
49
+ export function parseActivitySummary(activity) {
50
+ const narrative = [];
51
+ let failedCommands = 0;
52
+ let activeCommands = 0;
53
+ let completedCommands = 0;
54
+ const activeClaudeShells = new Map();
55
+ for (const rawLine of activity.split('\n')) {
56
+ const line = rawLine.replace(/\s+/g, ' ').trim();
57
+ if (!line)
58
+ continue;
59
+ const claudeShell = parseClaudeShellActivity(line);
60
+ if (claudeShell) {
61
+ const key = claudeShell.key || 'Run shell';
62
+ const current = activeClaudeShells.get(key) || 0;
63
+ if (claudeShell.status === 'active') {
64
+ activeClaudeShells.set(key, current + 1);
65
+ }
66
+ else {
67
+ if (current > 0)
68
+ activeClaudeShells.set(key, current - 1);
69
+ if (claudeShell.status === 'done')
70
+ completedCommands++;
71
+ else
72
+ failedCommands++;
73
+ }
74
+ continue;
75
+ }
76
+ if (line.startsWith('$ ')) {
77
+ activeCommands++;
78
+ continue;
79
+ }
80
+ if (line.startsWith('Ran: ')) {
81
+ completedCommands++;
82
+ continue;
83
+ }
84
+ const executed = line.match(/^Executed (\d+) command(?:s)?\.$/);
85
+ if (executed) {
86
+ completedCommands = Math.max(completedCommands, parseInt(executed[1], 10) || 0);
87
+ continue;
88
+ }
89
+ const running = line.match(/^Running (\d+) command(?:s)?\.\.\.$/);
90
+ if (running) {
91
+ activeCommands = Math.max(activeCommands, parseInt(running[1], 10) || 0);
92
+ continue;
93
+ }
94
+ const failed = line.match(/^Command failed \((\d+)\):/);
95
+ if (failed) {
96
+ failedCommands++;
97
+ continue;
98
+ }
99
+ if (/^Command failed \(\d+\)$/.test(line)) {
100
+ failedCommands++;
101
+ continue;
102
+ }
103
+ narrative.push(line);
104
+ }
105
+ for (const pending of activeClaudeShells.values()) {
106
+ activeCommands += pending;
107
+ }
108
+ return { narrative, failedCommands, completedCommands, activeCommands };
109
+ }
110
+ export function formatActivityCommandSummary(completedCommands, activeCommands, failedCommands = 0) {
111
+ const parts = [];
112
+ if (failedCommands > 0)
113
+ parts.push(`${failedCommands} failed`);
114
+ if (completedCommands > 0)
115
+ parts.push(`${completedCommands} done`);
116
+ if (activeCommands > 0)
117
+ parts.push(`${activeCommands} running`);
118
+ return parts.length ? `commands: ${parts.join(', ')}` : '';
119
+ }
120
+ export function summarizeActivityForPreview(activity) {
121
+ const summary = parseActivitySummary(activity);
122
+ const lines = [...summary.narrative];
123
+ const commandSummary = formatActivityCommandSummary(summary.completedCommands, summary.activeCommands, summary.failedCommands);
124
+ if (commandSummary)
125
+ lines.push(commandSummary);
126
+ return lines.join('\n');
127
+ }
128
+ export function hasPreviewMeta(meta) {
129
+ return meta?.contextPercent != null;
130
+ }
131
+ export function samePreviewMeta(a, b) {
132
+ return (a?.contextPercent ?? null) === (b?.contextPercent ?? null);
133
+ }
134
+ export function samePreviewPlan(a, b) {
135
+ if ((a?.explanation ?? null) !== (b?.explanation ?? null))
136
+ return false;
137
+ const aSteps = a?.steps ?? [];
138
+ const bSteps = b?.steps ?? [];
139
+ if (aSteps.length !== bSteps.length)
140
+ return false;
141
+ for (let i = 0; i < aSteps.length; i++) {
142
+ if (aSteps[i].status !== bSteps[i].status)
143
+ return false;
144
+ if (aSteps[i].step !== bSteps[i].step)
145
+ return false;
146
+ }
147
+ return true;
148
+ }
149
+ function normalizePlanStep(step) {
150
+ return step.replace(/\s+/g, ' ').trim();
151
+ }
152
+ export function renderPlanForPreview(plan) {
153
+ if (!plan?.steps.length)
154
+ return '';
155
+ const completed = plan.steps.filter(step => step.status === 'completed').length;
156
+ const total = plan.steps.length;
157
+ const lines = [`Plan ${completed}/${total}`];
158
+ for (const step of plan.steps.slice(0, 4)) {
159
+ const prefix = step.status === 'completed' ? '[x]' : step.status === 'inProgress' ? '[>]' : '[ ]';
160
+ lines.push(`${prefix} ${normalizePlanStep(step.step)}`);
161
+ }
162
+ if (plan.steps.length > 4)
163
+ lines.push(`... +${plan.steps.length - 4} more`);
164
+ return lines.join('\n');
165
+ }
@@ -0,0 +1,74 @@
1
+ import path from 'node:path';
2
+ import { listSubdirs } from './bot.js';
3
+ import { buildCompactSelectionTitle, compactCode } from './bot-telegram-render.js';
4
+ class PathRegistry {
5
+ pathToId = new Map();
6
+ idToPath = new Map();
7
+ nextId = 1;
8
+ register(dirPath) {
9
+ let id = this.pathToId.get(dirPath);
10
+ if (id != null)
11
+ return id;
12
+ id = this.nextId++;
13
+ this.pathToId.set(dirPath, id);
14
+ this.idToPath.set(id, dirPath);
15
+ if (this.pathToId.size > 500) {
16
+ const oldest = [...this.pathToId.entries()].slice(0, 200);
17
+ for (const [oldPath, oldId] of oldest) {
18
+ this.pathToId.delete(oldPath);
19
+ this.idToPath.delete(oldId);
20
+ }
21
+ }
22
+ return id;
23
+ }
24
+ resolve(id) {
25
+ return this.idToPath.get(id);
26
+ }
27
+ }
28
+ const pathRegistry = new PathRegistry();
29
+ const DIR_PAGE_SIZE = 8;
30
+ function buildDirKeyboard(browsePath, page) {
31
+ const dirs = listSubdirs(browsePath);
32
+ const totalPages = Math.max(1, Math.ceil(dirs.length / DIR_PAGE_SIZE));
33
+ const currentPage = Math.min(Math.max(0, page), totalPages - 1);
34
+ const slice = dirs.slice(currentPage * DIR_PAGE_SIZE, (currentPage + 1) * DIR_PAGE_SIZE);
35
+ const rows = [];
36
+ for (let i = 0; i < slice.length; i += 2) {
37
+ const row = [];
38
+ for (let j = i; j < Math.min(i + 2, slice.length); j++) {
39
+ const fullPath = path.join(browsePath, slice[j]);
40
+ const id = pathRegistry.register(fullPath);
41
+ row.push({ text: slice[j], callback_data: `sw:n:${id}:0` });
42
+ }
43
+ rows.push(row);
44
+ }
45
+ const navRow = [];
46
+ const parent = path.dirname(browsePath);
47
+ if (parent !== browsePath) {
48
+ navRow.push({ text: '⬆ ..', callback_data: `sw:n:${pathRegistry.register(parent)}:0` });
49
+ }
50
+ if (totalPages > 1) {
51
+ const browseId = pathRegistry.register(browsePath);
52
+ if (currentPage > 0)
53
+ navRow.push({ text: `◀ ${currentPage}/${totalPages}`, callback_data: `sw:n:${browseId}:${currentPage - 1}` });
54
+ if (currentPage < totalPages - 1)
55
+ navRow.push({ text: `${currentPage + 2}/${totalPages} ▶`, callback_data: `sw:n:${browseId}:${currentPage + 1}` });
56
+ }
57
+ if (navRow.length)
58
+ rows.push(navRow);
59
+ rows.push([{ text: 'Use This', callback_data: `sw:s:${pathRegistry.register(browsePath)}` }]);
60
+ return { inline_keyboard: rows };
61
+ }
62
+ export function buildSwitchWorkdirView(currentWorkdir, browsePath, page = 0) {
63
+ const lines = [buildCompactSelectionTitle('Workdir')];
64
+ lines.push(`● ${compactCode(currentWorkdir, 42)}`);
65
+ if (browsePath !== currentWorkdir)
66
+ lines.push(`○ ${compactCode(browsePath, 42)}`);
67
+ return {
68
+ text: lines.join('\n'),
69
+ keyboard: buildDirKeyboard(browsePath, page),
70
+ };
71
+ }
72
+ export function resolveRegisteredPath(id) {
73
+ return pathRegistry.resolve(id);
74
+ }
@@ -0,0 +1,192 @@
1
+ import { hasPreviewMeta, samePreviewMeta, samePreviewPlan } from './bot-streaming.js';
2
+ const STREAM_PREVIEW_HEARTBEAT_MS = 5_000;
3
+ const STREAM_TYPING_HEARTBEAT_MS = 4_000;
4
+ const STREAM_STALLED_NOTICE_MS = 15_000;
5
+ // ---------------------------------------------------------------------------
6
+ // LivePreview — generic streaming preview controller
7
+ // ---------------------------------------------------------------------------
8
+ export class LivePreview {
9
+ initialText;
10
+ agent;
11
+ chatId;
12
+ placeholderMessageId;
13
+ channel;
14
+ renderer;
15
+ streamEditIntervalMs;
16
+ startTimeMs;
17
+ canEditMessages;
18
+ canSendTyping;
19
+ messageThreadId;
20
+ parseMode;
21
+ log;
22
+ heartbeatTimer = null;
23
+ typingTimer = null;
24
+ flushTimer = null;
25
+ editChain = Promise.resolve();
26
+ previewVersion = 0;
27
+ editCount = 0;
28
+ lastEditAt = 0;
29
+ lastProgressAt;
30
+ lastPreview;
31
+ latestText = '';
32
+ latestThinking = '';
33
+ latestActivity = '';
34
+ latestMeta = null;
35
+ latestPlan = null;
36
+ constructor(options) {
37
+ this.agent = options.agent;
38
+ this.chatId = options.chatId;
39
+ this.placeholderMessageId = options.placeholderMessageId;
40
+ this.channel = options.channel;
41
+ this.renderer = options.renderer;
42
+ this.streamEditIntervalMs = options.streamEditIntervalMs;
43
+ this.startTimeMs = options.startTimeMs;
44
+ this.canEditMessages = options.canEditMessages;
45
+ this.canSendTyping = options.canSendTyping;
46
+ this.messageThreadId = options.messageThreadId;
47
+ this.parseMode = options.parseMode ?? 'HTML';
48
+ this.log = options.log ?? (() => { });
49
+ this.initialText = this.renderer.renderInitial(this.agent);
50
+ this.lastPreview = this.initialText;
51
+ this.lastProgressAt = this.startTimeMs;
52
+ }
53
+ start() {
54
+ this.sendTypingPulse();
55
+ if (this.canEditMessages) {
56
+ this.heartbeatTimer = setInterval(() => {
57
+ const idleMs = Date.now() - this.lastProgressAt;
58
+ const recentlyEdited = Date.now() - this.lastEditAt < STREAM_PREVIEW_HEARTBEAT_MS - 250;
59
+ if (recentlyEdited && idleMs < STREAM_STALLED_NOTICE_MS)
60
+ return;
61
+ this.queuePreviewEdit(true);
62
+ }, STREAM_PREVIEW_HEARTBEAT_MS);
63
+ this.heartbeatTimer.unref?.();
64
+ }
65
+ if (this.canSendTyping) {
66
+ this.typingTimer = setInterval(() => this.sendTypingPulse(), STREAM_TYPING_HEARTBEAT_MS);
67
+ this.typingTimer.unref?.();
68
+ }
69
+ }
70
+ update(text, thinking, activity = '', meta, plan) {
71
+ const nextMeta = hasPreviewMeta(meta) ? meta : null;
72
+ const nextPlan = plan?.steps?.length ? plan : null;
73
+ const changed = text !== this.latestText
74
+ || thinking !== this.latestThinking
75
+ || activity !== this.latestActivity
76
+ || !samePreviewMeta(nextMeta, this.latestMeta)
77
+ || !samePreviewPlan(nextPlan, this.latestPlan);
78
+ this.latestText = text;
79
+ this.latestThinking = thinking;
80
+ this.latestActivity = activity;
81
+ this.latestMeta = nextMeta;
82
+ this.latestPlan = nextPlan;
83
+ if (changed)
84
+ this.lastProgressAt = Date.now();
85
+ if (!text.trim() && !thinking.trim() && !activity.trim() && !nextMeta && !nextPlan)
86
+ return;
87
+ this.schedulePreviewEdit();
88
+ }
89
+ async settle() {
90
+ this.stopFeedback();
91
+ await this.flushPreviewEdits();
92
+ }
93
+ dispose() {
94
+ this.stopFeedback();
95
+ if (this.flushTimer) {
96
+ clearTimeout(this.flushTimer);
97
+ this.flushTimer = null;
98
+ }
99
+ this.previewVersion++;
100
+ }
101
+ getEditCount() {
102
+ return this.editCount;
103
+ }
104
+ stopFeedback() {
105
+ if (this.heartbeatTimer) {
106
+ clearInterval(this.heartbeatTimer);
107
+ this.heartbeatTimer = null;
108
+ }
109
+ if (this.typingTimer) {
110
+ clearInterval(this.typingTimer);
111
+ this.typingTimer = null;
112
+ }
113
+ }
114
+ sendTypingPulse() {
115
+ if (!this.canSendTyping)
116
+ return;
117
+ void this.channel.sendTyping(this.chatId, { messageThreadId: this.messageThreadId }).catch(() => { });
118
+ }
119
+ renderPreview() {
120
+ return this.renderer.renderStream({
121
+ agent: this.agent,
122
+ elapsedMs: Date.now() - this.startTimeMs,
123
+ bodyText: this.latestText,
124
+ thinking: this.latestThinking,
125
+ activity: this.latestActivity,
126
+ meta: this.latestMeta,
127
+ plan: this.latestPlan,
128
+ });
129
+ }
130
+ schedulePreviewEdit() {
131
+ if (!this.canEditMessages)
132
+ return;
133
+ const wait = this.streamEditIntervalMs - (Date.now() - this.lastEditAt);
134
+ if (wait <= 0) {
135
+ if (this.flushTimer) {
136
+ clearTimeout(this.flushTimer);
137
+ this.flushTimer = null;
138
+ }
139
+ this.queuePreviewEdit();
140
+ return;
141
+ }
142
+ if (this.flushTimer)
143
+ return;
144
+ this.flushTimer = setTimeout(() => {
145
+ this.flushTimer = null;
146
+ this.queuePreviewEdit();
147
+ }, wait);
148
+ }
149
+ queuePreviewEdit(force = false) {
150
+ if (!this.canEditMessages || this.placeholderMessageId == null)
151
+ return;
152
+ const placeholderMessageId = this.placeholderMessageId;
153
+ const preview = this.renderPreview();
154
+ if (!preview)
155
+ return;
156
+ if (!force && preview === this.lastPreview)
157
+ return;
158
+ this.lastPreview = preview;
159
+ const version = ++this.previewVersion;
160
+ this.editCount++;
161
+ this.lastEditAt = Date.now();
162
+ this.editChain = this.editChain
163
+ .catch(() => { })
164
+ .then(async () => {
165
+ if (version !== this.previewVersion)
166
+ return;
167
+ try {
168
+ await this.channel.editMessage(this.chatId, placeholderMessageId, preview, { parseMode: this.parseMode });
169
+ }
170
+ catch (error) {
171
+ this.log(`stream edit err: ${error?.message || error}`);
172
+ }
173
+ });
174
+ }
175
+ async flushPreviewEdits() {
176
+ if (!this.canEditMessages)
177
+ return;
178
+ if (this.flushTimer) {
179
+ clearTimeout(this.flushTimer);
180
+ this.flushTimer = null;
181
+ }
182
+ if (this.editCount > 0 || this.latestText.trim() || this.latestThinking.trim() || this.latestActivity.trim()) {
183
+ this.queuePreviewEdit(true);
184
+ }
185
+ await this.editChain.catch(() => { });
186
+ }
187
+ }
188
+ // ---------------------------------------------------------------------------
189
+ // Backward compat alias — existing code imports TelegramLivePreview
190
+ // ---------------------------------------------------------------------------
191
+ /** @deprecated Use `LivePreview` directly. */
192
+ export const TelegramLivePreview = LivePreview;