Skip to content

Commit fa22f57

Browse files
committed
Updated attachment support for telegram
1 parent 58a6820 commit fa22f57

4 files changed

Lines changed: 263 additions & 24 deletions

File tree

cmd/llm/chat.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ func (h *chatHooks) OnSessionReset() {
216216
h.tui.ClearHistory()
217217
}
218218

219+
func (h *chatHooks) ResetMeta() *schema.SessionMeta {
220+
return nil
221+
}
222+
219223
// runInteractive launches the bubbletea TUI for an interactive chat session.
220224
func (cmd *ChatCommand) runInteractive(ctx context.Context, globals *Globals, client *httpclient.Client, sessionID string, baseOpts []httpclient.ChatOpt) error {
221225
tui, err := btui.New()
@@ -303,6 +307,18 @@ func (cmd *ChatCommand) handleCommand(ctx context.Context, evt ui.Event, cmdHand
303307
cmd.pendingOpts = append(cmd.pendingOpts, httpclient.WithChatURL(u))
304308
return evt.Context.SendText(ctx, fmt.Sprintf("Attached URL: %s", u))
305309

310+
case "help":
311+
// Show shared help first, then append terminal-only commands.
312+
if err := cmdHandler.Handle(ctx, evt, sessionID); err != nil {
313+
return err
314+
}
315+
extra := "Terminal-only commands:\n\n" +
316+
"```\n" +
317+
"/file <path> - Attach file(s) to next message\n" +
318+
"/url <url> - Attach a URL to next message\n" +
319+
"```"
320+
return evt.Context.SendText(ctx, extra)
321+
306322
default:
307323
// Delegate to shared command handler.
308324
return cmdHandler.Handle(ctx, evt, sessionID)

cmd/llm/telegram.go

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ type TelegramCommand struct {
3232

3333
// sessions caches conversation-ID → session-ID mappings so we don't
3434
// query the API on every message.
35-
mu sync.Mutex `kong:"-"`
36-
sessions map[string]string `kong:"-"`
35+
mu sync.Mutex `kong:"-"`
36+
sessions map[string]string `kong:"-"`
37+
pendingOpts map[string][]httpclient.ChatOpt `kong:"-"`
3738
}
3839

3940
///////////////////////////////////////////////////////////////////////////////
@@ -69,6 +70,20 @@ func (h *telegramHooks) OnSessionReset() {
6970
// Nothing extra needed — OnSessionChanged already updated the cache.
7071
}
7172

73+
func (h *telegramHooks) ResetMeta() *schema.SessionMeta {
74+
return &schema.SessionMeta{
75+
GeneratorMeta: schema.GeneratorMeta{
76+
SystemPrompt: h.cmd.SystemPrompt,
77+
Thinking: h.cmd.Thinking,
78+
ThinkingBudget: h.cmd.ThinkingBudget,
79+
},
80+
Name: h.uctx.UserName(),
81+
Labels: map[string]string{
82+
telegramChatLabel: h.conversationID,
83+
},
84+
}
85+
}
86+
7287
///////////////////////////////////////////////////////////////////////////////
7388
// COMMANDS
7489

@@ -100,6 +115,7 @@ func (cmd *TelegramCommand) Run(ctx *Globals) (err error) {
100115
defer bot.Close()
101116

102117
cmd.sessions = make(map[string]string)
118+
cmd.pendingOpts = make(map[string][]httpclient.ChatOpt)
103119

104120
ctx.logger.Print(parent, "Telegram bot started")
105121

@@ -123,20 +139,43 @@ func (cmd *TelegramCommand) Run(ctx *Globals) (err error) {
123139
if err := cmd.handleChat(parent, evt, client, sessionID); err != nil {
124140
evt.Context.SendText(parent, fmt.Sprintf("Error: %v", err))
125141
}
142+
case ui.EventAttachment:
143+
ctx.logger.Print(parent, fmt.Sprintf("Attachment received: %d file(s), caption=%q, conv=%s",
144+
len(evt.Attachments), evt.Text, evt.Context.ConversationID()))
145+
for i, att := range evt.Attachments {
146+
ctx.logger.Print(parent, fmt.Sprintf(" att[%d]: name=%q type=%q", i, att.Filename, att.Type))
147+
}
148+
sessionID, err := cmd.resolveSession(parent, ctx, client, evt.Context)
149+
if err != nil {
150+
evt.Context.SendText(parent, fmt.Sprintf("Error: %v", err))
151+
continue
152+
}
153+
// Queue attachments as pending file opts.
154+
convID := evt.Context.ConversationID()
155+
for _, att := range evt.Attachments {
156+
name := att.Filename
157+
if name == "" {
158+
name = "attachment"
159+
}
160+
cmd.mu.Lock()
161+
cmd.pendingOpts[convID] = append(cmd.pendingOpts[convID], httpclient.WithChatFile(name, att.Data))
162+
cmd.mu.Unlock()
163+
}
164+
// If the attachment has caption text, send it as a chat message immediately.
165+
if evt.Text != "" {
166+
if err := cmd.handleChat(parent, evt, client, sessionID); err != nil {
167+
evt.Context.SendText(parent, fmt.Sprintf("Error: %v", err))
168+
}
169+
} else {
170+
evt.Context.SendText(parent, fmt.Sprintf("Attached %d file(s). Send a message to use them.", len(evt.Attachments)))
171+
}
126172
case ui.EventCommand:
127173
sessionID, err := cmd.resolveSession(parent, ctx, client, evt.Context)
128174
if err != nil {
129175
evt.Context.SendText(parent, fmt.Sprintf("Error: %v", err))
130176
continue
131177
}
132-
cmdHandler := uicmd.New(client, &telegramHooks{
133-
cmd: cmd,
134-
globals: ctx,
135-
client: client,
136-
conversationID: evt.Context.ConversationID(),
137-
uctx: evt.Context,
138-
})
139-
if err := cmdHandler.Handle(parent, evt, &sessionID); err != nil {
178+
if err := cmd.handleTelegramCommand(parent, evt, client, ctx, &sessionID); err != nil {
140179
evt.Context.SendText(parent, fmt.Sprintf("Error: %v", err))
141180
}
142181
}
@@ -198,16 +237,51 @@ func (cmd *TelegramCommand) resolveSession(ctx context.Context, globals *Globals
198237
return session.ID, nil
199238
}
200239

240+
// handleTelegramCommand processes slash commands for Telegram. It handles
241+
// /url locally and delegates everything else to the shared command handler.
242+
func (cmd *TelegramCommand) handleTelegramCommand(ctx context.Context, evt ui.Event, client *httpclient.Client, globals *Globals, sessionID *string) error {
243+
switch evt.Command {
244+
case "url":
245+
if len(evt.Args) == 0 {
246+
return evt.Context.SendText(ctx, "Usage: /url <url>")
247+
}
248+
u := evt.Args[0]
249+
convID := evt.Context.ConversationID()
250+
cmd.mu.Lock()
251+
cmd.pendingOpts[convID] = append(cmd.pendingOpts[convID], httpclient.WithChatURL(u))
252+
cmd.mu.Unlock()
253+
return evt.Context.SendText(ctx, fmt.Sprintf("Attached URL: %s\nSend a message to use it.", u))
254+
default:
255+
cmdHandler := uicmd.New(client, &telegramHooks{
256+
cmd: cmd,
257+
globals: globals,
258+
client: client,
259+
conversationID: evt.Context.ConversationID(),
260+
uctx: evt.Context,
261+
})
262+
return cmdHandler.Handle(ctx, evt, sessionID)
263+
}
264+
}
265+
201266
func (cmd *TelegramCommand) handleChat(ctx context.Context, evt ui.Event, client *httpclient.Client, sessionID string) error {
202267
evt.Context.SetTyping(ctx, true)
203268
evt.Context.StreamStart(ctx)
204269

205-
opts := []httpclient.ChatOpt{
206-
httpclient.WithChatStream(func(role, text string) {
207-
evt.Context.StreamChunk(ctx, role, text)
208-
}),
270+
// Consume any pending attachments for this conversation.
271+
convID := evt.Context.ConversationID()
272+
cmd.mu.Lock()
273+
pending := cmd.pendingOpts[convID]
274+
delete(cmd.pendingOpts, convID)
275+
cmd.mu.Unlock()
276+
277+
if len(pending) > 0 {
278+
fmt.Printf("[telegram] handleChat: consuming %d pending opts for conv=%s\n", len(pending), convID)
209279
}
210280

281+
opts := append(pending, httpclient.WithChatStream(func(role, text string) {
282+
evt.Context.StreamChunk(ctx, role, text)
283+
}))
284+
211285
req := schema.ChatRequest{
212286
Session: sessionID,
213287
Text: evt.Text,

pkg/ui/command/command.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Client interface {
2222
GetSession(ctx context.Context, id string) (*schema.Session, error)
2323
CreateSession(ctx context.Context, meta schema.SessionMeta) (*schema.Session, error)
2424
UpdateSession(ctx context.Context, id string, meta schema.SessionMeta) (*schema.Session, error)
25+
DeleteSession(ctx context.Context, id string) error
2526
ListSessions(ctx context.Context, opts ...opt.Opt) (*schema.ListSessionResponse, error)
2627
ListModels(ctx context.Context, opts ...opt.Opt) (*schema.ListModelsResponse, error)
2728
ListTools(ctx context.Context, opts ...opt.Opt) (*schema.ListToolResponse, error)
@@ -38,6 +39,10 @@ type Hooks interface {
3839
// OnSessionReset is called after /reset creates a new session.
3940
// The frontend can clear its message history.
4041
OnSessionReset()
42+
43+
// ResetMeta returns extra session metadata to include when /reset
44+
// creates a new session (e.g. labels, name). Return nil for defaults.
45+
ResetMeta() *schema.SessionMeta
4146
}
4247

4348
// Handler processes slash commands against the API.
@@ -80,6 +85,8 @@ func (h *Handler) Handle(ctx context.Context, evt ui.Event, sessionID *string) e
8085
return h.cmdProviders(ctx, evt)
8186
case "label":
8287
return h.cmdLabel(ctx, evt, sessionID)
88+
case "delete":
89+
return h.cmdDelete(ctx, evt, sessionID)
8390
case "help":
8491
return h.cmdHelp(ctx, evt)
8592
default:
@@ -225,12 +232,29 @@ func (h *Handler) cmdReset(ctx context.Context, evt ui.Event, sessionID *string)
225232
provider = current.Provider
226233
model = current.Model
227234
}
228-
session, err := h.client.CreateSession(ctx, schema.SessionMeta{
235+
meta := schema.SessionMeta{
229236
GeneratorMeta: schema.GeneratorMeta{
230237
Provider: provider,
231238
Model: model,
232239
},
233-
})
240+
}
241+
// Merge frontend-specific metadata (e.g. Telegram labels/name).
242+
if h.hooks != nil {
243+
if extra := h.hooks.ResetMeta(); extra != nil {
244+
meta.Name = extra.Name
245+
meta.Labels = extra.Labels
246+
if extra.SystemPrompt != "" {
247+
meta.SystemPrompt = extra.SystemPrompt
248+
}
249+
if extra.Thinking != nil {
250+
meta.Thinking = extra.Thinking
251+
}
252+
if extra.ThinkingBudget > 0 {
253+
meta.ThinkingBudget = extra.ThinkingBudget
254+
}
255+
}
256+
}
257+
session, err := h.client.CreateSession(ctx, meta)
234258
if err != nil {
235259
return err
236260
}
@@ -404,6 +428,20 @@ func (h *Handler) cmdLabel(ctx context.Context, evt ui.Event, sessionID *string)
404428
return evt.Context.SendText(ctx, fmt.Sprintf("Labels updated: %s", strings.Join(parts, ", ")))
405429
}
406430

431+
func (h *Handler) cmdDelete(ctx context.Context, evt ui.Event, sessionID *string) error {
432+
if len(evt.Args) == 0 {
433+
return fmt.Errorf("usage: /delete <session-id>")
434+
}
435+
target := evt.Args[0]
436+
if target == *sessionID {
437+
return fmt.Errorf("cannot delete the current session (use /reset first)")
438+
}
439+
if err := h.client.DeleteSession(ctx, target); err != nil {
440+
return err
441+
}
442+
return evt.Context.SendText(ctx, fmt.Sprintf("Deleted session %s", target))
443+
}
444+
407445
func (h *Handler) cmdHelp(ctx context.Context, evt ui.Event) error {
408446
help := "Available commands:\n\n" +
409447
"```\n" +
@@ -413,6 +451,7 @@ func (h *Handler) cmdHelp(ctx context.Context, evt ui.Event) error {
413451
"/session [id] - Show or switch the current session\n" +
414452
"/sessions - List all sessions\n" +
415453
"/reset [provider/model] - Start a new session\n" +
454+
"/delete <session-id> - Delete a session\n" +
416455
"/name [name] - Show or set the session name\n" +
417456
"/system [prompt] - Show or set the system prompt\n" +
418457
"/thinking [on|off] - Show or toggle thinking mode\n" +

0 commit comments

Comments
 (0)