claude-inbox 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c424c9f33451df3570e8784a053b26686eae4bd9af70d8794548be17f534dab1
4
- data.tar.gz: 7644408160f6f33a79e18f4ebc62aec2220884a95ac8985dac21a62ee54363cb
3
+ metadata.gz: 76328d9652ac26483494547c0b26c4099a5c8996816743c9b88b21dfebff0def
4
+ data.tar.gz: 15d80ce8e007a533cf70fe35a6215d148fe9f48925fb9b51b50a9913f43ebc9c
5
5
  SHA512:
6
- metadata.gz: 6c4e7d3dcd27bcc509f5d8bb70d5ffc88ceaf697dcb6a6a2423f86893feea151f56a0ae55c4c2182a1cb4c5df6715dc3ab603fbade98f1f764f076f3beaf7d82
7
- data.tar.gz: 59c64367da3aa81786b6344c790c3ba67adc9881823c99fd619626e4ae8f9594a1cd2c0140ce54d88523d820780c81fa287f6c716b4562e710f587f69278afd7
6
+ metadata.gz: 7cd8965adf8b34a34b8ee2fb412b8742d3457e2616cc3a0de549f4a9ee7b5fd9385635445e56bf6de37f3eef43da85ce5456df52da0319ac4a2312e223b872ed
7
+ data.tar.gz: 39c760a37be19c854d69c98b8e429fa5fcc48ab4bbfe03c00033e78bd217c161a29051074e70dbb98db234d1913f2320187eafcfabb68396a96a95c8eee1f0a3
data/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  Inbox-style triage on top of Claude Code's background sessions. A companion to
4
4
  `claude agents`, not a replacement: it reads the same daemon state through
5
- `claude agents --json` and adds **snooze**, **auto-settle** and the session's
6
- **pull request**.
5
+ `claude agents --json` and adds snooze, auto-settle and the session's pull
6
+ request.
7
7
 
8
8
  ![claude-inbox with one working session, one snoozed and the settled section folded](docs/screenshot.png)
9
9
 
@@ -20,24 +20,20 @@ are published to rubygems and listed under GitHub Releases.
20
20
 
21
21
  ## Sections
22
22
 
23
- 1. **Pinned.** Parked at the top by hand, regardless of state. `P` toggles it.
24
- 2. **Needs you.** `blocked` or `failed`, unless you've attached to it since (see
23
+ 1. **Pinned.** Parked at the top by hand, regardless of state. `t` toggles it.
24
+ 2. **Needs you.** `blocked` or `failed`, until you attach to it (see
25
25
  Acknowledge below).
26
- 3. **Active.** `working`, plus recently finished sessions that have not settled
27
- yet, plus interactive sessions, plus a needs-you session you've attached to
28
- but that hasn't resolved. Interactive sessions report only `status`, so it
29
- gets mapped: busy is working, idle is done, waiting needs you. The JSON calls
26
+ 3. **Active.** `working`, recently finished sessions that haven't settled yet,
27
+ interactive sessions, and a needs-you session you've attached to that
28
+ hasn't resolved. Interactive sessions report only `status`, so it gets
29
+ mapped: busy is working, idle is done, waiting needs you. The daemon calls
30
30
  a terminal you opened yourself, a Remote Control worker, a sub-agent and a
31
- headless `claude -p` run all "interactive", each named after its directory;
32
- the client tells them apart from the process tree (a remote worker runs with
33
- `--sdk-url` under a `claude rc` parent, a sub-agent has a `claude` for a
34
- parent, and a headless run gives itself away with `-p` or the SDK's stream
35
- flags, since the shell that spawned it hides the session behind it).
36
- Sub-agents and headless runs are dropped: nobody is sitting in them and
37
- attach lands on whatever asked for them. Remote sessions settle and snooze
38
- like any other row. A terminal you are sitting in never settles. Neither
39
- can be attached, peeked or stopped from outside; you can land on them, and
40
- Enter tells you why nothing happens.
31
+ headless `claude -p` run all "interactive"; the inbox tells them apart from
32
+ the process tree. Sub-agents and headless runs are dropped, since nobody is
33
+ sitting in them. Remote sessions settle and snooze like any other row,
34
+ and Enter pulls one into the daemon (see Remote Control below). A
35
+ terminal you are sitting in never settles and can't be attached, peeked
36
+ or stopped from outside; Enter tells you so.
41
37
  4. **Snoozed.** Sorted by wake time, parked ("until I wake it") entries last.
42
38
  Collapsed; Enter expands.
43
39
  5. **Settled.** Parked with `x`, or whose pull request is merged or closed.
@@ -48,247 +44,171 @@ A row with a pull request shows it after the state: `#885 open`, `#885 draft`,
48
44
 
49
45
  `working` from the daemon covers two situations, and the row says which. A
50
46
  spinner and `working` mean the agent is thinking. A steady `◌` and `idle · 1
51
- shell` mean the agent has stopped and is only waiting on work it started, such as
52
- a `--watch` shell or a sub-agent. That is why a session with nothing left to do
53
- can sit there for an hour. Whatever the agent is up to, the open work is named
54
- next to the state: `working · 2 agents`, `idle · 1 shell`. The count comes from
55
- the session's own job file, which `claude agents --json` does not expose; the
56
- inbox still takes the state itself from the daemon, the only thing that knows
57
- whether a session is alive.
58
-
59
- Under each row in Pinned, Needs You and Active is the session's own line,
60
- the one `claude agents` prints too: what it is waiting on while blocked
61
- (`↳ confirm: drop the top line?`), what it produced once done, otherwise its
62
- status line. It comes from the job file, like the open work above, so a
63
- terminal you opened yourself has none and shows its directory instead.
64
-
65
- A session you gave a color to with `/color` wears it on the label — see
66
- [Colors](#colors).
47
+ shell` mean the agent has stopped and is waiting on work it started, such as a
48
+ `--watch` shell or a sub-agent. That is why a session with nothing left to do
49
+ can sit there for an hour. The open work is named next to the state either
50
+ way: `working · 2 agents`, `idle · 1 shell`.
51
+
52
+ Under each row in Pinned, Needs You and Active is the line `claude agents`
53
+ prints too: what it is waiting on while blocked (`↳ confirm: drop the top
54
+ line?`), what it produced once done, otherwise its status line. A terminal you
55
+ opened yourself has none and shows its directory instead.
56
+
57
+ A session you gave a color to with `/color` wears it on the label. Nothing
58
+ else is tinted, so a blocked session still looks blocked.
67
59
 
68
60
  ## Keys
69
61
 
70
- Keyboard-first, vim flavoured, but the mouse works too. Arrows work as well
71
- as `j`/`k`. Clicking a row does what landing on it and pressing `Enter`
72
- would: selects it and attaches (or expands a fold, or refuses on a terminal
73
- row you can't be attached to from here). The wheel moves the selection
74
- rather than uncovering the scrollback behind us, in terminals that report
75
- mouse events (`\e[?1000h` + `\e[?1006h`, the same mode clicks use) or that at
76
- least support alternate scroll mode (`\e[?1007h`, wheel-as-arrows only, no
77
- clicks). Apple Terminal supports neither escape, so there the wheel scrolls
78
- its own history and clicking a row does nothing.
62
+ Vim bindings; arrows work as well as `j`/`k`. The mouse works too, in
63
+ terminals that report it: clicking a row selects and attaches, and the wheel
64
+ moves the selection. Apple Terminal reports neither, so there the wheel
65
+ scrolls its own history and clicks do nothing.
79
66
 
80
67
  | Key | Action |
81
68
  |---|---|
82
69
  | `j` `k` | move down / up |
83
70
  | `gg` `G` | first / last row |
84
71
  | `Ctrl-d` `Ctrl-u` | half page down / up (`Ctrl-f` `Ctrl-b` full page) |
85
- | `Enter` `l` | attach (full-screen handoff; `←` or `Ctrl+Z` return here), or expand Snoozed / Settled |
72
+ | `Enter` `l` | attach (full-screen handoff; `←` or `Ctrl+Z` return here), adopt a remote session, or expand Snoozed / Settled |
86
73
  | `Tab` `Shift+Tab` | jump to the next / previous section |
87
74
  | `h` | close the peek pane, else collapse the current Snoozed / Settled fold |
88
75
  | `za` `zo` `zc` | toggle / open / close the Snoozed or Settled fold under the cursor |
89
76
  | `p` | toggle the read-only peek pane |
90
77
  | `J` `K` (`Ctrl-e` `Ctrl-y`) | scroll the peek pane |
91
- | `n` | new session (full screen): multi-line prompt (`Enter` breaks a line, `Cmd-V` or `Ctrl-V` pastes an image, `Ctrl-S` starts it, `Ctrl-O` starts and opens it), name, directory (`Tab` completes), model, effort, permissions, worktree; "default" choices show what your settings resolve to |
92
- | `t` | pin / unpin (parks it in Pinned at the top, regardless of state) |
78
+ | `n` | new session (see below) |
79
+ | `t` | pin / unpin |
93
80
  | `s` | snooze: `1` 15m · `2` 1h · `3` tomorrow 9am · `4` until woken |
94
81
  | `u` | wake a snoozed session now, or bring back one you settled |
95
82
  | `x` | settle a working or needs-you session by hand (returns when it changes state, unless it just finished) |
96
83
  | `a` | set a local alias (never touches the real session name) |
97
84
  | `o` | open the session's pull request in the browser |
85
+ | `w` | open the session at claude.ai/code |
98
86
  | `P` | link a pull request by hand (empty clears; the scanned links return) |
99
87
  | `X` | stop the session (`y` to confirm) |
100
88
  | `Ctrl-x` | delete the session for good, conversation and worktree with it (`y` to confirm) |
101
- | `/` | filter by name or cwd; `Enter` keeps it, `Esc` clears |
89
+ | `/` | filter by name, cwd, the prompt it started from, or what it is doing now; `Enter` keeps it, `Esc` clears |
102
90
  | `R` | poll now |
103
91
  | `q` | quit |
104
92
 
105
- Bindings live in `ClaudeInbox::Keymap`, a pure resolver with chord support
106
- that is unit tested on its own. A fast `Esc` followed by a chord key is split
107
- back into two keys, since tty-reader would otherwise glue them together.
108
-
109
- `X` and `Ctrl-x` both ask before they act, and both take `y`, but they are not
110
- the same thing. `X` runs `claude stop`: the process ends, the conversation is
111
- kept, and `Enter` resumes it later. `Ctrl-x` runs `claude rm`. The session,
112
- its transcript and its worktree all go, and so does our own state entry for
113
- it, rather than sitting out the seven-day prune. Nothing resumes afterwards,
114
- so read the box before answering.
93
+ `X` and `Ctrl-x` both ask first, and both take `y`, but they are not the same
94
+ thing. `X` runs `claude stop`: the process ends, the conversation is kept, and
95
+ `Enter` resumes it later. `Ctrl-x` runs `claude rm`: the session, its
96
+ transcript and its worktree all go. Nothing resumes afterwards, so read the
97
+ box before answering.
115
98
 
116
99
  ### New session
117
100
 
118
- `n` opens a form. `Tab` / `Shift+Tab` move between fields, `Enter` moves on
119
- (inside the prompt it breaks a line), `Esc` cancels. Choice fields cycle with
120
- `h` `l` or the arrows. Text fields are a real editor: `←` `→` move the cursor,
121
- `Ctrl-A` / `Ctrl-E` jump to the ends, `Backspace` and `Delete` cut either side
122
- of it, `Ctrl-W` takes the word before it and `Ctrl-U` / `Ctrl-K` everything
123
- before / after it. The `/` filter, `:` command and alias / PR prompt lines
124
- are the same editor.
125
-
126
- `Ctrl-S` starts the session and drops you back in the inbox; `Ctrl-O` starts it
127
- and hands the terminal straight over. Either way the new row is selected once
128
- it shows up. Terminals send the same byte for `Ctrl-S` and `Ctrl-Shift-S`, so
129
- the second start needs a letter of its own.
130
-
131
- The directory defaults to the selected row's. It runs `claude --bg "<prompt>"`
132
- with only the flags you changed from default, in that directory.
133
-
134
- Images work as they do at Claude Code's own prompt. Paste one with `Cmd-V`
135
- (or `Ctrl-V` in a terminal that doesn't do bracketed paste), or drop a file
136
- onto the window, and it lands at the cursor as an `[Image #1]` token that
137
- moves and deletes as one character, so the prompt can point at it: "make the
138
- button look like [Image #1]". On macOS, `Cmd-Ctrl-Shift-4` copies a region of
139
- the screen; `n` and `Cmd-V` put it in the prompt. A pasted image is saved
140
- under `~/.config/claude-inbox/images/` (cleared after 14 days, as sessions
141
- are); a dropped file is referenced where it is. When the session starts, each
142
- token becomes an `@path` mention, which `claude` reads as an image the way it
143
- does at its own prompt. A dropped file that isn't an image, and pasted text,
144
- stay text, newlines included.
145
-
146
- Why the terminal can paste an image at all: with bracketed paste on, a paste
147
- arrives fenced between `\e[200~` and `\e[201~`, and an image, having no text
148
- form, arrives as an empty fence. That empty fence is the signal to read the
149
- clipboard, which is how Claude Code does it too. A dropped file arrives the
150
- same way, as its shell-escaped path. Reading the clipboard goes through
151
- `osascript`, so it is macOS only; elsewhere the paste is reported empty.
152
-
153
- Slash commands work as they do at Claude Code's own prompt: a prompt that
154
- starts with one, `/unslop README.md`, is expanded by `claude` into the skill
155
- with its arguments before the session starts; one further into the text is
156
- left for the agent to read and act on. Typing `/` at the start of any word
157
- opens a menu of what the CLI would offer — project and personal skills and
158
- commands, plugin skills as `plugin:name`, claude.ai's synced skills as
159
- `anthropic-skills:name` — narrowed as you type, each with the description
160
- from its front matter. `↑` `↓` choose, `Tab` or `Enter` drop the command in
161
- with a space after it, `Esc` closes the menu (a second `Esc` cancels the
162
- form). A `/` inside a word, as in `a/b`, opens nothing, and the menu only
163
- appears while something matches, so a path like `/Users/…` is left alone
164
- after its first letters. Built-ins such as `/init` live inside the CLI and
165
- are not listed; typing one still works. Project commands follow the
166
- Directory field.
167
-
168
- ### Why `←` comes back here and not to native agent view
169
-
170
- Inside an attached session, `←` on an empty prompt detaches. `claude attach`
171
- then execs itself in place as `claude agents`, so you would land in the native
172
- view and only get back to the inbox after quitting that. No flag or setting
173
- suppresses just that relaunch: the one switch that exists disables `attach`
174
- too. So the inbox watches its child's command line and, the moment it turns
175
- into the agents view, terminates it. Measured round trip is about 0.4s and the
176
- agents view never draws a frame. `Ctrl+Z` is handled locally by the attach
177
- client and returns directly with no trick needed.
178
-
179
- ## Rules
180
-
181
- All four rules are pure methods on `ClaudeInbox::Store::Row`, a session paired
182
- with its entry, and are the only place triage logic lives.
183
-
184
- **Wake.** A snoozed session returns when its timer elapses, when you press `u`,
185
- or when it *becomes* blocked or failed after being snoozed. A session that was
186
- already blocked when you snoozed it stays snoozed; that is the point of snoozing.
187
- `u` also brings back a settled row, whether it settled by hand or because its
188
- pull request resolved, and puts it wherever its raw state belongs — Active or
189
- Needs You. It stays there until the state actually changes again.
190
-
191
- **Acknowledge.** Attaching to a needs-you session (`Enter`) marks its current
192
- state seen, moving it to Active instead of leaving it in Needs You. It comes
193
- back to Needs You the moment its state changes again. Still blocked with a
194
- new prompt doesn't count, only an actual state change does, same as hand-settle.
101
+ `n` opens a form: prompt, name, directory, model, effort, permissions,
102
+ worktree, Remote Control. `Tab` / `Shift+Tab` move between fields, `Esc` cancels. Choice
103
+ fields cycle with `h` `l` or the arrows; "default" shows what your settings
104
+ resolve to. The directory defaults to the selected row's, and `Tab` completes
105
+ it.
106
+
107
+ `Ctrl-S` starts the session and drops you back in the inbox; `Ctrl-O` starts
108
+ it and attaches. Either way the new row is selected once it shows up. It runs
109
+ `claude --bg "<prompt>"` in that directory, with only the flags you changed.
110
+
111
+ The prompt is multi-line; `Enter` breaks a line. Paste an image with `Cmd-V`
112
+ (or `Ctrl-V`), or drop a file onto the window, and it lands at the cursor as
113
+ an `[Image #1]` token the prompt can point at: "make the button look like
114
+ [Image #1]". Pasted images are saved under `~/.config/claude-inbox/images/`
115
+ for 14 days; a dropped file is referenced where it is. Reading the clipboard
116
+ is macOS only.
117
+
118
+ Remote Control set to yes starts the session with `--remote-control`, so it
119
+ runs under the daemon like any other row and is also listed at claude.ai/code
120
+ and in the Claude mobile app. See Remote Control below.
121
+
122
+ Slash commands work as at Claude Code's own prompt. Type `/` at the start of a
123
+ word for a menu of your skills and commands, plugin skills as `plugin:name`,
124
+ claude.ai's synced skills as `anthropic-skills:name`. `↑` `↓` choose, `Tab` or
125
+ `Enter` insert, `Esc` closes the menu. Built-ins such as `/init` aren't listed
126
+ but still work.
127
+
128
+ ## How sessions move
129
+
130
+ **Wake.** A snoozed session returns when its timer elapses, when you press
131
+ `u`, or when it becomes blocked or failed after being snoozed. A session that
132
+ was already blocked when you snoozed it stays snoozed; that is the point of
133
+ snoozing. `u` also brings back a settled row and puts it wherever its raw
134
+ state belongs.
135
+
136
+ **Acknowledge.** Attaching to a needs-you session marks its current state
137
+ seen and moves it to Active. It comes back to Needs You when its state
138
+ changes again. Still blocked with a new prompt doesn't count.
195
139
 
196
140
  **Settle.** Settled by hand with `x`, or because every pull request it has is
197
141
  merged or closed. A hand-settled session stays put when it finishes, and
198
- returns as soon as it changes state in any other way: working again, blocked,
199
- or failed. `failed` never settles. A session with no pull request at all
200
- never settles on its own, however long it has been quiet — `x` is the only
201
- way in.
202
-
203
- A session with a pull request follows the PR instead: it stays Active while
204
- any of its PRs is open or a draft, however long it has been quiet, and
205
- settles the moment every one is merged or closed. A PR whose state is not
206
- known yet (no `gh`, offline) is ignored, so the session stays Active too.
207
-
208
- **Reap.** A background session quiet for `REAP_AFTER` (14 days) is deleted
209
- outright on the next poll: `claude rm`, so the transcript and the worktree go
210
- with it, same as `Ctrl-x`. This is the one thing here that destroys anything
211
- without asking first, so read the rest of this before you leave it running.
212
-
213
- Reaping does *not* key on Settled, deliberately. Settling answers "should I
214
- still be looking at this?", and the PR rule keeps a row in Active for as long
215
- as a pull request stays open, so an abandoned draft parks a session there for
216
- ever and the deadest rows in the list are precisely the ones Settled never
217
- reaches. Idle time, measured from `state_since`, is the only clock.
218
-
219
- Four things are never reaped, whatever the clock says: a `working` session, one
220
- that still holds a process, a pin, and a snooze. `failed` *is* reaped, even
221
- though it never settles. It earns a permanent row because you ought to see it,
222
- and after a fortnight of not seeing it you never will. A pin or a parked
223
- snooze ("until I wake it") is the way to keep a session indefinitely; both are
224
- deliberate gestures, so both outrank the reaper.
225
-
226
- Unpushed work is safe without us doing anything. `claude rm` refuses a worktree
227
- holding commits that aren't pushed and offers a `--discard-unpushed` token to
228
- override it; nothing here ever passes that token, so a refusal is the end of
229
- it. Refusals are logged and retried at most once a day.
230
-
231
- A session that `claude rm` has taken, by the reaper or by `Ctrl-x`, stays
232
- hidden for as long as the daemon still lists it; the store drops it from each list in between.
233
-
234
- Every reap appends a line to `~/.config/claude-inbox/reaped.log`, which is the
235
- last record a session existed once its transcript is gone. If that file can't
236
- be opened the sweep raises and nothing is deleted. `CLAUDE_INBOX_NO_REAP=1`
237
- turns the whole thing off, and `--fixture` runs never reap.
238
-
239
- ## Pull requests
240
-
241
- Claude Code already links sessions to PRs. The daemon scans each background
242
- session's transcript for links and writes them to
243
- `~/.claude/jobs/<id>/state.json` as `children` (`kind: "pr"`); `claude agents
244
- --json` does not expose that, so `JobState` reads the file. It is a link
245
- scan, so a session that only *reviews* a PR gets it too. Interactive sessions
246
- have no job file, so for them (or to correct a bad scan) `P` sets the link by
247
- hand; that lives in our own state file as `pr` and replaces the scanned list.
248
-
249
- State is seeded from `~/.claude/gh-pr-status-cache.json`, whatever Claude Code
250
- last saw, then refreshed with `gh pr view` on the poller thread, at most once a
251
- minute per PR and never for one already merged or closed. Without `gh` the
252
- cached state is all you get.
253
-
254
- `gh pr view` is a network round trip, so each poll publishes the list first,
255
- with whatever states are already known, and asks gh afterwards; the rows come
256
- round again only if an answer moved one. That is why the inbox is up in well
257
- under a second rather than after a dozen serial `gh` calls. Once gh has said a
258
- PR is merged or closed the answer is kept in `~/.config/claude-inbox/prs.json`,
259
- in the same shape as Claude Code's cache, because that cache only covers PRs
260
- its own sessions opened and a link scan picks up plenty of others — without
261
- the file every launch would ask about every merged PR again.
262
-
263
- Until the first poll lands the body is blank rather than claiming "nothing
264
- running"; past a second and a half it gets a spinner and a rotating excuse
265
- with the elapsed time, and past ten seconds a hint to check the daemon.
266
-
267
- ## Colors
268
-
269
- `/color` inside a session is the only way to set one; there is no key for it
270
- here, and nothing is stored on our side. The daemon writes it to the job file
271
- as `color`, `claude agents --json` leaves it out, so `JobState` reads it on
272
- every poll and a color you change shows up on the next one.
273
-
274
- It lands on the label and nowhere else. The glyph, the state badge and the PR
275
- badge keep the colors their own state gives them, so no color you pick can
276
- stop a blocked session looking blocked. Settled rows stay dim — that section is
277
- meant to be quiet, and a color shouting out of a collapsed fold would undo it.
278
-
279
- The eight colors `/color` offers are mapped the way Claude Code's own tmux
280
- code maps them when it tints a teammate pane: `red`, `blue`, `green`, `yellow`,
281
- `purple` and `cyan` go to the terminal's own ansi colors, so they follow your
282
- theme; `orange` and `pink` have no ansi name and go through 256-color indexes
283
- 208 and 205. Anything else is left unpainted. Colors close with SGR 39
284
- (default foreground) rather than 0, so a label that is already bold or italic
285
- stays that way.
286
-
287
- ## Usage
142
+ returns as soon as it changes state in any other way. `failed` never settles.
143
+ A session with no pull request never settles on its own, however long it has
144
+ been quiet. A session with one stays Active while any of its PRs is open or a
145
+ draft.
146
+
147
+ **Reap.** A background session quiet for 14 days is deleted on the next poll
148
+ with `claude rm`, so the transcript and the worktree go with it, same as
149
+ `Ctrl-x`. This is the one thing here that destroys anything without asking
150
+ first. Reaping does not key on Settled: an open draft keeps a row in Active
151
+ for ever, and those are precisely the rows that need reaping. Idle time is the
152
+ only clock.
153
+
154
+ Never reaped, whatever the clock says: a `working` session, one that still
155
+ holds a process, a pin, and a snooze. `failed` is reaped, even though it never
156
+ settles; after a fortnight of not looking at it you never will. A pin or a
157
+ parked snooze is the way to keep a session indefinitely.
158
+
159
+ Unpushed work is safe. `claude rm` refuses a worktree holding commits that
160
+ aren't pushed, and nothing here ever overrides that refusal. Every reap
161
+ appends a line to `~/.config/claude-inbox/reaped.log`, the last record a
162
+ session existed once its transcript is gone. `CLAUDE_INBOX_NO_REAP=1` turns
163
+ reaping off.
164
+
165
+ ## Remote Control
166
+
167
+ Every background session has a page at claude.ai/code, where you can follow
168
+ it from a browser or the Claude mobile app; `w` opens it. A session with
169
+ Remote Control on can also be driven from there, the way `claude
170
+ --remote-control` and `/rc` allow in a terminal. Its row wears a `⇅` after
171
+ the state. Two kinds of session have it:
172
+
173
+ - A background session the `n` form started with Remote Control set to yes,
174
+ or one you ran as `claude --bg "…" --remote-control` yourself. It is a
175
+ daemon row like any other: `Enter` attaches, `X` stops, and it keeps its
176
+ Remote Control across a stop and a wake, since the daemon saves the flag.
177
+ - A worker a `claude remote-control` server spawned for a request from your
178
+ phone. That is how a session started away from the desk lands on this
179
+ machine. The daemon calls it interactive and can't attach to it, so
180
+ `Enter` offers to adopt it instead: `y` ends the worker and resumes the
181
+ conversation as a background session under the same id, with Remote
182
+ Control on, and attaches. The phone session you were in ends there and a
183
+ new one takes its place, with the whole conversation; the server keeps
184
+ running for the next request. A worker nobody has messaged yet has
185
+ nothing to adopt, and Enter says so.
186
+
187
+ ## Pull request state
188
+
189
+ The daemon scans each background session's transcript for PR links, so a
190
+ session that only reviews a PR gets it too. Interactive sessions aren't
191
+ scanned; `P` sets the link by hand for those, or to correct a bad scan.
192
+
193
+ State comes from whatever Claude Code last saw, then `gh pr view` on the
194
+ poller thread, at most once a minute per PR. Without `gh` the cached state is
195
+ all you get. The list goes up before gh is asked, so the inbox is up in under
196
+ a second.
197
+
198
+ ## Setup
288
199
 
289
200
  The right end of the header shows how much of your Claude subscription's
290
- 5-hour and 7-day rate limit windows you have used, as `usage 5h 24% · 7d 41%`.
291
- It needs a Pro or Max account and a one-time setup.
201
+ 5-hour and 7-day rate limit windows you have used, as a ten-cell bar per
202
+ window that turns yellow at 70% and red at 90%, like the context bar many
203
+ status lines draw:
204
+
205
+ ```
206
+ session ██░░░░░░░░ 24% · 3h left week ████░░░░░░ 41% · 2d left
207
+ ```
208
+
209
+ `session` is the 5-hour window and `week` the 7-day one, as Claude's own
210
+ `/usage` names them; `3h left` is how long until that window resets. It needs
211
+ a Pro or Max account and a one-time setup.
292
212
 
293
213
  Claude Code passes the numbers to your [status line](https://code.claude.com/docs/en/statusline)
294
214
  script every turn, and the inbox reads them from a file that script writes.
@@ -305,138 +225,9 @@ writes one for you, then add the lines to that. Keep the `-n` check: a
305
225
  session's first status line run has no numbers yet, and writing anyway would
306
226
  blank the file.
307
227
 
308
- The label stays off until the file exists, and goes off again once the file
228
+ The bars stay off until the file exists, and goes off again once the file
309
229
  is more than fifteen minutes old.
310
230
 
311
- ## State
312
-
313
- `~/.config/claude-inbox/state.json`, keyed by session id, atomic writes.
314
- Holds `wake_at`, `snoozed_at`, `alias`, `pr`, `pinned`, `pinned_at`, `settled_at`, `acknowledged_at`, `revived_at`, `last_state`, `state_since`, `last_seen`,
315
- and `reap_failed_at` / `reap_error` for a session `claude rm` has refused.
316
- `Store::Entry` is where those names live; everything else reads an entry
317
- through it. Entries not seen in a poll for 7 days are pruned. Pruning only reaches entries
318
- the daemon has *forgotten*, which is a different thing from the reaper: the
319
- daemon still lists sessions a month old, so those keep their entry and it is
320
- the 14-day reap that clears them.
321
-
322
- ## Layout
323
-
324
- ```
325
- Sessions.load: AgentsClient → JobState → PullRequests → Poller → Store → Renderer → App
326
- (shells out) (jobs dir) (known states) (thread, then gh) (pure) (strings) (terminal + key loop)
327
- ```
328
-
329
- - `AgentsClient` is the only thing that runs `claude`. `FixtureClient` swaps in a JSON file.
330
- - `Sessions.load` is the one place the list is put together: `AgentsClient`,
331
- then `JobState`, then `PullRequests#enrich`, in that order because the links
332
- `PullRequests` wants are the ones `JobState` read. `Session` is immutable; a
333
- step that adds something hands back a copy.
334
- - `JobState` reads `~/.claude/jobs/<id>/state.json`, the daemon's own file: the scanned
335
- links `PullRequests` wants, the open work behind a `working` state, the session's
336
- own line (`detail`, `needs`, `output.result`) and the `/color` each session carries. Never cached. `enrich` returns each background session
337
- with its file attached, for `Sessions.load`.
338
- - `PullRequests` pairs each session with the PRs its `job_state` links and keeps
339
- their state fresh through `gh`. `enrich` never asks gh and runs inside
340
- `Sessions.load`; `refresh` is the slow half and runs after the list has gone
341
- up. `--fixture` points it at `test/fixtures/jobs` with `gh` off.
342
- - `Palette` maps a session color to an escape sequence and knows nothing else.
343
- - `RateLimits` reads `~/.claude/rate_limits.json`, the file a status line script writes, and gives the header its usage label. Nil without the file.
344
- - `Store` holds the last poll and the entry table behind a mutex, and folds each poll in.
345
- A key it was told to `hide` or `forget` stays out of every poll until it is released or the daemon stops listing it.
346
- `Store::Entry` is what is remembered about one session, and the only place the state file's key names appear.
347
- `Store::Row` is one session with its entry, and the rules are its methods.
348
- `Store::Sections` is one poll sorted into sections and knows where the cursor can land: the `/` filter, the fold-or-rows rule and which section a key is in live there.
349
- - `Renderer` turns sections into an array of fixed-width strings. `Painter` diffs frames
350
- and repaints only changed rows.
351
- - `Reaper` runs `claude rm` over whatever `Row#reapable?` picks and appends a
352
- line to the log for each one. `due` names them without touching anything;
353
- `sweep` does the deleting. Both run on the poller, and the list goes up
354
- between them with the rows `due` named hidden in the store, so a slow `rm`
355
- stalls neither a frame nor the first one and a row never paints while it is
356
- being deleted.
357
- - `Poller` is the thread that asks `Sessions.load` for the list, every four
358
- seconds and on demand, runs it past the `Reaper` and then the gh refresh,
359
- and hands each result to `App` over a queue. An on-demand poll queues onto
360
- that same thread rather than racing it, so a burst of them while it is idle
361
- is one `claude agents` call, one that lands mid-poll is a single follow-up,
362
- and no two polls overlap. `App` pauses it while `claude
363
- attach` has the terminal.
364
- - `Terminal` is the screen: the alt screen with its mouse and wheel modes, raw
365
- mode, the cached size and the `Painter` that diffs frames onto it. `release`
366
- lends it to `claude attach` and takes it back.
367
- - `App` owns the key loop, the poller thread and the logs thread, and is the
368
- only place that spawns a child.
369
- - `Peek` is the peek pane: whether it is open, how far back it is scrolled and
370
- what to paint for the selected row. `Logs` is where the lines come from: the
371
- `claude logs` replay of each session, fetched off the main thread, debounced
372
- and cached.
373
- - `VtScreen` is a small cursor-addressed grid used to turn the `claude logs` replay
374
- into readable lines for the peek pane.
375
- - `SlashCommands` reads the skills and commands `claude` would offer from the
376
- same directories it reads them, front matter included, for the new-session
377
- prompt's menu. Pure filesystem; it never runs `claude`.
378
- - `Dialog` is a box over the list that claims every key until it answers:
379
- the snooze menu, the stop and delete confirms, the alias and pull request
380
- prompts. Pure, like `NewSessionForm`; `App` acts on the answer.
381
- - `Mouse` turns the SGR escape sequences the terminal sends for clicks and
382
- wheel ticks into `Event`s; `App` maps a click's row back to whatever
383
- `Renderer` painted there.
384
-
385
- ## Things learned from the real CLI (2.1.273)
386
-
387
- - Terminal.app puts the tty's active process in the tab title, so a poller that
388
- forks `claude` every few seconds makes the title flicker. Every helper
389
- subprocess here is started with `setsid` so it has no controlling tty.
390
-
391
- - `claude agents --json --all` matches the documented shape exactly. A `done`
392
- session can still carry a `pid` and `status: idle`.
393
- - `claude logs <id>` is **not** plain text. It is a replay of the session's terminal
394
- output: cursor positioning, erase-line, color. Words are frequently separated by
395
- cursor motion rather than spaces, so stripping escapes yields run-together garbage.
396
- Feeding it through a screen grid produces readable text.
397
- - `claude logs` fails with "job not found" for a finished session whose process the
398
- supervisor has reaped. The peek pane shows a notice instead.
399
- - `claude attach --help` says `←` returns to agent view and `Ctrl+Z` drops to the
400
- shell. Under the hood `←` makes the attach process exec `claude agents` in place,
401
- same pid, using its own executable path, so a PATH shim never sees it.
402
- - `CLAUDE_CODE_DISABLE_AGENT_VIEW=1` disables `agents`, `attach` and `logs` alike.
403
- - A prompt beginning with a slash command is expanded, `$ARGUMENTS` and all,
404
- in `-p` and `--bg` alike (checked on 2.1.274 with a project command).
405
- Installed plugins are listed in `~/.claude/plugins/installed_plugins.json`,
406
- keyed `name@marketplace`, each pointing at its `installPath`; claude.ai's
407
- synced skills sit in `~/.claude/skills/synced/<bucket>/<name>/SKILL.md`
408
- and show up as `anthropic-skills:<name>`.
409
- - Interactive sessions (a `claude` you started in a terminal yourself) appear in
410
- the JSON with no `id` and no `state`, only `status`, and cannot be attached,
411
- peeked or stopped from outside.
412
- - `~/.claude/jobs/<id>/state.json` is where the daemon keeps what the JSON
413
- leaves out: `children` (scanned PR and issue links), `intent`, `worktreePath`,
414
- `worktreeBranch`, token count and the transcript path, plus the line the
415
- agents view prints under a row: `detail` is the session's status line,
416
- `needs` what it is waiting on while blocked, `output.result` its closing
417
- summary once done. `~/.claude/gh-pr-status-cache.json`
418
- is keyed by PR url and calls an open draft `DRAFT`; `gh pr view` reports
419
- `OPEN` plus `isDraft`.
420
-
421
- - The job file is also where `/color` ends up, as `color`. `claude agents --json`
422
- does not carry it, so a color is only ever visible by reading the file. The CLI
423
- offers eight, and its own tmux code is the authority on what they mean to a
424
- terminal: six are ansi names, `orange` and `pink` are 256-color indexes 208 and
425
- 205. Interactive sessions have no job file and so never have a color.
426
-
427
- ## Development
428
-
429
- ```
430
- bundle install
431
- bin/ci # full signoff: lint, gem audit, tests (see CONTRIBUTING.md)
432
- bin/release --dry-run # the version the commits since the last tag call for
433
- bundle exec rake test # minitest/spec, test/**/*_spec.rb
434
- bundle exec standardrb
435
- CLAUDE_INBOX_STDERR=/tmp/err.log bin/claude-inbox # crash traces off the alt screen
436
- DEBUG=1 bin/claude-inbox # slow-frame notes in /tmp/inbox-debug.log
437
- CLAUDE_INBOX_NO_REAP=1 bin/claude-inbox # never delete an idle session
438
- bin/screens # drive the fixture in a pty, print every screen
439
- ```
231
+ ## Contributing
440
232
 
441
- `bin/screens` is how a refactor is checked against the real screen: run it on
442
- `main` and on the branch and diff the two.
233
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
data/exe/claude-inbox CHANGED
@@ -19,7 +19,6 @@ if (i = ARGV.index("--fixture"))
19
19
  fixtures = File.dirname(agents)
20
20
  opts[:client] = ClaudeInbox::FixtureClient.new(agents, logs: File.binread(File.join(fixtures, "logs_raw.txt")))
21
21
  opts[:store] = ClaudeInbox::Store.new(path: nil)
22
- opts[:jobs_dir] = File.join(fixtures, "jobs")
23
22
  opts[:pull_requests] = ClaudeInbox::PullRequests.new(cache_path: File.join(fixtures, "gh-pr-status-cache.json"), resolved_path: nil, gh: nil)
24
23
  else
25
24
  # The only place the reaper is armed. Everything else — tests, --fixture,