Skip to content

feat: reversible thread inbox state operations#69

Open
gnapse wants to merge 1 commit intomainfrom
ernesto/inbox-state-undo
Open

feat: reversible thread inbox state operations#69
gnapse wants to merge 1 commit intomainfrom
ernesto/inbox-state-undo

Conversation

@gnapse
Copy link
Copy Markdown
Collaborator

@gnapse gnapse commented Mar 3, 2026

Summary

This PR adds reversible thread inbox-state operations and makes thread done safely undoable.

New Commands

  • tw thread reopen [thread-refs...]
  • tw thread read [thread-refs...]
  • tw thread unread [thread-refs...]
  • tw thread restore [thread-refs...] --unread

tw thread done now uses the same mutation flow as the commands above.

Mutation Behavior

  • Supports single and bulk refs (tw thread read 123 456).
  • Supports --yes, --dry-run, and --json.
  • Idempotent operations (reopen on open threads, unread on unread threads).
  • --json includes before/after fields: id, title, isArchived, inInbox, isUnread, closed, lastUpdated.
  • Non-zero exit only for true errors (invalid ref, permission, API failure), including mixed success/failure batches.

Docs and Skill Content

  • Updates README.md with examples and migration note: thread done is reversible via thread reopen.
  • Updates src/lib/skills/content.ts to reflect new thread commands and options.

Tests

  • Expands thread command tests to cover:
    • single and bulk operations
    • dry-run behavior
    • idempotency
    • mixed success/failure

@doistbot doistbot requested a review from craigcarlyle March 3, 2026 22:27
@gnapse gnapse force-pushed the ernesto/inbox-state-undo branch 2 times, most recently from 0c70fc3 to 02b29b4 Compare March 3, 2026 22:50
@gnapse gnapse self-assigned this Mar 3, 2026
@craigcarlyle craigcarlyle removed their request for review March 5, 2026 03:55
@scottlovegrove scottlovegrove force-pushed the ernesto/inbox-state-undo branch from 02b29b4 to ebe9108 Compare March 21, 2026 17:15
@scottlovegrove
Copy link
Copy Markdown
Collaborator

@doistbot /review

Copy link
Copy Markdown

@doistbot-app doistbot-app bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR introduces a great set of reversible thread inbox commands and updates the done command to share the new bulk mutation flow. The expanded functionality is a solid improvement, though the shared flow inadvertently introduces a breaking change to the JSON output schema for thread done that could impact existing automation. A few adjustments are needed to preserve this backward compatibility, restore built-in CLI argument validation, and optimize redundant API calls during bulk operations.

Share FeedbackReview Logs

.option('--json', 'Output result as JSON')
.action(markThreadDone)
withThreadMutationOptions(
thread.command('done [thread-refs...]').description('Archive thread(s) (mark as done)'),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Degraded argument validation. Using [thread-refs...] makes the arguments optional at the CLI parser level, meaning tw thread done bypasses Commander's native validation and throws a generic runtime error in executeThreadMutation. Use <thread-refs...> to leverage Commander's built-in required argument validation. (Please apply this to reopen, read, unread, and restore commands as well).

Suggested change
thread.command('done [thread-refs...]').description('Archive thread(s) (mark as done)'),
thread.command('done <thread-refs...>').description('Archive thread(s) (mark as done)'),

import { pluralize } from './helpers.js'

type DoneOptions = MutationOptions
type ThreadMutationOptions = MutationOptions & {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Redundant type property. MutationOptions already includes json?: boolean and full?: boolean. Redeclaring json?: boolean here is unnecessary and deviates from the repository conventions for reusing options.

Suggested change
type ThreadMutationOptions = MutationOptions & {
type ThreadMutationOptions = MutationOptions & {
yes?: boolean
}

const thread = await client.threads.getThread(threadId)
await assertChannelIsPublic(thread.channelId, thread.workspaceId)

const beforeUnreadIds = await getUnreadThreadIds(client, thread.workspaceId)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Redundant API calls during bulk operations. getUnreadThreadIds fetches all unread threads for a workspace. In a bulk operation affecting multiple threads in the same workspace, this will repeatedly call the API. Consider caching the unread thread IDs per workspace inside executeThreadMutation and passing it to applyThreadMutation, or finding a more efficient way to look up the unread state.


if (!dryRun && actions.length > 0) {
const refreshed = await client.threads.getThread(thread.id)
const afterUnreadIds = await getUnreadThreadIds(client, refreshed.workspaceId)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Redundant API call for unread state after mutation. Calling getUnreadThreadIds immediately after mutating the thread is a performance hit and could suffer from eventual consistency delays. Since the expected unread state is known after the mutation, you can rely on projectAfterState to determine isUnread without an extra network call.

Suggested change
const afterUnreadIds = await getUnreadThreadIds(client, refreshed.workspaceId)
const afterUnread = projectAfterState(before, mutation).isUnread
return {
ref,
status: 'changed',
actions,
before,
after: toSnapshot(refreshed, afterUnread),
}

if (options.json) {
console.log(formatJson({ id: threadId, isArchived: true }))
return
const output = {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] This changes the existing tw thread done <ref> --json payload from a flat status object into a batch envelope. Any automation that currently reads top-level id / isArchived fields will break on upgrade. Preserve the old single-ref schema for done, or gate the richer results payload behind a separate flag so existing scripts keep working.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants