v0.50.0 c4ec31f

interactive_telegram.go RawHistoryFolder

1package modes
2
3import (
4	"context"
5	"errors"
6	"fmt"
7
8	"github.com/bnema/zut/packages/agent/modes/telegram"
9)
10
11func (i *Interactive) openTelegramDialog() {
12	items := i.telegramMenuItems()
13	if len(items) == 0 {
14		i.mu.Lock()
15		i.statusErr = "telegram not configured. run `zut telegram-bot setup` first."
16		i.mu.Unlock()
17		i.invalidate()
18		return
19	}
20	i.telegramDialog.Open(items)
21	i.invalidate()
22}
23func (i *Interactive) telegramMenuItems() []telegramItem {
24	cfg, err := telegram.LoadConfig(i.cfg.ZutHome)
25	if err != nil || cfg.BotToken == "" {
26		return nil
27	}
28	var items []telegramItem
29	if i.telegramBridge != nil && i.telegramBridge.Active() {
30		items = append(items, telegramItem{label: "disconnect", action: "disconnect", hint: "stop mirroring"})
31		st := i.telegramBridge.State()
32		hint := "active"
33		if st.Username != "" {
34			hint += " as @" + st.Username
35		}
36		items = append(items, telegramItem{label: "status", action: "status", hint: hint})
37	} else {
38		label := "connect"
39		hint := "start mirroring dms into this session"
40		if cfg.AllowedUserID == 0 {
41			hint = "awaiting pairing (send /start to the bot once connected)"
42		}
43		items = append(items, telegramItem{label: label, action: "connect", hint: hint})
44		items = append(items, telegramItem{label: "status", action: "status", hint: "disconnected"})
45	}
46	return items
47}
48func (i *Interactive) doTelegram(action string) {
49	switch action {
50	case "connect":
51		i.telegramConnect()
52	case "disconnect":
53		i.telegramDisconnect()
54	case "status":
55		i.telegramStatus()
56	default:
57		i.mu.Lock()
58		i.statusErr = "unknown telegram action: " + action + " (use connect, disconnect, or status)"
59		i.mu.Unlock()
60		i.invalidate()
61	}
62}
63func (i *Interactive) telegramConnect() {
64	if i.telegramBridge != nil && i.telegramBridge.Active() {
65		i.mu.Lock()
66		i.statusOK = "telegram already connected"
67		i.statusErr = ""
68		i.mu.Unlock()
69		i.invalidate()
70		return
71	}
72	cfg, err := telegram.LoadConfig(i.cfg.ZutHome)
73	if err != nil {
74		i.mu.Lock()
75		i.statusErr = "telegram: " + err.Error()
76		i.mu.Unlock()
77		i.invalidate()
78		return
79	}
80	if cfg.BotToken == "" {
81		i.mu.Lock()
82		i.statusErr = "telegram: no bot token configured. run `zut telegram-bot setup` first."
83		i.mu.Unlock()
84		i.invalidate()
85		return
86	}
87	// Refuse to start when a background daemon is already polling
88	// the same bot. Two concurrent long-poll consumers race each
89	// update and one always loses, so DMs get half-delivered. The
90	// user can `zut telegram-bot stop` first, then /telegram connect.
91	if pid, alive, _ := telegram.IsRunning(i.cfg.ZutHome); alive && pid > 0 {
92		i.mu.Lock()
93		i.statusErr = fmt.Sprintf("telegram: bot daemon already running (pid %d). stop it with `zut telegram-bot stop` first.", pid)
94		i.mu.Unlock()
95		i.invalidate()
96		return
97	}
98	bridge := &telegram.Bridge{
99		Client: telegram.NewClient(cfg.BotToken),
100		Config: cfg,
101		Save: func(next telegram.Config) error {
102			return telegram.SaveConfig(i.cfg.ZutHome, next)
103		},
104		Host: &telegramHost{iv: i},
105	}
106	i.mu.Lock()
107	i.telegramBridge = bridge
108	i.mu.Unlock()
109	// Strip web_search before the bridge's startup handshake can accept a
110	// Telegram update. ApplyAgentPromptConfig also keeps it stripped from
111	// concurrent refreshes while this bridge pointer is attached.
112	i.applyTelegramTools(true)
113	if err := bridge.Start(i.runCtx); err != nil {
114		i.mu.Lock()
115		i.telegramBridge = nil
116		i.mu.Unlock()
117		refreshErr := i.refreshToolsAfterTelegram()
118		i.mu.Lock()
119		i.statusErr = "telegram connect failed: " + err.Error()
120		if refreshErr != nil {
121			i.statusErr += "; tool refresh: " + refreshErr.Error()
122		}
123		i.mu.Unlock()
124		i.invalidate()
125		return
126	}
127	state := bridge.State()
128	label := "telegram connected"
129	if state.Username != "" {
130		label += " as @" + state.Username
131	}
132	if state.PairedID == 0 {
133		label += " — send /start to the bot from your phone to claim it"
134	}
135	i.mu.Lock()
136	i.statusOK = label
137	i.statusErr = ""
138	i.mu.Unlock()
139	i.invalidate()
140}
141func (i *Interactive) telegramDisconnect() {
142	if i.telegramBridge == nil || !i.telegramBridge.Active() {
143		i.mu.Lock()
144		i.statusOK = "telegram already disconnected"
145		i.statusErr = ""
146		i.mu.Unlock()
147		i.invalidate()
148		return
149	}
150	bridge := i.telegramBridge
151	bridge.Stop()
152	i.mu.Lock()
153	// Clear the pointer before rebuilding the normal registry. This both
154	// allows web_search back into the refresh and marks the bridge inactive
155	// for any concurrent prompt-config commit.
156	i.telegramBridge = nil
157	i.mu.Unlock()
158	refreshErr := i.refreshToolsAfterTelegram()
159	i.mu.Lock()
160	if refreshErr != nil {
161		i.statusOK = ""
162		i.statusErr = "telegram disconnect tool refresh: " + refreshErr.Error()
163	} else {
164		i.statusOK = "telegram disconnected"
165		i.statusErr = ""
166	}
167	i.mu.Unlock()
168	i.invalidate()
169}
170func (i *Interactive) refreshToolsAfterTelegram() error {
171	if i.cfg.RefreshTools == nil {
172		i.stripWebSearchTool()
173		i.applyTelegramTools(false)
174		return errors.New("live tool refresh is unavailable")
175	}
176	if err := i.cfg.RefreshTools(); err != nil {
177		i.stripWebSearchTool()
178		i.applyTelegramTools(false)
179		return err
180	}
181	i.applyTelegramTools(false)
182	return nil
183}
184func (a telegramSenderAdapter) SendImage(ctx context.Context, path, caption string) error {
185	if a.bridge == nil {
186		return fmt.Errorf("telegram bridge is not connected")
187	}
188	return a.bridge.SendImage(ctx, path, caption)
189}
190func (a telegramSenderAdapter) SendDocument(ctx context.Context, path, caption string) error {
191	if a.bridge == nil {
192		return fmt.Errorf("telegram bridge is not connected")
193	}
194	return a.bridge.SendDocument(ctx, path, caption)
195}
196func (a telegramSenderAdapter) Active() bool {
197	return a.bridge != nil && a.bridge.Active()
198}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close