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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e14abc910988446bead1ad4edc841f64fe256e396765cea5a6b77cd6ae28777
4
- data.tar.gz: 8e705e40ce80454dd1d8265f567128fae801286b7112d84f3e63ef00b0228913
3
+ metadata.gz: 4be74e826e16c816d3589869198467177926961e11d885d7c811e6218b67fd2f
4
+ data.tar.gz: b09b5adfa01af2b64b00479faba7ce3c3312a8aec82916e1c0a003e7eec8f4ba
5
5
  SHA512:
6
- metadata.gz: 895a773cf9237ad41e23826fb4b29f863b55dbbe81954abb9fe3337530b4f9ef77e19642a2a7bfed1bad1710d59e1ca705deabee42205fe52d9e63c7079af1a5
7
- data.tar.gz: 211a75470bb66188394cfbb7b0809f0e685e37e4d6c59366b32a8dc3e90fc4cb5f78d5b6ff4c1b42c78ef68d720dcfd29327446f283f0f2343233d0710505e3a
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
- @store.settle(@selected.key)
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
- @store.snooze(@modal.id, @modal.choice)
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
- @store.snooze(@modal.id, wake_at)
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
@@ -58,7 +58,7 @@ module ClaudeInbox
58
58
 
59
59
  def press(name, key)
60
60
  return :cancel if name == :escape || key == "q"
61
- entry = MENU.find { |k, _, _| k == key }
61
+ entry = MENU.assoc(key)
62
62
  return nil unless entry
63
63
  @choice = entry[2]
64
64
  :snooze
@@ -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
- check_for_update if @updater
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
- @snapshot.update = @updater.check&.notice
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
@@ -158,6 +158,8 @@ module ClaudeInbox
158
158
  url.to_s.match?(%r{\Ahttps://github\.com/[^/\s]+/[^/\s]+/pull/\d+/?\z})
159
159
  end
160
160
 
161
+ private_class_method :health
162
+
161
163
  private
162
164
 
163
165
  def scanned(job_state)
@@ -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 || (live && row.unread?))
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) if holder.to_i.positive? && holder.to_i != Process.pid
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeInbox
4
- VERSION = "0.11.0"
4
+ VERSION = "0.11.1"
5
5
  end
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.11.0
4
+ version: 0.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Byron