Refine Telegram thread commands
This commit is contained in:
@@ -33,6 +33,10 @@ const (
|
||||
telegramDirectiveEnd = " -->"
|
||||
telegramCaptionLimit = 1024
|
||||
pictureMediaGroupLimit = 10
|
||||
threadActionResume = "resume"
|
||||
threadActionArchive = "archive"
|
||||
threadActionUnarchive = "unarchive"
|
||||
threadActionDelete = "delete"
|
||||
)
|
||||
|
||||
type Bot struct {
|
||||
@@ -50,7 +54,6 @@ type Bot struct {
|
||||
|
||||
mu sync.Mutex
|
||||
outputs map[string]*outputState
|
||||
diffs map[string]string
|
||||
}
|
||||
|
||||
type assistantMessageSegment struct {
|
||||
@@ -150,7 +153,6 @@ func NewBot(tg *Client, st *store.Store, codex *codexapp.Client, uploadDir, code
|
||||
defaultSandbox: defaultSandbox,
|
||||
pollTimeout: pollTimeout,
|
||||
outputs: make(map[string]*outputState),
|
||||
diffs: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,19 +229,20 @@ func (b *Bot) clearStaleActiveTurn(ctx context.Context, userID int64, thread sto
|
||||
|
||||
func botCommands() []BotCommand {
|
||||
return []BotCommand{
|
||||
{Command: "start", Description: "Show help"},
|
||||
{Command: "new", Description: "Start a new thread"},
|
||||
{Command: "thread", Description: "List or switch threads"},
|
||||
{Command: "resume", Description: "List or switch threads"},
|
||||
{Command: "rename", Description: "Rename a thread"},
|
||||
{Command: "fork", Description: "Fork the active thread"},
|
||||
{Command: "archive", Description: "Archive a thread"},
|
||||
{Command: "unarchive", Description: "Restore an archived thread"},
|
||||
{Command: "delete", Description: "Delete a thread"},
|
||||
{Command: "status", Description: "Show active settings"},
|
||||
{Command: "cancel", Description: "Interrupt the active turn"},
|
||||
{Command: "workspace", Description: "Select workspace"},
|
||||
{Command: "model", Description: "Choose model"},
|
||||
{Command: "sandbox", Description: "Choose sandbox"},
|
||||
{Command: "pic", Description: "Generate images"},
|
||||
{Command: "diff", Description: "Show latest diff"},
|
||||
{Command: "help", Description: "Show help"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,27 +300,27 @@ func (b *Bot) handleCommand(ctx context.Context, message *Message, session store
|
||||
chatID := message.Chat.ID
|
||||
|
||||
switch command {
|
||||
case "start", "help":
|
||||
return true, b.sendHelp(ctx, chatID)
|
||||
case "start":
|
||||
return true, b.sendStart(ctx, chatID)
|
||||
case "new":
|
||||
_, _, err := b.createNewThread(ctx, userID, chatID, session, true)
|
||||
return true, err
|
||||
case "thread":
|
||||
return true, b.threadCommand(ctx, userID, chatID, args)
|
||||
case "threads", "resume":
|
||||
return true, b.legacyThreadCommand(ctx, userID, chatID, args)
|
||||
case "resume":
|
||||
return true, b.resumeCommand(ctx, userID, chatID, args)
|
||||
case "rename":
|
||||
return true, b.renameThread(ctx, userID, chatID, session, args)
|
||||
case "fork":
|
||||
return true, b.forkThread(ctx, userID, chatID, session)
|
||||
case "archive":
|
||||
return true, b.archiveThread(ctx, userID, chatID, session, args)
|
||||
case "unarchive":
|
||||
return true, b.unarchiveThread(ctx, userID, chatID, session, args)
|
||||
case "delete":
|
||||
return true, b.deleteThread(ctx, userID, chatID, session, args)
|
||||
case "status":
|
||||
return true, b.sendStatus(ctx, userID, chatID, session)
|
||||
case "cancel":
|
||||
return true, b.cancelTurn(ctx, userID, chatID, session)
|
||||
case "workspaces":
|
||||
return true, b.sendWorkspaces(ctx, userID, chatID)
|
||||
case "workspace":
|
||||
return true, b.handleWorkspaceCommand(ctx, userID, chatID, session, args)
|
||||
case "model":
|
||||
@@ -326,44 +329,42 @@ func (b *Bot) handleCommand(ctx context.Context, message *Message, session store
|
||||
return true, b.handleSandboxCommand(ctx, userID, chatID, session, args)
|
||||
case "pic":
|
||||
return true, b.handlePictureCommand(ctx, userID, chatID, session, args)
|
||||
case "diff":
|
||||
return true, b.sendDiff(ctx, chatID, session)
|
||||
default:
|
||||
_, err := b.tg.SendMessage(ctx, chatID, "Unknown command. Use /help.", SendMessageOptions{})
|
||||
_, err := b.tg.SendMessage(ctx, chatID, "Unknown command.", SendMessageOptions{})
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) sendHelp(ctx context.Context, chatID int64) error {
|
||||
func (b *Bot) sendStart(ctx context.Context, chatID int64) error {
|
||||
text := strings.Join([]string{
|
||||
"Codex Telegram Bot",
|
||||
"",
|
||||
"/new - start a new Codex thread",
|
||||
"/thread - list recent threads",
|
||||
"/thread ID - switch to a thread",
|
||||
"/resume - list recent threads",
|
||||
"/resume ID - switch to a thread",
|
||||
"/rename TITLE or /rename ID TITLE - rename a thread",
|
||||
"/fork - fork the active thread",
|
||||
"/archive [ID] - archive a thread",
|
||||
"/archive [ID] - choose or archive a thread",
|
||||
"/unarchive [ID] - choose or restore an archived thread",
|
||||
"/delete [ID] - choose or delete a thread",
|
||||
"/status - show active settings",
|
||||
"/cancel - interrupt the active turn",
|
||||
"/workspaces - list workspaces",
|
||||
"/workspace [ID] - select workspace",
|
||||
"/model - choose model and reasoning effort",
|
||||
"/sandbox - choose sandbox",
|
||||
"/pic PROMPT - generate image(s) from a prompt",
|
||||
"/diff - show the latest streamed diff",
|
||||
"",
|
||||
"Plain text continues the active thread. Images are staged as local Codex image inputs; other files are staged and sent as paths.",
|
||||
}, "\n")
|
||||
return b.sendLong(ctx, chatID, text)
|
||||
}
|
||||
|
||||
func (b *Bot) threadCommand(ctx context.Context, userID, chatID int64, args []string) error {
|
||||
func (b *Bot) resumeCommand(ctx context.Context, userID, chatID int64, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return b.sendResumeChoices(ctx, userID, chatID, 0, 0)
|
||||
}
|
||||
if len(args) != 1 {
|
||||
_, err := b.tg.SendMessage(ctx, chatID, "Use /thread to choose a thread, or /thread ID to switch directly.", SendMessageOptions{})
|
||||
_, err := b.tg.SendMessage(ctx, chatID, "Use /resume to choose a thread, or /resume ID to switch directly.", SendMessageOptions{})
|
||||
return err
|
||||
}
|
||||
id, err := strconv.ParseInt(args[0], 10, 64)
|
||||
@@ -374,26 +375,29 @@ func (b *Bot) threadCommand(ctx context.Context, userID, chatID int64, args []st
|
||||
return b.resumeThreadByID(ctx, userID, chatID, id, 0)
|
||||
}
|
||||
|
||||
func (b *Bot) legacyThreadCommand(ctx context.Context, userID, chatID int64, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return b.sendResumeChoices(ctx, userID, chatID, 0, 0)
|
||||
}
|
||||
return b.threadCommand(ctx, userID, chatID, args)
|
||||
func (b *Bot) sendResumeChoices(ctx context.Context, userID, chatID int64, page int, messageID int) error {
|
||||
return b.sendThreadActionChoices(ctx, userID, chatID, threadActionResume, page, messageID)
|
||||
}
|
||||
|
||||
func (b *Bot) sendResumeChoices(ctx context.Context, userID, chatID int64, page int, messageID int) error {
|
||||
func (b *Bot) sendThreadActionChoices(ctx context.Context, userID, chatID int64, action string, page int, messageID int) error {
|
||||
if page < 0 {
|
||||
page = 0
|
||||
}
|
||||
threads, err := b.store.ListThreadsPage(ctx, userID, false, resumeThreadPageSize+1, page*resumeThreadPageSize)
|
||||
var threads []store.Thread
|
||||
var err error
|
||||
if action == threadActionUnarchive {
|
||||
threads, err = b.store.ListArchivedThreadsPage(ctx, userID, resumeThreadPageSize+1, page*resumeThreadPageSize)
|
||||
} else {
|
||||
threads, err = b.store.ListThreadsPage(ctx, userID, false, resumeThreadPageSize+1, page*resumeThreadPageSize)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(threads) == 0 && page > 0 {
|
||||
return b.sendResumeChoices(ctx, userID, chatID, page-1, messageID)
|
||||
return b.sendThreadActionChoices(ctx, userID, chatID, action, page-1, messageID)
|
||||
}
|
||||
if len(threads) == 0 {
|
||||
text := "No threads yet. Use /new."
|
||||
text := noThreadActionChoicesText(action)
|
||||
if messageID != 0 {
|
||||
_, err := b.tg.EditMessageText(ctx, chatID, messageID, text, EditMessageTextOptions{})
|
||||
return err
|
||||
@@ -406,8 +410,8 @@ func (b *Bot) sendResumeChoices(ctx context.Context, userID, chatID int64, page
|
||||
if hasNext {
|
||||
threads = threads[:resumeThreadPageSize]
|
||||
}
|
||||
text := resumeThreadListText(threads, page)
|
||||
markup := resumeThreadMarkup(threads, page, hasNext)
|
||||
text := threadActionListText(threads, page, action)
|
||||
markup := threadActionMarkup(threads, page, hasNext, action)
|
||||
if messageID != 0 {
|
||||
_, err := b.tg.EditMessageText(ctx, chatID, messageID, EscapeHTML(text), EditMessageTextOptions{ParseMode: "HTML", ReplyMarkup: editReplyMarkup(markup)})
|
||||
return err
|
||||
@@ -537,28 +541,149 @@ func (b *Bot) forkThread(ctx context.Context, userID, chatID int64, session stor
|
||||
}
|
||||
|
||||
func (b *Bot) archiveThread(ctx context.Context, userID, chatID int64, session store.Session, args []string) error {
|
||||
var thread store.Thread
|
||||
var err error
|
||||
if len(args) > 0 {
|
||||
id, parseErr := strconv.ParseInt(args[0], 10, 64)
|
||||
if parseErr != nil {
|
||||
_, sendErr := b.tg.SendMessage(ctx, chatID, "Thread ID must be a number.", SendMessageOptions{})
|
||||
return sendErr
|
||||
}
|
||||
thread, err = b.store.GetThreadByID(ctx, userID, id)
|
||||
} else {
|
||||
thread, err = b.activeThread(ctx, userID, session)
|
||||
_ = session
|
||||
if len(args) == 0 {
|
||||
return b.sendThreadActionChoices(ctx, userID, chatID, threadActionArchive, 0, 0)
|
||||
}
|
||||
id, handled, err := b.threadIDFromArgs(ctx, chatID, args, "Use /archive to choose a thread, or /archive ID.")
|
||||
if handled {
|
||||
return err
|
||||
}
|
||||
return b.archiveThreadByID(ctx, userID, chatID, id, 0)
|
||||
}
|
||||
|
||||
func (b *Bot) archiveThreadByID(ctx context.Context, userID, chatID int64, id int64, messageID int) error {
|
||||
thread, err := b.store.GetThreadByID(ctx, userID, id)
|
||||
if err != nil {
|
||||
return b.sendNoActiveThread(ctx, chatID, err)
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
return b.sendThreadActionNotFound(ctx, chatID, messageID)
|
||||
}
|
||||
if err := b.codex.ArchiveThread(ctx, thread.CodexThreadID); err != nil {
|
||||
return b.sendError(ctx, chatID, "Could not archive Codex thread", err)
|
||||
if !isMissingCodexThreadError(err) {
|
||||
return b.sendError(ctx, chatID, "Could not archive Codex thread", err)
|
||||
}
|
||||
b.logger.Printf("archive stale local thread #%d codex_thread_id=%s: %v", thread.ID, thread.CodexThreadID, err)
|
||||
}
|
||||
if err := b.store.ArchiveThread(ctx, userID, thread.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = b.tg.SendMessage(ctx, chatID, fmt.Sprintf("Archived thread #%d.", thread.ID), SendMessageOptions{})
|
||||
text := fmt.Sprintf("Archived thread #%d.", thread.ID)
|
||||
if messageID != 0 {
|
||||
_, err = b.tg.EditMessageText(ctx, chatID, messageID, text, EditMessageTextOptions{ReplyMarkup: clearInlineKeyboardMarkup()})
|
||||
return err
|
||||
}
|
||||
_, err = b.tg.SendMessage(ctx, chatID, text, SendMessageOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *Bot) unarchiveThread(ctx context.Context, userID, chatID int64, session store.Session, args []string) error {
|
||||
_ = session
|
||||
if len(args) == 0 {
|
||||
return b.sendThreadActionChoices(ctx, userID, chatID, threadActionUnarchive, 0, 0)
|
||||
}
|
||||
id, handled, err := b.threadIDFromArgs(ctx, chatID, args, "Use /unarchive to choose a thread, or /unarchive ID.")
|
||||
if handled {
|
||||
return err
|
||||
}
|
||||
return b.unarchiveThreadByID(ctx, userID, chatID, id, 0)
|
||||
}
|
||||
|
||||
func (b *Bot) unarchiveThreadByID(ctx context.Context, userID, chatID int64, id int64, messageID int) error {
|
||||
thread, err := b.store.GetThreadByID(ctx, userID, id)
|
||||
if err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
return b.sendThreadActionNotFound(ctx, chatID, messageID)
|
||||
}
|
||||
if err := b.codex.UnarchiveThread(ctx, thread.CodexThreadID); err != nil {
|
||||
return b.sendError(ctx, chatID, "Could not unarchive Codex thread", err)
|
||||
}
|
||||
if err := b.store.UnarchiveThread(ctx, userID, thread.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
text := fmt.Sprintf("Restored thread #%d.", thread.ID)
|
||||
if messageID != 0 {
|
||||
_, err = b.tg.EditMessageText(ctx, chatID, messageID, text, EditMessageTextOptions{ReplyMarkup: clearInlineKeyboardMarkup()})
|
||||
return err
|
||||
}
|
||||
_, err = b.tg.SendMessage(ctx, chatID, text, SendMessageOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *Bot) deleteThread(ctx context.Context, userID, chatID int64, session store.Session, args []string) error {
|
||||
_ = session
|
||||
if len(args) == 0 {
|
||||
return b.sendThreadActionChoices(ctx, userID, chatID, threadActionDelete, 0, 0)
|
||||
}
|
||||
id, handled, err := b.threadIDFromArgs(ctx, chatID, args, "Use /delete to choose a thread, or /delete ID.")
|
||||
if handled {
|
||||
return err
|
||||
}
|
||||
return b.deleteThreadByID(ctx, userID, chatID, id, 0)
|
||||
}
|
||||
|
||||
func (b *Bot) deleteThreadByID(ctx context.Context, userID, chatID int64, id int64, messageID int) error {
|
||||
thread, err := b.store.GetThreadByID(ctx, userID, id)
|
||||
if err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
return b.sendThreadActionNotFound(ctx, chatID, messageID)
|
||||
}
|
||||
if err := b.codex.DeleteThread(ctx, thread.CodexThreadID); err != nil {
|
||||
if !isMissingCodexThreadError(err) {
|
||||
return b.sendError(ctx, chatID, "Could not delete Codex thread", err)
|
||||
}
|
||||
b.logger.Printf("delete stale local thread #%d codex_thread_id=%s: %v", thread.ID, thread.CodexThreadID, err)
|
||||
}
|
||||
b.clearOutput(thread.CodexThreadID)
|
||||
if err := b.store.DeleteThread(ctx, userID, thread.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
text := fmt.Sprintf("Deleted thread #%d.", thread.ID)
|
||||
if messageID != 0 {
|
||||
_, err = b.tg.EditMessageText(ctx, chatID, messageID, text, EditMessageTextOptions{ReplyMarkup: clearInlineKeyboardMarkup()})
|
||||
return err
|
||||
}
|
||||
_, err = b.tg.SendMessage(ctx, chatID, text, SendMessageOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func isMissingCodexThreadError(err error) bool {
|
||||
var rpcErr codexapp.RPCError
|
||||
if !errors.As(err, &rpcErr) {
|
||||
return false
|
||||
}
|
||||
if rpcErr.Code != -32600 {
|
||||
return false
|
||||
}
|
||||
message := strings.ToLower(rpcErr.Message)
|
||||
return strings.Contains(message, "no rollout found") || strings.Contains(message, "thread not loaded")
|
||||
}
|
||||
|
||||
func (b *Bot) threadIDFromArgs(ctx context.Context, chatID int64, args []string, usage string) (int64, bool, error) {
|
||||
if len(args) != 1 {
|
||||
_, err := b.tg.SendMessage(ctx, chatID, usage, SendMessageOptions{})
|
||||
return 0, true, err
|
||||
}
|
||||
id, err := strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
_, sendErr := b.tg.SendMessage(ctx, chatID, "Thread ID must be a number.", SendMessageOptions{})
|
||||
return 0, true, sendErr
|
||||
}
|
||||
return id, false, nil
|
||||
}
|
||||
|
||||
func (b *Bot) sendThreadActionNotFound(ctx context.Context, chatID int64, messageID int) error {
|
||||
text := "Thread not found."
|
||||
if messageID != 0 {
|
||||
_, err := b.tg.EditMessageText(ctx, chatID, messageID, text, EditMessageTextOptions{ReplyMarkup: clearInlineKeyboardMarkup()})
|
||||
return err
|
||||
}
|
||||
_, err := b.tg.SendMessage(ctx, chatID, text, SendMessageOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -842,26 +967,6 @@ func isPicturePath(path string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) sendDiff(ctx context.Context, chatID int64, session store.Session) error {
|
||||
if session.ActiveThreadID == 0 {
|
||||
_, err := b.tg.SendMessage(ctx, chatID, "No active thread.", SendMessageOptions{})
|
||||
return err
|
||||
}
|
||||
thread, err := b.store.GetThreadByID(ctx, chatID, session.ActiveThreadID)
|
||||
if err != nil {
|
||||
_, sendErr := b.tg.SendMessage(ctx, chatID, "No active thread.", SendMessageOptions{})
|
||||
return sendErr
|
||||
}
|
||||
b.mu.Lock()
|
||||
diff := b.diffs[thread.CodexThreadID]
|
||||
b.mu.Unlock()
|
||||
if diff == "" {
|
||||
_, err := b.tg.SendMessage(ctx, chatID, "No diff has been streamed for this thread.", SendMessageOptions{})
|
||||
return err
|
||||
}
|
||||
return b.sendLong(ctx, chatID, diff)
|
||||
}
|
||||
|
||||
func (b *Bot) continueThread(ctx context.Context, message *Message, session store.Session) error {
|
||||
userID := message.From.ID
|
||||
chatID := message.Chat.ID
|
||||
@@ -1221,6 +1326,15 @@ func (b *Bot) handleCallback(ctx context.Context, callback *CallbackQuery) error
|
||||
}
|
||||
return b.sendResumeChoices(ctx, callback.From.ID, callback.Message.Chat.ID, resumePage, callback.Message.MessageID)
|
||||
}
|
||||
if action, threadID, ok := ParseThreadActionCallbackData(callback.Data); ok {
|
||||
return b.handleThreadActionCallback(ctx, callback, action, threadID)
|
||||
}
|
||||
if action, page, ok := ParseThreadActionPageCallbackData(callback.Data); ok {
|
||||
if err := b.tg.AnswerCallbackQuery(ctx, callback.ID, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.sendThreadActionChoices(ctx, callback.From.ID, callback.Message.Chat.ID, action, page, callback.Message.MessageID)
|
||||
}
|
||||
if modelID, ok := ParseModelCallbackData(callback.Data); ok {
|
||||
return b.handleModelCallback(ctx, callback, modelID)
|
||||
}
|
||||
@@ -1236,6 +1350,33 @@ func (b *Bot) handleCallback(ctx context.Context, callback *CallbackQuery) error
|
||||
return b.tg.AnswerCallbackQuery(ctx, callback.ID, "Unknown action.")
|
||||
}
|
||||
|
||||
func (b *Bot) handleThreadActionCallback(ctx context.Context, callback *CallbackQuery, action string, threadID int64) error {
|
||||
switch action {
|
||||
case threadActionResume:
|
||||
if err := b.tg.AnswerCallbackQuery(ctx, callback.ID, "Thread selected."); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.resumeThreadByID(ctx, callback.From.ID, callback.Message.Chat.ID, threadID, callback.Message.MessageID)
|
||||
case threadActionArchive:
|
||||
if err := b.tg.AnswerCallbackQuery(ctx, callback.ID, "Archiving thread."); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.archiveThreadByID(ctx, callback.From.ID, callback.Message.Chat.ID, threadID, callback.Message.MessageID)
|
||||
case threadActionUnarchive:
|
||||
if err := b.tg.AnswerCallbackQuery(ctx, callback.ID, "Restoring thread."); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.unarchiveThreadByID(ctx, callback.From.ID, callback.Message.Chat.ID, threadID, callback.Message.MessageID)
|
||||
case threadActionDelete:
|
||||
if err := b.tg.AnswerCallbackQuery(ctx, callback.ID, "Deleting thread."); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.deleteThreadByID(ctx, callback.From.ID, callback.Message.Chat.ID, threadID, callback.Message.MessageID)
|
||||
default:
|
||||
return b.tg.AnswerCallbackQuery(ctx, callback.ID, "Unknown thread action.")
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) handleApprovalCallback(ctx context.Context, callback *CallbackQuery, approvalID int64, decision string) error {
|
||||
approval, err := b.store.GetPendingApproval(ctx, callback.From.ID, approvalID)
|
||||
if err != nil {
|
||||
@@ -1550,6 +1691,8 @@ func argumentLabel(key string) string {
|
||||
return "CWD"
|
||||
case "cmd":
|
||||
return "cmd"
|
||||
case "environmentid":
|
||||
return "Environment ID"
|
||||
}
|
||||
label := strings.ReplaceAll(key, "_", " ")
|
||||
return strings.ToUpper(label[:1]) + label[1:]
|
||||
@@ -1804,20 +1947,6 @@ func (b *Bot) handleCodexNotification(ctx context.Context, event codexapp.Event)
|
||||
if params.ThreadID != "" && b.hasOutputThread(params.ThreadID) {
|
||||
return b.sendOutputBlock(ctx, params.ThreadID, "Codex warning: "+params.Message)
|
||||
}
|
||||
case "turn/diff/updated":
|
||||
var params struct {
|
||||
ThreadID string `json:"threadId"`
|
||||
TurnID string `json:"turnId"`
|
||||
Diff string `json:"diff"`
|
||||
}
|
||||
if err := json.Unmarshal(event.Params, ¶ms); err != nil {
|
||||
return err
|
||||
}
|
||||
if params.ThreadID != "" && b.shouldHandleOutputEvent(params.ThreadID, params.TurnID) {
|
||||
b.mu.Lock()
|
||||
b.diffs[params.ThreadID] = params.Diff
|
||||
b.mu.Unlock()
|
||||
}
|
||||
case "turn/completed":
|
||||
var params struct {
|
||||
ThreadID string `json:"threadId"`
|
||||
@@ -1854,6 +1983,17 @@ func (b *Bot) handleCodexNotification(ctx context.Context, event codexapp.Event)
|
||||
}
|
||||
return b.store.SyncThreadTitleByCodexID(ctx, params.ThreadID, title)
|
||||
}
|
||||
case "thread/deleted":
|
||||
var params struct {
|
||||
ThreadID string `json:"threadId"`
|
||||
}
|
||||
if err := json.Unmarshal(event.Params, ¶ms); err != nil {
|
||||
return err
|
||||
}
|
||||
if params.ThreadID != "" {
|
||||
b.clearOutput(params.ThreadID)
|
||||
return b.store.DeleteThreadByCodexID(ctx, params.ThreadID)
|
||||
}
|
||||
case "thread/settings/updated":
|
||||
var params struct {
|
||||
ThreadID string `json:"threadId"`
|
||||
@@ -3090,20 +3230,28 @@ func parseCommand(text string) (string, []string, bool) {
|
||||
}
|
||||
|
||||
func resumeThreadListText(threads []store.Thread, page int) string {
|
||||
lines := []string{fmt.Sprintf("Threads (page %d):", page+1), ""}
|
||||
return threadActionListText(threads, page, threadActionResume)
|
||||
}
|
||||
|
||||
func threadActionListText(threads []store.Thread, page int, action string) string {
|
||||
lines := []string{fmt.Sprintf("%s (page %d):", threadActionListTitle(action), page+1), ""}
|
||||
for _, thread := range threads {
|
||||
lines = append(lines, fmt.Sprintf("Thread ID %d: %s", thread.ID, threadDisplayTitle(thread)))
|
||||
}
|
||||
lines = append(lines, "", "Choose a button below, or use /thread THREAD_ID directly.")
|
||||
lines = append(lines, "", threadActionListFooter(action))
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func resumeThreadMarkup(threads []store.Thread, page int, hasNext bool) *InlineKeyboardMarkup {
|
||||
return threadActionMarkup(threads, page, hasNext, threadActionResume)
|
||||
}
|
||||
|
||||
func threadActionMarkup(threads []store.Thread, page int, hasNext bool, action string) *InlineKeyboardMarkup {
|
||||
keyboard := make([][]InlineKeyboardButton, 0, 4)
|
||||
for _, thread := range threads {
|
||||
button := InlineKeyboardButton{
|
||||
Text: fmt.Sprintf("ID %d", thread.ID),
|
||||
CallbackData: ResumeThreadCallbackData(thread.ID),
|
||||
Text: threadActionButtonLabel(action, thread.ID),
|
||||
CallbackData: threadActionButtonCallback(action, thread.ID),
|
||||
}
|
||||
if len(keyboard) == 0 || len(keyboard[len(keyboard)-1]) >= 4 {
|
||||
keyboard = append(keyboard, []InlineKeyboardButton{button})
|
||||
@@ -3113,10 +3261,10 @@ func resumeThreadMarkup(threads []store.Thread, page int, hasNext bool) *InlineK
|
||||
}
|
||||
var nav []InlineKeyboardButton
|
||||
if page > 0 {
|
||||
nav = append(nav, InlineKeyboardButton{Text: "Prev", CallbackData: ResumePageCallbackData(page - 1)})
|
||||
nav = append(nav, InlineKeyboardButton{Text: "Prev", CallbackData: threadActionPageCallback(action, page-1)})
|
||||
}
|
||||
if hasNext {
|
||||
nav = append(nav, InlineKeyboardButton{Text: "Next", CallbackData: ResumePageCallbackData(page + 1)})
|
||||
nav = append(nav, InlineKeyboardButton{Text: "Next", CallbackData: threadActionPageCallback(action, page+1)})
|
||||
}
|
||||
if len(nav) > 0 {
|
||||
keyboard = append(keyboard, nav)
|
||||
@@ -3124,6 +3272,72 @@ func resumeThreadMarkup(threads []store.Thread, page int, hasNext bool) *InlineK
|
||||
return &InlineKeyboardMarkup{InlineKeyboard: keyboard}
|
||||
}
|
||||
|
||||
func noThreadActionChoicesText(action string) string {
|
||||
switch action {
|
||||
case threadActionArchive:
|
||||
return "No threads to archive."
|
||||
case threadActionUnarchive:
|
||||
return "No archived threads to restore."
|
||||
case threadActionDelete:
|
||||
return "No threads to delete."
|
||||
default:
|
||||
return "No threads yet. Use /new."
|
||||
}
|
||||
}
|
||||
|
||||
func threadActionListTitle(action string) string {
|
||||
switch action {
|
||||
case threadActionArchive:
|
||||
return "Choose a thread to archive"
|
||||
case threadActionUnarchive:
|
||||
return "Choose an archived thread to restore"
|
||||
case threadActionDelete:
|
||||
return "Choose a thread to delete"
|
||||
default:
|
||||
return "Threads"
|
||||
}
|
||||
}
|
||||
|
||||
func threadActionListFooter(action string) string {
|
||||
switch action {
|
||||
case threadActionArchive:
|
||||
return "Choose a button below, or use /archive THREAD_ID directly."
|
||||
case threadActionUnarchive:
|
||||
return "Choose a button below, or use /unarchive THREAD_ID directly."
|
||||
case threadActionDelete:
|
||||
return "Choose a button below, or use /delete THREAD_ID directly."
|
||||
default:
|
||||
return "Choose a button below, or use /resume THREAD_ID directly."
|
||||
}
|
||||
}
|
||||
|
||||
func threadActionButtonLabel(action string, id int64) string {
|
||||
switch action {
|
||||
case threadActionArchive:
|
||||
return fmt.Sprintf("Archive %d", id)
|
||||
case threadActionUnarchive:
|
||||
return fmt.Sprintf("Restore %d", id)
|
||||
case threadActionDelete:
|
||||
return fmt.Sprintf("Delete %d", id)
|
||||
default:
|
||||
return fmt.Sprintf("ID %d", id)
|
||||
}
|
||||
}
|
||||
|
||||
func threadActionButtonCallback(action string, id int64) string {
|
||||
if action == threadActionResume {
|
||||
return ResumeThreadCallbackData(id)
|
||||
}
|
||||
return ThreadActionCallbackData(action, id)
|
||||
}
|
||||
|
||||
func threadActionPageCallback(action string, page int) string {
|
||||
if action == threadActionResume {
|
||||
return ResumePageCallbackData(page)
|
||||
}
|
||||
return ThreadActionPageCallbackData(action, page)
|
||||
}
|
||||
|
||||
func normalizeThreadTitle(title string) string {
|
||||
title = strings.Join(strings.Fields(title), " ")
|
||||
runes := []rune(title)
|
||||
@@ -3610,6 +3824,7 @@ func renderApprovalPayloadDetailsHTML(raw json.RawMessage, params map[string]any
|
||||
}
|
||||
|
||||
appendPart(renderApprovalFieldHTML("cwd", params["cwd"]))
|
||||
appendPart(renderApprovalFieldHTML("environmentId", params["environmentId"]))
|
||||
appendPart(renderApprovalFieldHTML("command", params["command"]))
|
||||
appendPart(renderApprovalFieldHTML("parsedCmd", params["parsedCmd"]))
|
||||
appendPart(renderApprovalFieldHTML("additionalPermissions", params["additionalPermissions"]))
|
||||
|
||||
Reference in New Issue
Block a user