README now carries the icon, TL;DR, description and features; the
technical sections live in docs/. tools/icon_png.py renders
images/icon.svg to a square PNG (512x512 by default, --resolution N).
plugin.json points at the plugin's own icon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
tools/bundle.py now sets `core.hooksPath scripts` the first time it runs in a
clone (skipped in CI, outside a git work tree, or when core.hooksPath is
already set), so the version auto-bump hooks work in fresh clones and repos
created from the template without a manual setup step. README documents the
hooks, the self-enabling behaviour and the bump stop words.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
pluginBase failed with "RELEASE_TOKEN is not set" while chatSummaries
published fine, from the same organisation secret. An organisation secret
is only exposed to a repository that has Actions enabled, so it can be
empty in one repo and present in another.
Try RELEASE_TOKEN first, then the runner-provided GITEA_TOKEN, which can
publish releases in its own repository, and report which one was used.
When none is set, name every variable that was tried and point at the
repository Actions setting rather than failing with a bare message.
The publish step also reports whether the organisation secret arrived,
printing only its length, so the fallback is visible in the job log.
The publish step set GITEA_TOKEN from the org-level API_TOKEN secret, but
the Actions runner injects its own GITEA_TOKEN into every step and that
value overrides a step-level env: entry of the same name. The org secret
was silently ignored and the job failed.
Pass the secret as RELEASE_TOKEN instead, which the runner does not
reserve, and read that name in release.py. --token-env makes the source
configurable, so the runner-provided GITEA_TOKEN can still be used
deliberately by passing --token-env GITEA_TOKEN.
Trigger the release workflow on pushes to main, not just on v* tags, so
every push leaves a downloadable .plugin attached to the current version.
The version in plugin.json decides what happens: a new version creates
tag v<version> and a new release, while an unchanged version rebuilds the
existing release and replaces its attachment. The tag is never moved, so
it keeps pointing at the commit that first introduced that version, but
the release notes are refreshed to name the commit the build came from.
Hand-pushed v* tags and manual runs are unaffected and still use the tag
verbatim.
Replace the sample plugin with an AI chat summarizer that renders its
output as a native RichMessage injected into the chat.
Summarization pipeline:
- Fetch chat/topic history via paginated TL requests.
- Send a system prompt, the user's instructions and an anonymized JSON
transcript as three separate roles. Only message text and attachment
names leave the device; senders and message ids are replaced by
indices, and out-of-range reply parents are included with negative ids
so replies keep their context.
- Stream the response over SSE, reading reasoning from reasoning_content,
reasoning or inline <think> tags.
- Substitute the model's {userN}/{msgN} placeholders back into real names
and deep links. Placeholders are used instead of Markdown links because
a half-streamed link renders as broken literal text.
Rendering:
- Compile Markdown into TL_iv page blocks (headings, lists, quotes, code,
dividers, inline styles) since the app's own converter works off
Android Spanned spans rather than Markdown source.
- Show progress stages immediately on click, animate reasoning while it
streams, then collapse it into a tappable details block.
- Attribute the card to a client-side mock sender and insert it directly
after the newest analyzed message, scrolling it into view.
Persistence:
- Store summaries in SQLite keyed by (dialog, topic, anchor) and
re-inject them when the chat is reopened.
Also add tools/release.py and a Gitea workflow that builds, verifies and
publishes dist/<id>.plugin to the Releases tab, and extend
tools/verify_bundle.py to 104 offline checks covering the transcript,
streaming, rendering, injection and storage behaviour.