pi-ui-extend 0.1.64 → 0.1.66

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.
@@ -1,211 +0,0 @@
1
- /**
2
- * Streaming-aware message renderer.
3
- *
4
- * Pipeline:
5
- * pix events → renderer.push(assistant_text|status)
6
- * → accumulate assistant-visible text only
7
- * → throttled (~1.2 s) editMessageText flushes, paginated on overflow
8
- * → on turn_end, flush remainder
9
- *
10
- * Tool calls/results and thinking deltas are intentionally ignored: Telegram
11
- * is a second screen for user-visible assistant messages, not a debug log.
12
- */
13
-
14
- import { chunkForTelegram, markdownToTelegram, TELEGRAM_MESSAGE_MAX } from "./format.js";
15
- import type { TelegramBot } from "./bot.js";
16
-
17
- const THROTTLE_MS = 1200;
18
- const SESSION_SEPARATOR = "\n\n━━━━━━━━━━━━\n\n";
19
-
20
- export type RendererEvent =
21
- | { kind: "turn_start"; instance?: RendererInstance }
22
- | { kind: "assistant_text"; delta: string }
23
- | { kind: "turn_end"; reason: "end" | "error" | "aborted" };
24
-
25
- export interface RendererInstance {
26
- label: string;
27
- cwd?: string;
28
- sessionName?: string;
29
- sessionId?: string;
30
- }
31
-
32
- interface TextEntry {
33
- kind: "text";
34
- content: string;
35
- }
36
-
37
- type Chunk = TextEntry;
38
-
39
- interface ActiveMessage {
40
- messageId: number;
41
- body: string;
42
- }
43
-
44
- interface SentPage {
45
- messageId: number;
46
- body: string;
47
- }
48
-
49
- export class TurnRenderer {
50
- private active: ActiveMessage | undefined;
51
- private chunks: Chunk[] = [];
52
- private header: string | undefined;
53
- private scheduledFlush: ReturnType<typeof setTimeout> | undefined;
54
- private readonly sentMessageIds: number[] = [];
55
- private pages: SentPage[] = [];
56
- private turnHasText = false;
57
- private turnOpen = false;
58
-
59
- constructor(
60
- private readonly bot: TelegramBot,
61
- private readonly logger: (msg: string) => void = () => undefined,
62
- ) {}
63
-
64
- get sentIds(): readonly number[] {
65
- return this.sentMessageIds;
66
- }
67
-
68
- push(event: RendererEvent): void {
69
- switch (event.kind) {
70
- case "turn_start":
71
- this.startTurn(event.instance);
72
- this.header = renderHeader(event.instance);
73
- return;
74
- case "assistant_text":
75
- if (!event.delta) return;
76
- if (!this.turnOpen) this.startTurn(undefined);
77
- this.turnHasText = true;
78
- this.appendText(event.delta);
79
- this.scheduleFlush();
80
- return;
81
- case "turn_end":
82
- if (this.turnHasText) {
83
- this.appendText(`\n\n— ${event.reason === "aborted" ? "aborted" : event.reason === "error" ? "error" : "done"} —\n`);
84
- }
85
- this.turnOpen = false;
86
- void this.flushNow();
87
- return;
88
- }
89
- }
90
-
91
- /** Force an immediate flush. */
92
- async flushNow(): Promise<void> {
93
- if (this.scheduledFlush) {
94
- clearTimeout(this.scheduledFlush);
95
- this.scheduledFlush = undefined;
96
- }
97
- await this.flushPending();
98
- }
99
-
100
- /** Drop everything without flushing (e.g. for cleanup). */
101
- reset(): void {
102
- if (this.scheduledFlush) {
103
- clearTimeout(this.scheduledFlush);
104
- this.scheduledFlush = undefined;
105
- }
106
- this.chunks = [];
107
- this.header = undefined;
108
- this.active = undefined;
109
- this.pages = [];
110
- this.turnHasText = false;
111
- this.turnOpen = false;
112
- }
113
-
114
- /** Replace the current Telegram-rendered buffer with a session transcript. */
115
- async showTranscript(instance: RendererInstance | undefined, transcript: string): Promise<void> {
116
- this.reset();
117
- this.header = renderHeader(instance);
118
- const trimmed = transcript.trim();
119
- if (!trimmed) return;
120
- this.chunks = [{ kind: "text", content: trimmed }];
121
- await this.flushNow();
122
- }
123
-
124
- /** Update the most recent message with [aborted] trailer. */
125
- async markAborted(): Promise<void> {
126
- if (!this.active) return;
127
- try {
128
- await this.bot.editMessageText(this.active.messageId, `${this.active.body}\n\n[aborted]`);
129
- } catch {
130
- // best-effort
131
- }
132
- }
133
-
134
- // ─── Internals ───────────────────────────────────────────────────────
135
-
136
- private appendText(delta: string): void {
137
- const last = this.chunks[this.chunks.length - 1];
138
- if (last && last.kind === "text") {
139
- last.content += delta;
140
- } else {
141
- this.chunks.push({ kind: "text", content: delta });
142
- }
143
- }
144
-
145
- private startTurn(instance: RendererInstance | undefined): void {
146
- if (this.turnOpen) return;
147
- if (!this.header) this.header = renderHeader(instance);
148
- if (this.chunks.length > 0) this.appendText(SESSION_SEPARATOR);
149
- this.turnHasText = false;
150
- this.turnOpen = true;
151
- }
152
-
153
- private scheduleFlush(): void {
154
- if (this.scheduledFlush) return;
155
- this.scheduledFlush = setTimeout(() => {
156
- this.scheduledFlush = undefined;
157
- void this.flushPending().catch((error) => {
158
- this.logger(`flush failed: ${error instanceof Error ? error.message : String(error)}`);
159
- });
160
- }, THROTTLE_MS);
161
- }
162
-
163
- private async flushPending(): Promise<void> {
164
- const body = this.renderChunks();
165
- if (!body) return;
166
-
167
- const html = markdownToTelegram(body);
168
-
169
- const chunks = chunkForTelegram(html, TELEGRAM_MESSAGE_MAX - 32);
170
- if (chunks.length === 0) return;
171
-
172
- try {
173
- for (let idx = 0; idx < chunks.length; idx += 1) {
174
- const chunk = chunks[idx];
175
- const page = this.pages[idx];
176
- if (page) {
177
- if (page.body !== chunk) {
178
- await this.bot.editMessageText(page.messageId, chunk);
179
- page.body = chunk;
180
- }
181
- continue;
182
- }
183
- const sent = await this.bot.sendMessage(chunk);
184
- if (sent?.message_id) {
185
- const next = { messageId: sent.message_id, body: chunk };
186
- this.pages.push(next);
187
- this.sentMessageIds.push(sent.message_id);
188
- }
189
- }
190
- this.active = this.pages[this.pages.length - 1];
191
- } catch (error) {
192
- this.logger(`telegram send failed: ${error instanceof Error ? error.message : String(error)}`);
193
- }
194
- }
195
-
196
- private renderChunks(): string {
197
- let out = this.header ? `${this.header}\n\n` : "";
198
- for (const chunk of this.chunks) {
199
- out += chunk.content;
200
- }
201
- return out;
202
- }
203
- }
204
-
205
- function renderHeader(instance: RendererInstance | undefined): string | undefined {
206
- if (!instance) return undefined;
207
- const bits = [instance.label];
208
- if (instance.sessionName) bits.push(instance.sessionName);
209
- else if (instance.sessionId) bits.push(instance.sessionId.slice(0, 8));
210
- return `🤖 ${bits.join(" · ")}`;
211
- }