mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
feat(telegram): mirror tui prompts into telegram thread
When the telegram bridge is connected, messages you type in the
zot tui now also appear in the paired chat so the telegram
transcript stays a complete record of the session. Format:
you: <what you typed> <- from tui editor, grey bubble
zot: <assistant reply> <- reply to a tui prompt
<your telegram dm> <- your own blue bubble
<assistant reply, bare> <- reply to a telegram dm, no prefix
The "zot: " prefix is only attached when the turn was initiated
from the tui side. Telegram-initiated turns reply bare so the
thread reads as a normal back-and-forth with the bot; the "you: "
bubble from the tui side would otherwise pair awkwardly with a
DM-initiated bare reply.
Implementation is small:
bridge.go
- OnUserTyped(text): sends with "you: " prefix. Called from
the interactive submit path when the bridge is active.
- OnAssistantText(text): sends with "zot: " prefix by
default, or bare when nextReplyFromTelegram is set.
- nextReplyFromTelegram is flipped to true inside
handleUpdate right before calling Host.SubmitOrQueue, and
back to false when the reply is flushed. One-slot flag,
safe against the actual serial turn drain the agent uses.
- On Start(), if Config.AllowedUserID is already known from
a previous session, prepopulate chatID so the bridge can
send immediately without waiting for a handshake DM
(private-chat id == user id on telegram).
- sendToPaired consolidates the chunk-and-send plumbing so
OnUserTyped, OnAssistantText, and future tap points share
one path.
interactive.go
- The editor submit path now calls telegramBridge.OnUserTyped
on a goroutine (network write off the event loop) before
queuing or starting the turn. No-op when the bridge is
stopped or no chat is paired.
No user-visible setup change: /telegram connect / disconnect /
status work the same; the two-way mirror is automatic once
connected.
This commit is contained in:
parent
098a79743d
commit
625c2382b7
2 changed files with 58 additions and 4 deletions
|
|
@ -1091,6 +1091,14 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) {
|
|||
i.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
// Mirror the user's typed prompt into the paired Telegram
|
||||
// chat (when the bridge is active) so the Telegram thread
|
||||
// stays a complete record of the session, not just the half
|
||||
// that originated on the phone. On a goroutine so the
|
||||
// network write doesn't delay the local turn.
|
||||
if i.telegramBridge != nil && i.telegramBridge.Active() {
|
||||
go i.telegramBridge.OnUserTyped(text)
|
||||
}
|
||||
// If a turn is already in flight, queue this prompt instead of
|
||||
// starting a second one. The drain loop at the end of startTurn
|
||||
// will pick it up when the current turn finishes.
|
||||
|
|
|
|||
|
|
@ -45,6 +45,14 @@ type Bridge struct {
|
|||
me *User
|
||||
chatID int64 // populated after first DM from the paired user
|
||||
replyBuf strings.Builder
|
||||
|
||||
// nextReplyFromTelegram is set when the next assistant reply
|
||||
// should be sent bare (no "zot: " prefix) because the turn was
|
||||
// initiated by a Telegram DM. The flag clears as soon as the
|
||||
// reply is flushed. TUI-originated turns leave the flag false
|
||||
// so the reply is tagged "zot: " for clarity on the two-sided
|
||||
// transcript.
|
||||
nextReplyFromTelegram bool
|
||||
}
|
||||
|
||||
// State is the snapshot /telegram status reports.
|
||||
|
|
@ -106,6 +114,12 @@ func (b *Bridge) Start(parent context.Context) error {
|
|||
b.running = true
|
||||
b.cancel = cancel
|
||||
b.me = me
|
||||
// Telegram private-chat ids are the same as the user id, so if
|
||||
// we've already paired in a previous session we can send to the
|
||||
// user immediately without waiting for them to DM first.
|
||||
if b.Config.AllowedUserID != 0 && b.chatID == 0 {
|
||||
b.chatID = b.Config.AllowedUserID
|
||||
}
|
||||
if b.Config.BotID != me.ID || b.Config.BotUsername != me.Username {
|
||||
b.Config.BotID = me.ID
|
||||
b.Config.BotUsername = me.Username
|
||||
|
|
@ -131,10 +145,37 @@ func (b *Bridge) Stop() {
|
|||
|
||||
// OnAssistantText should be called by the TUI with the assistant's
|
||||
// final visible text for each turn. The bridge forwards it to the
|
||||
// paired chat in message-sized chunks. Safe to call from any
|
||||
// goroutine; a no-op when the bridge is stopped or no chat is
|
||||
// known yet.
|
||||
// paired chat in message-sized chunks. Prefix depends on which
|
||||
// side initiated the turn: TUI-originated turns get "zot: " so the
|
||||
// two-sided transcript reads naturally ("you: ..." / "zot: ..."),
|
||||
// while Telegram-originated turns send bare text (the user's own
|
||||
// bubble is already on-screen, a "zot: " prefix would just add
|
||||
// visual noise to a plain back-and-forth).
|
||||
func (b *Bridge) OnAssistantText(text string) {
|
||||
b.mu.Lock()
|
||||
prefix := "zot: "
|
||||
if b.nextReplyFromTelegram {
|
||||
prefix = ""
|
||||
b.nextReplyFromTelegram = false
|
||||
}
|
||||
b.mu.Unlock()
|
||||
b.sendToPaired(text, prefix)
|
||||
}
|
||||
|
||||
// OnUserTyped mirrors a message the user typed in the zot TUI into
|
||||
// the paired Telegram chat, tagged "you:" so the Telegram thread
|
||||
// stays a complete record of the conversation (both TUI-originated
|
||||
// and Telegram-originated turns). Messages sent from Telegram
|
||||
// itself aren't mirrored back (they already appear as the user's
|
||||
// own bubble), only TUI-originated prompts flow through here.
|
||||
func (b *Bridge) OnUserTyped(text string) {
|
||||
b.sendToPaired(text, "you: ")
|
||||
}
|
||||
|
||||
// sendToPaired writes text (with an optional prefix, chunked to
|
||||
// Telegram's 4096-char cap) to the paired chat. No-op when the
|
||||
// bridge is stopped or before the paired chat id is known.
|
||||
func (b *Bridge) sendToPaired(text, prefix string) {
|
||||
b.mu.Lock()
|
||||
chatID := b.chatID
|
||||
running := b.running
|
||||
|
|
@ -146,7 +187,9 @@ func (b *Bridge) OnAssistantText(text string) {
|
|||
if text == "" {
|
||||
return
|
||||
}
|
||||
// Telegram caps at 4096 chars; chunk to be safe.
|
||||
if prefix != "" {
|
||||
text = prefix + text
|
||||
}
|
||||
for _, chunk := range chunkMessage(text, 4000) {
|
||||
if err := b.Client.SendMessage(context.Background(), chatID, chunk, 0); err != nil {
|
||||
fmt.Fprintln(stderr(), "telegram bridge: sendMessage:", err)
|
||||
|
|
@ -290,6 +333,9 @@ func (b *Bridge) handleUpdate(ctx context.Context, u Update) {
|
|||
return
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
b.nextReplyFromTelegram = true
|
||||
b.mu.Unlock()
|
||||
b.Host.SubmitOrQueue(prompt, images)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue