Inject the summary card only once the answer starts streaming
Publish plugin / publish (push) Successful in 3s

Pressing Summarize used to inject a status card immediately, at a guessed
anchor, and move it once the fetch returned. Any failure before the model
answered (fetch error, empty history, unreachable host, bad key, HTTP error)
then left an error bubble in the chat and had already scrolled the user away
from where they were reading.

The pipeline is now fetch -> stream -> card. A bulletin acknowledges the
press; every failure before the first chunk is a bulletin only and leaves the
chat untouched. On the first chunk the card is created with its final anchor
and virtual id and placed strictly after the newest analyzed message. If that
message is not loaded, the chat is asked to load it and placement retries; the
card never falls back to the chat bottom. Placement then hard-jumps the layout
manager to the card's row: scrollToMessageId only smooth-scrolls from cell
positions that are stale right after an insert. The provisional anchor and
reanchor() machinery is gone.

The row is reserved with INSERT .. DO NOTHING, so re-summarizing a saved range
no longer blanks its text before the new answer arrives; a fresh row whose
stream breaks is deleted instead of left empty.

Also:
- Finished summaries end with #aisummaries as a native TL_iv.textHashtag
  (rendered by RichMessageLayout like a hashtag entity) and carry a plain-text
  body with a TL_messageEntityHashtag.
- Deleting a card (MessagesController.deleteMessages, all overloads) removes
  its saved row and the card, with a "Summary deleted" bulletin.
- Settings show the saved summary count and approximate size, and a confirmed
  "Clear all summaries" action.
- Version 1.1.0.

Verified on device: no card before streaming, correct placement and jump,
restore on reopen, deletion, and the hashtag rendering. Telegram's search is
server-side, so local cards do not appear in its results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lyuksovannyyandClaude Opus 5.5 committed 2026-10-06 23:19:06 +02:00
1 parent b2105ca753
commit 16bf8de84c
10 files changed
+1123 -382

No files matched your search

+35
View File
@@ -78,6 +78,41 @@ plugins that need to reach beyond the public SDK surface.
- `intents.IntentsManager` — Android intent integration, **exteraGram 12.6.4+ only**.
<https://plugins.exteragram.app/docs/intents>
## Host internals relied on by the summary card
Verified against exteraGram's `ChatActivity`/`MessagesController` sources and the 12.5.x dex.
- **Scrolling.** `ChatActivity.scrollToMessageId(id, 0, false, 0, true, 0)`: for an id that
is not loaded it requests history around it and jumps there when it arrives; for a loaded
one it only `smoothScrollBy`s from the current cell positions, which are stale right after
an insert. To land on a row, set the layout manager's pending position instead:
`chatLayoutManager.scrollToPositionWithOffset(chatAdapter.messagesStartRow + index,
offset, false)` (`bottom=false` counts `offset` from the top edge). `messagesStartRow` is
public and current after `notifyDataSetChanged()`, which runs `updateRowsInternal()`.
- **Deleting.** Every delete goes through `MessagesController.deleteMessages` (4 overloads,
all starting `(messages, randoms, encryptedChat, dialogId, …)`). Only ids `> 0` are sent to
the server; the rest are deleted from the local cache and posted in
`NotificationCenter.messagesDeleted`. `ChatActivity` removes deleted rows by looking them up
in `messagesDict`, which injected cards are never added to, so the plugin removes them.
- **Hashtags in rich text.** `TL_iv.textHashtag` (single field `text`) is rendered by
`RichMessageLayout.formatText` exactly like a `TL_messageEntityHashtag` in a normal
message (`URLSpanNoUnderline`). Long-pressing it offers Open / Copy hashtag. A `textUrl`
whose URL starts with `#` is not a hashtag: it goes through the external-URL path and does
nothing.
- **Search.** In-chat and hashtag search are server-side (`messages.search`); local results
exist only for Saved Messages tags. A locally injected message cannot appear in them.
## SDK UI details used here
- `ui.settings.Text(text, subtext=, icon=, accent=, red=, on_click=)`, where `on_click`
receives the clicked `View`. `set_setting(key, value, reload_settings=True)` re-renders
the settings page.
- `AlertDialogBuilder(activity)`; button listeners receive `(builder, which)`;
`make_button_red(AlertDialogBuilder.BUTTON_POSITIVE)` after `show()`. A dismissed builder
cannot be shown again.
- `BulletinHelper.show_*` post to the UI thread themselves.
- `android_utils.run_on_ui_thread(fn, delay)`, where `delay` is in milliseconds.
## On-device dev server protocol
<https://plugins.exteragram.app/docs/dev-server>