From efebf2eba64c3bcacd4221f151ca47ea45a3a5e8 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Sat, 4 Apr 2026 16:33:08 +0300 Subject: [PATCH] fix(kit-telegram): add typing indicator and config fallback to global path - Send sendChatAction("typing") every 4s while agent is processing, started on AgentStart and stopped on AgentEnd/SessionShutdown - configPath() now checks project-local .kit/ first, then falls back to ~/.config/kit/kit-telegram.json for cross-project portability --- examples/extensions/kit-telegram/main.go | 75 +++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/examples/extensions/kit-telegram/main.go b/examples/extensions/kit-telegram/main.go index cdb5b404..1971f9b0 100644 --- a/examples/extensions/kit-telegram/main.go +++ b/examples/extensions/kit-telegram/main.go @@ -168,6 +168,10 @@ var ( // Test pendingTest *PendingTest + // Typing indicator + typingTicker *time.Ticker + typingStop chan struct{} + // Latest context for background goroutines latestCtx ext.Context latestCtxSet bool @@ -203,8 +207,23 @@ func configDir() string { return filepath.Join(home, ".config", "kit") } +func globalConfigDir() string { + home, _ := os.UserHomeDir() + return filepath.Join(home, ".config", "kit") +} + func configPath() string { - return filepath.Join(configDir(), "kit-telegram.json") + // Prefer project-local config, fall back to global config. + local := filepath.Join(configDir(), "kit-telegram.json") + if _, err := os.Stat(local); err == nil { + return local + } + global := filepath.Join(globalConfigDir(), "kit-telegram.json") + if _, err := os.Stat(global); err == nil { + return global + } + // Neither exists — return local path (will be created on connect). + return local } func failureLogDir() string { @@ -387,6 +406,14 @@ func tgEditMessageText(token string, chatID int64, messageID int, text string) ( return &msg, nil } +func tgSendChatAction(token string, chatID int64, action string) error { + _, err := telegramRequest(token, "sendChatAction", map[string]any{ + "chat_id": chatID, + "action": action, + }, 15) + return err +} + // ────────────────────────────────────────────── // Error classification // ────────────────────────────────────────────── @@ -637,6 +664,48 @@ func clearHealthTimer() { } } +// ────────────────────────────────────────────── +// Typing indicator +// ────────────────────────────────────────────── + +func startTypingLoop() { + mu.Lock() + defer mu.Unlock() + if typingTicker != nil { + return + } + cfg := config + if cfg == nil || !cfg.Enabled { + return + } + token := cfg.BotToken + chatID := cfg.ChatID + typingTicker = time.NewTicker(4 * time.Second) + typingStop = make(chan struct{}) + // Send immediately, then every 4 seconds. + go func() { + tgSendChatAction(token, chatID, "typing") + for { + select { + case <-typingTicker.C: + tgSendChatAction(token, chatID, "typing") + case <-typingStop: + return + } + } + }() +} + +func stopTypingLoop() { + mu.Lock() + defer mu.Unlock() + if typingTicker != nil { + typingTicker.Stop() + close(typingStop) + typingTicker = nil + } +} + // ────────────────────────────────────────────── // Polling lifecycle // ────────────────────────────────────────────── @@ -2105,6 +2174,7 @@ func Init(api ext.API) { mu.Unlock() sendShutdownDisconnectedMessage() + stopTypingLoop() stopPolling() clearHealthTimer() clearFooter() @@ -2128,6 +2198,7 @@ func Init(api ext.API) { mu.Unlock() report("run.start", fmt.Sprintf("runId=%d", run.ID)) + startTypingLoop() ensureProgressMessage() updateProgressMessage() }) @@ -2140,6 +2211,8 @@ func Init(api ext.API) { run := activeRun mu.Unlock() + stopTypingLoop() + if run != nil { // Capture final response from event if e.Response != "" {