Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions skills/hey/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ hey box view imbox --page next-cursor --json # Continue from an earlier listing

Box names: `imbox`, `feedbox`, `trailbox`, `asidebox`, `laterbox`, `bubblebox`

**Response format:** `hey box view --json` returns the box itself — `id`, `kind`, `name`, `app_url`, `next_history_url`, `next_page` — with a `postings` array of the email threads in it. Each posting has: `id` (box item ID), `topic_id` (thread ID), `name` (subject), `seen` (read status), `created_at`, `contacts`, `summary`, `app_url`, `visible_entry_count`. Use `id` for `hey seen`, `hey unseen`, `hey move`, `hey label add`, `hey label remove`, `hey trash`, `hey spam`, `hey ignore`, and `hey stop-ignoring`, and `topic_id` for `hey thread read`, `hey reply`, `hey forward`, `hey share` and `hey attachment list`. A box item `id` passed to `hey thread read` answers `not_found`, and so does a `topic_id` passed to `hey move`.
**Response format:** `hey box view --json` returns the box itself — `id`, `kind`, `name`, `app_url`, `next_history_url`, `next_page` — with a `postings` array of the email threads in it. Each posting has: `id` (box item ID), `topic_id` (thread ID), `name` (subject), `seen` (read status), `created_at`, `contacts`, `summary`, `app_url`, `visible_entry_count`. Use `id` for `hey seen`, `hey unseen`, `hey move`, `hey label add`, `hey label remove`, `hey trash`, `hey spam`, `hey ignore`, and `hey stop-ignoring`, and `topic_id` for `hey thread read`, `hey reply`, `hey forward`, `hey share` and `hey attachment list`. A box item `id` passed to `hey thread read` answers `not_found`. The reverse is not caught yet: `hey seen`, `hey unseen` and `hey move` silently ignore any id that is not one of your box items — a `topic_id`, a typo — and still answer success counting every id given, because HEY's endpoints do not report a non-match. Every id in the same call that *is* one of your box items is changed, so a mixed batch is a partial success reported as a whole one, and a `topic_id` that happens to equal one of your other box item ids marks or moves that unrelated thread. Confirm rather than trust the envelope: `hey box view <box> --json --all --jq '{notice, next_page: .data.next_page, match: [.data.postings[] | select(.id == <id>) | {id, topic_id, seen}]}'` for a mark, or the destination box for a move. `--all` reads every page up to the command's cap of 101; an empty `match` with `next_page` still set means the box is larger than that, so continue with `--page <next_page>` rather than calling it a non-match, and `topic_id` in the match says which thread an id actually named. `hey trash`, `hey spam` and the label commands do answer `not_found`.

A posting that bundles a contact's mail into one row can **omit `topic_id`**: a bundle names its sender rather than a thread, and its `name` joins the bundled subjects with `•`. A bundle that does carry a `topic_id` opens as that thread — its one unseen thread — and `hey threads` reads it as usual. For a bundle without one, never substitute the box item `id` (`hey threads <id>` answers `not_found`); there is no command that lists the threads inside a bundle, so run `hey contacts unbundle <contact_id>` — the contact is in the posting's `contacts` — to list that sender's mail as separate rows, or direct the user to open the bundle in HEY.

Expand Down Expand Up @@ -464,7 +464,9 @@ on an entry; use `hey reply`, which works the addressing out itself.

`hey share` returns a URL that shows the entire thread and future emails or replies sent to it. Anyone with the link can open it. `hey unshare` turns off the sharing link.

**ID note:** A thread listed through a box, label, collection, or search normally has two IDs: an `id` (its box item ID) and a `topic_id` (its thread ID). `hey seen`, `hey unseen`, `hey move`, `hey label add`, `hey label remove`, `hey trash`, `hey spam`, `hey ignore`, and `hey stop-ignoring` expect the box item `id`. `hey thread read`, `hey share`, `hey unshare`, `hey attachment list`, `hey reply`, `hey forward`, `hey collection add`, and `hey collection remove` expect `topic_id`. A `hey thread list` row comes directly from a topic, so its `id` is also a thread ID and `topic_id` repeats it explicitly. Passing a box item ID where a thread ID is expected, or the reverse, answers `not_found`, not a redirect.
**ID note:** Every email thread has two IDs: an `id` (its box item ID) and a `topic_id` (its thread ID). `hey seen`, `hey unseen`, `hey move`, `hey label add`, `hey label remove`, `hey trash`, `hey spam`, `hey ignore`, and `hey stop-ignoring` expect `id`. `hey thread read`, `hey share`, `hey unshare`, `hey attachment list`, `hey reply`, `hey forward`, `hey collection add`, and `hey collection remove` expect `topic_id`. Passing the wrong one answers `not_found`, not a redirect — except `hey seen`, `hey unseen` and `hey move`, which ignore an unmatched id, act on every id that does match, and answer success either way (confirm with `hey box view <box> --json --all`, as in the note above).

A `hey thread list` row comes directly from a topic, so its `id` is also a thread ID and `topic_id` repeats it explicitly. Neither field is a box item ID for organization commands.

`hey thread list --json`, `hey box view --json`, `hey label view --json`, `hey collection view --json` and `hey search --json` all carry `topic_id`; a bundle posting can omit it (see the Boxes section).

Expand Down
18 changes: 17 additions & 1 deletion tests/smoke/topic_views_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestThreadListPage(t *testing.T) {
first := heyJSON(t, "thread", "list", "--in", "sent")
nextPage, _ := first.Meta["next_page"].(string)
if nextPage == "" {
t.Skip("Sent has no next page in this fixture")
skipf(t, "Sent has no next page in this fixture")
}

response := heyJSON(t, "thread", "list", "--in", "sent", "--page", nextPage)
Expand All @@ -43,4 +43,20 @@ func TestThreadListPage(t *testing.T) {
if string(response.Data) == "null" {
t.Fatal("expected an array, got null")
}
firstIDs := make(map[int64]bool)
for _, topic := range dataAs[[]threadListItem](t, first) {
firstIDs[topic.TopicID] = true
}
nextTopics := dataAs[[]threadListItem](t, response)
if len(nextTopics) == 0 {
t.Fatal("next page returned no threads")
}
for _, topic := range nextTopics {
if topic.ID <= 0 || topic.TopicID != topic.ID {
t.Errorf("expected matching positive id and topic_id, got %#v", topic)
}
if firstIDs[topic.TopicID] {
t.Errorf("next page repeated thread %d from the first page", topic.TopicID)
}
}
}