@llblab/pi-telegram 0.46.0 → 0.48.0
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.
- package/AGENTS.md +4 -2
- package/BACKLOG.md +0 -6
- package/CHANGELOG.md +14 -0
- package/README.md +5 -2
- package/dist/api/activity.d.ts +6 -0
- package/dist/api/activity.js +6 -0
- package/dist/api/commands.d.ts +6 -0
- package/dist/api/commands.js +6 -0
- package/dist/api/delivery.d.ts +6 -0
- package/dist/api/delivery.js +6 -0
- package/dist/api/inbound.d.ts +6 -0
- package/dist/api/inbound.js +6 -0
- package/dist/api/keyboard.d.ts +6 -0
- package/dist/api/keyboard.js +6 -0
- package/dist/api/outbound.d.ts +6 -0
- package/dist/api/outbound.js +6 -0
- package/dist/api/sections.d.ts +7 -0
- package/dist/api/sections.js +6 -0
- package/dist/api/status.d.ts +6 -0
- package/dist/api/status.js +6 -0
- package/dist/api/updates.d.ts +6 -0
- package/dist/api/updates.js +6 -0
- package/dist/api/voice.d.ts +6 -0
- package/dist/api/voice.js +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lib/activity-verbosity.d.ts +52 -0
- package/dist/lib/activity-verbosity.js +596 -0
- package/dist/lib/activity.d.ts +220 -0
- package/dist/lib/activity.js +574 -0
- package/dist/lib/agent-messages.d.ts +28 -0
- package/dist/lib/agent-messages.js +86 -0
- package/dist/lib/bindings.d.ts +250 -0
- package/dist/lib/bindings.js +936 -0
- package/dist/lib/bus-api.d.ts +18 -0
- package/dist/lib/bus-api.js +254 -0
- package/dist/lib/bus-follower.d.ts +464 -0
- package/dist/lib/bus-follower.js +1686 -0
- package/dist/lib/bus-leader.d.ts +292 -0
- package/dist/lib/bus-leader.js +2242 -0
- package/dist/lib/bus-transport.d.ts +64 -0
- package/dist/lib/bus-transport.js +140 -0
- package/dist/lib/bus.d.ts +518 -0
- package/dist/lib/bus.js +2055 -0
- package/dist/lib/channel-posts.d.ts +170 -0
- package/dist/lib/channel-posts.js +611 -0
- package/dist/lib/command-templates.d.ts +70 -0
- package/dist/lib/command-templates.js +744 -0
- package/dist/lib/commands.d.ts +541 -0
- package/dist/lib/commands.js +1155 -0
- package/dist/lib/config.d.ts +252 -0
- package/dist/lib/config.js +889 -0
- package/dist/lib/delivery.d.ts +163 -0
- package/dist/lib/delivery.js +542 -0
- package/dist/lib/extension.d.ts +7 -0
- package/dist/lib/extension.js +1604 -0
- package/dist/lib/generative-app-worker.mjs +104 -0
- package/dist/lib/generative-apps.d.ts +212 -0
- package/dist/lib/generative-apps.js +1006 -0
- package/dist/lib/inbound.d.ts +91 -0
- package/dist/lib/inbound.js +502 -0
- package/dist/lib/journal.d.ts +665 -0
- package/dist/lib/journal.js +3701 -0
- package/dist/lib/keyboard.d.ts +23 -0
- package/dist/lib/keyboard.js +37 -0
- package/dist/lib/lifecycle.d.ts +157 -0
- package/dist/lib/lifecycle.js +395 -0
- package/dist/lib/locks.d.ts +164 -0
- package/dist/lib/locks.js +1208 -0
- package/dist/lib/logging.d.ts +50 -0
- package/dist/lib/logging.js +274 -0
- package/dist/lib/media.d.ts +181 -0
- package/dist/lib/media.js +602 -0
- package/dist/lib/menu-model.d.ts +217 -0
- package/dist/lib/menu-model.js +663 -0
- package/dist/lib/menu-queue.d.ts +37 -0
- package/dist/lib/menu-queue.js +408 -0
- package/dist/lib/menu-settings.d.ts +115 -0
- package/dist/lib/menu-settings.js +595 -0
- package/dist/lib/menu-status.d.ts +32 -0
- package/dist/lib/menu-status.js +112 -0
- package/dist/lib/menu-thinking.d.ts +29 -0
- package/dist/lib/menu-thinking.js +82 -0
- package/dist/lib/menu.d.ts +171 -0
- package/dist/lib/menu.js +323 -0
- package/dist/lib/model.d.ts +127 -0
- package/dist/lib/model.js +409 -0
- package/dist/lib/outbound-attachments.d.ts +241 -0
- package/dist/lib/outbound-attachments.js +639 -0
- package/dist/lib/outbound-buttons.d.ts +69 -0
- package/dist/lib/outbound-buttons.js +248 -0
- package/dist/lib/outbound-markup.d.ts +42 -0
- package/dist/lib/outbound-markup.js +678 -0
- package/dist/lib/outbound-voice.d.ts +55 -0
- package/dist/lib/outbound-voice.js +152 -0
- package/dist/lib/outbound.d.ts +185 -0
- package/dist/lib/outbound.js +516 -0
- package/dist/lib/ownership.d.ts +77 -0
- package/dist/lib/ownership.js +174 -0
- package/dist/lib/paths.d.ts +40 -0
- package/dist/lib/paths.js +94 -0
- package/dist/lib/pi.d.ts +72 -0
- package/dist/lib/pi.js +121 -0
- package/dist/lib/polling.d.ts +351 -0
- package/dist/lib/polling.js +1196 -0
- package/dist/lib/preview.d.ts +192 -0
- package/dist/lib/preview.js +614 -0
- package/dist/lib/prompt-templates.d.ts +24 -0
- package/dist/lib/prompt-templates.js +118 -0
- package/dist/lib/prompts.d.ts +57 -0
- package/dist/lib/prompts.js +167 -0
- package/dist/lib/queue.d.ts +766 -0
- package/dist/lib/queue.js +1965 -0
- package/dist/lib/recovery.d.ts +86 -0
- package/dist/lib/recovery.js +285 -0
- package/dist/lib/rendering.d.ts +20 -0
- package/dist/lib/rendering.js +983 -0
- package/dist/lib/replies.d.ts +214 -0
- package/dist/lib/replies.js +737 -0
- package/dist/lib/routing.d.ts +207 -0
- package/dist/lib/routing.js +2282 -0
- package/dist/lib/runtime.d.ts +172 -0
- package/dist/lib/runtime.js +402 -0
- package/dist/lib/sections.d.ts +165 -0
- package/dist/lib/sections.js +327 -0
- package/dist/lib/setup.d.ts +82 -0
- package/dist/lib/setup.js +150 -0
- package/dist/lib/skills.d.ts +8 -0
- package/dist/lib/skills.js +12 -0
- package/dist/lib/status.d.ts +442 -0
- package/dist/lib/status.js +1008 -0
- package/dist/lib/sync.d.ts +179 -0
- package/dist/lib/sync.js +782 -0
- package/dist/lib/target.d.ts +20 -0
- package/dist/lib/target.js +27 -0
- package/dist/lib/telegram-api.d.ts +541 -0
- package/dist/lib/telegram-api.js +1159 -0
- package/dist/lib/text-groups.d.ts +89 -0
- package/dist/lib/text-groups.js +317 -0
- package/dist/lib/thread-cleanup-manager.d.ts +288 -0
- package/dist/lib/thread-cleanup-manager.js +560 -0
- package/dist/lib/thread-display.d.ts +46 -0
- package/dist/lib/thread-display.js +257 -0
- package/dist/lib/thread-naming.d.ts +46 -0
- package/dist/lib/thread-naming.js +78 -0
- package/dist/lib/thread-reconciler.d.ts +239 -0
- package/dist/lib/thread-reconciler.js +644 -0
- package/dist/lib/threads.d.ts +621 -0
- package/dist/lib/threads.js +3679 -0
- package/dist/lib/time-injection.d.ts +15 -0
- package/dist/lib/time-injection.js +56 -0
- package/dist/lib/turns.d.ts +109 -0
- package/dist/lib/turns.js +500 -0
- package/dist/lib/updates.d.ts +1290 -0
- package/dist/lib/updates.js +3634 -0
- package/dist/lib/voice.d.ts +117 -0
- package/dist/lib/voice.js +174 -0
- package/dist/lib/workspace-admission.d.ts +264 -0
- package/dist/lib/workspace-admission.js +1136 -0
- package/dist/lib/workspace-retirement.d.ts +219 -0
- package/dist/lib/workspace-retirement.js +587 -0
- package/dist/lib/workspace-slots.d.ts +30 -0
- package/dist/lib/workspace-slots.js +54 -0
- package/dist/package.json +126 -0
- package/dist/pi-telegram/index.js +1 -0
- package/dist/skills/generated-control-surface/SKILL.md +107 -0
- package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
- package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
- package/dist/skills/generative-apps/SKILL.md +115 -0
- package/dist/skills/show-me/SKILL.md +166 -0
- package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
- package/dist/skills/telegram-bridge/SKILL.md +129 -0
- package/dist/skills/telegram-bridge/references/configuration.md +15 -0
- package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
- package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
- package/docs/README.md +2 -0
- package/docs/architecture.md +1 -1
- package/docs/compact-matrix-literal.md +10 -3
- package/docs/generative-apps.md +15 -13
- package/docs/multi-instance-bus.md +6 -4
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +3 -3
- package/lib/bindings.ts +89 -6
- package/lib/bus-follower.ts +7 -3
- package/lib/bus-leader.ts +44 -19
- package/lib/bus.ts +4 -1
- package/lib/commands.ts +2 -2
- package/lib/config.ts +14 -7
- package/lib/delivery.ts +38 -6
- package/lib/extension.ts +7 -0
- package/lib/generative-apps.ts +379 -13
- package/lib/menu-settings.ts +20 -6
- package/lib/outbound-markup.ts +16 -7
- package/lib/status.ts +15 -12
- package/lib/telegram-api.ts +12 -1
- package/lib/thread-display.ts +96 -18
- package/package.json +56 -13
- package/scripts/build-dist.mjs +44 -0
- package/scripts/measure-bus.mjs +8 -1
- package/scripts/measure-workspace.mjs +8 -1
- package/skills/generative-apps/SKILL.md +1 -1
- package/skills/show-me/SKILL.md +1 -1
- package/skills/show-me/references/telegram-surfaces.md +1 -1
- package/skills/telegram-bridge/SKILL.md +1 -1
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram preview streaming helpers
|
|
3
|
+
* Zones: telegram outbound, native rich markdown drafts
|
|
4
|
+
* Owns safe draft preview selection, runtime updates, and preview finalization
|
|
5
|
+
*/
|
|
6
|
+
import { normalizeTelegramNativeMarkdown } from "./replies.js";
|
|
7
|
+
import { stripTelegramCommentMarkupForPreview } from "./outbound-markup.js";
|
|
8
|
+
import { getTelegramTargetThreadParams, } from "./target.js";
|
|
9
|
+
import { shouldSuppressPreviewForVoice } from "./voice.js";
|
|
10
|
+
const TELEGRAM_DRAFT_ID_MAX = 2_147_483_647;
|
|
11
|
+
const TELEGRAM_DRAFT_PREVIEW_MAX_CHARS = 4096;
|
|
12
|
+
// Native draft cadence: at most one frame per interval, and a fresh preview
|
|
13
|
+
// segment holds its first frame for one full interval so the opening frame is
|
|
14
|
+
// an accumulated passage rather than a single streamed word.
|
|
15
|
+
const TELEGRAM_DRAFT_INTERVAL_MS = 2_000;
|
|
16
|
+
export function createTelegramPreviewControllerRuntime(deps) {
|
|
17
|
+
return createTelegramPreviewController({
|
|
18
|
+
getDefaultReplyToMessageId: deps.getDefaultReplyToMessageId,
|
|
19
|
+
maxMessageLength: deps.maxMessageLength,
|
|
20
|
+
initialDraftSupport: deps.initialDraftSupport,
|
|
21
|
+
sendDraft: deps.sendDraft,
|
|
22
|
+
canSend: deps.canSend,
|
|
23
|
+
maxDraftId: deps.maxDraftId,
|
|
24
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function sealTelegramPreviewState(state) {
|
|
28
|
+
if (!state)
|
|
29
|
+
return;
|
|
30
|
+
state.sealed = true;
|
|
31
|
+
state.flushRequested = false;
|
|
32
|
+
if (state.flushTimer)
|
|
33
|
+
clearTimeout(state.flushTimer);
|
|
34
|
+
state.flushTimer = undefined;
|
|
35
|
+
}
|
|
36
|
+
export function createTelegramNativeMarkdownPreviewFinalizer(deps) {
|
|
37
|
+
return (...args) => prepareTelegramNativeMarkdownPreviewFinalizer(deps)(...args);
|
|
38
|
+
}
|
|
39
|
+
function prepareTelegramNativeMarkdownPreviewFinalizer(deps) {
|
|
40
|
+
const state = deps.getState();
|
|
41
|
+
sealTelegramPreviewState(state);
|
|
42
|
+
const inFlight = state?.flushPromise ?? state?.precedingFlush;
|
|
43
|
+
return async (chatId, markdown, replyToMessageId, options) => {
|
|
44
|
+
if (deps.isDeliveryActive?.() === false)
|
|
45
|
+
return false;
|
|
46
|
+
await inFlight?.catch(() => { });
|
|
47
|
+
if (deps.getState() !== state || deps.isDeliveryActive?.() === false)
|
|
48
|
+
return false;
|
|
49
|
+
await deps.sendMarkdownReply(chatId, replyToMessageId, markdown, options);
|
|
50
|
+
if (deps.getState() === state && deps.isDeliveryActive?.() !== false)
|
|
51
|
+
deps.discard?.();
|
|
52
|
+
return true;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function createTelegramAssistantPreviewRuntime(deps) {
|
|
56
|
+
const controller = createTelegramPreviewControllerRuntime(deps);
|
|
57
|
+
const finalizerDeps = {
|
|
58
|
+
getState: controller.getState,
|
|
59
|
+
clear: controller.clear,
|
|
60
|
+
discard: () => controller.setState(undefined),
|
|
61
|
+
sendMarkdownReply: deps.sendMarkdownReply,
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
...controller,
|
|
65
|
+
finalizeMarkdown: createTelegramNativeMarkdownPreviewFinalizer(finalizerDeps),
|
|
66
|
+
prepareDelivery(isDeliveryActive) {
|
|
67
|
+
const state = controller.getState();
|
|
68
|
+
return {
|
|
69
|
+
setPreviewPendingText(text) {
|
|
70
|
+
if (controller.getState() === state && isDeliveryActive())
|
|
71
|
+
controller.setPendingText(text);
|
|
72
|
+
},
|
|
73
|
+
clearPreview: async (chatId, options) => {
|
|
74
|
+
if (controller.getState() !== state || !isDeliveryActive())
|
|
75
|
+
return;
|
|
76
|
+
await controller.prepareClear(chatId, { ...options, isDeliveryActive })();
|
|
77
|
+
},
|
|
78
|
+
finalizeMarkdownPreview: prepareTelegramNativeMarkdownPreviewFinalizer({ ...finalizerDeps, isDeliveryActive }),
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
...createTelegramAssistantMessagePreviewHooks({
|
|
82
|
+
getActiveTurn: deps.getActiveTurn,
|
|
83
|
+
isAssistantMessage: deps.isAssistantMessage,
|
|
84
|
+
getState: controller.getState,
|
|
85
|
+
setState: controller.setState,
|
|
86
|
+
createPreviewState: controller.createState,
|
|
87
|
+
canSend: deps.canSend,
|
|
88
|
+
getMessageText: deps.getMessageText,
|
|
89
|
+
minDraftIntervalMs: TELEGRAM_DRAFT_INTERVAL_MS,
|
|
90
|
+
schedulePreviewFlush: controller.scheduleFlush,
|
|
91
|
+
}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function createTelegramPreviewController(deps) {
|
|
95
|
+
let state;
|
|
96
|
+
let generation = 0;
|
|
97
|
+
const maxDraftId = deps.maxDraftId ?? TELEGRAM_DRAFT_ID_MAX;
|
|
98
|
+
const maxMessageLength = deps.maxMessageLength ?? TELEGRAM_DRAFT_PREVIEW_MAX_CHARS;
|
|
99
|
+
let draftSupport = deps.initialDraftSupport ?? "unknown";
|
|
100
|
+
let nextDraftId = 0;
|
|
101
|
+
const setState = (nextState) => {
|
|
102
|
+
if (state !== nextState)
|
|
103
|
+
sealTelegramPreviewState(state);
|
|
104
|
+
state = nextState;
|
|
105
|
+
};
|
|
106
|
+
const getRuntimeDeps = (operationGeneration = generation) => ({
|
|
107
|
+
getState: () => state,
|
|
108
|
+
setState,
|
|
109
|
+
maxMessageLength,
|
|
110
|
+
minDraftIntervalMs: TELEGRAM_DRAFT_INTERVAL_MS,
|
|
111
|
+
getDraftSupport: () => draftSupport,
|
|
112
|
+
setDraftSupport: (support) => {
|
|
113
|
+
draftSupport = support;
|
|
114
|
+
},
|
|
115
|
+
allocateDraftId: () => {
|
|
116
|
+
nextDraftId = allocateTelegramDraftId(nextDraftId, maxDraftId);
|
|
117
|
+
return nextDraftId;
|
|
118
|
+
},
|
|
119
|
+
sendDraft: deps.sendDraft,
|
|
120
|
+
canSend: () => operationGeneration === generation && (deps.canSend?.() ?? true),
|
|
121
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
getState: () => state,
|
|
125
|
+
setState,
|
|
126
|
+
setPendingText: (text) => {
|
|
127
|
+
if (state)
|
|
128
|
+
state.pendingText = text;
|
|
129
|
+
},
|
|
130
|
+
createState: () => createTelegramPreviewRuntimeState(),
|
|
131
|
+
resetState: () => {
|
|
132
|
+
generation += 1;
|
|
133
|
+
setState({ ...createTelegramPreviewRuntimeState(), nextDraftAt: state?.nextDraftAt });
|
|
134
|
+
},
|
|
135
|
+
invalidate: () => {
|
|
136
|
+
generation += 1;
|
|
137
|
+
setState(undefined);
|
|
138
|
+
},
|
|
139
|
+
seal: () => sealTelegramPreviewState(state),
|
|
140
|
+
preparePublication: () => {
|
|
141
|
+
if (!state)
|
|
142
|
+
return undefined;
|
|
143
|
+
sealTelegramPreviewState(state);
|
|
144
|
+
const prior = state.publicationPromise ?? state.flushPromise ?? state.precedingFlush;
|
|
145
|
+
let settle;
|
|
146
|
+
state.publicationPromise = new Promise((resolve) => { settle = resolve; });
|
|
147
|
+
return { wait: async () => { await prior?.catch(() => { }); }, settle };
|
|
148
|
+
},
|
|
149
|
+
prepareClear: (chatId, options) => {
|
|
150
|
+
const admittedState = state;
|
|
151
|
+
const runtime = getRuntimeDeps();
|
|
152
|
+
return async () => {
|
|
153
|
+
if (state !== admittedState)
|
|
154
|
+
return;
|
|
155
|
+
await clearTelegramPreview(chatId, runtime, {
|
|
156
|
+
...options,
|
|
157
|
+
isDeliveryActive: () => runtime.canSend?.() !== false && options?.isDeliveryActive?.() !== false,
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
clear: (chatId, options) => clearTelegramPreview(chatId, getRuntimeDeps(), options),
|
|
162
|
+
flush: (chatId, options) => flushTelegramPreview(chatId, getRuntimeDeps(), options),
|
|
163
|
+
scheduleFlush: (chatId, options) => {
|
|
164
|
+
if (!state)
|
|
165
|
+
return;
|
|
166
|
+
void flushTelegramPreview(chatId, getRuntimeDeps(), options);
|
|
167
|
+
},
|
|
168
|
+
finalize: (chatId, _replyToMessageId, options) => finalizeTelegramPreview(chatId, getRuntimeDeps(), options),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export function createTelegramAssistantMessagePreviewHooks(deps) {
|
|
172
|
+
return {
|
|
173
|
+
onMessageStart: async (event) => {
|
|
174
|
+
await handleTelegramAssistantMessagePreviewStart(event.message, deps);
|
|
175
|
+
},
|
|
176
|
+
onMessageUpdate: async (event) => {
|
|
177
|
+
await handleTelegramAssistantMessagePreviewUpdate(event.message, deps);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Returns true when the active turn is a Telegram Guest Mode query. A guest
|
|
183
|
+
* query allows exactly one answer within a limited Telegram response window,
|
|
184
|
+
* so it must never emit streaming draft previews.
|
|
185
|
+
*/
|
|
186
|
+
export function shouldSuppressPreviewForGuestTurn(turn) {
|
|
187
|
+
return !!turn?.guestQueryId;
|
|
188
|
+
}
|
|
189
|
+
export async function handleTelegramAssistantMessagePreviewStart(message, deps) {
|
|
190
|
+
const turn = deps.getActiveTurn();
|
|
191
|
+
if (!turn || !deps.isAssistantMessage(message))
|
|
192
|
+
return;
|
|
193
|
+
if (deps.canSend && !deps.canSend()) {
|
|
194
|
+
deps.setState(undefined);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (shouldSuppressPreviewForVoice(turn)) {
|
|
198
|
+
deps.setState(undefined);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (shouldSuppressPreviewForGuestTurn(turn)) {
|
|
202
|
+
deps.setState(undefined);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const state = deps.getState();
|
|
206
|
+
sealTelegramPreviewState(state);
|
|
207
|
+
const next = deps.createPreviewState();
|
|
208
|
+
// Carry the previous delivery boundary; permanent text remains with its publication owner.
|
|
209
|
+
next.draftId = state?.draftId;
|
|
210
|
+
next.nextDraftAt = state?.nextDraftAt;
|
|
211
|
+
next.precedingFlush = state?.publicationPromise ?? state?.flushPromise ?? state?.precedingFlush;
|
|
212
|
+
deps.setState(next);
|
|
213
|
+
}
|
|
214
|
+
export async function handleTelegramAssistantMessagePreviewUpdate(message, deps) {
|
|
215
|
+
const turn = deps.getActiveTurn();
|
|
216
|
+
if (!turn || !deps.isAssistantMessage(message))
|
|
217
|
+
return;
|
|
218
|
+
if (deps.canSend && !deps.canSend()) {
|
|
219
|
+
deps.setState(undefined);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (shouldSuppressPreviewForVoice(turn))
|
|
223
|
+
return;
|
|
224
|
+
if (shouldSuppressPreviewForGuestTurn(turn))
|
|
225
|
+
return;
|
|
226
|
+
let state = deps.getState();
|
|
227
|
+
if (!state) {
|
|
228
|
+
state = deps.createPreviewState();
|
|
229
|
+
deps.setState(state);
|
|
230
|
+
}
|
|
231
|
+
if (state.sealed)
|
|
232
|
+
return;
|
|
233
|
+
const hadVisibleText = state.pendingText.length > 0;
|
|
234
|
+
state.pendingText = stripTelegramCommentMarkupForPreview(deps.getMessageText(message));
|
|
235
|
+
// The first visible text of a preview segment opens an initial accumulation
|
|
236
|
+
// window, so the segment's first frame cannot ship as a single word even
|
|
237
|
+
// when the previous cadence boundary has already passed (fresh turn, slow
|
|
238
|
+
// first token, or message rollover after tool work). Later deltas keep the
|
|
239
|
+
// trailing deadline instead of sliding it on every update.
|
|
240
|
+
const interval = deps.minDraftIntervalMs ?? 0;
|
|
241
|
+
if (interval > 0 &&
|
|
242
|
+
!hadVisibleText &&
|
|
243
|
+
!state.lastSentText &&
|
|
244
|
+
state.pendingText.length > 0) {
|
|
245
|
+
state.nextDraftAt = Math.max(state.nextDraftAt ?? 0, Date.now() + interval);
|
|
246
|
+
}
|
|
247
|
+
deps.schedulePreviewFlush(turn.chatId, { target: turn.target });
|
|
248
|
+
}
|
|
249
|
+
export function buildTelegramPreviewFinalText(state) {
|
|
250
|
+
const finalText = state.pendingText.trim();
|
|
251
|
+
if (finalText)
|
|
252
|
+
return finalText;
|
|
253
|
+
return state.lastSentText.trim() || undefined;
|
|
254
|
+
}
|
|
255
|
+
export function createTelegramPreviewRuntimeState() {
|
|
256
|
+
return {
|
|
257
|
+
mode: "draft",
|
|
258
|
+
pendingText: "",
|
|
259
|
+
lastSentText: "",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
export function allocateTelegramDraftId(currentDraftId, maxDraftId) {
|
|
263
|
+
return currentDraftId >= maxDraftId ? 1 : currentDraftId + 1;
|
|
264
|
+
}
|
|
265
|
+
export function shouldUseTelegramDraftPreview(_options) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
export async function clearTelegramPreview(chatId, deps, options = {}) {
|
|
269
|
+
const state = deps.getState();
|
|
270
|
+
if (!state || options.isDeliveryActive?.() === false)
|
|
271
|
+
return;
|
|
272
|
+
sealTelegramPreviewState(state);
|
|
273
|
+
const inFlight = state.flushPromise ?? state.precedingFlush;
|
|
274
|
+
if (inFlight && options.awaitFlush !== false) {
|
|
275
|
+
state.flushRequested = false;
|
|
276
|
+
await inFlight.catch(() => { });
|
|
277
|
+
if (deps.getState() !== state)
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (options.isDeliveryActive?.() === false)
|
|
281
|
+
return;
|
|
282
|
+
deps.setState(undefined);
|
|
283
|
+
if (state.mode === "draft" && state.draftId !== undefined && deps.canSend?.() !== false) {
|
|
284
|
+
try {
|
|
285
|
+
await deps.sendDraft(chatId, state.draftId, undefined, {
|
|
286
|
+
...getTelegramTargetThreadParams(options.target ?? { chatId }),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
deps.recordRuntimeEvent?.("preview", error, {
|
|
291
|
+
phase: "clear-draft",
|
|
292
|
+
chatId,
|
|
293
|
+
draftId: state.draftId,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function createTelegramDraftInlineState() {
|
|
299
|
+
return {
|
|
300
|
+
codeTicks: 0,
|
|
301
|
+
htmlComment: false,
|
|
302
|
+
displayMath: false,
|
|
303
|
+
strongAsterisk: false,
|
|
304
|
+
emphasisAsterisk: false,
|
|
305
|
+
strongUnderscore: false,
|
|
306
|
+
emphasisUnderscore: false,
|
|
307
|
+
strike: false,
|
|
308
|
+
linkText: false,
|
|
309
|
+
linkDestination: false,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function isTelegramDraftInlineStateClosed(state) {
|
|
313
|
+
return (state.codeTicks === 0 &&
|
|
314
|
+
!state.htmlComment &&
|
|
315
|
+
!state.displayMath &&
|
|
316
|
+
!state.fence &&
|
|
317
|
+
!state.strongAsterisk &&
|
|
318
|
+
!state.emphasisAsterisk &&
|
|
319
|
+
!state.strongUnderscore &&
|
|
320
|
+
!state.emphasisUnderscore &&
|
|
321
|
+
!state.strike &&
|
|
322
|
+
!state.linkText &&
|
|
323
|
+
!state.linkDestination);
|
|
324
|
+
}
|
|
325
|
+
function countRepeatedChars(text, index, char) {
|
|
326
|
+
let count = 0;
|
|
327
|
+
while (text[index + count] === char)
|
|
328
|
+
count += 1;
|
|
329
|
+
return count;
|
|
330
|
+
}
|
|
331
|
+
function isEscapedMarkdownChar(text, index) {
|
|
332
|
+
let slashCount = 0;
|
|
333
|
+
for (let cursor = index - 1; cursor >= 0 && text[cursor] === "\\"; cursor -= 1) {
|
|
334
|
+
slashCount += 1;
|
|
335
|
+
}
|
|
336
|
+
return slashCount % 2 === 1;
|
|
337
|
+
}
|
|
338
|
+
function isInlineDelimiterCandidate(text, index, length) {
|
|
339
|
+
const previous = text[index - 1] ?? "";
|
|
340
|
+
const next = text[index + length] ?? "";
|
|
341
|
+
if (!next || /\s/.test(next))
|
|
342
|
+
return previous.length > 0 && !/\s/.test(previous);
|
|
343
|
+
if (!previous || /\s/.test(previous))
|
|
344
|
+
return true;
|
|
345
|
+
return /[\p{P}\p{S}]/u.test(previous) || /[\p{P}\p{S}]/u.test(next);
|
|
346
|
+
}
|
|
347
|
+
function updateTelegramDraftInlineStateForLine(line, state) {
|
|
348
|
+
if (state.fence || state.displayMath)
|
|
349
|
+
return;
|
|
350
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
351
|
+
if (state.htmlComment) {
|
|
352
|
+
const closeIndex = line.indexOf("-->", index);
|
|
353
|
+
if (closeIndex === -1)
|
|
354
|
+
return;
|
|
355
|
+
state.htmlComment = false;
|
|
356
|
+
index = closeIndex + 2;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (state.codeTicks > 0) {
|
|
360
|
+
const ticks = countRepeatedChars(line, index, "`");
|
|
361
|
+
if (ticks >= state.codeTicks) {
|
|
362
|
+
state.codeTicks = 0;
|
|
363
|
+
index += ticks - 1;
|
|
364
|
+
}
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (isEscapedMarkdownChar(line, index))
|
|
368
|
+
continue;
|
|
369
|
+
if (line.startsWith("<!--", index)) {
|
|
370
|
+
const closeIndex = line.indexOf("-->", index + 4);
|
|
371
|
+
if (closeIndex === -1) {
|
|
372
|
+
state.htmlComment = true;
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
index = closeIndex + 2;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const ticks = countRepeatedChars(line, index, "`");
|
|
379
|
+
if (ticks > 0) {
|
|
380
|
+
state.codeTicks = ticks;
|
|
381
|
+
index += ticks - 1;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (line.startsWith("][", index) || line.startsWith("](", index)) {
|
|
385
|
+
state.linkText = false;
|
|
386
|
+
state.linkDestination = true;
|
|
387
|
+
index += 1;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (line[index] === "[" && !state.linkDestination) {
|
|
391
|
+
state.linkText = true;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (line[index] === ")" && state.linkDestination) {
|
|
395
|
+
state.linkDestination = false;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (line.startsWith("~~", index) &&
|
|
399
|
+
isInlineDelimiterCandidate(line, index, 2)) {
|
|
400
|
+
state.strike = !state.strike;
|
|
401
|
+
index += 1;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (line.startsWith("**", index) &&
|
|
405
|
+
isInlineDelimiterCandidate(line, index, 2)) {
|
|
406
|
+
state.strongAsterisk = !state.strongAsterisk;
|
|
407
|
+
index += 1;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (line[index] === "*" && isInlineDelimiterCandidate(line, index, 1)) {
|
|
411
|
+
state.emphasisAsterisk = !state.emphasisAsterisk;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (line.startsWith("__", index) &&
|
|
415
|
+
isInlineDelimiterCandidate(line, index, 2)) {
|
|
416
|
+
state.strongUnderscore = !state.strongUnderscore;
|
|
417
|
+
index += 1;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (line[index] === "_" && isInlineDelimiterCandidate(line, index, 1)) {
|
|
421
|
+
state.emphasisUnderscore = !state.emphasisUnderscore;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function updateTelegramDraftBlockStateForLine(line, state) {
|
|
426
|
+
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
427
|
+
if (state.fence) {
|
|
428
|
+
if (new RegExp(`^ {0,3}${state.fence.marker}{${state.fence.length},}\\s*$`).test(line)) {
|
|
429
|
+
state.fence = undefined;
|
|
430
|
+
}
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
if (state.displayMath) {
|
|
434
|
+
if (line.trim() === "$$")
|
|
435
|
+
state.displayMath = false;
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
if (fenceMatch) {
|
|
439
|
+
const markerText = fenceMatch[1] ?? "```";
|
|
440
|
+
state.fence = {
|
|
441
|
+
marker: markerText[0],
|
|
442
|
+
length: markerText.length,
|
|
443
|
+
};
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
if (line.trim() === "$$") {
|
|
447
|
+
state.displayMath = true;
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
function findSafeTelegramRichMarkdownDraftEnd(markdown) {
|
|
453
|
+
const state = createTelegramDraftInlineState();
|
|
454
|
+
let offset = 0;
|
|
455
|
+
let safeEnd = 0;
|
|
456
|
+
for (const line of markdown.split("\n")) {
|
|
457
|
+
const lineEnd = offset + line.length;
|
|
458
|
+
const consumedAsBlock = updateTelegramDraftBlockStateForLine(line, state);
|
|
459
|
+
if (!consumedAsBlock)
|
|
460
|
+
updateTelegramDraftInlineStateForLine(line, state);
|
|
461
|
+
const nextOffset = lineEnd + 1;
|
|
462
|
+
if (isTelegramDraftInlineStateClosed(state))
|
|
463
|
+
safeEnd = lineEnd;
|
|
464
|
+
offset = nextOffset;
|
|
465
|
+
}
|
|
466
|
+
if (isTelegramDraftInlineStateClosed(state))
|
|
467
|
+
return markdown.length;
|
|
468
|
+
return safeEnd;
|
|
469
|
+
}
|
|
470
|
+
function hasTelegramPreviewVisibleContent(markdown) {
|
|
471
|
+
return /[\p{L}\p{N}]/u.test(markdown);
|
|
472
|
+
}
|
|
473
|
+
export function getSafeTelegramRichMarkdownDraftPrefix(markdown, maxMessageLength) {
|
|
474
|
+
const source = markdown.trim();
|
|
475
|
+
if (!source)
|
|
476
|
+
return undefined;
|
|
477
|
+
const limited = source.length > maxMessageLength
|
|
478
|
+
? source.slice(0, maxMessageLength)
|
|
479
|
+
: source;
|
|
480
|
+
const safeEnd = findSafeTelegramRichMarkdownDraftEnd(limited);
|
|
481
|
+
if (safeEnd > 0) {
|
|
482
|
+
const safePrefix = limited.slice(0, safeEnd).trimEnd();
|
|
483
|
+
return hasTelegramPreviewVisibleContent(safePrefix)
|
|
484
|
+
? safePrefix
|
|
485
|
+
: undefined;
|
|
486
|
+
}
|
|
487
|
+
let candidateEnd = limited.length;
|
|
488
|
+
while (candidateEnd > 0) {
|
|
489
|
+
candidateEnd = limited.lastIndexOf(" ", candidateEnd - 1);
|
|
490
|
+
if (candidateEnd <= 0)
|
|
491
|
+
return undefined;
|
|
492
|
+
const candidate = limited.slice(0, candidateEnd).trimEnd();
|
|
493
|
+
if (hasTelegramPreviewVisibleContent(candidate) &&
|
|
494
|
+
findSafeTelegramRichMarkdownDraftEnd(candidate) === candidate.length) {
|
|
495
|
+
return candidate || undefined;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
function buildTelegramNativeMarkdownPreviewSnapshot(state, maxMessageLength) {
|
|
501
|
+
const safeText = getSafeTelegramRichMarkdownDraftPrefix(state.pendingText, maxMessageLength);
|
|
502
|
+
if (!safeText || safeText === state.lastSentText)
|
|
503
|
+
return undefined;
|
|
504
|
+
return { text: safeText };
|
|
505
|
+
}
|
|
506
|
+
async function performTelegramPreviewFlush(chatId, state, deps, options = {}) {
|
|
507
|
+
if (deps.canSend && !deps.canSend()) {
|
|
508
|
+
await clearTelegramPreview(chatId, deps, {
|
|
509
|
+
awaitFlush: false,
|
|
510
|
+
target: options.target,
|
|
511
|
+
});
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const snapshot = buildTelegramNativeMarkdownPreviewSnapshot(state, deps.maxMessageLength);
|
|
515
|
+
if (!snapshot)
|
|
516
|
+
return;
|
|
517
|
+
if (shouldUseTelegramDraftPreview({
|
|
518
|
+
draftSupport: deps.getDraftSupport(),
|
|
519
|
+
snapshot,
|
|
520
|
+
})) {
|
|
521
|
+
const draftId = state.draftId ?? deps.allocateDraftId();
|
|
522
|
+
state.draftId = draftId;
|
|
523
|
+
state.nextDraftAt = Date.now() + (deps.minDraftIntervalMs ?? 0);
|
|
524
|
+
try {
|
|
525
|
+
const delivered = await deps.sendDraft(chatId, draftId, normalizeTelegramNativeMarkdown(snapshot.text), { ...getTelegramTargetThreadParams(options.target ?? { chatId }) });
|
|
526
|
+
if (delivered === false || deps.getState() !== state || deps.canSend?.() === false)
|
|
527
|
+
return;
|
|
528
|
+
deps.setDraftSupport("supported");
|
|
529
|
+
state.mode = "draft";
|
|
530
|
+
state.lastSentText = snapshot.text;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
deps.recordRuntimeEvent?.("preview", error, {
|
|
535
|
+
phase: "draft",
|
|
536
|
+
chatId,
|
|
537
|
+
draftId,
|
|
538
|
+
});
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
export async function flushTelegramPreview(chatId, deps, options = {}) {
|
|
544
|
+
const state = deps.getState();
|
|
545
|
+
if (!state || state.sealed)
|
|
546
|
+
return;
|
|
547
|
+
if (state.flushPromise) {
|
|
548
|
+
state.flushRequested = true;
|
|
549
|
+
await state.flushPromise;
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
state.flushPromise = (async () => {
|
|
553
|
+
if (state.precedingFlush) {
|
|
554
|
+
await state.precedingFlush.catch(() => { });
|
|
555
|
+
state.precedingFlush = undefined;
|
|
556
|
+
if (deps.getState() !== state || state.sealed)
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
do {
|
|
560
|
+
state.flushRequested = false;
|
|
561
|
+
const delay = (state.nextDraftAt ?? 0) - Date.now();
|
|
562
|
+
if (delay > 0) {
|
|
563
|
+
if (!state.flushTimer) {
|
|
564
|
+
state.flushTimer = setTimeout(() => {
|
|
565
|
+
state.flushTimer = undefined;
|
|
566
|
+
if (deps.getState() === state && !state.sealed)
|
|
567
|
+
void flushTelegramPreview(chatId, deps, options);
|
|
568
|
+
}, delay);
|
|
569
|
+
state.flushTimer.unref?.();
|
|
570
|
+
}
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
if (state.flushTimer)
|
|
574
|
+
clearTimeout(state.flushTimer);
|
|
575
|
+
state.flushTimer = undefined;
|
|
576
|
+
try {
|
|
577
|
+
await performTelegramPreviewFlush(chatId, state, deps, options);
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
deps.recordRuntimeEvent?.("preview", error, {
|
|
581
|
+
phase: "flush",
|
|
582
|
+
chatId,
|
|
583
|
+
draftId: state.draftId,
|
|
584
|
+
});
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
} while (deps.getState() === state && !state.sealed && state.flushRequested);
|
|
588
|
+
})();
|
|
589
|
+
try {
|
|
590
|
+
await state.flushPromise;
|
|
591
|
+
}
|
|
592
|
+
finally {
|
|
593
|
+
if (deps.getState() === state) {
|
|
594
|
+
state.flushPromise = undefined;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
export async function finalizeTelegramPreview(chatId, deps, options = {}) {
|
|
599
|
+
const state = deps.getState();
|
|
600
|
+
if (!state)
|
|
601
|
+
return false;
|
|
602
|
+
if (deps.canSend && !deps.canSend()) {
|
|
603
|
+
await clearTelegramPreview(chatId, deps, options);
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
await flushTelegramPreview(chatId, deps, options);
|
|
607
|
+
const finalText = buildTelegramPreviewFinalText(state);
|
|
608
|
+
if (!finalText) {
|
|
609
|
+
await clearTelegramPreview(chatId, deps, options);
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
deps.setState(undefined);
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi prompt-template bridge helpers
|
|
3
|
+
* Zones: pi agent prompts, telegram controls, filesystem
|
|
4
|
+
* Discovers Pi prompt-template slash commands and expands them before Telegram queue dispatch
|
|
5
|
+
*/
|
|
6
|
+
import type { PiSlashCommandInfo } from "./pi.ts";
|
|
7
|
+
export interface TelegramPromptTemplateCommand {
|
|
8
|
+
command: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
path: string;
|
|
11
|
+
}
|
|
12
|
+
export type TelegramPromptTemplateReader = (path: string) => string;
|
|
13
|
+
export declare function parsePromptTemplateArgs(argsString: string): string[];
|
|
14
|
+
export declare function substitutePromptTemplateArgs(content: string, args: readonly string[]): string;
|
|
15
|
+
export declare function isTelegramPromptTemplateCommandName(name: string): boolean;
|
|
16
|
+
export declare function mapPiPromptTemplateNameToTelegramCommandName(name: string): string | undefined;
|
|
17
|
+
export interface TelegramPromptTemplateCommandGetterDeps {
|
|
18
|
+
getCommands: () => readonly PiSlashCommandInfo[];
|
|
19
|
+
reservedCommandNames?: readonly string[];
|
|
20
|
+
getReservedCommandNames?: () => readonly string[];
|
|
21
|
+
}
|
|
22
|
+
export declare function getTelegramPromptTemplateCommands(commands: readonly PiSlashCommandInfo[], reservedNames?: ReadonlySet<string>): TelegramPromptTemplateCommand[];
|
|
23
|
+
export declare function createTelegramPromptTemplateCommandGetter(deps: TelegramPromptTemplateCommandGetterDeps): () => TelegramPromptTemplateCommand[];
|
|
24
|
+
export declare function expandTelegramPromptTemplateCommand(commandName: string, args: string, commands: readonly TelegramPromptTemplateCommand[], readTemplate?: TelegramPromptTemplateReader): string | undefined;
|