Skip to content

feat(inbox): add 'linear inbox' — Linear notifications from the CLI#25

Merged
muqsitnawaz merged 2 commits into
mainfrom
inbox-command
Jul 19, 2026
Merged

feat(inbox): add 'linear inbox' — Linear notifications from the CLI#25
muqsitnawaz merged 2 commits into
mainfrom
inbox-command

Conversation

@muqsitnawaz

Copy link
Copy Markdown
Contributor

What

Adds a linear inbox command that shows your Linear inbox — notifications: comments, mentions, assignments, and status changes on issues you follow — directly from the CLI, no browser needed.

linear inbox              # unread notifications
linear inbox --all        # include already-read
linear inbox --limit 50   # bound the fetch (default 30)
linear inbox --json       # raw output

Backed by the Linear GraphQL notifications query. Output shows an unread marker, ticket id, a friendly type label (comment/mention/assigned/status), the actor, the title, and a comment snippet.

Why

Progress notes and mentions land in the Linear inbox, but there was no CLI surface for them — you had to open the web app (or drive a browser profile). This closes that gap for agents and humans working from the terminal.

Verified

  • python3 test_linear.py — 22/22 pass (added InboxTypeLabelTest).
  • Ran live: linear inbox returned 24 unread notifications formatted correctly; --json returns structured data; inbox appears in --help.

Version 0.11.1 -> 0.12.0; CHANGELOG updated.

New command surfaces your Linear inbox (comments, mentions, assignments, status
changes) via the GraphQL notifications query — no browser needed. Unread-only by
default; --all includes read, --limit bounds the fetch, --json for raw output.
Bumps 0.11.1 -> 0.12.0; adds InboxTypeLabelTest.
@prix-cloud

prix-cloud Bot commented Jul 19, 2026

Copy link
Copy Markdown

Code Reviewer

Verdict: Changes requested (small, targeted fixes — not a blocker on the core feature)

Build: N/A — single-file stdlib Python CLI, no build step. python3 -m py_compile linear clean.
Tests: 22/22 passed (python3 -m unittest -v), matches the PR description. CI (unittest) is green on both required checks.

Read README.md and skill.md for conventions (no AGENTS.md/CLAUDE.md/GEMINI.md exist in this repo — confirmed via find). CI workflow (.github/workflows/*.yml) confirms python3 -m unittest -v is the authoritative test command, which matches what I ran.

Changes that work well

  • _INBOX_TYPE_LABEL mapping + cmd_inbox follow the existing command structure closely (same gql/check_errors pattern as cmd_states, same getattr(args, ...) style for optional flags).
  • Filtering out non-issue notifications (items = [n for n in nodes if n.get("issue")]) correctly excludes project/other notification kinds — verified with a mocked ProjectNotification node, it's dropped as intended.
  • --json, --all, empty-inbox messaging all work as described when exercised with mocked gql() data.

Issues that need attention

1. Row formatting breaks on any notification type not in _INBOX_TYPE_LABEL (linear:2817-2820)

label = _INBOX_TYPE_LABEL.get(n.get("type"), n.get("type") or "note")
...
print(f"  {dot} {iss.get('identifier','-'):11s}{label:11s}{who:14.14s}{title}")

label falls back to the raw camelCase type string when unmapped, but the format spec {label:11s} only pads — it doesn't truncate. A real (or future) notification type like issueWeirdFutureType blows past the 11-char column and misaligns every row after it. Reproduced by monkey-patching gql() with a node of an unmapped type:

=== default (unread only) ===
Inbox — 2 unread (2 unread):
  * ANT-1      comment    Bisma         Fix login bug that is really quite long indeed y
      "hey can you check this out please, it's blocking release"
  * ANT-4      issueWeirdFutureType-             Mystery

Notice the last row: no space between the raw type and the identifier padding, and the actor/title columns are pushed out of alignment. The PR's own new test, test_unmapped_type_absent_so_cmd_inbox_falls_back_to_raw (test_linear.py:61-62), asserts the fallback exists but never checks what it renders as — so this shipped untested. Simple fix: truncate like title does, e.g. label[:10]:10s or label[:11]:11s.

2. --limit 0 silently becomes --limit 30 (linear:2777)

limit = getattr(args, "limit", None) or 30

argparse already sets default=30 (linear:3059), so args.limit is never None — the or 30 only fires when the user explicitly passes --limit 0, which then gets silently overridden to 30 instead of fetching zero or erroring. Confirmed by capturing the GraphQL variables sent for --limit 0: {'n': 30}. Low impact (unlikely flag value) but worth a one-line fix (limit = args.limit if args.limit is not None else 30, or just args.limit since it always has a default).

Things to verify manually

  • Test coverage gap: test_linear.py already has a precedent for exercising a cmd_* function end-to-end with a monkey-patched dependency and captured stdout (test_cmd_update_dedupes_stdin_and_keeps_going_after_bad_ticket, test_linear.py:112-154). cmd_inbox has no equivalent — only the standalone dict-lookup test for _INBOX_TYPE_LABEL. A test in that style (mock gql, assert on stdout) would have caught issue feat: add --due-date flag to create and update #1 directly. Given the PR description leans on "22/22 pass" as verification, it's worth closing this gap before merge.
  • Docs: README.md's Usage section documents every other command (states, agents, cycles, users, labels, milestones, projects, ...) but wasn't updated for inbox. skill.md — the agent-facing reference that explicitly calls out linear states/linear agents/linear users — also doesn't mention inbox, even though the PR's stated motivation is closing this gap "for agents and humans working from the terminal." Worth adding a couple of lines to both so agents actually discover the command. Separately, README's blanket claim "Every list is fully paginated" (just above the command examples) now slightly overclaims, since inbox is bounded by --limit rather than paginated — might be worth a one-word carve-out.
  • I couldn't hit the real Linear API (no credentials in this sandbox) to confirm the live claim of "24 unread notifications formatted correctly" — the mocked-data run above is a reasonable proxy but isn't the same as the real GraphQL schema response.

Reviewed by Code Reviewer — actually ran the build and tests on this branch.

…t, add cmd_inbox test

1. label column now uses {:11.11s} so an unmapped notification type is truncated
   instead of overflowing and misaligning every following row.
2. --limit no longer silently coerces 0 to 30; explicit <1 gets a clear error.
3. Add cmd_inbox stdout test (monkeypatched gql) asserting a mapped + unmapped
   type stay column-aligned — catches the alignment bug directly.
@prix-cloud

prix-cloud Bot commented Jul 19, 2026

Copy link
Copy Markdown

Code Reviewer

Verdict: Ready to merge

Build: N/A — single-file stdlib-only Python CLI, no build step (./linear --help runs directly).
Tests: 23 passed, 0 failed (python3 test_linear.py -v), including the 3 new InboxTypeLabelTest cases. CI (unittest workflow) is green on both required jobs.

Repo has no AGENTS.md/CLAUDE.md/GEMINI.md at root or in touched directories, so I followed README.md for toolchain/conventions.

Changes that work well

  • --limit/--all/--json all behave as documented — verified live with a stubbed gql(): --limit 0 and --limit -5 correctly reject with inbox: --limit must be >= 1 and never hit the network; --limit 50 passes {"n": 50} through to the query variable.
  • The fixed-width label/actor columns ({label:11.11s}{who:14.14s}) stay aligned even with an unmapped, longer-than-column type value — reproduced the scenario from test_cmd_inbox_truncates_unmapped_type_and_keeps_rows_aligned manually and confirmed both rows land at the same offset.
  • actor and issue are both guarded with or {} before field access, so a system-generated notification with no actor renders - instead of crashing.
  • This is clearly the "address prix review" follow-up commit (997b338) — the --limit validation and column truncation fixes it describes are both present and correct.

Things to verify manually

  • linear (linear.py:2823) formats iss.get('identifier','-').get only substitutes '-' when the key is missing, not when it's present but None. I reproduced a crash by feeding a notification with issue.identifier: None:
    TypeError: unsupported format string passed to NoneType.__format__
    
    This is almost certainly unreachable in practice — Linear's Issue.identifier is a non-null field — but every other field in this function (actor, issue, comment.body, title) is defensively guarded against None, so this one line breaks that pattern. Not blocking, just flagging in case Linear's API ever returns a partial/degraded issue payload for a stale notification.
  • Cosmetic: with the default (non---all) scope, the summary line reads Inbox — 2 unread (2 unread): — the count is stated twice since unread == len(items) whenever unread-only filtering is active. Harmless but reads oddly; --all mode (Inbox — 3 (2 unread):) doesn't have this issue.
  • README.md (command reference around line 115) and skill.md (core workflow list around line 26) both give every other command a one-line usage example (linear states, linear agents, etc.) but neither mentions linear inbox. CHANGELOG.md is updated, but these two docs — the ones a human or an agent actually reads to discover what the CLI can do — aren't. Worth a follow-up line in each.

Reviewed by Code Reviewer — actually ran the build and tests on this branch.

@muqsitnawaz
muqsitnawaz merged commit 0824127 into main Jul 19, 2026
2 checks passed
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.

1 participant