claude-inbox 0.1.0 → 0.3.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +185 -386
  3. data/exe/claude-inbox +23 -5
  4. data/lib/claude_inbox/agents_client.rb +104 -44
  5. data/lib/claude_inbox/app.rb +114 -62
  6. data/lib/claude_inbox/config.rb +24 -0
  7. data/lib/claude_inbox/dialog.rb +10 -4
  8. data/lib/claude_inbox/images.rb +37 -6
  9. data/lib/claude_inbox/job_state.rb +32 -9
  10. data/lib/claude_inbox/keymap.rb +2 -2
  11. data/lib/claude_inbox/logs.rb +5 -2
  12. data/lib/claude_inbox/new_session_form.rb +64 -52
  13. data/lib/claude_inbox/painter.rb +32 -0
  14. data/lib/claude_inbox/palette.rb +4 -9
  15. data/lib/claude_inbox/peek.rb +4 -4
  16. data/lib/claude_inbox/poller.rb +9 -14
  17. data/lib/claude_inbox/pull_requests.rb +5 -5
  18. data/lib/claude_inbox/rate_limits.rb +19 -7
  19. data/lib/claude_inbox/reaper.rb +13 -16
  20. data/lib/claude_inbox/records.rb +7 -2
  21. data/lib/claude_inbox/remote/http.rb +137 -0
  22. data/lib/claude_inbox/remote/icon.png +0 -0
  23. data/lib/claude_inbox/remote/icon.svg +8 -0
  24. data/lib/claude_inbox/remote/listener.rb +436 -0
  25. data/lib/claude_inbox/remote/page.html +477 -0
  26. data/lib/claude_inbox/remote/pairing.rb +111 -0
  27. data/lib/claude_inbox/remote/pairing_dialog.rb +93 -0
  28. data/lib/claude_inbox/remote/start.rb +202 -0
  29. data/lib/claude_inbox/remote.rb +10 -0
  30. data/lib/claude_inbox/renderer.rb +135 -113
  31. data/lib/claude_inbox/session.rb +19 -19
  32. data/lib/claude_inbox/session_request.rb +151 -0
  33. data/lib/claude_inbox/sessions.rb +2 -3
  34. data/lib/claude_inbox/settings.rb +19 -7
  35. data/lib/claude_inbox/slash_commands.rb +6 -6
  36. data/lib/claude_inbox/store/row.rb +3 -3
  37. data/lib/claude_inbox/store.rb +11 -4
  38. data/lib/claude_inbox/subprocess.rb +12 -3
  39. data/lib/claude_inbox/terminal.rb +9 -3
  40. data/lib/claude_inbox/text.rb +13 -11
  41. data/lib/claude_inbox/text_buffer.rb +3 -2
  42. data/lib/claude_inbox/theme.rb +5 -4
  43. data/lib/claude_inbox/trust.rb +23 -0
  44. data/lib/claude_inbox/vt_screen.rb +1 -3
  45. data/lib/claude_inbox.rb +1 -1
  46. metadata +15 -2
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # claude-inbox
2
2
 
3
- Inbox-style triage on top of Claude Code's background sessions. A companion to
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**.
3
+ Inbox-style triage for Claude Code's background sessions. It reads the same
4
+ daemon state as `claude agents`, through `claude agents --json`, and adds
5
+ snooze, auto-settle and each session's pull request. Use it next to
6
+ `claude agents`, not instead of it.
7
7
 
8
8
  ![claude-inbox with one working session, one snoozed and the settled section folded](docs/screenshot.png)
9
9
 
@@ -18,281 +18,210 @@ Requires Ruby 3.2+ and a `claude` on PATH with the agents feature. Versions
18
18
  are published to rubygems and listed under GitHub Releases.
19
19
  `claude-inbox --version` prints the installed version.
20
20
 
21
+ Put arguments you want on every launch in `~/.config/claude-inbox/config`.
22
+
21
23
  ## Sections
22
24
 
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
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
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.
41
- 4. **Snoozed.** Sorted by wake time, parked ("until I wake it") entries last.
42
- Collapsed; Enter expands.
43
- 5. **Settled.** Parked with `x`, or whose pull request is merged or closed.
44
- Collapsed; Enter expands.
45
-
46
- A row with a pull request shows it after the state: `#885 open`, `#885 draft`,
47
- `#885 merged`, `#885 closed`. `o` opens it in the browser.
48
-
49
- `working` from the daemon covers two situations, and the row says which. A
50
- 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).
25
+ 1. **Pinned.** Parked at the top by hand, whatever its state. `t` toggles it.
26
+ 2. **Needs you.** `blocked` or `failed`, until you attach to it.
27
+ 3. **Active.** `working`, finished but not yet settled, interactive, or a
28
+ needs-you session you attached to that hasn't resolved. An interactive
29
+ session only reports busy, idle or waiting, which show as working, done
30
+ and needs you. A terminal you're sitting in never settles and can't be
31
+ attached, peeked or stopped from here. Sub-agents and headless `claude -p`
32
+ runs don't show up at all.
33
+ 4. **Snoozed.** Sorted by wake time, with "until I wake it" entries last.
34
+ Collapsed. Enter expands it.
35
+ 5. **Settled.** Settled with `x`, or every pull request is merged or closed.
36
+ Collapsed. Enter expands it.
37
+
38
+ A row with a pull request shows it after the state, as `#885 open`,
39
+ `#885 draft`, `#885 merged` or `#885 closed`. `o` opens it in the browser.
40
+
41
+ The daemon's `working` covers two cases, and the row tells them apart. A
42
+ spinner and `working` mean the agent is thinking. A steady `◌` and
43
+ `idle · 1 shell` mean it stopped and is waiting on something it started, such
44
+ as a `--watch` shell or a sub-agent. That's why a session with nothing left to
45
+ do can sit there for an hour.
46
+
47
+ Under each row in Pinned, Needs you and Active is the same line
48
+ `claude agents` prints. While blocked it's what the session waits on
49
+ (`↳ confirm: drop the top line?`), once done it's what it produced, and
50
+ otherwise it's the status line. A terminal you opened yourself has none, so
51
+ it shows its directory.
52
+
53
+ A session you gave a color with `/color` wears it on the label. Nothing else
54
+ is tinted, so a blocked session still looks blocked.
67
55
 
68
56
  ## Keys
69
57
 
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.
58
+ Vim bindings. Arrows work as well as `j`/`k`. In terminals that report the
59
+ mouse, a click selects and attaches and the wheel moves the selection. Apple
60
+ Terminal reports neither.
79
61
 
80
62
  | Key | Action |
81
63
  |---|---|
82
64
  | `j` `k` | move down / up |
83
65
  | `gg` `G` | first / last row |
84
66
  | `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 |
67
+ | `Enter` `l` | attach (full-screen handoff; `←` or `Ctrl+Z` return here), adopt a remote session, or expand Snoozed / Settled |
86
68
  | `Tab` `Shift+Tab` | jump to the next / previous section |
87
69
  | `h` | close the peek pane, else collapse the current Snoozed / Settled fold |
88
70
  | `za` `zo` `zc` | toggle / open / close the Snoozed or Settled fold under the cursor |
89
71
  | `p` | toggle the read-only peek pane |
90
72
  | `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) |
73
+ | `n` | new session |
74
+ | `N` | pair a phone: whether the inbox is listening, and the URL to pair with |
75
+ | `t` | pin / unpin |
93
76
  | `s` | snooze: `1` 15m · `2` 1h · `3` tomorrow 9am · `4` until woken |
94
77
  | `u` | wake a snoozed session now, or bring back one you settled |
95
78
  | `x` | settle a working or needs-you session by hand (returns when it changes state, unless it just finished) |
96
79
  | `a` | set a local alias (never touches the real session name) |
97
80
  | `o` | open the session's pull request in the browser |
81
+ | `w` | open the session at claude.ai/code |
98
82
  | `P` | link a pull request by hand (empty clears; the scanned links return) |
99
83
  | `X` | stop the session (`y` to confirm) |
100
84
  | `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 |
85
+ | `/` | filter by name, cwd, the prompt it started from, or what it is doing now; `Enter` keeps it, `Esc` clears |
102
86
  | `R` | poll now |
103
87
  | `q` | quit |
104
88
 
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.
89
+ `X` and `Ctrl-x` both ask first and both take `y`, but they do different
90
+ things. `X` runs `claude stop`, which ends the process and keeps the
91
+ conversation, and `Enter` resumes it later. `Ctrl-x` runs `claude rm`, which
92
+ deletes the session, its transcript and its worktree. Nothing resumes after
93
+ that, so read the box before answering.
115
94
 
116
95
  ### New session
117
96
 
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.
195
-
196
- **Settle.** Settled by hand with `x`, or because every pull request it has is
197
- 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
288
-
289
- 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.
292
-
293
- Claude Code passes the numbers to your [status line](https://code.claude.com/docs/en/statusline)
294
- script every turn, and the inbox reads them from a file that script writes.
295
- Put these two lines in the script, just after it reads stdin:
97
+ `n` opens a form with prompt, name, directory, model, effort, permissions,
98
+ worktree and Remote Control. `Tab` and `Shift+Tab` move between fields and
99
+ `Esc` cancels. Choice fields cycle with `h` `l` or the arrows, and "default"
100
+ shows what your settings resolve to. The directory starts as the selected
101
+ row's, and `Tab` completes it.
102
+
103
+ `Ctrl-S` starts the session and drops you back in the inbox. `Ctrl-O` starts
104
+ it and attaches. It runs `claude --bg "<prompt>"` in that directory with only
105
+ the flags you changed, and the new row is selected once it shows up.
106
+
107
+ The prompt is multi-line, and `Enter` breaks a line. Paste an image with
108
+ `Cmd-V` (or `Ctrl-V`) or drop a file on the window, and an `[Image #1]` token
109
+ lands at the cursor for the prompt to point at: "make the button look like
110
+ [Image #1]". Pasted images are kept in `~/.config/claude-inbox/images/` for
111
+ 14 days. A dropped file is referenced where it is. Pasting images works on
112
+ macOS only.
113
+
114
+ Remote Control set to yes adds `--remote-control`, so the session also shows
115
+ up at claude.ai/code and in the Claude mobile app. It defaults to yes when
116
+ "Enable Remote Control for all sessions" is on in `/config`.
117
+
118
+ Type `/` at the start of a word for a menu of your skills and commands.
119
+ Plugin skills appear as `plugin:name` and claude.ai's synced skills as
120
+ `anthropic-skills:name`. `↑` `↓` choose, `Tab` or `Enter` insert, `Esc`
121
+ closes the menu. Built-ins such as `/init` aren't listed but still work.
122
+
123
+ ## How sessions move
124
+
125
+ **Wake.** A snoozed session comes back when its timer runs out, when you
126
+ press `u`, or when it turns blocked or failed after you snoozed it. One that
127
+ was already blocked when you snoozed it stays snoozed, which is the point of
128
+ snoozing. `u` also brings back a settled row.
129
+
130
+ **Acknowledge.** Attaching to a needs-you session marks its state seen and
131
+ moves it to Active. It returns to Needs you when its state changes again. A
132
+ new prompt while still blocked doesn't count.
133
+
134
+ **Settle.** A session settles when you press `x` or when every pull request
135
+ it has is merged or closed. A hand-settled session stays put when it
136
+ finishes, and comes back on any other state change. `failed` never settles.
137
+ A session with no pull request never settles on its own, and one with an
138
+ open or draft PR stays Active.
139
+
140
+ **Reap.** On the next poll, the inbox deletes a background session that has
141
+ been quiet for 14 days with `claude rm`, transcript and worktree included.
142
+ It's the one thing here that deletes without asking. Reaping ignores Settled,
143
+ because an open draft keeps a row in Active for ever and those are exactly
144
+ the rows that need reaping. Idle time is the only clock.
145
+
146
+ The inbox never reaps a `working` session, one that still holds a process, a
147
+ pinned one or a snoozed one. It does reap `failed`, even though failed never
148
+ settles. After a fortnight of not looking at it, you won't. Pin or park a
149
+ session to keep it.
150
+
151
+ Unpushed work is safe, because `claude rm` refuses a worktree with unpushed
152
+ commits and the inbox never overrides that. Every reap adds a line to
153
+ `~/.config/claude-inbox/reaped.log`. `CLAUDE_INBOX_NO_REAP=1` turns reaping
154
+ off.
155
+
156
+ ## Remote Control
157
+
158
+ A session with Remote Control on has a page at claude.ai/code, where you can
159
+ follow and drive it from a browser or the Claude mobile app. `w` opens it,
160
+ and the row shows `⇅` after the state. Two kinds of session have it.
161
+
162
+ - A background session started with Remote Control on, from the `n` form or
163
+ as `claude --bg "…" --remote-control`. It behaves like any other row, and
164
+ keeps Remote Control across a stop and a resume.
165
+ - A worker a `claude remote-control` server spawned when you started a
166
+ session from your phone. The daemon can't attach to it, so `Enter` offers
167
+ to adopt it. `y` ends the worker, resumes the conversation as a background
168
+ session under the same id with Remote Control on, and attaches. The phone
169
+ session you were in ends and a new one with the whole conversation takes
170
+ its place. The server keeps running. A worker nobody has messaged yet has
171
+ nothing to adopt.
172
+
173
+ ## Starting sessions from your phone
174
+
175
+ With `--listen-lan`, a phone on your Wi-Fi or VPN can start sessions the way
176
+ `n` does. It's off unless you ask for it.
177
+
178
+ ![The inbox beside the phone form, pairing and then starting a session](docs/remote-start.gif)
179
+
180
+ 1. Start the inbox with `claude-inbox --listen-lan`, or put `--listen-lan`
181
+ in `~/.config/claude-inbox/config` or `export CLAUDE_INBOX_LISTEN=lan` in
182
+ your shell rc.
183
+ 2. Press `N`, then `c` to copy the pairing URL. Universal Clipboard takes it
184
+ to the phone.
185
+ 3. Open it in Safari. To keep it, tap Share, then Add to Home Screen.
186
+ 4. Open the new Inbox icon. It has its own storage, apart from Safari's, so
187
+ paste the pairing URL once more and tap Pair.
188
+
189
+ LAN mode is plain HTTP, so the token and every prompt cross the network
190
+ readable by anyone on it. Use it only on a network you control. Anyone with
191
+ the token can start sessions as you in your projects. If the URL leaks, press
192
+ `r` in the `N` dialog to replace it.
193
+
194
+ A remote start may use the `default`, `auto` and `plan` permission modes.
195
+ [docs/remote-start.md](docs/remote-start.md) covers the rest, including ssh,
196
+ the firewall prompt, the JSON API and an iOS Shortcut.
197
+
198
+ ## Pull request state
199
+
200
+ The daemon scans each background session's transcript for PR links, so a
201
+ session that only reviews a PR gets it too. It doesn't scan interactive
202
+ sessions. Use `P` to link one by hand, or to fix a bad scan.
203
+
204
+ The state comes from Claude Code's own cache, then from `gh pr view` at most
205
+ once a minute per PR. Without `gh`, you only get the cached state.
206
+
207
+ ## Usage bars
208
+
209
+ The right end of the header shows how much of your 5-hour and 7-day rate
210
+ limit windows you've used. Each is a ten-cell bar that turns yellow at 70%
211
+ and red at 90%.
212
+
213
+ ```
214
+ session ██░░░░░░░░ 24% · 3h left week ████░░░░░░ 41% · 2d left
215
+ ```
216
+
217
+ `session` is the 5-hour window and `week` the 7-day one, as `/usage` names
218
+ them. `3h left` is the time until that window resets. The bars need a Pro or
219
+ Max account and a one-time setup.
220
+
221
+ Claude Code passes the numbers to your
222
+ [status line](https://code.claude.com/docs/en/statusline) script every turn,
223
+ and the inbox reads them from a file that script writes. Put these lines in
224
+ the script, just after it reads stdin:
296
225
 
297
226
  ```bash
298
227
  input=$(cat)
@@ -300,143 +229,13 @@ limits=$(echo "$input" | jq -c '.rate_limits // empty')
300
229
  [ -n "$limits" ] && echo "$limits" > ~/.claude/rate_limits.json.tmp && mv ~/.claude/rate_limits.json.tmp ~/.claude/rate_limits.json
301
230
  ```
302
231
 
303
- If you have no status line script, run `/statusline` in any session and it
304
- writes one for you, then add the lines to that. Keep the `-n` check: a
305
- session's first status line run has no numbers yet, and writing anyway would
306
- blank the file.
307
-
308
- The label stays off until the file exists, and goes off again once the file
309
- is more than fifteen minutes old.
232
+ With no status line script, run `/statusline` in any session to get one,
233
+ then add the lines. Keep the `-n` check. A session's first status line run
234
+ has no numbers yet, and writing anyway would blank the file.
310
235
 
311
- ## State
236
+ The bars stay hidden until the file exists, and hide again once it's more
237
+ than fifteen minutes old.
312
238
 
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
- ```
239
+ ## Contributing
440
240
 
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.
241
+ See [CONTRIBUTING.md](CONTRIBUTING.md).