feat(mail): add message types and threading support
- Add MessageType enum (task, scavenge, notification, reply) - Expand Priority from 2 to 4 levels (low, normal, high, urgent) - Add ThreadID and ReplyTo fields to Message struct - Add --type and --reply-to flags to 'gt mail send' - Add 'gt mail thread <id>' command to view conversation threads - Update inbox/read display to show type and threading info - Auto-generate thread IDs for new messages - Reply messages inherit thread from original Closes: gt-hgk 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -18,12 +20,15 @@ var (
|
|||||||
mailSubject string
|
mailSubject string
|
||||||
mailBody string
|
mailBody string
|
||||||
mailPriority string
|
mailPriority string
|
||||||
|
mailType string
|
||||||
|
mailReplyTo string
|
||||||
mailNotify bool
|
mailNotify bool
|
||||||
mailInboxJSON bool
|
mailInboxJSON bool
|
||||||
mailReadJSON bool
|
mailReadJSON bool
|
||||||
mailInboxUnread bool
|
mailInboxUnread bool
|
||||||
mailCheckInject bool
|
mailCheckInject bool
|
||||||
mailCheckJSON bool
|
mailCheckJSON bool
|
||||||
|
mailThreadJSON bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var mailCmd = &cobra.Command{
|
var mailCmd = &cobra.Command{
|
||||||
@@ -46,10 +51,21 @@ Addresses:
|
|||||||
<rig>/<polecat> - Send to a specific polecat
|
<rig>/<polecat> - Send to a specific polecat
|
||||||
<rig>/ - Broadcast to a rig
|
<rig>/ - Broadcast to a rig
|
||||||
|
|
||||||
|
Message types:
|
||||||
|
task - Required processing
|
||||||
|
scavenge - Optional first-come work
|
||||||
|
notification - Informational (default)
|
||||||
|
reply - Response to message
|
||||||
|
|
||||||
|
Priority levels:
|
||||||
|
low, normal (default), high, urgent
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
gt mail send gastown/Toast -s "Status check" -m "How's that bug fix going?"
|
gt mail send gastown/Toast -s "Status check" -m "How's that bug fix going?"
|
||||||
gt mail send mayor/ -s "Work complete" -m "Finished gt-abc"
|
gt mail send mayor/ -s "Work complete" -m "Finished gt-abc"
|
||||||
gt mail send gastown/ -s "All hands" -m "Swarm starting" --notify`,
|
gt mail send gastown/ -s "All hands" -m "Swarm starting" --notify
|
||||||
|
gt mail send gastown/Toast -s "Task" -m "Fix bug" --type task --priority high
|
||||||
|
gt mail send mayor/ -s "Re: Status" -m "Done" --reply-to msg-abc123`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: runMailSend,
|
RunE: runMailSend,
|
||||||
}
|
}
|
||||||
@@ -108,11 +124,26 @@ Examples:
|
|||||||
RunE: runMailCheck,
|
RunE: runMailCheck,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mailThreadCmd = &cobra.Command{
|
||||||
|
Use: "thread <thread-id>",
|
||||||
|
Short: "View a message thread",
|
||||||
|
Long: `View all messages in a conversation thread.
|
||||||
|
|
||||||
|
Shows messages in chronological order (oldest first).
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt mail thread thread-abc123`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runMailThread,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Send flags
|
// Send flags
|
||||||
mailSendCmd.Flags().StringVarP(&mailSubject, "subject", "s", "", "Message subject (required)")
|
mailSendCmd.Flags().StringVarP(&mailSubject, "subject", "s", "", "Message subject (required)")
|
||||||
mailSendCmd.Flags().StringVarP(&mailBody, "message", "m", "", "Message body")
|
mailSendCmd.Flags().StringVarP(&mailBody, "message", "m", "", "Message body")
|
||||||
mailSendCmd.Flags().StringVar(&mailPriority, "priority", "normal", "Message priority (normal, high)")
|
mailSendCmd.Flags().StringVar(&mailPriority, "priority", "normal", "Message priority (low, normal, high, urgent)")
|
||||||
|
mailSendCmd.Flags().StringVar(&mailType, "type", "notification", "Message type (task, scavenge, notification, reply)")
|
||||||
|
mailSendCmd.Flags().StringVar(&mailReplyTo, "reply-to", "", "Message ID this is replying to")
|
||||||
mailSendCmd.Flags().BoolVarP(&mailNotify, "notify", "n", false, "Send tmux notification to recipient")
|
mailSendCmd.Flags().BoolVarP(&mailNotify, "notify", "n", false, "Send tmux notification to recipient")
|
||||||
mailSendCmd.MarkFlagRequired("subject")
|
mailSendCmd.MarkFlagRequired("subject")
|
||||||
|
|
||||||
@@ -127,12 +158,16 @@ func init() {
|
|||||||
mailCheckCmd.Flags().BoolVar(&mailCheckInject, "inject", false, "Output format for Claude Code hooks")
|
mailCheckCmd.Flags().BoolVar(&mailCheckInject, "inject", false, "Output format for Claude Code hooks")
|
||||||
mailCheckCmd.Flags().BoolVar(&mailCheckJSON, "json", false, "Output as JSON")
|
mailCheckCmd.Flags().BoolVar(&mailCheckJSON, "json", false, "Output as JSON")
|
||||||
|
|
||||||
|
// Thread flags
|
||||||
|
mailThreadCmd.Flags().BoolVar(&mailThreadJSON, "json", false, "Output as JSON")
|
||||||
|
|
||||||
// Add subcommands
|
// Add subcommands
|
||||||
mailCmd.AddCommand(mailSendCmd)
|
mailCmd.AddCommand(mailSendCmd)
|
||||||
mailCmd.AddCommand(mailInboxCmd)
|
mailCmd.AddCommand(mailInboxCmd)
|
||||||
mailCmd.AddCommand(mailReadCmd)
|
mailCmd.AddCommand(mailReadCmd)
|
||||||
mailCmd.AddCommand(mailDeleteCmd)
|
mailCmd.AddCommand(mailDeleteCmd)
|
||||||
mailCmd.AddCommand(mailCheckCmd)
|
mailCmd.AddCommand(mailCheckCmd)
|
||||||
|
mailCmd.AddCommand(mailThreadCmd)
|
||||||
|
|
||||||
rootCmd.AddCommand(mailCmd)
|
rootCmd.AddCommand(mailCmd)
|
||||||
}
|
}
|
||||||
@@ -158,10 +193,36 @@ func runMailSend(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set priority
|
// Set priority
|
||||||
if mailPriority == "high" || mailNotify {
|
msg.Priority = mail.ParsePriority(mailPriority)
|
||||||
|
if mailNotify && msg.Priority == mail.PriorityNormal {
|
||||||
msg.Priority = mail.PriorityHigh
|
msg.Priority = mail.PriorityHigh
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set message type
|
||||||
|
msg.Type = mail.ParseMessageType(mailType)
|
||||||
|
|
||||||
|
// Handle reply-to: auto-set type to reply and look up thread
|
||||||
|
if mailReplyTo != "" {
|
||||||
|
msg.ReplyTo = mailReplyTo
|
||||||
|
if msg.Type == mail.TypeNotification {
|
||||||
|
msg.Type = mail.TypeReply
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up original message to get thread ID
|
||||||
|
router := mail.NewRouter(workDir)
|
||||||
|
mailbox, err := router.GetMailbox(from)
|
||||||
|
if err == nil {
|
||||||
|
if original, err := mailbox.Get(mailReplyTo); err == nil {
|
||||||
|
msg.ThreadID = original.ThreadID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate thread ID for new threads
|
||||||
|
if msg.ThreadID == "" {
|
||||||
|
msg.ThreadID = generateThreadID()
|
||||||
|
}
|
||||||
|
|
||||||
// Send via router
|
// Send via router
|
||||||
router := mail.NewRouter(workDir)
|
router := mail.NewRouter(workDir)
|
||||||
if err := router.Send(msg); err != nil {
|
if err := router.Send(msg); err != nil {
|
||||||
@@ -170,6 +231,9 @@ func runMailSend(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
fmt.Printf("%s Message sent to %s\n", style.Bold.Render("✓"), to)
|
fmt.Printf("%s Message sent to %s\n", style.Bold.Render("✓"), to)
|
||||||
fmt.Printf(" Subject: %s\n", mailSubject)
|
fmt.Printf(" Subject: %s\n", mailSubject)
|
||||||
|
if msg.Type != mail.TypeNotification {
|
||||||
|
fmt.Printf(" Type: %s\n", msg.Type)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -229,12 +293,16 @@ func runMailInbox(cmd *cobra.Command, args []string) error {
|
|||||||
if msg.Read {
|
if msg.Read {
|
||||||
readMarker = "○"
|
readMarker = "○"
|
||||||
}
|
}
|
||||||
|
typeMarker := ""
|
||||||
|
if msg.Type != "" && msg.Type != mail.TypeNotification {
|
||||||
|
typeMarker = fmt.Sprintf(" [%s]", msg.Type)
|
||||||
|
}
|
||||||
priorityMarker := ""
|
priorityMarker := ""
|
||||||
if msg.Priority == mail.PriorityHigh {
|
if msg.Priority == mail.PriorityHigh || msg.Priority == mail.PriorityUrgent {
|
||||||
priorityMarker = " " + style.Bold.Render("!")
|
priorityMarker = " " + style.Bold.Render("!")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf(" %s %s%s\n", readMarker, msg.Subject, priorityMarker)
|
fmt.Printf(" %s %s%s%s\n", readMarker, msg.Subject, typeMarker, priorityMarker)
|
||||||
fmt.Printf(" %s from %s\n",
|
fmt.Printf(" %s from %s\n",
|
||||||
style.Dim.Render(msg.ID),
|
style.Dim.Render(msg.ID),
|
||||||
msg.From)
|
msg.From)
|
||||||
@@ -281,16 +349,30 @@ func runMailRead(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
// Human-readable output
|
// Human-readable output
|
||||||
priorityStr := ""
|
priorityStr := ""
|
||||||
if msg.Priority == mail.PriorityHigh {
|
if msg.Priority == mail.PriorityUrgent {
|
||||||
|
priorityStr = " " + style.Bold.Render("[URGENT]")
|
||||||
|
} else if msg.Priority == mail.PriorityHigh {
|
||||||
priorityStr = " " + style.Bold.Render("[HIGH PRIORITY]")
|
priorityStr = " " + style.Bold.Render("[HIGH PRIORITY]")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s%s\n\n", style.Bold.Render("Subject:"), msg.Subject, priorityStr)
|
typeStr := ""
|
||||||
|
if msg.Type != "" && msg.Type != mail.TypeNotification {
|
||||||
|
typeStr = fmt.Sprintf(" [%s]", msg.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s %s%s%s\n\n", style.Bold.Render("Subject:"), msg.Subject, typeStr, priorityStr)
|
||||||
fmt.Printf("From: %s\n", msg.From)
|
fmt.Printf("From: %s\n", msg.From)
|
||||||
fmt.Printf("To: %s\n", msg.To)
|
fmt.Printf("To: %s\n", msg.To)
|
||||||
fmt.Printf("Date: %s\n", msg.Timestamp.Format("2006-01-02 15:04:05"))
|
fmt.Printf("Date: %s\n", msg.Timestamp.Format("2006-01-02 15:04:05"))
|
||||||
fmt.Printf("ID: %s\n", style.Dim.Render(msg.ID))
|
fmt.Printf("ID: %s\n", style.Dim.Render(msg.ID))
|
||||||
|
|
||||||
|
if msg.ThreadID != "" {
|
||||||
|
fmt.Printf("Thread: %s\n", style.Dim.Render(msg.ThreadID))
|
||||||
|
}
|
||||||
|
if msg.ReplyTo != "" {
|
||||||
|
fmt.Printf("Reply-To: %s\n", style.Dim.Render(msg.ReplyTo))
|
||||||
|
}
|
||||||
|
|
||||||
if msg.Body != "" {
|
if msg.Body != "" {
|
||||||
fmt.Printf("\n%s\n", msg.Body)
|
fmt.Printf("\n%s\n", msg.Body)
|
||||||
}
|
}
|
||||||
@@ -467,3 +549,78 @@ func runMailCheck(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runMailThread(cmd *cobra.Command, args []string) error {
|
||||||
|
threadID := args[0]
|
||||||
|
|
||||||
|
// Find workspace
|
||||||
|
workDir, err := findBeadsWorkDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("not in a Gas Town workspace: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which inbox
|
||||||
|
address := detectSender()
|
||||||
|
|
||||||
|
// Get mailbox and thread messages
|
||||||
|
router := mail.NewRouter(workDir)
|
||||||
|
mailbox, err := router.GetMailbox(address)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting mailbox: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
messages, err := mailbox.ListByThread(threadID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting thread: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON output
|
||||||
|
if mailThreadJSON {
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Human-readable output
|
||||||
|
fmt.Printf("%s Thread: %s (%d messages)\n\n",
|
||||||
|
style.Bold.Render("🧵"), threadID, len(messages))
|
||||||
|
|
||||||
|
if len(messages) == 0 {
|
||||||
|
fmt.Printf(" %s\n", style.Dim.Render("(no messages in thread)"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, msg := range messages {
|
||||||
|
typeMarker := ""
|
||||||
|
if msg.Type != "" && msg.Type != mail.TypeNotification {
|
||||||
|
typeMarker = fmt.Sprintf(" [%s]", msg.Type)
|
||||||
|
}
|
||||||
|
priorityMarker := ""
|
||||||
|
if msg.Priority == mail.PriorityHigh || msg.Priority == mail.PriorityUrgent {
|
||||||
|
priorityMarker = " " + style.Bold.Render("!")
|
||||||
|
}
|
||||||
|
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Printf(" %s\n", style.Dim.Render("│"))
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s %s%s%s\n", style.Bold.Render("●"), msg.Subject, typeMarker, priorityMarker)
|
||||||
|
fmt.Printf(" %s from %s to %s\n",
|
||||||
|
style.Dim.Render(msg.ID),
|
||||||
|
msg.From, msg.To)
|
||||||
|
fmt.Printf(" %s\n",
|
||||||
|
style.Dim.Render(msg.Timestamp.Format("2006-01-02 15:04")))
|
||||||
|
|
||||||
|
if msg.Body != "" {
|
||||||
|
fmt.Printf(" %s\n", msg.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateThreadID creates a random thread ID for new message threads.
|
||||||
|
func generateThreadID() string {
|
||||||
|
b := make([]byte, 6)
|
||||||
|
rand.Read(b)
|
||||||
|
return "thread-" + hex.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|||||||
@@ -379,3 +379,71 @@ func (m *Mailbox) rewriteLegacy(messages []*Message) error {
|
|||||||
// Atomic rename
|
// Atomic rename
|
||||||
return os.Rename(tmpPath, m.path)
|
return os.Rename(tmpPath, m.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListByThread returns all messages in a given thread.
|
||||||
|
func (m *Mailbox) ListByThread(threadID string) ([]*Message, error) {
|
||||||
|
if m.legacy {
|
||||||
|
return m.listByThreadLegacy(threadID)
|
||||||
|
}
|
||||||
|
return m.listByThreadBeads(threadID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mailbox) listByThreadBeads(threadID string) ([]*Message, error) {
|
||||||
|
// bd message thread <thread-id> --json
|
||||||
|
cmd := exec.Command("bd", "message", "thread", threadID, "--json")
|
||||||
|
cmd.Dir = m.workDir
|
||||||
|
cmd.Env = append(cmd.Environ(), "BD_IDENTITY="+m.identity)
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
errMsg := strings.TrimSpace(stderr.String())
|
||||||
|
if errMsg != "" {
|
||||||
|
return nil, errors.New(errMsg)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var beadsMsgs []BeadsMessage
|
||||||
|
if err := json.Unmarshal(stdout.Bytes(), &beadsMsgs); err != nil {
|
||||||
|
if len(stdout.Bytes()) == 0 || string(stdout.Bytes()) == "null" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var messages []*Message
|
||||||
|
for _, bm := range beadsMsgs {
|
||||||
|
messages = append(messages, bm.ToMessage())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by timestamp (oldest first for thread view)
|
||||||
|
sort.Slice(messages, func(i, j int) bool {
|
||||||
|
return messages[i].Timestamp.Before(messages[j].Timestamp)
|
||||||
|
})
|
||||||
|
|
||||||
|
return messages, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mailbox) listByThreadLegacy(threadID string) ([]*Message, error) {
|
||||||
|
messages, err := m.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var thread []*Message
|
||||||
|
for _, msg := range messages {
|
||||||
|
if msg.ThreadID == threadID {
|
||||||
|
thread = append(thread, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by timestamp (oldest first for thread view)
|
||||||
|
sort.Slice(thread, func(i, j int) bool {
|
||||||
|
return thread[i].Timestamp.Before(thread[j].Timestamp)
|
||||||
|
})
|
||||||
|
|
||||||
|
return thread, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,9 +36,23 @@ func (r *Router) Send(msg *Message) error {
|
|||||||
"-s", msg.Subject,
|
"-s", msg.Subject,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add importance flag for high priority
|
// Add priority flag
|
||||||
if msg.Priority == PriorityHigh {
|
beadsPriority := PriorityToBeads(msg.Priority)
|
||||||
args = append(args, "--importance", "high")
|
args = append(args, "--priority", fmt.Sprintf("%d", beadsPriority))
|
||||||
|
|
||||||
|
// Add message type if set
|
||||||
|
if msg.Type != "" && msg.Type != TypeNotification {
|
||||||
|
args = append(args, "--type", string(msg.Type))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add thread ID if set
|
||||||
|
if msg.ThreadID != "" {
|
||||||
|
args = append(args, "--thread-id", msg.ThreadID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add reply-to if set
|
||||||
|
if msg.ReplyTo != "" {
|
||||||
|
args = append(args, "--reply-to", msg.ReplyTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("bd", args...)
|
cmd := exec.Command("bd", args...)
|
||||||
@@ -57,7 +71,7 @@ func (r *Router) Send(msg *Message) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Optionally notify if recipient is a polecat with active session
|
// Optionally notify if recipient is a polecat with active session
|
||||||
if isPolecat(msg.To) && msg.Priority == PriorityHigh {
|
if isPolecat(msg.To) && (msg.Priority == PriorityHigh || msg.Priority == PriorityUrgent) {
|
||||||
r.notifyPolecat(msg)
|
r.notifyPolecat(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,34 @@ import (
|
|||||||
type Priority string
|
type Priority string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// PriorityLow is for non-urgent messages.
|
||||||
|
PriorityLow Priority = "low"
|
||||||
|
|
||||||
// PriorityNormal is the default priority.
|
// PriorityNormal is the default priority.
|
||||||
PriorityNormal Priority = "normal"
|
PriorityNormal Priority = "normal"
|
||||||
|
|
||||||
// PriorityHigh indicates an urgent message.
|
// PriorityHigh indicates an important message.
|
||||||
PriorityHigh Priority = "high"
|
PriorityHigh Priority = "high"
|
||||||
|
|
||||||
|
// PriorityUrgent indicates an urgent message requiring immediate attention.
|
||||||
|
PriorityUrgent Priority = "urgent"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MessageType indicates the purpose of a message.
|
||||||
|
type MessageType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TypeTask indicates a message requiring action from the recipient.
|
||||||
|
TypeTask MessageType = "task"
|
||||||
|
|
||||||
|
// TypeScavenge indicates optional first-come-first-served work.
|
||||||
|
TypeScavenge MessageType = "scavenge"
|
||||||
|
|
||||||
|
// TypeNotification is an informational message (default).
|
||||||
|
TypeNotification MessageType = "notification"
|
||||||
|
|
||||||
|
// TypeReply is a response to another message.
|
||||||
|
TypeReply MessageType = "reply"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Message represents a mail message between agents.
|
// Message represents a mail message between agents.
|
||||||
@@ -44,9 +67,18 @@ type Message struct {
|
|||||||
|
|
||||||
// Priority is the message priority.
|
// Priority is the message priority.
|
||||||
Priority Priority `json:"priority"`
|
Priority Priority `json:"priority"`
|
||||||
|
|
||||||
|
// Type indicates the message type (task, scavenge, notification, reply).
|
||||||
|
Type MessageType `json:"type"`
|
||||||
|
|
||||||
|
// ThreadID groups related messages into a conversation thread.
|
||||||
|
ThreadID string `json:"thread_id,omitempty"`
|
||||||
|
|
||||||
|
// ReplyTo is the ID of the message this is replying to.
|
||||||
|
ReplyTo string `json:"reply_to,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMessage creates a new message with a generated ID (for legacy JSONL mode).
|
// NewMessage creates a new message with a generated ID and thread ID.
|
||||||
func NewMessage(from, to, subject, body string) *Message {
|
func NewMessage(from, to, subject, body string) *Message {
|
||||||
return &Message{
|
return &Message{
|
||||||
ID: generateID(),
|
ID: generateID(),
|
||||||
@@ -57,6 +89,25 @@ func NewMessage(from, to, subject, body string) *Message {
|
|||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Read: false,
|
Read: false,
|
||||||
Priority: PriorityNormal,
|
Priority: PriorityNormal,
|
||||||
|
Type: TypeNotification,
|
||||||
|
ThreadID: generateThreadID(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReplyMessage creates a reply message that inherits the thread from the original.
|
||||||
|
func NewReplyMessage(from, to, subject, body string, original *Message) *Message {
|
||||||
|
return &Message{
|
||||||
|
ID: generateID(),
|
||||||
|
From: from,
|
||||||
|
To: to,
|
||||||
|
Subject: subject,
|
||||||
|
Body: body,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Read: false,
|
||||||
|
Priority: PriorityNormal,
|
||||||
|
Type: TypeReply,
|
||||||
|
ThreadID: original.ThreadID,
|
||||||
|
ReplyTo: original.ID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +118,13 @@ func generateID() string {
|
|||||||
return "msg-" + hex.EncodeToString(b)
|
return "msg-" + hex.EncodeToString(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateThreadID creates a random thread ID.
|
||||||
|
func generateThreadID() string {
|
||||||
|
b := make([]byte, 6)
|
||||||
|
rand.Read(b)
|
||||||
|
return "thread-" + hex.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
// BeadsMessage represents a message as returned by bd mail commands.
|
// BeadsMessage represents a message as returned by bd mail commands.
|
||||||
type BeadsMessage struct {
|
type BeadsMessage struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@@ -74,16 +132,34 @@ type BeadsMessage struct {
|
|||||||
Description string `json:"description"` // Body
|
Description string `json:"description"` // Body
|
||||||
Sender string `json:"sender"` // From identity
|
Sender string `json:"sender"` // From identity
|
||||||
Assignee string `json:"assignee"` // To identity
|
Assignee string `json:"assignee"` // To identity
|
||||||
Priority int `json:"priority"` // 0=urgent, 2=normal
|
Priority int `json:"priority"` // 0=urgent, 1=high, 2=normal, 3=low
|
||||||
Status string `json:"status"` // open=unread, closed=read
|
Status string `json:"status"` // open=unread, closed=read
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Type string `json:"type,omitempty"` // Message type
|
||||||
|
ThreadID string `json:"thread_id,omitempty"` // Thread identifier
|
||||||
|
ReplyTo string `json:"reply_to,omitempty"` // Original message ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMessage converts a BeadsMessage to a GGT Message.
|
// ToMessage converts a BeadsMessage to a GGT Message.
|
||||||
func (bm *BeadsMessage) ToMessage() *Message {
|
func (bm *BeadsMessage) ToMessage() *Message {
|
||||||
priority := PriorityNormal
|
// Convert beads priority (0=urgent, 1=high, 2=normal, 3=low) to GGT Priority
|
||||||
if bm.Priority == 0 {
|
var priority Priority
|
||||||
|
switch bm.Priority {
|
||||||
|
case 0:
|
||||||
|
priority = PriorityUrgent
|
||||||
|
case 1:
|
||||||
priority = PriorityHigh
|
priority = PriorityHigh
|
||||||
|
case 3:
|
||||||
|
priority = PriorityLow
|
||||||
|
default:
|
||||||
|
priority = PriorityNormal
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert message type, default to notification
|
||||||
|
msgType := TypeNotification
|
||||||
|
switch MessageType(bm.Type) {
|
||||||
|
case TypeTask, TypeScavenge, TypeReply:
|
||||||
|
msgType = MessageType(bm.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Message{
|
return &Message{
|
||||||
@@ -95,6 +171,44 @@ func (bm *BeadsMessage) ToMessage() *Message {
|
|||||||
Timestamp: bm.CreatedAt,
|
Timestamp: bm.CreatedAt,
|
||||||
Read: bm.Status == "closed",
|
Read: bm.Status == "closed",
|
||||||
Priority: priority,
|
Priority: priority,
|
||||||
|
Type: msgType,
|
||||||
|
ThreadID: bm.ThreadID,
|
||||||
|
ReplyTo: bm.ReplyTo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PriorityToBeads converts a GGT Priority to beads priority integer.
|
||||||
|
// Returns: 0=urgent, 1=high, 2=normal, 3=low
|
||||||
|
func PriorityToBeads(p Priority) int {
|
||||||
|
switch p {
|
||||||
|
case PriorityUrgent:
|
||||||
|
return 0
|
||||||
|
case PriorityHigh:
|
||||||
|
return 1
|
||||||
|
case PriorityLow:
|
||||||
|
return 3
|
||||||
|
default:
|
||||||
|
return 2 // normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePriority parses a priority string, returning PriorityNormal for invalid values.
|
||||||
|
func ParsePriority(s string) Priority {
|
||||||
|
switch Priority(s) {
|
||||||
|
case PriorityLow, PriorityNormal, PriorityHigh, PriorityUrgent:
|
||||||
|
return Priority(s)
|
||||||
|
default:
|
||||||
|
return PriorityNormal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseMessageType parses a message type string, returning TypeNotification for invalid values.
|
||||||
|
func ParseMessageType(s string) MessageType {
|
||||||
|
switch MessageType(s) {
|
||||||
|
case TypeTask, TypeScavenge, TypeNotification, TypeReply:
|
||||||
|
return MessageType(s)
|
||||||
|
default:
|
||||||
|
return TypeNotification
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user