@llblab/pi-telegram 0.32.0 → 0.33.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
+ ## 0.33.1: Rendered Command Replies And Visible Compaction
6
+
7
+ - `Command Rendering`: Restored the `/next` empty-queue emphasis by preserving its HTML source and forwarding `parseMode: "HTML"` through the command reply adapter, so command helpers select the renderer explicitly instead of leaking HTML or Markdown syntax as plain text.
8
+ - `Compaction Visibility`: Reports `compacting` ahead of generic active or pending status and sends the same start/completion notices for observed automatic compaction as for manually requested compaction, while retaining queue blocking and deferred dispatch.
9
+
10
+ ## 0.33.0: Matrix Controls And Pinned Filesystem Navigation
11
+
12
+ - `Button Matrix`: Extended `telegram_button` and its plural alias from flat arrays to JSON matrices: top-level objects remain full-width rows, while nested arrays intentionally group one to three peer controls horizontally; empty, oversized, or deeper rows fail closed without changing existing object, attribute, or flat-array behavior.
13
+ - `Filesystem Navigation`: Stabilized Generated Control Surface directory layout: parent traversal is pinned as the first full-width row outside root, available Previous/Next controls share the next compact row, ten-entry pages follow as full-width rows, one JSON-matrix action carries the surface, visible text avoids duplicate plain or monospaced inventories unless user preference overrides it, Refresh is omitted by default, and numbered fallback preserves navigation when buttons are unavailable.
14
+ - `Command Rendering`: Replaced the `/next` empty-queue response's leaked HTML tag with native Rich Markdown so Telegram renders emphasis instead of displaying `<b>` literally.
15
+
5
16
  ## 0.32.0: Compact Button Arrays And Filesystem Surfaces
6
17
 
7
18
  - `Button Arrays`: Added JSON-array payloads to the canonical colon-free `telegram_button` action so one hidden comment can declare many ordered button rows; `telegram_buttons` is an exact plural alias, while existing single-object and double-quoted attribute forms remain valid.
package/README.md CHANGED
@@ -192,7 +192,7 @@ Voice notes, audio, images, PDFs, and other media can pass through configured in
192
192
 
193
193
  ### Buttons And Callbacks
194
194
 
195
- Assistant replies can include top-level hidden `telegram_button` comments using a JSON object, a JSON array of button objects, or compact double-quoted attributes; `telegram_buttons` is a plural alias. Prefer one array comment for multiple buttons. Buttons use `label` plus `prompt`, or the compact `value` key when both are identical. The action marker is colon-free for every payload form. The bridge strips the comments from visible text, renders inline buttons, and routes callbacks back into Pi as queued prompts or extension-owned callback actions. Button-only replies receive the standard `☑️ **Choose an option:**` heading as automatic visible fallback text. Once a generated prompt button is accepted, only that exact button switches to its optional `selected_style` (`primary` blue by default, `success` green, or `danger` red) without altering its agent-authored label or emoji; every style still queues the selected prompt.
195
+ Assistant replies can include top-level hidden `telegram_button` comments using a JSON object, JSON matrix, or compact double-quoted attributes; `telegram_buttons` is a plural alias. In a matrix, top-level objects become full-width rows while nested arrays group one to three buttons horizontally. Prefer one array comment for multiple buttons. Buttons use `label` plus `prompt`, or the compact `value` key when both are identical. The action marker is colon-free for every payload form. The bridge strips the comments from visible text, renders inline buttons, and routes callbacks back into Pi as queued prompts or extension-owned callback actions. Button-only replies receive the standard `☑️ **Choose an option:**` heading as automatic visible fallback text. Once a generated prompt button is accepted, only that exact button switches to its optional `selected_style` (`primary` blue by default, `success` green, or `danger` red) without altering its agent-authored label or emoji; every style still queues the selected prompt.
196
196
 
197
197
  ### Threaded Mode And Multi-Instance Bus
198
198
 
@@ -327,7 +327,7 @@ Outbound files staged during an active Telegram turn are delivered after that tu
327
327
  Assistant-authored final-message actions use hidden top-level comments with colon-free markers; colon-prefixed payloads are rejected:
328
328
 
329
329
  - `telegram_voice` accepts a JSON object or double-quoted HTML-like attributes and creates voice reply artifacts through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
330
- - `telegram_button` accepts a JSON object, a JSON array of button objects, or double-quoted attributes; `telegram_buttons` aliases the same parser. Each array object creates one inline button row whose callback enqueues the configured prompt text as a normal Telegram prompt turn.
330
+ - `telegram_button` accepts a JSON object, JSON matrix, or double-quoted attributes; `telegram_buttons` aliases the same parser. Each top-level matrix object creates one full-width row, while a nested array groups one to three buttons horizontally; every callback enqueues its configured prompt text as a normal Telegram prompt turn.
331
331
 
332
332
  Preview delivery strips top-level action comments before streaming draft Markdown. Comments inside code fences, quotes, lists, or indented examples stay literal.
333
333
 
@@ -352,7 +352,7 @@ The bridge does not mirror arbitrary `ctx.ui.confirm/input/select/custom` prompt
352
352
 
353
353
  ## Diagnostics And Operational Behavior
354
354
 
355
- Status rendering distinguishes connected, active, dispatching, queued, tool-running, model-switching, and compacting states. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, status remains active instead of degrading to connected.
355
+ Status rendering distinguishes connected, active, dispatching, queued, tool-running, model-switching, and compacting states; the Telegram status menu gives compaction precedence over generic active or pending work. Observed automatic compaction sends the same start and completion notices as the manual command without duplicating notices for command-owned compaction. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, status remains active instead of degrading to connected.
356
356
 
357
357
  Queue reactions are reversible shortcut controls for waiting turns. The runtime reconciles each complete `MessageReactionUpdated.new_reaction` set: any removal reaction (`👎`, `👻`, `💔`, `💩`, `🗑`) suppresses the governed prompt without discarding its queue authority; otherwise any promotion reaction (`👍`, `⚡️`, `❤️`, `🕊`, `🔥`) moves it to priority; otherwise it returns to the default lane. Suppressed prompts remain visible in the queue menu, survive authenticated queue handoff, and do not block unrelated dispatch. Reaction changes first flush a matching delayed text or media group so the governed turn exists before mutation. Once Pi has consumed a prompt, reactions cannot retract it.
358
358
 
@@ -362,7 +362,7 @@ Complete intermediate assistant text blocks from Telegram-originated activity ar
362
362
 
363
363
  `assistant.activity` is an independent bridge-owned projection over normalized Activity events. Each process reloads the shared file-backed setting at `agent-start` before activity admission, so multi-instance mode cannot continue projecting a stale broader process-local selection. Omitted values resolve to `verbose`, while invalid values fail closed to `quiet`; `thinking` and `tools` select one technical class, while `verbose` enables both. Provider-exposed thinking uses persistent ordinary HTML containing only a standard expandable blockquote with a bounded redacted latest-text window and inline Markdown rendered as Telegram HTML. Completed executed tools use native Rich Messages: each closed `<Tool>: <status>` root details node renders snake-case names as title words while preserving an uppercase two- or three-letter repeated prefix per word, then the native disclosure chevron reveals an open-by-default `arguments` child plus closed retained `update N` and `result`/`error` child details with lowercase monospaced, marker-free summaries and JSON pre blocks; known-safe Rich rejections fall back to the previous HTML disclosure. The projection captures the exact target and transport stamp at activity admission, serializes updates, preserves tool-start order, closes coalescing across assistant/thinking boundaries, bounds retained text/update memory plus edit frames and message/tool size, disables previews and HTTP(S) auto-link recognition inside technical evidence, and never replays a possibly committed send. Session generations own independent queues, so replacement drops queued old work without waiting on an old call. Both proactive prose and active-turn final delivery wait for the admitted activity queue inside their extension-owned delivery tasks, preserving technical-before-semantic ordering without delaying Pi lifecycle completion. Settlement, replacement, disconnect, failure, or stale authority clears only local ownership; already-sent activity messages remain in chat.
364
364
 
365
- Telegram prompt guidance is context- and authority-aware. The package and source-checkout extension both contribute `telegram-bridge` plus the optional `generated-control-surface` Skill through Pi resource discovery. The latter treats `interface = f(state, capabilities, intent)` as a renderer-neutral primitive, compiling transient evidence-backed controls over domain-owned workflows, systems, navigation, supervision, and decisions without creating parallel application state. Only an exact direct owner or live registered follower exposes the two pi-telegram delivery tools, their active-tool metadata, and the compact routing suffix. Disconnect or authority loss removes those tool surfaces for subsequent requests without touching foreign tools; reconnect/recovery restores only the pi-telegram subset that was active before suspension, including across same-process reload. Telegram-originated turns route to the stable Skill contract and retain dynamic blocks such as `[voice] delivery: automatic voice`; the Skill and public documentation own syntax, target routing, Threaded Mode behavior, and diagnostics.
365
+ Telegram prompt guidance is context- and authority-aware. The package and source-checkout extension both contribute `telegram-bridge` plus the optional `generated-control-surface` Skill through Pi resource discovery. The latter treats `interface = f(state, capabilities, intent)` as a renderer-neutral primitive, compiling transient evidence-backed controls over domain-owned workflows, systems, navigation, supervision, and decisions without creating parallel application state. Its filesystem adapter reserves the first full-width row for parent traversal outside root, places available Previous/Next controls together in one compact row immediately afterward, paginates directory entries in fixed pages of ten as full-width rows, emits the complete Telegram control set through one JSON-matrix action, suppresses duplicate plain/monospaced listings and default Refresh unless user preference overrides presentation, and retains an ordinary numbered fallback when buttons are unavailable. Only an exact direct owner or live registered follower exposes the two pi-telegram delivery tools, their active-tool metadata, and the compact routing suffix. Disconnect or authority loss removes those tool surfaces for subsequent requests without touching foreign tools; reconnect/recovery restores only the pi-telegram subset that was active before suspension, including across same-process reload. Telegram-originated turns route to the stable Skill contract and retain dynamic blocks such as `[voice] delivery: automatic voice`; the Skill and public documentation own syntax, target routing, Threaded Mode behavior, and diagnostics.
366
366
 
367
367
  ## In-Flight Model Switching
368
368
 
package/docs/outbound.md CHANGED
@@ -155,18 +155,18 @@ I can continue.
155
155
 
156
156
  <!-- telegram_button {"value":"Done"} -->
157
157
 
158
- <!-- telegram_button [{"label":"📁 etc","prompt":"/etc"},{"label":"📁 home","prompt":"/home"}] -->
158
+ <!-- telegram_button [{"label":"⬆️ Up","prompt":"/"},[{"value":"⬅️ Previous"},{"value":"➡️ Next"}],{"label":"📁 etc","prompt":"/etc"}] -->
159
159
 
160
- <!-- telegram_buttons [{"value":"Approve"},{"value":"Reject"}] -->
160
+ <!-- telegram_buttons [[{"value":"Approve"},{"value":"Reject"}]] -->
161
161
  ```
162
162
 
163
163
  Rules:
164
164
 
165
- - `telegram_button` accepts a JSON object, a JSON array of button objects, or double-quoted HTML-like attributes; `telegram_buttons` is an exact plural alias. Shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are rejected.
165
+ - `telegram_button` accepts a JSON object, JSON matrix, or double-quoted HTML-like attributes; `telegram_buttons` is an exact plural alias. Shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are rejected.
166
166
  - A colon after either button marker is rejected so every payload form shares one unambiguous action marker.
167
167
  - Use `label` plus `prompt`, or the compact `value` key when both strings are identical. Explicit `label` or `prompt` takes precedence over its `value` fallback. Use JSON with `\n` escapes for multiline prompts.
168
168
  - The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it remains literal Markdown.
169
- - Prefer one JSON array comment for multiple buttons; each object becomes one inline-keyboard row in source order. Repeated singular comments remain valid.
169
+ - Prefer one JSON array comment for multiple buttons. Each top-level object becomes one full-width inline-keyboard row in source order; a nested array groups one to three button objects into one horizontal row. Empty rows, rows above three buttons, and deeper nesting are rejected. Repeated singular comments remain valid.
170
170
  - Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
171
171
  - After Telegram accepts a generated button callback as a queued prompt, the bridge changes that exact button to its configured selection style without changing agent-authored text or emoji. Set `selected_style` to `primary` (blue), `success` (green), or `danger` (red); omitted or invalid values fall back to `primary`. The style never suppresses queue admission. Other choices stay visually unchanged and remain available; the callback acknowledgement remains the fallback on clients that do not render button styles.
172
172
  - When generated button markup is the entire assistant reply, the bridge supplies the standard `☑️ **Choose an option:**` heading as visible message text so Telegram has a message to which it can attach the inline keyboard.
@@ -184,7 +184,7 @@ The extension injects prompt guidance by context:
184
184
  - For Telegram-originated turns, the compact note routes the agent to `telegram-bridge`, which owns voice/button/direct-delivery/Threaded Mode/formatting/debug guidance.
185
185
  - For Telegram-originated turns, write the full technical answer as normal Markdown.
186
186
  - Add `telegram_voice` with either a JSON object or double-quoted attributes when a Telegram-native voice message is useful. A companion summary is optional, no specific summary format is required.
187
- - Add `telegram_button` with a JSON object, a JSON array of button objects, or double-quoted attributes; `telegram_buttons` is an alias. Prefer one array for multiple controls. Use `label` plus `prompt`, or `value` when they are identical; `selected_style` is optional. A button-only reply may omit parent text because the bridge supplies `☑️ **Choose an option:**` automatically.
187
+ - Add `telegram_button` with a JSON object, JSON matrix, or double-quoted attributes; `telegram_buttons` is an alias. Prefer one array for multiple controls. Use `label` plus `prompt`, or `value` when they are identical; `selected_style` is optional. A button-only reply may omit parent text because the bridge supplies `☑️ **Choose an option:**` automatically.
188
188
  - For ordinary Telegram-turn replies, do not call transport tools for voice or buttons; the bridge owns delivery, while registered voice synthesis providers own TTS and OGG/Opus conversion. For explicit local/TUI direct sends, `telegram_message` may include top-level `telegram_button` comments in its Markdown text because those buttons are attached to that text message.
189
189
  - Prefer meaningful visible parent text when it adds context; for a button-only answer, rely on the bridge's automatic `☑️ **Choose an option:**` fallback rather than manufacturing duplicate text.
190
190
 
@@ -65,7 +65,7 @@ This command surface is a mobile companion subset, not a raw terminal-command br
65
65
  - `telegram_message(text, chat_id?, thread_id?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. During an active Telegram turn, omitted targeting and an explicit target equal to that turn are rejected so the ordinary final-reply path remains the sole current-target response; an explicit different chat/thread target remains allowed for requested cross-target delivery. Outside active turns, paired/default local/TUI delivery remains unchanged. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
66
66
  - The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, and profile-specific debugging guidance. The regular prompt routes applicable turns to that Skill. `telegram_attach` and `telegram_message` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
67
67
  - `telegram_voice` hidden comments request Telegram-native voice delivery through either a JSON object or double-quoted attributes. Equivalent `text` or `value` supplies the spoken payload; explicit `text` takes precedence.
68
- - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, a JSON array whose objects become separate button rows, or double-quoted attributes; `telegram_buttons` is an exact plural alias. Prefer one array comment for multiple buttons. Use `label` plus `prompt`, or `value` when both strings are identical. Action markers are colon-free; colon-prefixed payloads are rejected. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit standalone button actions.
68
+ - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, JSON matrix, or double-quoted attributes; `telegram_buttons` is an exact plural alias. Top-level matrix objects become full-width rows, while nested arrays group one to three buttons horizontally. Prefer one array comment for multiple buttons. Use `label` plus `prompt`, or `value` when both strings are identical. Action markers are colon-free; colon-prefixed payloads are rejected. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit standalone button actions.
69
69
 
70
70
  Prompt guidance is context-aware: local/TUI prompts see only explicit direct-delivery guidance, while Telegram-originated turns receive the full action-comment syntax and phone-width output contract.
71
71
 
package/lib/bindings.ts CHANGED
@@ -860,6 +860,21 @@ export function registerTelegramLifecycleRuntimeHooks({
860
860
  target: turn?.target,
861
861
  });
862
862
  };
863
+ let observedAutomaticCompaction = false;
864
+ const sendCompactionNotice = async (text: string): Promise<void> => {
865
+ const turn = activeTurnRuntime.get();
866
+ const target = turn?.target ?? proactivePushTargetGetter?.();
867
+ if (!target) return;
868
+ try {
869
+ await sendMarkdownReply(target.chatId, turn?.replyToMessageId, text, {
870
+ target,
871
+ });
872
+ } catch (error) {
873
+ recordRuntimeEvent("delivery", error, {
874
+ phase: "compaction-notice",
875
+ });
876
+ }
877
+ };
863
878
  const compactionObserver = Lifecycle.createTelegramCompactionObserverRuntime({
864
879
  isContextActive: isSessionContextActive,
865
880
  setCompactionInProgress: lifecycle.setCompactionInProgress,
@@ -870,7 +885,10 @@ export function registerTelegramLifecycleRuntimeHooks({
870
885
  deferredQueueDispatchRuntime.request,
871
886
  dispatchNextQueuedTelegramTurn,
872
887
  recordRuntimeEvent,
873
- onCompactionAbandoned: activityRuntime.onCompactionAbandoned,
888
+ onCompactionAbandoned: () => {
889
+ observedAutomaticCompaction = false;
890
+ activityRuntime.onCompactionAbandoned();
891
+ },
874
892
  });
875
893
  const messageActivityTypingHooks =
876
894
  Lifecycle.createTelegramMessageActivityTypingHooks({
@@ -902,6 +920,7 @@ export function registerTelegramLifecycleRuntimeHooks({
902
920
  activityRuntime.onSessionShutdown();
903
921
  activityVerbosityRuntime?.reset();
904
922
  assistantOutputRuntime.stop();
923
+ observedAutomaticCompaction = false;
905
924
  compactionObserver.onSessionShutdown();
906
925
  if (event.reason === "quit" && disconnectOnQuit) {
907
926
  try {
@@ -916,15 +935,24 @@ export function registerTelegramLifecycleRuntimeHooks({
916
935
  }
917
936
  await sessionLifecycleRuntime.onSessionShutdown(event, ctx);
918
937
  },
919
- onSessionBeforeCompact(event, ctx) {
938
+ async onSessionBeforeCompact(event, ctx) {
920
939
  if (!isSessionContextActive(ctx)) return;
940
+ const shouldNotify = !(lifecycle.isCompactionInProgress?.() ?? false);
941
+ if (shouldNotify) observedAutomaticCompaction = true;
921
942
  activityRuntime.onCompactionStart(Pi.getSessionCompactionReason(event));
922
943
  compactionObserver.onSessionBeforeCompact(event, ctx);
944
+ if (shouldNotify) {
945
+ await sendCompactionNotice(Commands.TELEGRAM_COMPACTION_STARTED_TEXT);
946
+ }
923
947
  },
924
- onSessionCompact(event, ctx) {
948
+ async onSessionCompact(event, ctx) {
925
949
  if (!isSessionContextActive(ctx)) return;
926
950
  activityRuntime.onCompactionEnd(Pi.getSessionCompactionReason(event));
927
951
  compactionObserver.onSessionCompact(event, ctx);
952
+ if (observedAutomaticCompaction) {
953
+ observedAutomaticCompaction = false;
954
+ await sendCompactionNotice(Commands.TELEGRAM_COMPACTION_COMPLETED_TEXT);
955
+ }
928
956
  },
929
957
  async onAgentStart(event, ctx) {
930
958
  if (!isSessionContextActive(ctx)) return;
package/lib/commands.ts CHANGED
@@ -185,6 +185,10 @@ export function formatTelegramCommandEmojiPrefix(
185
185
  return `${getTelegramCommandEmoji(command)} `;
186
186
  }
187
187
 
188
+ export const TELEGRAM_COMPACTION_STARTED_TEXT =
189
+ `${formatTelegramCommandEmojiPrefix("compact")}Compaction started.`;
190
+ export const TELEGRAM_COMPACTION_COMPLETED_TEXT = "✅ Compaction completed.";
191
+
188
192
  function formatTelegramBotCommandDescription(
189
193
  command: TelegramCommandEmojiName,
190
194
  description: string,
@@ -716,7 +720,10 @@ export interface TelegramCommandTargetRuntimeDeps<TContext> {
716
720
  chatId: number,
717
721
  replyToMessageId: number,
718
722
  text: string,
719
- options?: { target?: { chatId: number; threadId?: number } },
723
+ options?: {
724
+ parseMode?: "HTML";
725
+ target?: { chatId: number; threadId?: number };
726
+ },
720
727
  ) => Promise<unknown>;
721
728
  }
722
729
 
@@ -734,7 +741,11 @@ export interface TelegramCommandTargetRuntime<
734
741
  showStatus: (message: TMessage, ctx: TContext) => Promise<void>;
735
742
  openModelMenu: (message: TMessage, ctx: TContext) => Promise<void>;
736
743
  openSettingsMenu: (message: TMessage, ctx: TContext) => Promise<void>;
737
- sendTextReply: (message: TMessage, text: string) => Promise<void>;
744
+ sendTextReply: (
745
+ message: TMessage,
746
+ text: string,
747
+ options?: { parseMode?: "HTML" },
748
+ ) => Promise<void>;
738
749
  }
739
750
 
740
751
  export function getTelegramCommandMessageTarget(
@@ -917,9 +928,10 @@ export function createTelegramCommandTargetRuntime<
917
928
  target.threadId,
918
929
  );
919
930
  },
920
- sendTextReply: async (message, text) => {
931
+ sendTextReply: async (message, text, options) => {
921
932
  const target = getTelegramCommandMessageTarget(message);
922
933
  await deps.sendTextReply(target.chatId, target.replyToMessageId, text, {
934
+ ...options,
923
935
  target,
924
936
  });
925
937
  },
@@ -1002,7 +1014,11 @@ export interface TelegramCommandRuntimeDeps<
1002
1014
  registerBotCommands: () => Promise<void>;
1003
1015
  getPromptTemplateCommands?: () => readonly TelegramPromptTemplateMenuCommand[];
1004
1016
  persistConfig: () => Promise<void>;
1005
- sendTextReply: (message: TMessage, text: string) => Promise<void>;
1017
+ sendTextReply: (
1018
+ message: TMessage,
1019
+ text: string,
1020
+ options?: { parseMode?: "HTML" },
1021
+ ) => Promise<void>;
1006
1022
  sendInteractiveMessage?: TelegramCompactConfirmationDeps["sendInteractiveMessage"];
1007
1023
  assertExecutionCurrent?: (message: TMessage) => void;
1008
1024
  }
@@ -1185,11 +1201,14 @@ export async function handleTelegramNextCommand(deps: {
1185
1201
  dispatchNextQueuedTurn: () => void;
1186
1202
  clearFoldForDispatch: () => void;
1187
1203
  updateStatus: () => void;
1188
- sendTextReply: (text: string) => Promise<void>;
1204
+ sendTextReply: (
1205
+ text: string,
1206
+ options?: { parseMode?: "HTML" },
1207
+ ) => Promise<void>;
1189
1208
  }): Promise<void> {
1190
1209
  deps.clearPendingModelSwitch();
1191
1210
  if (!deps.hasQueuedItems()) {
1192
- await deps.sendTextReply("<b>Queue is empty.</b>");
1211
+ await deps.sendTextReply("<b>Queue is empty.</b>", { parseMode: "HTML" });
1193
1212
  return;
1194
1213
  }
1195
1214
  if (!deps.isIdle() && deps.hasAbortHandler()) {
@@ -1294,7 +1313,7 @@ export async function handleTelegramCompactConfirmationCallback<TContext>(
1294
1313
  await deps.editInteractiveMessage(
1295
1314
  chatId,
1296
1315
  messageId,
1297
- `${formatTelegramCommandEmojiPrefix("compact")}Compaction started.`,
1316
+ TELEGRAM_COMPACTION_STARTED_TEXT,
1298
1317
  "plain",
1299
1318
  { inline_keyboard: [] },
1300
1319
  );
@@ -1335,7 +1354,7 @@ export async function handleTelegramCompactCommand(
1335
1354
  deps.setCompactionInProgress(false);
1336
1355
  deps.updateStatus();
1337
1356
  dispatchNextQueuedTelegramTurnAfterCompact(deps);
1338
- void deps.sendTextReply("✅ Compaction completed.");
1357
+ void deps.sendTextReply(TELEGRAM_COMPACTION_COMPLETED_TEXT);
1339
1358
  },
1340
1359
  onError: (error) => {
1341
1360
  deps.stopTypingLoop?.();
@@ -1358,7 +1377,7 @@ export async function handleTelegramCompactCommand(
1358
1377
  }
1359
1378
  if (!deps.suppressStartNotice) {
1360
1379
  await deps.sendTextReply(
1361
- `${formatTelegramCommandEmojiPrefix("compact")}Compaction started.`,
1380
+ TELEGRAM_COMPACTION_STARTED_TEXT,
1362
1381
  );
1363
1382
  }
1364
1383
  }
@@ -1631,11 +1650,13 @@ async function handleTelegramCommandRuntime<
1631
1650
  ): Promise<boolean> {
1632
1651
  const assertExecutionCurrentFor = (nextMessage: TMessage) => (): void =>
1633
1652
  deps.assertExecutionCurrent?.(nextMessage);
1634
- const sendReplyFor = (nextMessage: TMessage) => async (text: string) => {
1635
- deps.assertExecutionCurrent?.(nextMessage);
1636
- await deps.sendTextReply(nextMessage, text);
1637
- deps.assertExecutionCurrent?.(nextMessage);
1638
- };
1653
+ const sendReplyFor =
1654
+ (nextMessage: TMessage) =>
1655
+ async (text: string, options?: { parseMode?: "HTML" }) => {
1656
+ deps.assertExecutionCurrent?.(nextMessage);
1657
+ await deps.sendTextReply(nextMessage, text, options);
1658
+ deps.assertExecutionCurrent?.(nextMessage);
1659
+ };
1639
1660
  const updateStatusFor = (commandCtx: TContext) => () =>
1640
1661
  deps.updateStatus(commandCtx);
1641
1662
  return executeTelegramCommandAction(
@@ -11,7 +11,7 @@ import type {
11
11
  TelegramInlineKeyboardMarkup,
12
12
  } from "./keyboard.ts";
13
13
  import {
14
- parseTelegramActionPayloads,
14
+ parseTelegramActionPayloadRows,
15
15
  parseTopLevelTelegramComment,
16
16
  replaceTopLevelHtmlComments,
17
17
  } from "./outbound-markup.ts";
@@ -167,16 +167,15 @@ export function planTelegramButtonReply(
167
167
  parseTopLevelTelegramComment(comment, candidate),
168
168
  );
169
169
  if (!command) return comment.raw;
170
- const payloads = parseTelegramActionPayloads(comment, command) ?? [];
171
- for (const payload of payloads) {
172
- const action = parseTelegramButtonAction(payload);
173
- if (!action) continue;
174
- keyboard.push([
175
- {
176
- text: action.text,
177
- callback_data: deps.registerAction(action),
178
- },
179
- ]);
170
+ const payloadRows = parseTelegramActionPayloadRows(comment, command) ?? [];
171
+ for (const payloadRow of payloadRows) {
172
+ const row = payloadRow.flatMap((payload) => {
173
+ const action = parseTelegramButtonAction(payload);
174
+ return action
175
+ ? [{ text: action.text, callback_data: deps.registerAction(action) }]
176
+ : [];
177
+ });
178
+ if (row.length > 0) keyboard.push(row);
180
179
  }
181
180
  return "";
182
181
  });
@@ -205,26 +205,41 @@ export function parseTelegramActionPayload(
205
205
  return parseCanonicalTelegramActionAttributes(payload.source);
206
206
  }
207
207
 
208
- export function parseTelegramActionPayloads(
208
+ export function parseTelegramActionPayloadRows(
209
209
  comment: TelegramTopLevelHtmlComment,
210
210
  command: string,
211
- ): Record<string, unknown>[] | undefined {
211
+ ): Record<string, unknown>[][] | undefined {
212
212
  const payload = getTelegramActionPayloadSource(comment, command);
213
213
  if (!payload) return undefined;
214
214
  if (payload.source.startsWith("[") || payload.source.startsWith("{")) {
215
215
  try {
216
216
  const value: unknown = JSON.parse(payload.source);
217
- if (isTelegramActionPayload(value)) return [value];
218
- return Array.isArray(value) && value.every(isTelegramActionPayload)
219
- ? value
220
- : undefined;
217
+ if (isTelegramActionPayload(value)) return [[value]];
218
+ if (!Array.isArray(value)) return undefined;
219
+ const rows: Record<string, unknown>[][] = [];
220
+ for (const entry of value) {
221
+ if (isTelegramActionPayload(entry)) {
222
+ rows.push([entry]);
223
+ continue;
224
+ }
225
+ if (
226
+ !Array.isArray(entry) ||
227
+ entry.length === 0 ||
228
+ entry.length > 3 ||
229
+ !entry.every(isTelegramActionPayload)
230
+ ) {
231
+ return undefined;
232
+ }
233
+ rows.push(entry);
234
+ }
235
+ return rows;
221
236
  } catch {
222
237
  return undefined;
223
238
  }
224
239
  }
225
240
  if (payload.hasBody) return undefined;
226
241
  const attributes = parseCanonicalTelegramActionAttributes(payload.source);
227
- return attributes ? [attributes] : undefined;
242
+ return attributes ? [[attributes]] : undefined;
228
243
  }
229
244
 
230
245
  export function normalizeMarkdownAfterVoiceExtraction(
package/lib/status.ts CHANGED
@@ -1505,6 +1505,7 @@ function buildContextSummary(
1505
1505
  }
1506
1506
 
1507
1507
  function buildStatusSummary(ctx: TelegramStatusContext): string {
1508
+ if (ctx.isCompactionInProgress?.()) return "compacting";
1508
1509
  if (ctx.hasPendingMessages?.()) return "pending";
1509
1510
  if (ctx.isIdle?.() === false) return "active";
1510
1511
  if (ctx.isIdle?.() === true) return "idle";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.32.0",
3
+ "version": "0.33.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -116,11 +116,11 @@ Re-check mutable targets immediately before execution. Access denial never autho
116
116
 
117
117
  ## Prompt Buttons
118
118
 
119
- Use the transport's canonical prompt-button syntax. For pi-telegram, one top-level `telegram_button` comment accepts either one JSON object, double-quoted attributes, or a JSON array whose objects become separate button rows. Prefer one array comment for multiple controls instead of repeating the marker; `telegram_buttons` is a plural alias, not a different format.
119
+ Use the transport's canonical prompt-button syntax. For pi-telegram, one top-level `telegram_button` comment accepts one JSON object, double-quoted attributes, or a JSON matrix. A top-level object becomes one full-width row; a nested array groups one to three objects into one compact horizontal row. Prefer one array comment for multiple controls instead of repeating the marker; `telegram_buttons` is a plural alias, not a different format. Use compact rows only when the controls form one coherent peer group such as Previous/Next, approval alternatives, or view modes; keep ordinary actions full-width.
120
120
 
121
121
  ```html
122
122
  <!-- telegram_button {"label":"🔍 Inspect run","prompt":"Inspect Run run:example read-only, summarize its current status and latest material evidence, then regenerate relevant supervision controls."} -->
123
- <!-- telegram_button [{"label":"📁 etc","prompt":"/etc"},{"label":"📁 home","prompt":"/home"}] -->
123
+ <!-- telegram_button [{"label":"⬆️ Up","prompt":"/"},[{"value":"⬅️ Previous"},{"value":"➡️ Next"}],{"label":"📁 etc","prompt":"/etc"}] -->
124
124
  ```
125
125
 
126
126
  Button prompts must:
@@ -142,7 +142,14 @@ Use normal console programs as the capability owner. Check exit status and stder
142
142
 
143
143
  ### Filesystem
144
144
 
145
- Treat a user prompt that is exactly a plausible filesystem path—including `/`—as legitimate intent to render that location through a generated filesystem surface. Resolve and freshly inspect it before display. For directories, show safe names and types without reading file contents, paginate large listings, and offer useful Up, Previous, Next, and Refresh controls. A filesystem entry button may use the exact target path as its entire prompt because this Skill defines path-only prompts as navigation intent; use the entry name plus a semantic folder/file emoji as its label. Never preview credential stores, private keys, browser profiles, cookies, tokens, wallets, or other secret-bearing files, and never raise privileges merely to enumerate a path.
145
+ Treat a user prompt that is exactly a plausible filesystem path—including `/`—as legitimate intent to render that location through a generated filesystem surface. Resolve and freshly inspect it before display. Directory surfaces use one stable navigation layout:
146
+
147
+ 1. Pin `⬆️ Up` as the first full-width row whenever the current path is not filesystem root; its entire prompt is the exact parent path. Omit Up at `/`.
148
+ 2. When page traversal exists, place `⬅️ Previous` and `➡️ Next` together in one compact row immediately after Up, omitting either unavailable direction. Page traversal re-inspects the directory and preserves a fixed 10-entry page size; moving Up opens the parent at page one.
149
+ 3. Render at most 10 alphabetically ordered entry buttons as full-width rows after structural navigation. Each label uses the entry name plus a semantic folder/file emoji, and its entire prompt may be the exact target path because this Skill defines path-only prompts as navigation intent.
150
+ 4. Keep visible text to a compact path and range summary such as `📁 /home/llb · 1–10 of 52`; do not duplicate entry names as a plain or monospaced directory listing. Omit Refresh by default because resubmitting the current path already requests fresh rendering.
151
+
152
+ For pi-telegram, emit the complete filesystem control set—Up, compact page traversal, then current-page entries—in one `telegram_button` JSON matrix rather than repeating one hidden comment per button. If prompt buttons are unavailable or fail to render, preserve the same ordering and pagination as an ordinary numbered text fallback, not a monospaced inventory, so free-form path entry remains sufficient. Show a plain or monospaced directory listing instead only when the user explicitly requests it or durable user Knowledge establishes that presentation preference. Never preview credential stores, private keys, browser profiles, cookies, tokens, wallets, or other secret-bearing files, and never raise privileges merely to enumerate a path.
146
153
 
147
154
  ### Workflows And Actor Runs
148
155
 
@@ -55,11 +55,11 @@ Button forms:
55
55
  ```html
56
56
  <!-- telegram_button {"label":"Continue","prompt":"Continue with the current plan."} -->
57
57
  <!-- telegram_button value="Continue" -->
58
- <!-- telegram_button [{"label":"📁 etc","prompt":"/etc"},{"label":"📁 home","prompt":"/home"}] -->
59
- <!-- telegram_buttons [{"value":"Approve"},{"value":"Reject"}] -->
58
+ <!-- telegram_button [{"label":"⬆️ Up","prompt":"/"},[{"value":"⬅️ Previous"},{"value":"➡️ Next"}],{"label":"📁 etc","prompt":"/etc"}] -->
59
+ <!-- telegram_buttons [[{"value":"Approve"},{"value":"Reject"}]] -->
60
60
  ```
61
61
 
62
- - `telegram_button` accepts one JSON object, a JSON array of button objects, or double-quoted attributes; `telegram_buttons` is an exact plural alias. Prefer one array comment for multiple buttons. Keep the complete action in one top-level comment and encode JSON line breaks as `\n`.
62
+ - `telegram_button` accepts one JSON object, a JSON matrix, or double-quoted attributes; `telegram_buttons` is an exact plural alias. In a matrix, each top-level object becomes a full-width row and each nested array groups one to three buttons into one horizontal row. Prefer one array comment for multiple buttons. Keep the complete action in one top-level comment and encode JSON line breaks as `\n`.
63
63
  - Use `label` plus a self-contained `prompt`, or non-empty `value` when both are identical.
64
64
  - Optional `selected_style` is `primary` (default), `success`, or `danger`; style never suppresses prompt admission.
65
65
  - If button comments form the whole reply, the bridge supplies the standard choice heading.