claude-inbox 0.3.0 → 0.4.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 +4 -4
- data/README.md +86 -164
- data/exe/claude-inbox +44 -0
- data/lib/claude_inbox/actions.rb +91 -0
- data/lib/claude_inbox/agents_client.rb +11 -7
- data/lib/claude_inbox/app.rb +48 -14
- data/lib/claude_inbox/headless.rb +31 -0
- data/lib/claude_inbox/job_state.rb +4 -4
- data/lib/claude_inbox/keymap.rb +0 -2
- data/lib/claude_inbox/mod.rb +71 -0
- data/lib/claude_inbox/new_session_form.rb +18 -17
- data/lib/claude_inbox/painter.rb +3 -4
- data/lib/claude_inbox/paste.rb +2 -0
- data/lib/claude_inbox/poller.rb +48 -25
- data/lib/claude_inbox/pull_requests.rb +31 -17
- data/lib/claude_inbox/rate_limits.rb +1 -3
- data/lib/claude_inbox/remote/http.rb +10 -7
- data/lib/claude_inbox/remote/listener.rb +2 -3
- data/lib/claude_inbox/remote/start.rb +2 -3
- data/lib/claude_inbox/renderer.rb +4 -4
- data/lib/claude_inbox/settings.rb +1 -0
- data/lib/claude_inbox/slash_commands.rb +1 -0
- data/lib/claude_inbox/snapshot.rb +48 -0
- data/lib/claude_inbox/store/entry.rb +0 -2
- data/lib/claude_inbox/store/row.rb +7 -3
- data/lib/claude_inbox/store.rb +24 -9
- data/lib/claude_inbox/switch.rb +40 -0
- data/lib/claude_inbox/text_buffer.rb +84 -33
- data/lib/claude_inbox/writer_lock.rb +65 -0
- data/lib/claude_inbox.rb +1 -10
- data/mod/.claude-plugin/plugin.json +20 -0
- data/mod/hooks/hooks.json +1 -0
- data/mod/hooks/register.tsx +470 -0
- data/mod/tests/contract.test.tsx +23 -0
- data/mod/tests/pane.test.tsx +199 -0
- data/mod/tests/snapshot.fixture.ts +47 -0
- data/mod/tests/snooze.test.tsx +31 -0
- data/mod/tsconfig.json +3 -0
- data/mod/types/index.d.ts +30 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c29f6bef2ba7ecc7f4d63161cfc064517e21f95a3a187313a1c5b882ae34786
|
|
4
|
+
data.tar.gz: 5195d163ef619f7f82a7224ca0a9b4dfe508fd23d8c1b3d87ab0a7b3b419cb28
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b051d9eeba2b19da5c9ced63f9dcd00030619cc4c7ac667f873da8db411ecf3c777352d2b373dfb7def00c9737c6d1ed0bc8edb0663b0a73d850c01e429d20d
|
|
7
|
+
data.tar.gz: cfe120a05f387965ba538ea48dbf726ad1bf379684b1515d213bead19d02803be8030b14abd938457671c6d80fe0d65ce1de1715632db8c304b768a5e6d46ae8
|
data/README.md
CHANGED
|
@@ -1,227 +1,156 @@
|
|
|
1
1
|
# claude-inbox
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
snooze, auto-settle and each session's pull request. Use it next to
|
|
6
|
-
`claude agents`, not instead of it.
|
|
3
|
+
An inbox for Claude Code's background sessions. It lists what `claude agents`
|
|
4
|
+
lists and adds snooze, settle, pinning and each session's pull request.
|
|
7
5
|
|
|
8
6
|

|
|
9
7
|
|
|
10
8
|
## Install
|
|
11
9
|
|
|
10
|
+
Requires Ruby 3.2+ and `claude` on PATH.
|
|
11
|
+
|
|
12
12
|
```
|
|
13
13
|
gem install claude-inbox
|
|
14
14
|
claude-inbox
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Requires Ruby 3.2+ and a `claude` on PATH with the agents feature. Versions
|
|
18
|
-
are published to rubygems and listed under GitHub Releases.
|
|
19
|
-
`claude-inbox --version` prints the installed version.
|
|
20
|
-
|
|
21
17
|
Put arguments you want on every launch in `~/.config/claude-inbox/config`.
|
|
22
18
|
|
|
23
|
-
|
|
19
|
+
To add the pane to every Claude Code session:
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
```
|
|
22
|
+
claude-inbox install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`claude-inbox install --force` also replaces any other copy of the pane.
|
|
26
|
+
|
|
27
|
+
## Claude Code pane
|
|
28
|
+
|
|
29
|
+
The same inbox as a pane inside Claude Code, docked beside the transcript in
|
|
30
|
+
a fullscreen terminal.
|
|
31
|
+
|
|
32
|
+

|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
`/inbox` opens it, `ctrl+x tab` focuses it and `esc` leaves. The keys are the
|
|
35
|
+
ones below, with `ge` for `G`, `n` `p` for `Tab` `Shift+Tab`, and no `a`,
|
|
36
|
+
`P`, `X`, `Ctrl-x`, `/` or `R`. `Enter` switches to the session when you
|
|
37
|
+
attached through `claude-inbox`.
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
39
|
+
It opens when a session starts. To stop that, set
|
|
40
|
+
`pluginConfigs.inbox-pane.options.openOnStart` to `false` in
|
|
41
|
+
`~/.claude/settings.json`.
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
## Sections
|
|
44
|
+
|
|
45
|
+
1. **Pinned.** Rows you pinned with `t`.
|
|
46
|
+
2. **Needs you.** Blocked or failed sessions you haven't attached to yet.
|
|
47
|
+
3. **Active.** Everything else that's running or not yet settled, including
|
|
48
|
+
interactive terminals.
|
|
49
|
+
4. **Snoozed.** Sorted by wake time. Collapsed.
|
|
50
|
+
5. **Settled.** Settled with `x`, or every pull request is merged or closed.
|
|
51
|
+
Collapsed.
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
Under each row is what the session is waiting on, what it produced, or its
|
|
54
|
+
status line. A row with a pull request shows it as `#885 open`, `#885 merged`
|
|
55
|
+
and so on. A steady `◌` with `idle · 1 shell` means the agent stopped and is
|
|
56
|
+
waiting on a shell or sub-agent it started.
|
|
55
57
|
|
|
56
58
|
## Keys
|
|
57
59
|
|
|
58
|
-
Vim bindings. Arrows
|
|
59
|
-
mouse, a click selects and attaches and the wheel moves the selection. Apple
|
|
60
|
-
Terminal reports neither.
|
|
60
|
+
Vim bindings. Arrows and the mouse work too.
|
|
61
61
|
|
|
62
62
|
| Key | Action |
|
|
63
63
|
|---|---|
|
|
64
64
|
| `j` `k` | move down / up |
|
|
65
65
|
| `gg` `G` | first / last row |
|
|
66
66
|
| `Ctrl-d` `Ctrl-u` | half page down / up (`Ctrl-f` `Ctrl-b` full page) |
|
|
67
|
-
| `Enter` `l` | attach (
|
|
68
|
-
| `Tab` `Shift+Tab` |
|
|
69
|
-
| `h` | close the peek pane, else collapse the current
|
|
70
|
-
| `za` `zo` `zc` | toggle / open / close the
|
|
67
|
+
| `Enter` `l` | attach (`←` or `Ctrl+Z` returns here), adopt a remote session, or expand Snoozed / Settled |
|
|
68
|
+
| `Tab` `Shift+Tab` | next / previous section |
|
|
69
|
+
| `h` | close the peek pane, else collapse the current fold |
|
|
70
|
+
| `za` `zo` `zc` | toggle / open / close the fold under the cursor |
|
|
71
71
|
| `p` | toggle the read-only peek pane |
|
|
72
72
|
| `J` `K` (`Ctrl-e` `Ctrl-y`) | scroll the peek pane |
|
|
73
73
|
| `n` | new session |
|
|
74
|
-
| `N` | pair a phone
|
|
74
|
+
| `N` | pair a phone |
|
|
75
75
|
| `t` | pin / unpin |
|
|
76
76
|
| `s` | snooze: `1` 15m · `2` 1h · `3` tomorrow 9am · `4` until woken |
|
|
77
|
-
| `u` | wake a snoozed session
|
|
78
|
-
| `x` | settle
|
|
79
|
-
| `a` | set a local alias
|
|
80
|
-
| `o` | open the
|
|
77
|
+
| `u` | wake a snoozed session, or bring back a settled one |
|
|
78
|
+
| `x` | settle by hand |
|
|
79
|
+
| `a` | set a local alias |
|
|
80
|
+
| `o` | open the pull request in the browser |
|
|
81
81
|
| `w` | open the session at claude.ai/code |
|
|
82
|
-
| `P` | link a pull request by hand (empty clears
|
|
83
|
-
| `X` | stop the session
|
|
84
|
-
| `Ctrl-x` | delete the session
|
|
85
|
-
| `/` | filter
|
|
82
|
+
| `P` | link a pull request by hand (empty clears) |
|
|
83
|
+
| `X` | stop the session; `Enter` resumes it later |
|
|
84
|
+
| `Ctrl-x` | delete the session, transcript and worktree for good |
|
|
85
|
+
| `/` | filter; `Enter` keeps it, `Esc` clears |
|
|
86
86
|
| `R` | poll now |
|
|
87
87
|
| `q` | quit |
|
|
88
88
|
|
|
89
|
-
`X` and `Ctrl-x` both ask
|
|
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.
|
|
89
|
+
`X` and `Ctrl-x` both ask for `y` first.
|
|
94
90
|
|
|
95
91
|
### New session
|
|
96
92
|
|
|
97
|
-
`n` opens a form
|
|
98
|
-
worktree and Remote Control. `Tab`
|
|
99
|
-
|
|
100
|
-
shows what your settings resolve to. The directory starts as the selected
|
|
101
|
-
row's, and `Tab` completes it.
|
|
93
|
+
`n` opens a form for the prompt, name, directory, model, effort, permissions,
|
|
94
|
+
worktree and Remote Control. `Tab` moves between fields, `Ctrl-S` starts the
|
|
95
|
+
session and `Ctrl-O` starts it and attaches.
|
|
102
96
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
the flags you changed, and the new row is selected once it shows up.
|
|
97
|
+
Paste an image on macOS, or drop a file into the prompt, and refer to it as
|
|
98
|
+
`[Image #1]`. Type `/` for a menu of your skills.
|
|
106
99
|
|
|
107
|
-
|
|
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.
|
|
100
|
+
## How sessions move
|
|
113
101
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"Enable Remote Control for all sessions" is on in `/config`.
|
|
102
|
+
A snoozed session wakes when its timer runs out, when you press `u`, or when
|
|
103
|
+
it becomes blocked or failed.
|
|
117
104
|
|
|
118
|
-
|
|
119
|
-
|
|
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.
|
|
105
|
+
Attaching to a needs-you session moves it to Active until its state changes
|
|
106
|
+
again.
|
|
122
107
|
|
|
123
|
-
|
|
108
|
+
A session settles when you press `x` or when all its pull requests are merged
|
|
109
|
+
or closed. A session without a pull request only settles by hand.
|
|
124
110
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
111
|
+
The inbox deletes background sessions that have been idle for 14 days, with
|
|
112
|
+
`claude rm`. Working, pinned and snoozed sessions are kept, and `claude rm`
|
|
113
|
+
refuses a worktree with unpushed commits. Each deletion is logged to
|
|
114
|
+
`~/.config/claude-inbox/reaped.log`. Set `CLAUDE_INBOX_NO_REAP=1` to turn it
|
|
154
115
|
off.
|
|
155
116
|
|
|
156
117
|
## Remote Control
|
|
157
118
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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.
|
|
119
|
+
Sessions with Remote Control show `⇅`, and `w` opens them at claude.ai/code.
|
|
120
|
+
A session started from your phone through `claude remote-control` can't be
|
|
121
|
+
attached directly, so `Enter` offers to adopt it as a background session.
|
|
172
122
|
|
|
173
123
|
## Starting sessions from your phone
|
|
174
124
|
|
|
175
|
-
With `--listen-lan`, a phone on your
|
|
176
|
-
`n` does. It's off unless you ask for it.
|
|
125
|
+
With `--listen-lan`, a phone on your network can start sessions.
|
|
177
126
|
|
|
178
127
|

|
|
179
128
|
|
|
180
|
-
1.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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.
|
|
129
|
+
1. Run `claude-inbox --listen-lan`.
|
|
130
|
+
2. Press `N`, then `c` to copy the pairing URL.
|
|
131
|
+
3. Open it on the phone in Safari and tap Share, then Add to Home Screen.
|
|
132
|
+
4. Open the Inbox icon, paste the URL again and tap Pair.
|
|
188
133
|
|
|
189
|
-
LAN mode is plain HTTP
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
`r` in the `N` dialog to replace it.
|
|
134
|
+
LAN mode is plain HTTP. Use it only on a network you control, and press `r`
|
|
135
|
+
in the `N` dialog if the URL leaks. [docs/remote-start.md](docs/remote-start.md)
|
|
136
|
+
covers ssh, the JSON API and an iOS Shortcut.
|
|
193
137
|
|
|
194
|
-
|
|
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.
|
|
138
|
+
## Pull requests
|
|
197
139
|
|
|
198
|
-
|
|
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.
|
|
140
|
+
Pull requests show up when a session's transcript links to them. Install
|
|
141
|
+
`gh` to keep their state current. Use `P` to link one by hand.
|
|
206
142
|
|
|
207
143
|
## Usage bars
|
|
208
144
|
|
|
209
|
-
The
|
|
210
|
-
limit windows you've used. Each is a ten-cell bar that turns yellow at 70%
|
|
211
|
-
and red at 90%.
|
|
145
|
+
The header can show your 5-hour and 7-day rate limit usage:
|
|
212
146
|
|
|
213
147
|
```
|
|
214
148
|
session ██░░░░░░░░ 24% · 3h left week ████░░░░░░ 41% · 2d left
|
|
215
149
|
```
|
|
216
150
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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:
|
|
151
|
+
This needs a Pro or Max account. Add these lines to your
|
|
152
|
+
[status line](https://code.claude.com/docs/en/statusline) script, just after
|
|
153
|
+
it reads stdin. Run `/statusline` first if you don't have one.
|
|
225
154
|
|
|
226
155
|
```bash
|
|
227
156
|
input=$(cat)
|
|
@@ -229,13 +158,6 @@ limits=$(echo "$input" | jq -c '.rate_limits // empty')
|
|
|
229
158
|
[ -n "$limits" ] && echo "$limits" > ~/.claude/rate_limits.json.tmp && mv ~/.claude/rate_limits.json.tmp ~/.claude/rate_limits.json
|
|
230
159
|
```
|
|
231
160
|
|
|
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.
|
|
235
|
-
|
|
236
|
-
The bars stay hidden until the file exists, and hide again once it's more
|
|
237
|
-
than fifteen minutes old.
|
|
238
|
-
|
|
239
161
|
## Contributing
|
|
240
162
|
|
|
241
163
|
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
data/exe/claude-inbox
CHANGED
|
@@ -8,8 +8,30 @@ if ARGV.include?("--version")
|
|
|
8
8
|
exit
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
require "claude_inbox/mod"
|
|
12
|
+
|
|
13
|
+
# The stable path to the Claude Code pane, for CLAUDE_CODE_PLUGIN_DIRS.
|
|
14
|
+
if ARGV.include?("--mod-dir")
|
|
15
|
+
puts ClaudeInbox::Mod.link(force: true)
|
|
16
|
+
exit
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if ARGV.first == "install"
|
|
20
|
+
begin
|
|
21
|
+
installed = ClaudeInbox::Mod.install(force: ARGV.include?("--force"))
|
|
22
|
+
rescue JSON::ParserError
|
|
23
|
+
abort "claude-inbox: ~/.claude/settings.json is not valid JSON, so it was left alone"
|
|
24
|
+
end
|
|
25
|
+
installed.removed.each { |dir| puts "Removed #{dir} from ~/.claude/settings.json." }
|
|
26
|
+
puts "The pane now runs #{ClaudeInbox::Mod::COMMAND || "claude-inbox from your PATH"}." if installed.command_set
|
|
27
|
+
puts installed.added ? "Added the pane to ~/.claude/settings.json. New Claude Code sessions load it; /inbox opens it." :
|
|
28
|
+
"The pane is already in ~/.claude/settings.json."
|
|
29
|
+
exit
|
|
30
|
+
end
|
|
31
|
+
|
|
11
32
|
require "claude_inbox/app"
|
|
12
33
|
require "claude_inbox/config"
|
|
34
|
+
require "claude_inbox/writer_lock"
|
|
13
35
|
require "tmpdir"
|
|
14
36
|
|
|
15
37
|
fixture = ARGV.include?("--fixture")
|
|
@@ -44,8 +66,30 @@ else
|
|
|
44
66
|
opts[:client] = ClaudeInbox::AgentsClient.new
|
|
45
67
|
opts[:store] = ClaudeInbox::Store.new
|
|
46
68
|
opts[:reaper] = ClaudeInbox::Reaper.new(opts[:client], opts[:store])
|
|
69
|
+
opts[:snapshot] = ClaudeInbox::Snapshot.new
|
|
70
|
+
opts[:switch] = ClaudeInbox::Switch.new
|
|
71
|
+
opts[:actions] = ClaudeInbox::Actions.new
|
|
47
72
|
opts[:listen] = listen
|
|
48
73
|
end
|
|
49
74
|
opts[:color] = false if argv.include?("--no-color")
|
|
50
75
|
|
|
76
|
+
if argv.include?("--headless")
|
|
77
|
+
abort "claude-inbox: --headless takes no --fixture" if fixture
|
|
78
|
+
require "claude_inbox/headless"
|
|
79
|
+
exit(ClaudeInbox::Headless.new(client: opts[:client], store: opts[:store]).run ? 0 : 1)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The first inbox you can see is the snapshot's writer: a headless one gives
|
|
83
|
+
# way to it, and a second inbox reads along, writing and deleting nothing.
|
|
84
|
+
# Held for the life of this process by this reference.
|
|
85
|
+
writer_lock = fixture ? ClaudeInbox::WriterLock.disabled : ClaudeInbox::WriterLock.new
|
|
86
|
+
unless writer_lock.take(role: "inbox")
|
|
87
|
+
opts[:store] = ClaudeInbox::Store.new(writable: false, relay: ClaudeInbox::Actions.new)
|
|
88
|
+
opts[:reaper] = ClaudeInbox::Reaper.disabled
|
|
89
|
+
opts[:snapshot] = ClaudeInbox::Snapshot.disabled
|
|
90
|
+
opts[:actions] = ClaudeInbox::Actions.disabled
|
|
91
|
+
end
|
|
92
|
+
# So the pane settings.json names is the installed version's, after a gem update too.
|
|
93
|
+
ClaudeInbox::Mod.link unless fixture
|
|
94
|
+
|
|
51
95
|
ClaudeInbox::App.new(**opts).run
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module ClaudeInbox
|
|
7
|
+
# What another front end asks the inbox to do to a session: one file per
|
|
8
|
+
# request under a directory, taken and applied here, so the triage rules
|
|
9
|
+
# and the state file have one owner. A request for a session the last
|
|
10
|
+
# poll did not list waits a little, since it may have just started; one
|
|
11
|
+
# the store cannot honour after that, or a verb it lacks, is dropped.
|
|
12
|
+
class Actions
|
|
13
|
+
DEFAULT_DIR = File.join(Dir.home, ".config", "claude-inbox", "actions")
|
|
14
|
+
VERBS = %w[settle wake pin snooze acknowledge].freeze
|
|
15
|
+
SNOOZES = %w[m15 h1 tomorrow_9am until_woken].freeze
|
|
16
|
+
# A file still being written parses as nothing for milliseconds; a
|
|
17
|
+
# session not yet polled appears within one poll.
|
|
18
|
+
GRACE = 10
|
|
19
|
+
|
|
20
|
+
def self.disabled = new(dir: nil)
|
|
21
|
+
|
|
22
|
+
def initialize(dir: DEFAULT_DIR, clock: -> { Time.now })
|
|
23
|
+
@dir = dir
|
|
24
|
+
@clock = clock
|
|
25
|
+
@sent = 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Leaves one request for the writer, as the pane does; `from` names the
|
|
29
|
+
# asker, and a count keeps two requests in one millisecond apart.
|
|
30
|
+
def request(action, id, choice: nil, from: "inbox-#{Process.pid}")
|
|
31
|
+
return unless @dir
|
|
32
|
+
FileUtils.mkdir_p(@dir)
|
|
33
|
+
at = (@clock.call.to_f * 1000).to_i
|
|
34
|
+
request = {"action" => action, "id" => id, "at" => at}
|
|
35
|
+
request["choice"] = choice if choice
|
|
36
|
+
@sent += 1
|
|
37
|
+
File.write(File.join(@dir, "#{from}-#{at}-#{@sent}.json"), JSON.generate(request))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Applies every request on disk to `store` and returns how many it took.
|
|
41
|
+
def drain(store)
|
|
42
|
+
return 0 unless @dir && File.directory?(@dir)
|
|
43
|
+
keys = store.sessions.select(&:actionable?).map(&:key)
|
|
44
|
+
Dir.glob(File.join(@dir, "*.json")).sort.count { |path| take(path, store, keys) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# One file: false when it is left for a later pass or dropped, true once
|
|
50
|
+
# applied. Another drainer may take it first, which is nothing to report.
|
|
51
|
+
def take(path, store, keys)
|
|
52
|
+
request = parse(path)
|
|
53
|
+
return false if request.nil?
|
|
54
|
+
return false if !keys.include?(request["id"]) && young?(request)
|
|
55
|
+
File.delete(path)
|
|
56
|
+
apply(store, request, keys)
|
|
57
|
+
rescue Errno::ENOENT
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Nil leaves the file for the next pass: it is torn or early, and young.
|
|
62
|
+
def parse(path)
|
|
63
|
+
request = JSON.parse(File.read(path))
|
|
64
|
+
request = {} unless request.is_a?(Hash)
|
|
65
|
+
return request if File.mtime(path) < @clock.call - GRACE
|
|
66
|
+
(request.key?("id") && request.key?("action")) ? request : nil
|
|
67
|
+
rescue JSON::ParserError
|
|
68
|
+
(File.mtime(path) < @clock.call - GRACE) ? {} : nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def young?(request)
|
|
72
|
+
at = request["at"].to_i / 1000
|
|
73
|
+
at.positive? && Time.at(at) >= @clock.call - GRACE
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def apply(store, request, keys)
|
|
77
|
+
id = request["id"]
|
|
78
|
+
return false unless keys.include?(id) && VERBS.include?(request["action"])
|
|
79
|
+
case request["action"]
|
|
80
|
+
when "settle" then store.settle(id)
|
|
81
|
+
when "wake" then store.wake(id)
|
|
82
|
+
when "pin" then store.toggle_pin(id)
|
|
83
|
+
when "acknowledge" then store.acknowledge(id)
|
|
84
|
+
when "snooze"
|
|
85
|
+
return false unless SNOOZES.include?(request["choice"])
|
|
86
|
+
store.snooze(id, request["choice"].to_sym)
|
|
87
|
+
end
|
|
88
|
+
true
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "debug"
|
|
5
5
|
require_relative "job_state"
|
|
6
|
+
require_relative "session"
|
|
6
7
|
require_relative "subprocess"
|
|
7
8
|
|
|
8
9
|
module ClaudeInbox
|
|
@@ -45,9 +46,11 @@ module ClaudeInbox
|
|
|
45
46
|
# that exists disables attach too. So we watch the child's command line
|
|
46
47
|
# and, the moment it becomes the agents view, terminate it. The user then
|
|
47
48
|
# lands back in the inbox instead of native agent view.
|
|
48
|
-
|
|
49
|
+
# The block, polled with the command line, ends the attach early when it
|
|
50
|
+
# answers true: how a switch request from another front end is honoured.
|
|
51
|
+
def attach(id, &stop)
|
|
49
52
|
pid = Process.spawn(*Subprocess.command(@bin, "attach", id))
|
|
50
|
-
watchdog = Thread.new { kill_when_agents_view(pid) }
|
|
53
|
+
watchdog = Thread.new { kill_when_agents_view(pid, &stop) }
|
|
51
54
|
_, status = Process.wait2(pid)
|
|
52
55
|
status
|
|
53
56
|
ensure
|
|
@@ -203,8 +206,6 @@ module ClaudeInbox
|
|
|
203
206
|
pid, ppid, *cmd = l.split
|
|
204
207
|
[pid.to_i, ppid.to_i, cmd.join(" ")]
|
|
205
208
|
}
|
|
206
|
-
rescue Errno::ENOENT
|
|
207
|
-
[]
|
|
208
209
|
end
|
|
209
210
|
|
|
210
211
|
# pid => command, for the given parent pids.
|
|
@@ -218,15 +219,18 @@ module ClaudeInbox
|
|
|
218
219
|
}
|
|
219
220
|
end
|
|
220
221
|
|
|
221
|
-
def kill_when_agents_view(pid)
|
|
222
|
+
def kill_when_agents_view(pid, &stop)
|
|
222
223
|
loop do
|
|
223
224
|
sleep WATCH_INTERVAL
|
|
224
225
|
cmd = Subprocess.capture("ps", "-o", "command=", "-p", pid.to_s).out
|
|
225
226
|
break if cmd.empty?
|
|
226
|
-
next unless cmd.split[1] == "agents"
|
|
227
|
+
next unless cmd.split[1] == "agents" || stop&.call
|
|
227
228
|
Debug.log("watchdog saw #{cmd.inspect}")
|
|
228
229
|
Process.kill("TERM", pid)
|
|
229
|
-
|
|
230
|
+
10.times do
|
|
231
|
+
sleep 0.1
|
|
232
|
+
break if Subprocess.capture("ps", "-o", "pid=", "-p", pid.to_s).out.empty?
|
|
233
|
+
end
|
|
230
234
|
Process.kill("KILL", pid)
|
|
231
235
|
break
|
|
232
236
|
end
|