@llblab/pi-telegram 0.45.11 → 0.47.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.
Files changed (204) hide show
  1. package/BACKLOG.md +5 -1
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +6 -6
  4. package/dist/api/activity.d.ts +6 -0
  5. package/dist/api/activity.js +6 -0
  6. package/dist/api/commands.d.ts +6 -0
  7. package/dist/api/commands.js +6 -0
  8. package/dist/api/delivery.d.ts +6 -0
  9. package/dist/api/delivery.js +6 -0
  10. package/dist/api/inbound.d.ts +6 -0
  11. package/dist/api/inbound.js +6 -0
  12. package/dist/api/keyboard.d.ts +6 -0
  13. package/dist/api/keyboard.js +6 -0
  14. package/dist/api/outbound.d.ts +6 -0
  15. package/dist/api/outbound.js +6 -0
  16. package/dist/api/sections.d.ts +7 -0
  17. package/dist/api/sections.js +6 -0
  18. package/dist/api/status.d.ts +6 -0
  19. package/dist/api/status.js +6 -0
  20. package/dist/api/updates.d.ts +6 -0
  21. package/dist/api/updates.js +6 -0
  22. package/dist/api/voice.d.ts +6 -0
  23. package/dist/api/voice.js +6 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.js +6 -0
  26. package/dist/lib/activity-verbosity.d.ts +52 -0
  27. package/dist/lib/activity-verbosity.js +596 -0
  28. package/dist/lib/activity.d.ts +220 -0
  29. package/dist/lib/activity.js +574 -0
  30. package/dist/lib/agent-messages.d.ts +28 -0
  31. package/dist/lib/agent-messages.js +86 -0
  32. package/dist/lib/bindings.d.ts +250 -0
  33. package/dist/lib/bindings.js +936 -0
  34. package/dist/lib/bus-api.d.ts +18 -0
  35. package/dist/lib/bus-api.js +254 -0
  36. package/dist/lib/bus-follower.d.ts +464 -0
  37. package/dist/lib/bus-follower.js +1686 -0
  38. package/dist/lib/bus-leader.d.ts +292 -0
  39. package/dist/lib/bus-leader.js +2242 -0
  40. package/dist/lib/bus-transport.d.ts +64 -0
  41. package/dist/lib/bus-transport.js +140 -0
  42. package/dist/lib/bus.d.ts +518 -0
  43. package/dist/lib/bus.js +2055 -0
  44. package/dist/lib/channel-posts.d.ts +170 -0
  45. package/dist/lib/channel-posts.js +611 -0
  46. package/dist/lib/command-templates.d.ts +70 -0
  47. package/dist/lib/command-templates.js +744 -0
  48. package/dist/lib/commands.d.ts +541 -0
  49. package/dist/lib/commands.js +1155 -0
  50. package/dist/lib/config.d.ts +252 -0
  51. package/dist/lib/config.js +889 -0
  52. package/dist/lib/delivery.d.ts +163 -0
  53. package/dist/lib/delivery.js +542 -0
  54. package/dist/lib/extension.d.ts +7 -0
  55. package/dist/lib/extension.js +1608 -0
  56. package/dist/lib/generative-app-worker.mjs +104 -0
  57. package/dist/lib/generative-apps.d.ts +212 -0
  58. package/dist/lib/generative-apps.js +1006 -0
  59. package/dist/lib/inbound.d.ts +91 -0
  60. package/dist/lib/inbound.js +502 -0
  61. package/dist/lib/journal.d.ts +665 -0
  62. package/dist/lib/journal.js +3701 -0
  63. package/dist/lib/keyboard.d.ts +23 -0
  64. package/dist/lib/keyboard.js +37 -0
  65. package/dist/lib/lifecycle.d.ts +157 -0
  66. package/dist/lib/lifecycle.js +395 -0
  67. package/dist/lib/locks.d.ts +164 -0
  68. package/dist/lib/locks.js +1208 -0
  69. package/dist/lib/logging.d.ts +50 -0
  70. package/dist/lib/logging.js +274 -0
  71. package/dist/lib/media.d.ts +181 -0
  72. package/dist/lib/media.js +602 -0
  73. package/dist/lib/menu-model.d.ts +217 -0
  74. package/dist/lib/menu-model.js +663 -0
  75. package/dist/lib/menu-queue.d.ts +37 -0
  76. package/dist/lib/menu-queue.js +408 -0
  77. package/dist/lib/menu-settings.d.ts +116 -0
  78. package/dist/lib/menu-settings.js +595 -0
  79. package/dist/lib/menu-status.d.ts +32 -0
  80. package/dist/lib/menu-status.js +112 -0
  81. package/dist/lib/menu-thinking.d.ts +29 -0
  82. package/dist/lib/menu-thinking.js +82 -0
  83. package/dist/lib/menu.d.ts +171 -0
  84. package/dist/lib/menu.js +323 -0
  85. package/dist/lib/model.d.ts +127 -0
  86. package/dist/lib/model.js +409 -0
  87. package/dist/lib/outbound-attachments.d.ts +241 -0
  88. package/dist/lib/outbound-attachments.js +639 -0
  89. package/dist/lib/outbound-buttons.d.ts +69 -0
  90. package/dist/lib/outbound-buttons.js +248 -0
  91. package/dist/lib/outbound-markup.d.ts +42 -0
  92. package/dist/lib/outbound-markup.js +678 -0
  93. package/dist/lib/outbound-voice.d.ts +55 -0
  94. package/dist/lib/outbound-voice.js +152 -0
  95. package/dist/lib/outbound.d.ts +185 -0
  96. package/dist/lib/outbound.js +516 -0
  97. package/dist/lib/ownership.d.ts +77 -0
  98. package/dist/lib/ownership.js +174 -0
  99. package/dist/lib/paths.d.ts +40 -0
  100. package/dist/lib/paths.js +94 -0
  101. package/dist/lib/pi.d.ts +72 -0
  102. package/dist/lib/pi.js +121 -0
  103. package/dist/lib/polling.d.ts +351 -0
  104. package/dist/lib/polling.js +1196 -0
  105. package/dist/lib/preview.d.ts +192 -0
  106. package/dist/lib/preview.js +614 -0
  107. package/dist/lib/prompt-templates.d.ts +24 -0
  108. package/dist/lib/prompt-templates.js +118 -0
  109. package/dist/lib/prompts.d.ts +57 -0
  110. package/dist/lib/prompts.js +167 -0
  111. package/dist/lib/queue.d.ts +766 -0
  112. package/dist/lib/queue.js +1965 -0
  113. package/dist/lib/recovery.d.ts +86 -0
  114. package/dist/lib/recovery.js +285 -0
  115. package/dist/lib/rendering.d.ts +20 -0
  116. package/dist/lib/rendering.js +983 -0
  117. package/dist/lib/replies.d.ts +214 -0
  118. package/dist/lib/replies.js +737 -0
  119. package/dist/lib/routing.d.ts +207 -0
  120. package/dist/lib/routing.js +2282 -0
  121. package/dist/lib/runtime.d.ts +172 -0
  122. package/dist/lib/runtime.js +402 -0
  123. package/dist/lib/sections.d.ts +165 -0
  124. package/dist/lib/sections.js +327 -0
  125. package/dist/lib/setup.d.ts +82 -0
  126. package/dist/lib/setup.js +150 -0
  127. package/dist/lib/skills.d.ts +8 -0
  128. package/dist/lib/skills.js +12 -0
  129. package/dist/lib/status.d.ts +442 -0
  130. package/dist/lib/status.js +1006 -0
  131. package/dist/lib/sync.d.ts +179 -0
  132. package/dist/lib/sync.js +782 -0
  133. package/dist/lib/target.d.ts +20 -0
  134. package/dist/lib/target.js +27 -0
  135. package/dist/lib/telegram-api.d.ts +541 -0
  136. package/dist/lib/telegram-api.js +1159 -0
  137. package/dist/lib/text-groups.d.ts +89 -0
  138. package/dist/lib/text-groups.js +317 -0
  139. package/dist/lib/thread-cleanup-manager.d.ts +288 -0
  140. package/dist/lib/thread-cleanup-manager.js +560 -0
  141. package/dist/lib/thread-display.d.ts +46 -0
  142. package/dist/lib/thread-display.js +257 -0
  143. package/dist/lib/thread-naming.d.ts +46 -0
  144. package/dist/lib/thread-naming.js +78 -0
  145. package/dist/lib/thread-reconciler.d.ts +239 -0
  146. package/dist/lib/thread-reconciler.js +644 -0
  147. package/dist/lib/threads.d.ts +621 -0
  148. package/dist/lib/threads.js +3679 -0
  149. package/dist/lib/time-injection.d.ts +15 -0
  150. package/dist/lib/time-injection.js +56 -0
  151. package/dist/lib/turns.d.ts +109 -0
  152. package/dist/lib/turns.js +500 -0
  153. package/dist/lib/updates.d.ts +1290 -0
  154. package/dist/lib/updates.js +3634 -0
  155. package/dist/lib/voice.d.ts +117 -0
  156. package/dist/lib/voice.js +174 -0
  157. package/dist/lib/workspace-admission.d.ts +264 -0
  158. package/dist/lib/workspace-admission.js +1136 -0
  159. package/dist/lib/workspace-retirement.d.ts +219 -0
  160. package/dist/lib/workspace-retirement.js +587 -0
  161. package/dist/lib/workspace-slots.d.ts +30 -0
  162. package/dist/lib/workspace-slots.js +54 -0
  163. package/dist/package.json +126 -0
  164. package/dist/pi-telegram/index.js +1 -0
  165. package/dist/skills/generated-control-surface/SKILL.md +107 -0
  166. package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
  167. package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
  168. package/dist/skills/generative-apps/SKILL.md +115 -0
  169. package/dist/skills/show-me/SKILL.md +166 -0
  170. package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
  171. package/dist/skills/telegram-bridge/SKILL.md +129 -0
  172. package/dist/skills/telegram-bridge/references/configuration.md +15 -0
  173. package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
  174. package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
  175. package/docs/architecture.md +1 -1
  176. package/docs/compact-matrix-literal.md +10 -3
  177. package/docs/generative-apps.md +15 -13
  178. package/docs/multi-instance-bus.md +17 -3
  179. package/docs/outbound.md +1 -1
  180. package/docs/public-api.md +2 -2
  181. package/lib/bindings.ts +89 -6
  182. package/lib/bus-follower.ts +11 -3
  183. package/lib/bus-leader.ts +57 -20
  184. package/lib/bus.ts +12 -2
  185. package/lib/commands.ts +2 -2
  186. package/lib/config.ts +14 -7
  187. package/lib/delivery.ts +38 -6
  188. package/lib/extension.ts +26 -1
  189. package/lib/generative-apps.ts +379 -13
  190. package/lib/menu-settings.ts +21 -5
  191. package/lib/outbound-markup.ts +16 -7
  192. package/lib/pi.ts +4 -0
  193. package/lib/sync.ts +3 -0
  194. package/lib/telegram-api.ts +12 -1
  195. package/lib/thread-cleanup-manager.ts +36 -6
  196. package/lib/thread-display.ts +96 -18
  197. package/lib/threads.ts +150 -30
  198. package/package.json +56 -13
  199. package/scripts/build-dist.mjs +44 -0
  200. package/scripts/measure-bus.mjs +8 -1
  201. package/scripts/measure-workspace.mjs +8 -1
  202. package/skills/generative-apps/SKILL.md +1 -1
  203. package/skills/show-me/SKILL.md +1 -1
  204. package/skills/telegram-bridge/SKILL.md +1 -1
package/lib/extension.ts CHANGED
@@ -64,6 +64,7 @@ const telegramBusProtocolIdentity =
64
64
  Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
65
65
  Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
66
66
  Bus.TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
67
+ Bus.TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
67
68
  ]);
68
69
 
69
70
  // --- Extension Runtime ---
@@ -672,11 +673,15 @@ export default function (pi: Pi.ExtensionAPI) {
672
673
  getHandlers: configStore.getOutboundHandlers,
673
674
  recordRuntimeEvent,
674
675
  });
676
+ const generativeAppLiveSurfaceBinding =
677
+ Bindings.createTelegramGenerativeAppLiveSurfaceBinding();
675
678
  const invokeGenerativeAppBoundButtonAction =
676
679
  Bindings.createTelegramGenerativeAppBoundButtonActionInvoker({
677
680
  agentDir: Paths.resolveAgentDir(),
678
681
  assertExecutionCurrent: Updates.assertTelegramUpdateExecutionCurrent,
679
682
  getExecutionFence: Updates.getTelegramUpdateExecutionFence,
683
+ getActiveProfileName: configStore.getActiveProfileName,
684
+ getLiveSurfaceRuntime: generativeAppLiveSurfaceBinding.get,
680
685
  planOutput: planGenerativeAppOutput,
681
686
  sendMarkdownReply,
682
687
  editInteractiveMessage,
@@ -861,6 +866,18 @@ export default function (pi: Pi.ExtensionAPI) {
861
866
  return threadStore.getBotState().threadMode === "enabled"
862
867
  ? Config.resolveTelegramThreadDisplayMode(configStore.get()) : undefined;
863
868
  },
869
+ getThreadDisplayPreview(mode) {
870
+ const bindings = threadStore.listWorkspaceBindings();
871
+ return [...ThreadDisplay.resolveTelegramWorkspaceDisplayNames(
872
+ bindings,
873
+ mode,
874
+ ThreadDisplay.resolveTelegramLiveWorkspaceBindingKeys(
875
+ bindings,
876
+ threadStore.getActiveByInstanceId(telegramInstanceId)?.target,
877
+ telegramBusFollowerRegistry.list(),
878
+ ),
879
+ ).values()];
880
+ },
864
881
  async setThreadDisplayMode(mode) {
865
882
  try {
866
883
  await ThreadDisplay.applyTelegramThreadDisplaySetting(mode, {
@@ -1124,6 +1141,7 @@ export default function (pi: Pi.ExtensionAPI) {
1124
1141
  topicTargetStore: threadStore,
1125
1142
  instanceId: telegramInstanceId,
1126
1143
  getActiveProfileName: configStore.getActiveProfileName,
1144
+ getSessionId: Pi.getExtensionContextSessionId,
1127
1145
  getWorkspaceAdmission: workspaceAdmissionRuntime.resolve,
1128
1146
  async startLeader(ctx, election, onAcquired): Promise<boolean> {
1129
1147
  const result = await lockedPollingRuntime.start(ctx, {
@@ -1202,6 +1220,7 @@ export default function (pi: Pi.ExtensionAPI) {
1202
1220
  getProcessBirthId() {
1203
1221
  return telegramQueueProcessBirthId;
1204
1222
  },
1223
+ getSessionId: Pi.getExtensionContextSessionId,
1205
1224
  getSessionGeneration: telegramSessionContextStore.getGeneration,
1206
1225
  async onRegistered(ctx) {
1207
1226
  await followerAdmissionLifecycleRuntime.onTransportChanged(ctx);
@@ -1318,6 +1337,7 @@ export default function (pi: Pi.ExtensionAPI) {
1318
1337
  getAllowedUserId: configStore.getAllowedUserId,
1319
1338
  instanceId: telegramInstanceId,
1320
1339
  getCwd: Pi.getExtensionContextCwd,
1340
+ getSessionId: Pi.getExtensionContextSessionId,
1321
1341
  getTelegramProfile: configStore.getActiveProfileName,
1322
1342
  getThreadDisplayMode() {
1323
1343
  return Config.resolveTelegramThreadDisplayMode(configStore.get());
@@ -1412,7 +1432,10 @@ export default function (pi: Pi.ExtensionAPI) {
1412
1432
  });
1413
1433
  },
1414
1434
  hasRememberedWorkspaceBinding(ctx) {
1415
- return threadStore.hasWorkspaceBinding(Pi.getExtensionContextCwd(ctx));
1435
+ return threadStore.hasWorkspaceBinding(
1436
+ Pi.getExtensionContextCwd(ctx),
1437
+ Pi.getExtensionContextSessionId(ctx),
1438
+ );
1416
1439
  },
1417
1440
  suspendLiveThreadTarget: telegramBusLeaderState.clear,
1418
1441
  stopFollowerRegistration: telegramBusFollowerRegistration.stop,
@@ -1797,6 +1820,7 @@ export default function (pi: Pi.ExtensionAPI) {
1797
1820
  getDefaultChatId: proactivePushChatIdGetter,
1798
1821
  getDefaultTarget: proactivePushTargetGetter,
1799
1822
  ...agentMessageToolRoutingRuntime,
1823
+ setGenerativeAppLiveSurfaceRuntime: generativeAppLiveSurfaceBinding.set,
1800
1824
  updateStatus,
1801
1825
  recordRuntimeEvent,
1802
1826
  });
@@ -1825,6 +1849,7 @@ export default function (pi: Pi.ExtensionAPI) {
1825
1849
  deferredQueueDispatchRuntime,
1826
1850
  modelContextAvailabilityRuntime,
1827
1851
  disconnectOnQuit: cleanupTelegramThreadForSessionRestart,
1852
+ shutdownGenerativeAppLiveSurfaces: generativeAppLiveSurfaceBinding.shutdown,
1828
1853
  resolveAutomaticThreadCleanupEnabled:
1829
1854
  configControls.resolveAutomaticThreadCleanupEnabled,
1830
1855
  buttonActionStore,
@@ -4,7 +4,7 @@
4
4
  * Owns Generative App identity, installation, invocation, state history, and telegram_bind
5
5
  */
6
6
 
7
- import { randomUUID } from "node:crypto";
7
+ import { createHash, randomUUID } from "node:crypto";
8
8
  import {
9
9
  appendFile,
10
10
  copyFile,
@@ -33,6 +33,8 @@ const GENERATIVE_APP_LOCK_WAIT_MS = 12_000;
33
33
  const GENERATIVE_APP_RUN_MAX_TIMEOUT_MS = 30_000;
34
34
  const GENERATIVE_APP_RUN_MAX_ARGS = 64;
35
35
  const GENERATIVE_APP_RUN_MAX_STREAM_BYTES = 64 * 1024;
36
+ export const GENERATIVE_APP_MIN_REFRESH_AFTER_MS = 2_000;
37
+ export const GENERATIVE_APP_MAX_REFRESH_AFTER_MS = 24 * 60 * 60 * 1_000;
36
38
 
37
39
  export type GenerativeAppJsonValue =
38
40
  | null
@@ -66,6 +68,7 @@ export interface GenerativeAppMethodContext {
66
68
 
67
69
  export interface GenerativeAppMethodResult {
68
70
  output: string;
71
+ refreshAfterMs?: number;
69
72
  state?: GenerativeAppJsonValue;
70
73
  viewMode?: "new" | "edit";
71
74
  }
@@ -82,6 +85,7 @@ export interface GenerativeAppInvocationResult {
82
85
  output: string;
83
86
  app: string;
84
87
  revision: number;
88
+ refreshAfterMs?: number;
85
89
  stateChanged: boolean;
86
90
  viewMode: "new" | "edit";
87
91
  }
@@ -104,7 +108,89 @@ export interface GenerativeAppRuntimeOptions {
104
108
  methodTimeoutMs?: number;
105
109
  }
106
110
 
111
+ export interface GenerativeAppLiveSurfaceFrame<THandle> {
112
+ digest: string;
113
+ handle: THandle;
114
+ }
115
+
116
+ export interface GenerativeAppLiveSurface<THandle> {
117
+ app: string;
118
+ appGeneration: string;
119
+ appRevision: number;
120
+ handle: THandle;
121
+ initialDigest: string;
122
+ key: string;
123
+ refreshAfterMs: number;
124
+ }
125
+
126
+ export interface GenerativeAppLiveSurfaceRuntime<THandle> {
127
+ open: (surface: GenerativeAppLiveSurface<THandle>) => void;
128
+ cancel: (key: string) => void;
129
+ take: (key: string) => GenerativeAppLiveSurface<THandle> | undefined;
130
+ resume: (
131
+ surface: GenerativeAppLiveSurface<THandle>,
132
+ result: GenerativeAppInvocationResult,
133
+ ) => Promise<void>;
134
+ refreshNow: (key: string) => Promise<void>;
135
+ shutdown: () => void;
136
+ }
137
+
138
+ export interface GenerativeAppLiveSurfaceRuntimeDeps<THandle> {
139
+ agentDir: string;
140
+ isCurrent: (surface: GenerativeAppLiveSurface<THandle>) => boolean;
141
+ plan: (
142
+ result: GenerativeAppInvocationResult,
143
+ handle: THandle,
144
+ ) => GenerativeAppLiveSurfaceFrame<THandle>;
145
+ edit: (
146
+ frame: GenerativeAppLiveSurfaceFrame<THandle>,
147
+ ) => Promise<THandle>;
148
+ classifyEditError?: (error: unknown) =>
149
+ | { kind: "retry"; retryAfterMs?: number }
150
+ | { kind: "terminal" | "unavailable" | "unknown" };
151
+ recordRuntimeEvent?: (
152
+ category: string,
153
+ error: unknown,
154
+ details?: Record<string, unknown>,
155
+ ) => void;
156
+ setTimer?: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
157
+ clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
158
+ }
159
+
160
+ export interface TelegramBindDeliveryHandle {
161
+ readonly target: { chatId: number; threadId?: number };
162
+ readonly messageIds: readonly number[];
163
+ readonly generation: string;
164
+ }
165
+
166
+ export interface TelegramBindLiveHandle {
167
+ delivery: TelegramBindDeliveryHandle;
168
+ view?: { text: string; parseMode: "markdown"; replyMarkup?: unknown };
169
+ }
170
+
171
+ export function getTelegramBindLiveSurfaceKey(
172
+ app: string,
173
+ profile: string,
174
+ target: { chatId: number; threadId?: number },
175
+ ): string {
176
+ return `${app}/${profile}/${target.chatId}/${target.threadId ?? 0}`;
177
+ }
178
+
107
179
  export interface TelegramBindToolRegistrationDeps extends GenerativeAppRuntimeOptions {
180
+ getActiveProfileName?: () => string | undefined;
181
+ liveSurfaceSetTimer?: GenerativeAppLiveSurfaceRuntimeDeps<unknown>["setTimer"];
182
+ liveSurfaceClearTimer?: GenerativeAppLiveSurfaceRuntimeDeps<unknown>["clearTimer"];
183
+ setLiveSurfaceRuntime?: (
184
+ runtime: GenerativeAppLiveSurfaceRuntime<TelegramBindLiveHandle> | undefined,
185
+ ) => void;
186
+ isDeliveryHandleCurrent?: (handle: TelegramBindDeliveryHandle) => boolean;
187
+ editView?: (
188
+ handle: TelegramBindDeliveryHandle,
189
+ view: { text: string; parseMode: "markdown"; replyMarkup?: unknown },
190
+ ) => Promise<
191
+ | { ok: true; value: TelegramBindDeliveryHandle }
192
+ | { ok: false; reason: string; message: string; retryAfterMs?: number }
193
+ >;
108
194
  getActiveTurn?: () =>
109
195
  | {
110
196
  chatId: number;
@@ -125,6 +211,13 @@ export interface TelegramBindToolRegistrationDeps extends GenerativeAppRuntimeOp
125
211
  target?: { chatId: number; threadId?: number };
126
212
  },
127
213
  ) => Promise<number | undefined>;
214
+ sendView?: (
215
+ view: { text: string; parseMode: "markdown"; replyMarkup?: unknown },
216
+ options: { scope: { kind: "active-turn" }; replyToMessageId: number },
217
+ ) => Promise<
218
+ | { ok: true; value: TelegramBindDeliveryHandle }
219
+ | { ok: false; reason?: string; message: string }
220
+ >;
128
221
  recordRuntimeEvent?: (
129
222
  category: string,
130
223
  error: unknown,
@@ -365,8 +458,24 @@ function normalizeMethodResult(value: unknown): GenerativeAppMethodResult {
365
458
  if (viewMode !== "new" && viewMode !== "edit") {
366
459
  throw new Error("Generative App viewMode must be new or edit.");
367
460
  }
461
+ let refreshAfterMs: number | undefined;
462
+ if (Object.hasOwn(result, "refreshAfterMs")) {
463
+ if (
464
+ typeof result.refreshAfterMs !== "number" ||
465
+ !Number.isFinite(result.refreshAfterMs) ||
466
+ !Number.isInteger(result.refreshAfterMs) ||
467
+ result.refreshAfterMs <= 0
468
+ ) {
469
+ throw new Error("Generative App refreshAfterMs must be a finite positive integer.");
470
+ }
471
+ refreshAfterMs = Math.min(
472
+ GENERATIVE_APP_MAX_REFRESH_AFTER_MS,
473
+ Math.max(GENERATIVE_APP_MIN_REFRESH_AFTER_MS, result.refreshAfterMs),
474
+ );
475
+ }
368
476
  return {
369
477
  output: result.output,
478
+ ...(refreshAfterMs !== undefined ? { refreshAfterMs } : {}),
370
479
  ...(Object.hasOwn(result, "state")
371
480
  ? { state: assertJsonValue(result.state, "Generative App state") }
372
481
  : {}),
@@ -595,6 +704,9 @@ async function invokeMethod(options: {
595
704
  if (options.method === "init" && result.state === undefined) {
596
705
  throw new Error("Generative App init must return state.");
597
706
  }
707
+ if (options.method === "refresh" && result.state !== undefined) {
708
+ throw new Error("Generative App refresh must be output-only.");
709
+ }
598
710
  let nextRevision = revision;
599
711
  if (result.state !== undefined) {
600
712
  options.execution?.assertCurrent();
@@ -623,6 +735,9 @@ async function invokeMethod(options: {
623
735
  output: result.output,
624
736
  app: options.app,
625
737
  revision: nextRevision,
738
+ ...(result.refreshAfterMs !== undefined
739
+ ? { refreshAfterMs: result.refreshAfterMs }
740
+ : {}),
626
741
  stateChanged: result.state !== undefined,
627
742
  viewMode: result.viewMode ?? "new",
628
743
  };
@@ -848,6 +963,154 @@ export async function bindGenerativeApp(options: GenerativeAppRuntimeOptions & {
848
963
  });
849
964
  }
850
965
 
966
+ export function createGenerativeAppLiveSurfaceRuntime<THandle>(
967
+ deps: GenerativeAppLiveSurfaceRuntimeDeps<THandle>,
968
+ ): GenerativeAppLiveSurfaceRuntime<THandle> {
969
+ interface Record {
970
+ surface: GenerativeAppLiveSurface<THandle>;
971
+ digest?: string;
972
+ timer?: ReturnType<typeof setTimeout>;
973
+ inFlight: boolean;
974
+ retryDelayMs: number;
975
+ pending?: {
976
+ frame: GenerativeAppLiveSurfaceFrame<THandle>;
977
+ result: GenerativeAppInvocationResult;
978
+ };
979
+ }
980
+ const records = new Map<string, Record>();
981
+ const setTimer: NonNullable<GenerativeAppLiveSurfaceRuntimeDeps<THandle>["setTimer"]> =
982
+ deps.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
983
+ const clearTimer: NonNullable<GenerativeAppLiveSurfaceRuntimeDeps<THandle>["clearTimer"]> =
984
+ deps.clearTimer ?? ((timer) => clearTimeout(timer));
985
+ let active = true;
986
+
987
+ const clearRecordTimer = (record: Record): void => {
988
+ if (record.timer !== undefined) clearTimer(record.timer);
989
+ record.timer = undefined;
990
+ };
991
+ const take = (key: string): GenerativeAppLiveSurface<THandle> | undefined => {
992
+ const record = records.get(key);
993
+ if (!record) return undefined;
994
+ clearRecordTimer(record);
995
+ records.delete(key);
996
+ return { ...record.surface };
997
+ };
998
+ const cancel = (key: string): void => {
999
+ take(key);
1000
+ };
1001
+ const ownsRecord = (key: string, record: Record): boolean =>
1002
+ active && records.get(key) === record;
1003
+ const schedule = (record: Record, delayMs: number): void => {
1004
+ clearRecordTimer(record);
1005
+ if (!active || records.get(record.surface.key) !== record) return;
1006
+ const timer = setTimer(() => void refreshNow(record.surface.key), delayMs);
1007
+ record.timer = timer;
1008
+ timer.unref?.();
1009
+ };
1010
+ const refreshNow = async (key: string): Promise<void> => {
1011
+ const record = records.get(key);
1012
+ if (!active || !record || record.inFlight) return;
1013
+ clearRecordTimer(record);
1014
+ if (!deps.isCurrent(record.surface)) {
1015
+ cancel(key);
1016
+ return;
1017
+ }
1018
+ record.inFlight = true;
1019
+ try {
1020
+ let pending = record.pending;
1021
+ if (!pending) {
1022
+ const result = await invokeGenerativeApp({
1023
+ agentDir: deps.agentDir,
1024
+ expectedGeneration: record.surface.appGeneration,
1025
+ expectedRevision: record.surface.appRevision,
1026
+ method: "refresh",
1027
+ app: record.surface.app,
1028
+ });
1029
+ if (!ownsRecord(key, record) || !deps.isCurrent(record.surface)) {
1030
+ if (records.get(key) === record) cancel(key);
1031
+ return;
1032
+ }
1033
+ const frame = deps.plan(result, record.surface.handle);
1034
+ pending = { frame, result };
1035
+ if (frame.digest !== record.digest) record.pending = pending;
1036
+ }
1037
+ const { frame, result } = pending;
1038
+ if (!ownsRecord(key, record)) return;
1039
+ if (frame.digest !== record.digest) {
1040
+ record.surface.handle = await deps.edit(frame);
1041
+ if (!ownsRecord(key, record)) return;
1042
+ record.digest = frame.digest;
1043
+ record.pending = undefined;
1044
+ }
1045
+ if (result.refreshAfterMs === undefined) {
1046
+ cancel(key);
1047
+ return;
1048
+ }
1049
+ record.retryDelayMs = GENERATIVE_APP_MIN_REFRESH_AFTER_MS;
1050
+ record.surface.appRevision = result.revision;
1051
+ record.surface.refreshAfterMs = result.refreshAfterMs;
1052
+ schedule(record, result.refreshAfterMs);
1053
+ } catch (error) {
1054
+ const classification = deps.classifyEditError?.(error) ?? { kind: "unknown" as const };
1055
+ if (!ownsRecord(key, record)) return;
1056
+ deps.recordRuntimeEvent?.("generative-app", error, {
1057
+ phase: "live-surface-refresh",
1058
+ app: record.surface.app,
1059
+ outcome: classification.kind,
1060
+ });
1061
+ if (classification.kind !== "retry" || !deps.isCurrent(record.surface)) {
1062
+ cancel(key);
1063
+ return;
1064
+ }
1065
+ const delay = classification.retryAfterMs === undefined
1066
+ ? record.retryDelayMs
1067
+ : Math.max(GENERATIVE_APP_MIN_REFRESH_AFTER_MS, classification.retryAfterMs);
1068
+ record.retryDelayMs = Math.min(60_000, Math.max(4_000, delay * 2));
1069
+ schedule(record, delay);
1070
+ } finally {
1071
+ record.inFlight = false;
1072
+ }
1073
+ };
1074
+ const open = (surface: GenerativeAppLiveSurface<THandle>): void => {
1075
+ cancel(surface.key);
1076
+ if (!active) return;
1077
+ const record: Record = {
1078
+ surface: { ...surface },
1079
+ digest: surface.initialDigest,
1080
+ inFlight: false,
1081
+ retryDelayMs: GENERATIVE_APP_MIN_REFRESH_AFTER_MS,
1082
+ };
1083
+ records.set(surface.key, record);
1084
+ schedule(record, surface.refreshAfterMs);
1085
+ };
1086
+ return {
1087
+ open,
1088
+ cancel,
1089
+ take,
1090
+ async resume(surface, result) {
1091
+ if (!active || !deps.isCurrent(surface)) return;
1092
+ const frame = deps.plan(result, surface.handle);
1093
+ const handle = frame.digest === surface.initialDigest
1094
+ ? surface.handle
1095
+ : await deps.edit(frame);
1096
+ if (result.refreshAfterMs === undefined || !deps.isCurrent({ ...surface, handle })) return;
1097
+ open({
1098
+ ...surface,
1099
+ appGeneration: result.generation,
1100
+ appRevision: result.revision,
1101
+ handle,
1102
+ initialDigest: frame.digest,
1103
+ refreshAfterMs: result.refreshAfterMs,
1104
+ });
1105
+ },
1106
+ refreshNow,
1107
+ shutdown() {
1108
+ active = false;
1109
+ for (const key of [...records.keys()]) cancel(key);
1110
+ },
1111
+ };
1112
+ }
1113
+
851
1114
  export function formatGenerativeAppToolOutput(output: string): string {
852
1115
  const normalized = output.replace(/^\n+/u, "");
853
1116
  return `\n${normalized || "(Generative App returned no output)"}`;
@@ -884,6 +1147,72 @@ export function registerTelegramBindTool(
884
1147
  pi: ExtensionAPI,
885
1148
  deps: TelegramBindToolRegistrationDeps,
886
1149
  ): void {
1150
+ const planFrame = (
1151
+ result: GenerativeAppInvocationResult,
1152
+ handle: TelegramBindLiveHandle,
1153
+ ): GenerativeAppLiveSurfaceFrame<TelegramBindLiveHandle> => {
1154
+ const planned = deps.planOutput!(result.output, {
1155
+ binding: {
1156
+ generation: result.generation,
1157
+ app: result.app,
1158
+ revision: result.revision,
1159
+ },
1160
+ });
1161
+ const view = {
1162
+ text: planned.markdown,
1163
+ parseMode: "markdown" as const,
1164
+ ...(planned.replyMarkup !== undefined ? { replyMarkup: planned.replyMarkup } : {}),
1165
+ };
1166
+ return {
1167
+ digest: createHash("sha256").update(JSON.stringify(view)).digest("hex"),
1168
+ handle: { delivery: handle.delivery, view },
1169
+ };
1170
+ };
1171
+ const liveSurfaces = deps.planOutput && deps.editView && deps.isDeliveryHandleCurrent
1172
+ ? createGenerativeAppLiveSurfaceRuntime<TelegramBindLiveHandle>({
1173
+ agentDir: deps.agentDir,
1174
+ isCurrent: (surface) => deps.isDeliveryHandleCurrent!(surface.handle.delivery),
1175
+ plan: planFrame,
1176
+ async edit(frame) {
1177
+ const view = frame.handle.view;
1178
+ if (!view) throw new Error("Generative App live frame is missing its planned view.");
1179
+ const edited = await deps.editView!(frame.handle.delivery, view);
1180
+ if (!edited.ok) throw Object.assign(new Error(edited.message), {
1181
+ deliveryFailureReason: edited.reason,
1182
+ ...(edited.retryAfterMs === undefined ? {} : { retryAfterMs: edited.retryAfterMs }),
1183
+ });
1184
+ return { delivery: edited.value, view };
1185
+ },
1186
+ classifyEditError(error) {
1187
+ const failure = error as {
1188
+ deliveryFailureReason?: string;
1189
+ retryAfterMs?: number;
1190
+ };
1191
+ if (failure.deliveryFailureReason === "rate-limited") {
1192
+ return { kind: "retry", retryAfterMs: failure.retryAfterMs };
1193
+ }
1194
+ if (failure.deliveryFailureReason === "transport-retryable") {
1195
+ return { kind: "retry" };
1196
+ }
1197
+ if (failure.deliveryFailureReason === "message-unavailable") {
1198
+ return { kind: "unavailable" };
1199
+ }
1200
+ return {
1201
+ kind: failure.deliveryFailureReason === "commit-unknown" ? "unknown" : "terminal",
1202
+ };
1203
+ },
1204
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1205
+ ...(deps.liveSurfaceSetTimer ? { setTimer: deps.liveSurfaceSetTimer } : {}),
1206
+ ...(deps.liveSurfaceClearTimer ? { clearTimer: deps.liveSurfaceClearTimer } : {}),
1207
+ })
1208
+ : undefined;
1209
+ deps.setLiveSurfaceRuntime?.(liveSurfaces);
1210
+ const surfaceKey = (app: string, handle: TelegramBindDeliveryHandle): string =>
1211
+ getTelegramBindLiveSurfaceKey(
1212
+ app,
1213
+ deps.getActiveProfileName?.() ?? "default",
1214
+ handle.target,
1215
+ );
887
1216
  pi.registerTool({
888
1217
  name: "telegram_bind",
889
1218
  label: "Telegram Bind",
@@ -919,7 +1248,7 @@ export function registerTelegramBindTool(
919
1248
  const activeTurn = params.display === false
920
1249
  ? undefined
921
1250
  : deps.getActiveTurn?.();
922
- if (activeTurn && deps.planOutput && deps.sendMarkdownReply) {
1251
+ if (activeTurn && deps.planOutput && (deps.sendView || deps.sendMarkdownReply)) {
923
1252
  try {
924
1253
  const planned = deps.planOutput(result.output, {
925
1254
  binding: {
@@ -928,17 +1257,54 @@ export function registerTelegramBindTool(
928
1257
  revision: result.revision,
929
1258
  },
930
1259
  });
931
- const messageId = await deps.sendMarkdownReply(
932
- activeTurn.chatId,
933
- activeTurn.replyToMessageId,
934
- planned.markdown,
935
- {
936
- ...(planned.replyMarkup !== undefined
937
- ? { replyMarkup: planned.replyMarkup }
938
- : {}),
939
- ...(activeTurn.target ? { target: activeTurn.target } : {}),
940
- },
941
- );
1260
+ let messageId: number | undefined;
1261
+ if (deps.sendView) {
1262
+ const delivered = await deps.sendView(
1263
+ {
1264
+ text: planned.markdown,
1265
+ parseMode: "markdown",
1266
+ ...(planned.replyMarkup !== undefined
1267
+ ? { replyMarkup: planned.replyMarkup }
1268
+ : {}),
1269
+ },
1270
+ {
1271
+ scope: { kind: "active-turn" },
1272
+ replyToMessageId: activeTurn.replyToMessageId,
1273
+ },
1274
+ );
1275
+ if (!delivered.ok) throw new Error(delivered.message);
1276
+ messageId = delivered.value.messageIds[0];
1277
+ if (liveSurfaces) {
1278
+ const key = surfaceKey(result.app, delivered.value);
1279
+ if (result.refreshAfterMs === undefined) {
1280
+ liveSurfaces.cancel(key);
1281
+ } else {
1282
+ const handle = { delivery: delivered.value };
1283
+ const frame = planFrame(result, handle);
1284
+ liveSurfaces.open({
1285
+ app: result.app,
1286
+ appGeneration: result.generation,
1287
+ appRevision: result.revision,
1288
+ handle,
1289
+ initialDigest: frame.digest,
1290
+ key,
1291
+ refreshAfterMs: result.refreshAfterMs,
1292
+ });
1293
+ }
1294
+ }
1295
+ } else {
1296
+ messageId = await deps.sendMarkdownReply!(
1297
+ activeTurn.chatId,
1298
+ activeTurn.replyToMessageId,
1299
+ planned.markdown,
1300
+ {
1301
+ ...(planned.replyMarkup !== undefined
1302
+ ? { replyMarkup: planned.replyMarkup }
1303
+ : {}),
1304
+ ...(activeTurn.target ? { target: activeTurn.target } : {}),
1305
+ },
1306
+ );
1307
+ }
942
1308
  return {
943
1309
  content: [{ type: "text", text: formatDisplayedGenerativeAppToolOutput() }],
944
1310
  details: { ...result, displayed: true, messageId },
@@ -23,6 +23,7 @@ export type TelegramSettingsMenuReplyMarkup = TelegramInlineKeyboardMarkup;
23
23
 
24
24
  export interface TelegramSettingsStateDeps {
25
25
  getThreadDisplayMode?: () => TelegramThreadDisplayMode | undefined;
26
+ getThreadDisplayPreview?: (mode: TelegramThreadDisplayMode) => readonly string[];
26
27
  areDraftPreviewsEnabled: () => boolean;
27
28
  getAssistantRenderingMode: () => TelegramAssistantRenderingMode;
28
29
  getActivityVerbosity: () => TelegramActivityVerbosity;
@@ -175,7 +176,15 @@ export function buildTelegramSettingsMenuText(): string {
175
176
  return SETTINGS_MENU_TITLE;
176
177
  }
177
178
 
178
- export function buildThreadDisplaySettingsText(mode: TelegramThreadDisplayMode): string {
179
+ function escapeThreadDisplayPreview(text: string): string {
180
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
181
+ }
182
+
183
+ export function buildThreadDisplaySettingsText(
184
+ mode: TelegramThreadDisplayMode,
185
+ preview: readonly string[] = [],
186
+ ): string {
187
+ const visiblePreview = preview.slice(0, 5).map(escapeThreadDisplayPreview);
179
188
  return [
180
189
  `${THREAD_DISPLAY_SETTINGS_TITLE} <code>${mode}</code>`,
181
190
  "",
@@ -183,7 +192,10 @@ export function buildThreadDisplaySettingsText(mode: TelegramThreadDisplayMode):
183
192
  "",
184
193
  "<code>-</code> <code>letters</code> (default): show the unique slot, such as <b><i>A</i></b> or <b><i>B</i></b>.",
185
194
  "<code>-</code> <code>names</code>: show the generated dictionary name for the slot, such as <b><i>Anchor</i></b> or <b><i>Briar</i></b>.",
186
- "<code>-</code> <code>directories</code>: show the directory, such as <b><i>extensions</i></b>; shared Workspaces keep slot suffixes, such as <b><i>extensions_a</i></b> and <b><i>extensions_c</i></b>.",
195
+ "<code>-</code> <code>directories</code>: legacy directory formatting retained without automatic migration.",
196
+ "<code>-</code> <code>directory-snake</code>: normalize the distinguishing directory path, such as <b><i>api_tools</i></b> or <b><i>frontend_extensions_a</i></b>.",
197
+ "<code>-</code> <code>directory-title</code>: humanize the distinguishing directory path, such as <b><i>Api Tools</i></b> or <b><i>Frontend / Extensions · A</i></b>.",
198
+ ...(visiblePreview.length ? ["", `<b>Live preview:</b> ${visiblePreview.map((label) => `<code>${label}</code>`).join(", ")}${preview.length > visiblePreview.length ? `, +${preview.length - visiblePreview.length} more` : ""}`] : []),
187
199
  ].join("\n");
188
200
  }
189
201
 
@@ -414,7 +426,7 @@ export async function openTelegramSettingsMenu<
414
426
  export function buildThreadDisplaySettingsReplyMarkup(mode: TelegramThreadDisplayMode): TelegramSettingsMenuReplyMarkup {
415
427
  return { inline_keyboard: [
416
428
  [{ text: "⬆️ Back", callback_data: "settings:list" }],
417
- ...(["letters", "names", "directories"] as const).map((value) => [{
429
+ ...(["letters", "names", "directories", "directory-snake", "directory-title"] as const).map((value) => [{
418
430
  text: `${mode === value ? "🟢 " : ""}${value}`,
419
431
  callback_data: `settings:set:thread-display:${value}`,
420
432
  }]),
@@ -630,7 +642,8 @@ export async function handleTelegramSettingsMenuCallbackAction(
630
642
  }
631
643
  if (data.startsWith("settings:set:thread-display:")) {
632
644
  const mode = data.slice("settings:set:thread-display:".length);
633
- if (mode !== "letters" && mode !== "names" && mode !== "directories") {
645
+ if (mode !== "letters" && mode !== "names" && mode !== "directories" &&
646
+ mode !== "directory-snake" && mode !== "directory-title") {
634
647
  await deps.answerCallbackQuery(callbackQueryId, "Unknown Thread display mode.");
635
648
  return true;
636
649
  }
@@ -647,7 +660,7 @@ export async function handleTelegramSettingsMenuCallbackAction(
647
660
  return true;
648
661
  }
649
662
  await deps.updateSettingsMessage(
650
- buildThreadDisplaySettingsText(mode),
663
+ buildThreadDisplaySettingsText(mode, deps.getThreadDisplayPreview?.(mode)),
651
664
  buildThreadDisplaySettingsReplyMarkup(mode),
652
665
  );
653
666
  await deps.answerCallbackQuery(callbackQueryId);
@@ -863,6 +876,7 @@ export function createTelegramSettingsMenuRuntime<
863
876
  getTimeInjectionMode: deps.getTimeInjectionMode,
864
877
  isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
865
878
  getThreadDisplayMode: deps.getThreadDisplayMode,
879
+ getThreadDisplayPreview: deps.getThreadDisplayPreview,
866
880
  sendSettingsMenu: (state, text, replyMarkup) =>
867
881
  deps.sendInteractiveMessage(
868
882
  state.chatId,
@@ -887,6 +901,7 @@ export function createTelegramSettingsMenuRuntime<
887
901
  getTimeInjectionMode: deps.getTimeInjectionMode,
888
902
  isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
889
903
  getThreadDisplayMode: deps.getThreadDisplayMode,
904
+ getThreadDisplayPreview: deps.getThreadDisplayPreview,
890
905
  updateSettingsMessage: (text, replyMarkup) =>
891
906
  deps.editInteractiveMessage(
892
907
  state.chatId,
@@ -931,6 +946,7 @@ export function createTelegramSettingsMenuRuntime<
931
946
  getTimeInjectionMode: deps.getTimeInjectionMode,
932
947
  isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
933
948
  getThreadDisplayMode: deps.getThreadDisplayMode,
949
+ getThreadDisplayPreview: deps.getThreadDisplayPreview,
934
950
  setThreadDisplayMode: deps.setThreadDisplayMode,
935
951
  setDraftPreviewsEnabled: deps.setDraftPreviewsEnabled,
936
952
  setAssistantRenderingMode: deps.setAssistantRenderingMode,