feat: add gt mail mark-read command for desire path (bd-rjuu6)
Adds mark-read and mark-unread commands that allow marking messages as read without archiving them. Uses a "read" label to track status. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
2c73cf35f1
commit
1418b1123a
@@ -191,6 +191,38 @@ Examples:
|
||||
RunE: runMailArchive,
|
||||
}
|
||||
|
||||
var mailMarkReadCmd = &cobra.Command{
|
||||
Use: "mark-read <message-id> [message-id...]",
|
||||
Short: "Mark messages as read without archiving",
|
||||
Long: `Mark one or more messages as read without removing them from inbox.
|
||||
|
||||
This adds a 'read' label to the message, which is reflected in the inbox display.
|
||||
The message remains in your inbox (unlike archive which closes/removes it).
|
||||
|
||||
Use case: You've read a message but want to keep it visible in your inbox
|
||||
for reference or follow-up.
|
||||
|
||||
Examples:
|
||||
gt mail mark-read hq-abc123
|
||||
gt mail mark-read hq-abc123 hq-def456`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: runMailMarkRead,
|
||||
}
|
||||
|
||||
var mailMarkUnreadCmd = &cobra.Command{
|
||||
Use: "mark-unread <message-id> [message-id...]",
|
||||
Short: "Mark messages as unread",
|
||||
Long: `Mark one or more messages as unread.
|
||||
|
||||
This removes the 'read' label from the message.
|
||||
|
||||
Examples:
|
||||
gt mail mark-unread hq-abc123
|
||||
gt mail mark-unread hq-abc123 hq-def456`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: runMailMarkUnread,
|
||||
}
|
||||
|
||||
var mailCheckCmd = &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Check for new mail (for hooks)",
|
||||
@@ -438,6 +470,8 @@ func init() {
|
||||
mailCmd.AddCommand(mailPeekCmd)
|
||||
mailCmd.AddCommand(mailDeleteCmd)
|
||||
mailCmd.AddCommand(mailArchiveCmd)
|
||||
mailCmd.AddCommand(mailMarkReadCmd)
|
||||
mailCmd.AddCommand(mailMarkUnreadCmd)
|
||||
mailCmd.AddCommand(mailCheckCmd)
|
||||
mailCmd.AddCommand(mailThreadCmd)
|
||||
mailCmd.AddCommand(mailReplyCmd)
|
||||
|
||||
@@ -292,6 +292,98 @@ func runMailArchive(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMailMarkRead(cmd *cobra.Command, args []string) error {
|
||||
// Determine which inbox
|
||||
address := detectSender()
|
||||
|
||||
// All mail uses town beads (two-level architecture)
|
||||
workDir, err := findMailWorkDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a Gas Town workspace: %w", err)
|
||||
}
|
||||
|
||||
// Get mailbox
|
||||
router := mail.NewRouter(workDir)
|
||||
mailbox, err := router.GetMailbox(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting mailbox: %w", err)
|
||||
}
|
||||
|
||||
// Mark all specified messages as read
|
||||
marked := 0
|
||||
var errors []string
|
||||
for _, msgID := range args {
|
||||
if err := mailbox.MarkReadOnly(msgID); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("%s: %v", msgID, err))
|
||||
} else {
|
||||
marked++
|
||||
}
|
||||
}
|
||||
|
||||
// Report results
|
||||
if len(errors) > 0 {
|
||||
fmt.Printf("%s Marked %d/%d messages as read\n",
|
||||
style.Bold.Render("⚠"), marked, len(args))
|
||||
for _, e := range errors {
|
||||
fmt.Printf(" Error: %s\n", e)
|
||||
}
|
||||
return fmt.Errorf("failed to mark %d messages", len(errors))
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
fmt.Printf("%s Message marked as read\n", style.Bold.Render("✓"))
|
||||
} else {
|
||||
fmt.Printf("%s Marked %d messages as read\n", style.Bold.Render("✓"), marked)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMailMarkUnread(cmd *cobra.Command, args []string) error {
|
||||
// Determine which inbox
|
||||
address := detectSender()
|
||||
|
||||
// All mail uses town beads (two-level architecture)
|
||||
workDir, err := findMailWorkDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a Gas Town workspace: %w", err)
|
||||
}
|
||||
|
||||
// Get mailbox
|
||||
router := mail.NewRouter(workDir)
|
||||
mailbox, err := router.GetMailbox(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting mailbox: %w", err)
|
||||
}
|
||||
|
||||
// Mark all specified messages as unread
|
||||
marked := 0
|
||||
var errors []string
|
||||
for _, msgID := range args {
|
||||
if err := mailbox.MarkUnreadOnly(msgID); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("%s: %v", msgID, err))
|
||||
} else {
|
||||
marked++
|
||||
}
|
||||
}
|
||||
|
||||
// Report results
|
||||
if len(errors) > 0 {
|
||||
fmt.Printf("%s Marked %d/%d messages as unread\n",
|
||||
style.Bold.Render("⚠"), marked, len(args))
|
||||
for _, e := range errors {
|
||||
fmt.Printf(" Error: %s\n", e)
|
||||
}
|
||||
return fmt.Errorf("failed to mark %d messages", len(errors))
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
fmt.Printf("%s Message marked as unread\n", style.Bold.Render("✓"))
|
||||
} else {
|
||||
fmt.Printf("%s Marked %d messages as unread\n", style.Bold.Render("✓"), marked)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMailClear(cmd *cobra.Command, args []string) error {
|
||||
// Determine which inbox to clear (target arg or auto-detect)
|
||||
address := ""
|
||||
|
||||
@@ -371,6 +371,61 @@ func (m *Mailbox) markReadLegacy(id string) error {
|
||||
return m.rewriteLegacy(messages)
|
||||
}
|
||||
|
||||
// MarkReadOnly marks a message as read WITHOUT archiving/closing it.
|
||||
// For beads mode, this adds a "read" label to the message.
|
||||
// For legacy mode, this sets the Read field to true.
|
||||
// The message remains in the inbox but is displayed as read.
|
||||
func (m *Mailbox) MarkReadOnly(id string) error {
|
||||
if m.legacy {
|
||||
return m.markReadLegacy(id)
|
||||
}
|
||||
return m.markReadOnlyBeads(id)
|
||||
}
|
||||
|
||||
func (m *Mailbox) markReadOnlyBeads(id string) error {
|
||||
// Add "read" label to mark as read without closing
|
||||
args := []string{"label", "add", id, "read"}
|
||||
|
||||
_, err := runBdCommand(args, m.workDir, m.beadsDir)
|
||||
if err != nil {
|
||||
if bdErr, ok := err.(*bdError); ok && bdErr.ContainsError("not found") {
|
||||
return ErrMessageNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkUnreadOnly marks a message as unread (removes "read" label).
|
||||
// For beads mode, this removes the "read" label from the message.
|
||||
// For legacy mode, this sets the Read field to false.
|
||||
func (m *Mailbox) MarkUnreadOnly(id string) error {
|
||||
if m.legacy {
|
||||
return m.markUnreadLegacy(id)
|
||||
}
|
||||
return m.markUnreadOnlyBeads(id)
|
||||
}
|
||||
|
||||
func (m *Mailbox) markUnreadOnlyBeads(id string) error {
|
||||
// Remove "read" label to mark as unread
|
||||
args := []string{"label", "remove", id, "read"}
|
||||
|
||||
_, err := runBdCommand(args, m.workDir, m.beadsDir)
|
||||
if err != nil {
|
||||
if bdErr, ok := err.(*bdError); ok && bdErr.ContainsError("not found") {
|
||||
return ErrMessageNotFound
|
||||
}
|
||||
// Ignore error if label doesn't exist
|
||||
if bdErr, ok := err.(*bdError); ok && bdErr.ContainsError("does not have label") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkUnread marks a message as unread (reopens in beads).
|
||||
func (m *Mailbox) MarkUnread(id string) error {
|
||||
if m.legacy {
|
||||
@@ -686,15 +741,11 @@ func (m *Mailbox) Count() (total, unread int, err error) {
|
||||
}
|
||||
|
||||
total = len(messages)
|
||||
if m.legacy {
|
||||
for _, msg := range messages {
|
||||
if !msg.Read {
|
||||
unread++
|
||||
}
|
||||
// Count messages that are NOT marked as read (including via "read" label)
|
||||
for _, msg := range messages {
|
||||
if !msg.Read {
|
||||
unread++
|
||||
}
|
||||
} else {
|
||||
// For beads, inbox only returns unread
|
||||
unread = total
|
||||
}
|
||||
|
||||
return total, unread, nil
|
||||
|
||||
@@ -256,7 +256,7 @@ func (bm *BeadsMessage) ToMessage() *Message {
|
||||
Subject: bm.Title,
|
||||
Body: bm.Description,
|
||||
Timestamp: bm.CreatedAt,
|
||||
Read: bm.Status == "closed",
|
||||
Read: bm.Status == "closed" || bm.HasLabel("read"),
|
||||
Priority: priority,
|
||||
Type: msgType,
|
||||
ThreadID: bm.threadID,
|
||||
@@ -266,6 +266,16 @@ func (bm *BeadsMessage) ToMessage() *Message {
|
||||
}
|
||||
}
|
||||
|
||||
// HasLabel checks if the message has a specific label.
|
||||
func (bm *BeadsMessage) HasLabel(label string) bool {
|
||||
for _, l := range bm.Labels {
|
||||
if l == label {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PriorityToBeads converts a GGT Priority to beads priority integer.
|
||||
// Returns: 0=urgent, 1=high, 2=normal, 3=low
|
||||
func PriorityToBeads(p Priority) int {
|
||||
|
||||
Reference in New Issue
Block a user