claude-inbox 0.11.0 → 0.11.1
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/lib/claude_inbox/app/navigating.rb +9 -0
- data/lib/claude_inbox/app/triaging.rb +11 -3
- data/lib/claude_inbox/dialog.rb +1 -1
- data/lib/claude_inbox/headless.rb +18 -3
- data/lib/claude_inbox/pull_requests.rb +2 -0
- data/lib/claude_inbox/renderer.rb +1 -1
- data/lib/claude_inbox/writer_lock.rb +11 -2
- data/lib/claude_inbox.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4be74e826e16c816d3589869198467177926961e11d885d7c811e6218b67fd2f
|
|
4
|
+
data.tar.gz: b09b5adfa01af2b64b00479faba7ce3c3312a8aec82916e1c0a003e7eec8f4ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 93865715d77fe8092727f66c32da8e6cdbbac67068c2ead4af429f35d488d034834c188dc74905e42b0c4ecdea8470a3770d870251c8f229a532f3006cc463b0
|
|
7
|
+
data.tar.gz: 5334bb968b0fc31caea4249abe7b838266a70e9513c2693baa68432bc20f6c8eef3f9ff2491a14af15caa168550a27fffc0d20a2879c953904029a526206f4e1
|
|
@@ -47,6 +47,15 @@ module ClaudeInbox
|
|
|
47
47
|
select(stops[(idx + delta).clamp(0, stops.size - 1)])
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# Before a row leaves the list: the stop below it, or above it at the
|
|
51
|
+
# bottom, so working down a section doesn't throw the cursor to the top.
|
|
52
|
+
def select_neighbour
|
|
53
|
+
stops = filtered.selections(@expanded)
|
|
54
|
+
idx = stops.index(@selected)
|
|
55
|
+
rest = stops - [@selected]
|
|
56
|
+
select(rest[idx.clamp(..rest.size - 1)]) if idx && rest.any?
|
|
57
|
+
end
|
|
58
|
+
|
|
50
59
|
# Tab / Shift-Tab: first selectable row of the next / previous section.
|
|
51
60
|
def jump_section(dir)
|
|
52
61
|
sections = filtered
|
|
@@ -33,7 +33,9 @@ module ClaudeInbox
|
|
|
33
33
|
|
|
34
34
|
def settle_selected
|
|
35
35
|
return unless require_storable
|
|
36
|
-
@
|
|
36
|
+
key = @selected.key
|
|
37
|
+
select_neighbour
|
|
38
|
+
@store.settle(key)
|
|
37
39
|
notice("settled — u brings it back")
|
|
38
40
|
end
|
|
39
41
|
|
|
@@ -44,10 +46,16 @@ module ClaudeInbox
|
|
|
44
46
|
|
|
45
47
|
def snooze_chosen
|
|
46
48
|
return @modal = Dialog::Prompt.new(:snooze, @modal.id, "") if @modal.choice == :custom
|
|
47
|
-
|
|
49
|
+
snooze_modal_row(@modal.choice)
|
|
48
50
|
@modal = nil
|
|
49
51
|
end
|
|
50
52
|
|
|
53
|
+
# A poll can move the cursor while the menu is up, so only step off a row still under it.
|
|
54
|
+
def snooze_modal_row(choice)
|
|
55
|
+
select_neighbour if @selected == Store::Selection.row(@modal.id)
|
|
56
|
+
@store.snooze(@modal.id, choice)
|
|
57
|
+
end
|
|
58
|
+
|
|
51
59
|
def open_alias_editor
|
|
52
60
|
return unless require_storable && require_writable("rename")
|
|
53
61
|
current = @store.alias_for(@selected.key) || ""
|
|
@@ -67,7 +75,7 @@ module ClaudeInbox
|
|
|
67
75
|
elsif @modal.kind == :snooze
|
|
68
76
|
wake_at = SnoozeTime.parse(value, Time.now)
|
|
69
77
|
return notice("try 45m, 2h, 3d, 17:00, 5pm, fri, tomorrow or tonight") unless wake_at
|
|
70
|
-
|
|
78
|
+
snooze_modal_row(wake_at)
|
|
71
79
|
elsif value.empty? || PullRequests.valid_url?(value)
|
|
72
80
|
@store.set_pr(@modal.id, value)
|
|
73
81
|
@poller.soon
|
data/lib/claude_inbox/dialog.rb
CHANGED
|
@@ -15,11 +15,15 @@ module ClaudeInbox
|
|
|
15
15
|
# reaps, since deleting sessions stays with the inbox you can see, and it
|
|
16
16
|
# exits at once while an inbox holds the writer lock.
|
|
17
17
|
class Headless
|
|
18
|
+
RECHECK_EVERY = 60
|
|
19
|
+
|
|
18
20
|
def initialize(client:, store:, pull_requests: PullRequests.new, actions: Actions.new, snapshot: Snapshot.new,
|
|
19
|
-
lock: WriterLock.new, updater: nil, notifier: Notifier.disabled)
|
|
21
|
+
lock: WriterLock.new, updater: nil, notifier: Notifier.disabled, recheck_every: RECHECK_EVERY)
|
|
20
22
|
@lock = lock
|
|
21
23
|
@snapshot = snapshot
|
|
22
24
|
@updater = updater
|
|
25
|
+
@recheck_every = recheck_every
|
|
26
|
+
@notice = nil
|
|
23
27
|
@queue = Queue.new
|
|
24
28
|
@poller = Poller.new(client: client, store: store, pull_requests: pull_requests, reaper: Reaper.disabled,
|
|
25
29
|
queue: @queue, snapshot: snapshot, actions: actions, notifier: notifier)
|
|
@@ -29,7 +33,7 @@ module ClaudeInbox
|
|
|
29
33
|
def run
|
|
30
34
|
return false unless @lock.take(role: "headless")
|
|
31
35
|
@poller.start
|
|
32
|
-
|
|
36
|
+
Thread.new { watch_for_updates } if @updater
|
|
33
37
|
loop { @queue.pop }
|
|
34
38
|
end
|
|
35
39
|
|
|
@@ -38,8 +42,19 @@ module ClaudeInbox
|
|
|
38
42
|
# After the poller is off, since the check may wait on the network. The
|
|
39
43
|
# pane cannot swap the process under Claude Code, so the snapshot only
|
|
40
44
|
# says a newer one is out, and a check that cannot tell says nothing.
|
|
45
|
+
# Re-checked because this process outlives an update applied from a
|
|
46
|
+
# terminal; the updater's day-long cache keeps that off the network.
|
|
47
|
+
def watch_for_updates
|
|
48
|
+
loop do
|
|
49
|
+
check_for_update
|
|
50
|
+
sleep @recheck_every
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
41
54
|
def check_for_update
|
|
42
|
-
|
|
55
|
+
notice = @updater.check&.notice
|
|
56
|
+
return if notice == @notice
|
|
57
|
+
@notice = @snapshot.update = notice
|
|
43
58
|
@poller.soon
|
|
44
59
|
rescue Updater::Error
|
|
45
60
|
nil
|
|
@@ -317,7 +317,7 @@ module ClaudeInbox
|
|
|
317
317
|
|
|
318
318
|
label_w = [width - chrome - Text.width(project_text), 0].max
|
|
319
319
|
label = Text.truncate(row.label, label_w)
|
|
320
|
-
label = style_label(label, row, quiet, sel ||
|
|
320
|
+
label = style_label(label, row, quiet, sel || unread)
|
|
321
321
|
first = " #{marker} #{glyph} " + Text.pad(label, label_w) + " " + meta + " " + project + " "
|
|
322
322
|
|
|
323
323
|
return [Text.pad(first, width)] unless live
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require_relative "subprocess"
|
|
4
5
|
|
|
5
6
|
module ClaudeInbox
|
|
6
7
|
# One snapshot writer per machine: the inbox you can see, else a headless
|
|
@@ -25,8 +26,8 @@ module ClaudeInbox
|
|
|
25
26
|
file = File.open(@path, File::RDWR | File::CREAT, 0o600)
|
|
26
27
|
return hold(file, role) if file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
27
28
|
holder_role, holder = file.read.split
|
|
28
|
-
return refuse(file) unless role == "inbox" && holder_role == "headless"
|
|
29
|
-
Process.kill("TERM", holder.to_i)
|
|
29
|
+
return refuse(file) unless role == "inbox" && holder_role == "headless" && headless?(holder.to_i)
|
|
30
|
+
Process.kill("TERM", holder.to_i)
|
|
30
31
|
EVICT_TRIES.times do
|
|
31
32
|
return hold(file, role) if file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
32
33
|
sleep 0.1
|
|
@@ -39,6 +40,14 @@ module ClaudeInbox
|
|
|
39
40
|
|
|
40
41
|
private
|
|
41
42
|
|
|
43
|
+
# The file can name a pid that no longer holds the lock (#322), which by
|
|
44
|
+
# now may be some other process; only a headless claude-inbox is ended.
|
|
45
|
+
def headless?(pid)
|
|
46
|
+
return false unless pid.positive? && pid != Process.pid
|
|
47
|
+
command = Subprocess.capture("ps", "-o", "command=", "-p", pid.to_s).out
|
|
48
|
+
command.include?("claude-inbox") && command.split.include?("--headless")
|
|
49
|
+
end
|
|
50
|
+
|
|
42
51
|
# After reading the holder the position sits at the end, and truncate does
|
|
43
52
|
# not move it, so the write is rewound first or it lands after NUL padding.
|
|
44
53
|
def hold(file, role)
|
data/lib/claude_inbox.rb
CHANGED