claude-inbox 0.6.0 → 0.7.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 +1 -3
- data/exe/claude-inbox +2 -125
- data/lib/claude_inbox/agents_client.rb +1 -11
- data/lib/claude_inbox/app/attaching.rb +77 -0
- data/lib/claude_inbox/app/browsing.rb +29 -0
- data/lib/claude_inbox/app/filtering.rb +37 -0
- data/lib/claude_inbox/app/navigating.rb +91 -0
- data/lib/claude_inbox/app/spawning.rb +62 -0
- data/lib/claude_inbox/app/triaging.rb +76 -0
- data/lib/claude_inbox/app/updating.rb +56 -0
- data/lib/claude_inbox/app.rb +93 -470
- data/lib/claude_inbox/cli.rb +201 -0
- data/lib/claude_inbox/input.rb +65 -0
- data/lib/claude_inbox/job_state.rb +10 -2
- data/lib/claude_inbox/keymap.rb +2 -4
- data/lib/claude_inbox/mod.rb +8 -5
- data/lib/claude_inbox/mouse.rb +1 -1
- data/lib/claude_inbox/pull_requests.rb +2 -3
- data/lib/claude_inbox/remote/listener.rb +6 -7
- data/lib/claude_inbox/renderer.rb +12 -39
- data/lib/claude_inbox/session.rb +4 -0
- data/lib/claude_inbox/slash_commands.rb +6 -7
- data/lib/claude_inbox/store/row.rb +1 -1
- data/lib/claude_inbox/store/sections.rb +2 -5
- data/lib/claude_inbox/store.rb +1 -1
- data/lib/claude_inbox/vt_screen.rb +4 -6
- data/lib/claude_inbox.rb +1 -1
- data/mod/hooks/register.tsx +11 -5
- data/mod/tests/pane.test.tsx +32 -0
- metadata +10 -3
- data/lib/claude_inbox/logs.rb +0 -68
- data/lib/claude_inbox/peek.rb +0 -77
data/mod/hooks/register.tsx
CHANGED
|
@@ -197,14 +197,20 @@ async function requestSwitch($: EngineInterface, host: Host, id: string): Promis
|
|
|
197
197
|
})
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
200
|
+
// Every key that takes the person out of the pane, to another session or the browser, calls
|
|
201
|
+
// this first: the pane would keep the keys while they are away, and their typing on coming
|
|
202
|
+
// back would land in it. The engine has no blur: reopening without `focus` is the one way
|
|
203
|
+
// to give the keys to the prompt.
|
|
203
204
|
async function handBackKeys($: EngineInterface): Promise<void> {
|
|
204
205
|
await $.ui.close({ id: PANE })
|
|
205
206
|
await $.ui.open(OPEN)
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
async function openInBrowser($: EngineInterface, url: string): Promise<void> {
|
|
210
|
+
await handBackKeys($)
|
|
211
|
+
await $.process.run(['open', url])
|
|
212
|
+
}
|
|
213
|
+
|
|
208
214
|
// Only while no inbox writes the snapshot; the gem's writer lock turns a second one away.
|
|
209
215
|
function startHeadless($: EngineInterface, host: Host, now: number): void {
|
|
210
216
|
if (host.isHeadlessRunning || now - host.headlessStartedAt < RESTART_S) return
|
|
@@ -410,8 +416,8 @@ export const register: Register = (on, options) => {
|
|
|
410
416
|
$.ui.toast(copied.isCopied ? `copied: claude attach ${row.id}` : 'no clipboard here'),
|
|
411
417
|
)
|
|
412
418
|
}),
|
|
413
|
-
key('o', () => (row?.pr ? void
|
|
414
|
-
key('w', () => (row?.remote ? void
|
|
419
|
+
key('o', () => (row?.pr ? void openInBrowser($, row.pr.url) : $.ui.toast('no pull request linked'))),
|
|
420
|
+
key('w', () => (row?.remote ? void openInBrowser($, row.remote) : $.ui.toast('no claude.ai/code page for this session'))),
|
|
415
421
|
key('q', () => void $.ui.close({ id: PANE })),
|
|
416
422
|
]
|
|
417
423
|
|
data/mod/tests/pane.test.tsx
CHANGED
|
@@ -220,6 +220,38 @@ test('a switch hands the keys back to the prompt before it asks, so a return lan
|
|
|
220
220
|
await ui.unmount()
|
|
221
221
|
})
|
|
222
222
|
|
|
223
|
+
test('o and w hand the keys back to the prompt before they open the browser', async ($, on) => {
|
|
224
|
+
mock.env(on, { HOME: '/Users/me' })
|
|
225
|
+
mock.clock(on, { now: 1_700_000_000_000 })
|
|
226
|
+
const remote = { ...ROWS[1], remote: 'https://claude.ai/code/session_b2' }
|
|
227
|
+
on('state.get', ($, e, next) => (e.key === 'rows' ? seeded([remote]) : e.key === 'writtenAt' ? seeded(1_700_000_000) : next(e)))
|
|
228
|
+
const calls: string[] = []
|
|
229
|
+
on('ui.close', ($, e) => {
|
|
230
|
+
calls.push(`close ${e.id}`)
|
|
231
|
+
return { value: undefined }
|
|
232
|
+
})
|
|
233
|
+
on('ui.open', ($, e) => {
|
|
234
|
+
calls.push(`open ${e.id}${e.focus ? ' focused' : ''}`)
|
|
235
|
+
return { value: { isPlaced: true } }
|
|
236
|
+
})
|
|
237
|
+
on('process.run', ($, e) => {
|
|
238
|
+
calls.push(e.argv.join(' '))
|
|
239
|
+
return { value: { code: 0, stdout: '', stderr: '' } }
|
|
240
|
+
})
|
|
241
|
+
const ui = await $.ui.mount({ ...PANE, surface: 'terminal' })
|
|
242
|
+
await ui.press({ key: 'o' })
|
|
243
|
+
await ui.press({ key: 'w' })
|
|
244
|
+
expect(calls).toEqual([
|
|
245
|
+
'close inbox',
|
|
246
|
+
'open inbox',
|
|
247
|
+
'open https://github.com/o/r/pull/7',
|
|
248
|
+
'close inbox',
|
|
249
|
+
'open inbox',
|
|
250
|
+
'open https://claude.ai/code/session_b2',
|
|
251
|
+
])
|
|
252
|
+
await ui.unmount()
|
|
253
|
+
})
|
|
254
|
+
|
|
223
255
|
test('a click on a row puts the cursor on it before it asks for the switch', async ($, on) => {
|
|
224
256
|
mock.env(on, { HOME: '/Users/me' })
|
|
225
257
|
mock.clock(on, { now: 1_700_000_000_000 })
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: claude-inbox
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jordan Byron
|
|
@@ -108,21 +108,28 @@ files:
|
|
|
108
108
|
- lib/claude_inbox/actions.rb
|
|
109
109
|
- lib/claude_inbox/agents_client.rb
|
|
110
110
|
- lib/claude_inbox/app.rb
|
|
111
|
+
- lib/claude_inbox/app/attaching.rb
|
|
112
|
+
- lib/claude_inbox/app/browsing.rb
|
|
113
|
+
- lib/claude_inbox/app/filtering.rb
|
|
114
|
+
- lib/claude_inbox/app/navigating.rb
|
|
115
|
+
- lib/claude_inbox/app/spawning.rb
|
|
116
|
+
- lib/claude_inbox/app/triaging.rb
|
|
117
|
+
- lib/claude_inbox/app/updating.rb
|
|
118
|
+
- lib/claude_inbox/cli.rb
|
|
111
119
|
- lib/claude_inbox/config.rb
|
|
112
120
|
- lib/claude_inbox/debug.rb
|
|
113
121
|
- lib/claude_inbox/dialog.rb
|
|
114
122
|
- lib/claude_inbox/headless.rb
|
|
115
123
|
- lib/claude_inbox/images.rb
|
|
124
|
+
- lib/claude_inbox/input.rb
|
|
116
125
|
- lib/claude_inbox/job_state.rb
|
|
117
126
|
- lib/claude_inbox/keymap.rb
|
|
118
|
-
- lib/claude_inbox/logs.rb
|
|
119
127
|
- lib/claude_inbox/mod.rb
|
|
120
128
|
- lib/claude_inbox/mouse.rb
|
|
121
129
|
- lib/claude_inbox/new_session_form.rb
|
|
122
130
|
- lib/claude_inbox/painter.rb
|
|
123
131
|
- lib/claude_inbox/palette.rb
|
|
124
132
|
- lib/claude_inbox/paste.rb
|
|
125
|
-
- lib/claude_inbox/peek.rb
|
|
126
133
|
- lib/claude_inbox/poller.rb
|
|
127
134
|
- lib/claude_inbox/pull_requests.rb
|
|
128
135
|
- lib/claude_inbox/rate_limits.rb
|
data/lib/claude_inbox/logs.rb
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "vt_screen"
|
|
4
|
-
|
|
5
|
-
module ClaudeInbox
|
|
6
|
-
# The `claude logs <id>` replay of each session as readable lines, fetched
|
|
7
|
-
# off the main thread, debounced and cached.
|
|
8
|
-
class Logs
|
|
9
|
-
DEBOUNCE = 0.25
|
|
10
|
-
TTL = 10
|
|
11
|
-
MAX_LINES = 400
|
|
12
|
-
|
|
13
|
-
def initialize(client, clock: -> { Time.now })
|
|
14
|
-
@client = client
|
|
15
|
-
@clock = clock
|
|
16
|
-
@cache = {}
|
|
17
|
-
@mutex = Mutex.new
|
|
18
|
-
@pending = nil
|
|
19
|
-
@pending_at = nil
|
|
20
|
-
@requests = Queue.new
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def start
|
|
24
|
-
return if @thread&.alive?
|
|
25
|
-
@thread = Thread.new { worker }
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def cached(id)
|
|
29
|
-
@mutex.synchronize { @cache[id]&.first }
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def want(id)
|
|
33
|
-
@pending = id
|
|
34
|
-
@pending_at = @clock.call
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Promotes a request that has sat still for DEBOUNCE to the worker,
|
|
38
|
-
# unless the cache already has a fresh answer.
|
|
39
|
-
def tick
|
|
40
|
-
return unless @pending && @clock.call - @pending_at >= DEBOUNCE
|
|
41
|
-
id = @pending
|
|
42
|
-
@pending = nil
|
|
43
|
-
fresh = @mutex.synchronize { (e = @cache[id]) && @clock.call - e[1] < TTL }
|
|
44
|
-
@requests << id unless fresh
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def stop = @thread&.kill
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def worker
|
|
52
|
-
loop do
|
|
53
|
-
id = @requests.pop
|
|
54
|
-
id = @requests.pop until @requests.empty? # only the latest matters
|
|
55
|
-
lines = fetch(id)
|
|
56
|
-
@mutex.synchronize { @cache[id] = [lines, @clock.call] }
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def fetch(id)
|
|
61
|
-
raw = @client.logs(id)
|
|
62
|
-
return ["(no output available — the session's process is not running)"] if raw.nil?
|
|
63
|
-
VtScreen.new.feed(raw).lines.last(MAX_LINES)
|
|
64
|
-
rescue => e
|
|
65
|
-
["(logs failed: #{e.message})"]
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
data/lib/claude_inbox/peek.rb
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ClaudeInbox
|
|
4
|
-
# The peek pane: whether it is open, which row it is on, how far back it
|
|
5
|
-
# is scrolled, and what to paint for that. The lines themselves come from
|
|
6
|
-
# Logs, which it asks on every selection change so that opening the pane
|
|
7
|
-
# lands on a warm cache. Main-thread state only.
|
|
8
|
-
class Peek
|
|
9
|
-
TERMINAL_NOTE = "This is a claude you opened in a terminal yourself. The daemon can't attach to it, read its output, or stop it from outside. Switch to that window."
|
|
10
|
-
REMOTE_NOTE = "This is a Remote Control session driven from claude.ai/code. The daemon can't attach to it or read its output from here. Enter pulls it into the daemon; w opens it in the web or mobile app."
|
|
11
|
-
|
|
12
|
-
# What Renderer paints: the body lines, the title bar, the dim line under it.
|
|
13
|
-
View = Struct.new(:lines, :title, :subtitle)
|
|
14
|
-
|
|
15
|
-
def initialize(logs)
|
|
16
|
-
@logs = logs
|
|
17
|
-
@open = false
|
|
18
|
-
@offset = 0
|
|
19
|
-
@session = nil
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def open? = @open
|
|
23
|
-
|
|
24
|
-
def toggle
|
|
25
|
-
@open = !@open
|
|
26
|
-
@logs.want(@session.id) if @open && @session&.actionable?
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def close = @open = false
|
|
30
|
-
|
|
31
|
-
# Called on every selection change; a fold has no session.
|
|
32
|
-
def select(session)
|
|
33
|
-
@session = session
|
|
34
|
-
@offset = 0
|
|
35
|
-
@logs.want(session.id) if session&.actionable?
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Positive scrolls back into history, negative towards the tail.
|
|
39
|
-
def scroll(delta)
|
|
40
|
-
@offset = [@offset + delta, 0].max
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# `row` is the selected row as the frame shows it, or nil on a fold or
|
|
44
|
-
# with nothing selected. Answers nil when there is no pane to paint.
|
|
45
|
-
def view(row, body_h)
|
|
46
|
-
return nil unless @open && row
|
|
47
|
-
View.new(scrolled(body(row), body_h), row.label, subtitle(row))
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def body(row)
|
|
53
|
-
return interactive_note(row.session) if row.session.interactive?
|
|
54
|
-
@logs.cached(row.session.id) || ["(loading…)"]
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def interactive_note(s)
|
|
58
|
-
[s.remote? ? REMOTE_NOTE : TERMINAL_NOTE, "", "pid #{s.pid} · #{s.cwd}", "session #{s.session_id}", s.remote_url].compact
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def subtitle(row)
|
|
62
|
-
s = row.session
|
|
63
|
-
parts = [s.effective_state, s.status, s.waiting_for, s.id, s.started_at&.strftime("started %b %-d %H:%M")].compact
|
|
64
|
-
parts += s.prs.map { |pr| "#{pr.short} #{pr.state&.downcase || "?"}" }
|
|
65
|
-
parts.join(" · ")
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Lines are shown tail-first; the offset scrolls back into history and is
|
|
69
|
-
# clamped to however much history there is.
|
|
70
|
-
def scrolled(lines, view_h)
|
|
71
|
-
return lines if @offset.zero?
|
|
72
|
-
max_off = [lines.size - view_h, 0].max
|
|
73
|
-
@offset = [@offset, max_off].min
|
|
74
|
-
lines[0, lines.size - @offset]
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|