Skip to content

feat(thread): add reversible inbox-state commands#69

Open
gnapse wants to merge 3 commits intomainfrom
ernesto/inbox-state-undo
Open

feat(thread): add reversible inbox-state commands#69
gnapse wants to merge 3 commits intomainfrom
ernesto/inbox-state-undo

Conversation

@gnapse
Copy link
Copy Markdown
Collaborator

@gnapse gnapse commented Mar 3, 2026

Summary

This PR makes thread inbox state reversible and scriptable from the CLI.

It adds explicit commands to reopen threads and toggle read state, and moves tw thread done onto the same mutation flow so accidental inbox actions can be undone safely.

What Changed

  • add tw thread reopen [thread-refs...]
  • add tw thread mark-read [thread-refs...]
  • add tw thread mark-unread [thread-refs...]
  • add tw thread restore [thread-refs...] --unread
  • move tw thread done onto the same shared batch/state mutation path

Behavior

  • supports one or many refs, plus --from-file
  • supports --dry-run
  • supports --json with per-item before / after snapshots:
    • id
    • title
    • isArchived
    • inInbox
    • isUnread
    • closed
    • lastUpdated
  • idempotent for already-open, already-read, and already-unread cases
  • bulk mutations require --yes unless --dry-run
  • mixed-result batches return per-item results and only set a non-zero exit when there are actual item failures

Docs And Tests

  • update thread help text and examples
  • update README.md
  • update skill content and generated skills/twist-cli/SKILL.md
  • add tests for single-item flows, bulk flows, dry-run, idempotency, restore --unread, and mixed success/failure

Verification

  • npx vitest run src/__tests__/thread.test.ts

Notes

Repo-wide npm test / npm run build are currently blocked by unrelated pre-existing issues in:

  • src/commands/away/set.ts
  • src/commands/thread/reply.ts
  • src/lib/secure-store.ts

@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

doistbot-app[bot]

This comment was marked as outdated.

@gnapse gnapse force-pushed the ernesto/inbox-state-undo branch from ebe9108 to 10eeaa4 Compare April 6, 2026 14:21
@gnapse gnapse changed the title feat: reversible thread inbox state operations Add reversible thread inbox-state commands Apr 6, 2026
@gnapse gnapse changed the title Add reversible thread inbox-state commands feat(thread): add reversible inbox-state commands Apr 6, 2026
@gnapse gnapse force-pushed the ernesto/inbox-state-undo branch from 10eeaa4 to 534abc4 Compare April 6, 2026 14:27
@gnapse gnapse added the 🙋 Ask PR PR must be reviewed before merging label Apr 6, 2026
@gnapse
Copy link
Copy Markdown
Collaborator Author

gnapse commented Apr 6, 2026

@doistbot /review

Copy link
Copy Markdown
Member

@doistbot doistbot 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 valuable commands for managing thread inbox states and read toggles, bringing them into a unified mutation flow with dry-run and JSON support. These additions significantly improve CLI scriptability and provide a safer path for reversing accidental inbox actions. A few minor adjustments have been noted around simplifying redundant variadic argument checks, ensuring JSON bulk mutations without confirmation correctly trigger an error rather than returning a successful preview, and adjusting the sequence of no-op checks so dry-runs accurately reflect unchanged items.

Share FeedbackReview Logs

)
.action(markThreadDone)
.action((refs, options) => {
if ((!refs || refs.length === 0) && !options.fromFile) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P3] Commander variadic arguments ([thread-refs...]) are always passed as an array, defaulting to [] if none are provided. The falsy checks and nullish coalescing are redundant here and in the other new commands.

You can simplify this condition to if (refs.length === 0 && !options.fromFile) and call the action directly with refs instead of refs ?? [].

)
}

const needsConfirmation = rawRefs.length > 1 && !options.yes && !options.dryRun
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] In --json mode this turns a bulk mutation without --yes into a successful preview payload with exit code 0, even though nothing was changed. That is easy for automation to misread as success, and it diverges from thread delete, which throws MISSING_YES_FLAG in JSON mode. Consider treating options.json && needsConfirmation as an error (or at least a non-zero exit) instead of a successful preview.

}
}

if (options.dryRun) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] The no-op check happens after the confirmation/dry-run branches, so already-open/read/unread threads get reported as preview (Would ... / Dry run: would ...) instead of unchanged. That makes the preview output inaccurate and changes the JSON status for idempotent inputs. Moving the mutationPlan.length === 0 branch above these preview returns would keep no-op items idempotent in dry-run and bulk-preview mode too.

@gnapse gnapse force-pushed the ernesto/inbox-state-undo branch 2 times, most recently from 8722e53 to 9bc402a Compare April 6, 2026 14:49
@gnapse gnapse requested a review from scottlovegrove April 7, 2026 17:28
@gnapse gnapse force-pushed the ernesto/inbox-state-undo branch from 9bc402a to 82e9b4a Compare April 7, 2026 17:44
import { assertChannelIsPublic } from '../../lib/public-channels.js'
import { resolveThreadId } from '../../lib/refs.js'

export type ThreadStateAction = 'done' | 'read' | 'reopen' | 'restore' | 'unread'
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we not have something for reversing marking a thread as "done"?


\`--notify\` automatically resolves IDs: group IDs are routed to the \`groups\` API field, user IDs to \`recipients\`. No special syntax needed.

Migration note: \`tw thread done\` is reversible via \`tw thread reopen\`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is that right? I thought reopen was just for threads that had been explicitly closed? Calling reopen on a non-closed thread will move it to done?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I forgot about closing threads. I'll rethink this a bit then.

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

Labels

🙋 Ask PR PR must be reviewed before merging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants