claude-inbox 0.6.1 → 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/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 +1 -1
- data/lib/claude_inbox/input.rb +65 -0
- 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/renderer.rb +12 -39
- data/lib/claude_inbox/slash_commands.rb +6 -7
- 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
- metadata +9 -3
- data/lib/claude_inbox/logs.rb +0 -68
- data/lib/claude_inbox/peek.rb +0 -77
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b432b1272fd3e3b923eaefe34fa4e3c30cbd9dfcf57d4beacd761f1cef70785b
|
|
4
|
+
data.tar.gz: '090ff2d41f9701cc7476196af19e4604241b3b3079376a896b441ab9dbf0ce9d'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a075eee59befc0de0e667d5b4325db8086a457040d544534f75b62d8a05b91a5e703b370bd1f3ce52fe89638cefa93403b1d6acd7170f143a86111a9d4c75277
|
|
7
|
+
data.tar.gz: 1d3dbb6ead15764e91834af2de5b8cfa9873493830e0cf2cee6080a4884fe3d74476b2a260b47565f6e70c99f5611d9c57bb422950435b80cc90b12eb55bc859
|
data/README.md
CHANGED
|
@@ -73,10 +73,8 @@ Vim bindings. Arrows and the mouse work too.
|
|
|
73
73
|
| `Ctrl-d` `Ctrl-u` | half page down / up (`Ctrl-f` `Ctrl-b` full page) |
|
|
74
74
|
| `Enter` `l` | attach (`←` or `Ctrl+Z` returns here), adopt a remote session, or expand Snoozed / Settled |
|
|
75
75
|
| `Tab` `Shift+Tab` | next / previous section |
|
|
76
|
-
| `h` |
|
|
76
|
+
| `h` | collapse the current fold |
|
|
77
77
|
| `za` `zo` `zc` | toggle / open / close the fold under the cursor |
|
|
78
|
-
| `p` | toggle the read-only peek pane |
|
|
79
|
-
| `J` `K` (`Ctrl-e` `Ctrl-y`) | scroll the peek pane |
|
|
80
78
|
| `n` | new session |
|
|
81
79
|
| `N` | pair a phone |
|
|
82
80
|
| `t` | pin / unpin |
|
|
@@ -27,13 +27,6 @@ module ClaudeInbox
|
|
|
27
27
|
classify_origins(parse(r.out))
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
# Raw terminal replay for a session, or nil when the daemon can't serve it
|
|
31
|
-
# (finished sessions whose process is gone report "job not found").
|
|
32
|
-
def logs(id)
|
|
33
|
-
r = Subprocess.capture(@bin, "logs", id)
|
|
34
|
-
(r.success? && !r.out.empty?) ? r.out : nil
|
|
35
|
-
end
|
|
36
|
-
|
|
37
30
|
# Poll interval for the agents-view watchdog below.
|
|
38
31
|
WATCH_INTERVAL = 0.05
|
|
39
32
|
|
|
@@ -259,10 +252,9 @@ module ClaudeInbox
|
|
|
259
252
|
|
|
260
253
|
# Reads a committed JSON fixture instead of the daemon.
|
|
261
254
|
class FixtureClient < AgentsClient
|
|
262
|
-
def initialize(path,
|
|
255
|
+
def initialize(path, origins: {}, bridges: {})
|
|
263
256
|
super(jobs_dir: File.join(File.dirname(path), "jobs"))
|
|
264
257
|
@path = path
|
|
265
|
-
@logs = logs
|
|
266
258
|
@origins = origins
|
|
267
259
|
@bridges = bridges
|
|
268
260
|
end
|
|
@@ -274,8 +266,6 @@ module ClaudeInbox
|
|
|
274
266
|
assign_origins(parse(File.read(@path)), @origins, @bridges)
|
|
275
267
|
end
|
|
276
268
|
|
|
277
|
-
def logs(_id) = @logs
|
|
278
|
-
|
|
279
269
|
# Stand-in child: prints, waits for a line, exits — enough to prove the
|
|
280
270
|
# terminal round-trips through cooked mode and back.
|
|
281
271
|
def attach(id) = system("sh", "-c", "printf 'fake attach to %s\\npress enter to detach: ' \"$1\"; read -r _", "attach", id)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeInbox
|
|
4
|
+
class App
|
|
5
|
+
# Handing the terminal to a session and getting it back, and asking the
|
|
6
|
+
# daemon to stop or delete one.
|
|
7
|
+
module Attaching
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def activate
|
|
11
|
+
return set_expanded(true) if @selected&.fold?
|
|
12
|
+
return @modal = Dialog::Confirm.new(:adopt, @selected.key) if selected_session&.remote?
|
|
13
|
+
attach(@selected.key) if require_actionable
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Guard for attach/stop: refuse politely on a terminal or remote row.
|
|
17
|
+
def require_actionable
|
|
18
|
+
s = selected_session
|
|
19
|
+
return true if s&.actionable?
|
|
20
|
+
if s&.interactive?
|
|
21
|
+
notice(s.remote? ? "that's a remote session — Enter adopts it, w opens it at claude.ai/code" : "that's your own terminal — switch to that window")
|
|
22
|
+
end
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The pane in the attached session may ask for a different one: the
|
|
27
|
+
# attach ends and the next starts without passing through the list. The
|
|
28
|
+
# poller keeps running meanwhile, so the pane's snapshot stays fresh. A
|
|
29
|
+
# failed attach leaves its reason on screen until a key is pressed.
|
|
30
|
+
def attach(id)
|
|
31
|
+
while id
|
|
32
|
+
@store.acknowledge(id)
|
|
33
|
+
@switch.clear
|
|
34
|
+
from = session_for(id)&.session_id
|
|
35
|
+
@terminal.release do
|
|
36
|
+
@terminal.pause unless @client.attach(id) { @switch.requested_for?(from) }
|
|
37
|
+
end
|
|
38
|
+
id = @switch.take(@store.sessions.select(&:actionable?).map(&:key))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def adopt_session(key)
|
|
43
|
+
s = session_for(key)
|
|
44
|
+
return unless s&.remote?
|
|
45
|
+
notice("adopting #{s.display_name}…")
|
|
46
|
+
in_background do
|
|
47
|
+
id = @client.adopt(session_id: s.session_id, cwd: s.cwd, pid: s.pid)
|
|
48
|
+
@queue << [:notice, "adopted as #{id}"]
|
|
49
|
+
@queue << [:select, id]
|
|
50
|
+
@queue << [:attach, id]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def open_confirm(kind)
|
|
55
|
+
return unless require_actionable
|
|
56
|
+
@modal = Dialog::Confirm.new(kind, @selected.key)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def stop_session(id)
|
|
60
|
+
in_background do
|
|
61
|
+
@client.stop(id)
|
|
62
|
+
@poller.soon
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def delete_session(id)
|
|
67
|
+
notice("deleting #{id}…")
|
|
68
|
+
in_background do
|
|
69
|
+
@client.rm(id)
|
|
70
|
+
@store.forget(id)
|
|
71
|
+
@queue << [:notice, "deleted #{id}"]
|
|
72
|
+
@poller.soon
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeInbox
|
|
4
|
+
class App
|
|
5
|
+
# o opens the selected session's pull request and w its claude.ai/code
|
|
6
|
+
# page, through whatever opens URLs on this platform.
|
|
7
|
+
module Browsing
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def open_pr
|
|
11
|
+
pr = selected_session&.pr
|
|
12
|
+
return notice("no pull request linked — P sets one") unless pr
|
|
13
|
+
open_in_browser(pr.url, pr.short)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def open_remote
|
|
17
|
+
url = selected_session&.remote_url
|
|
18
|
+
return notice("no claude.ai/code page for this session") unless url
|
|
19
|
+
open_in_browser(url, "claude.ai/code")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def open_in_browser(url, what)
|
|
23
|
+
opener = RUBY_PLATFORM.include?("darwin") ? "open" : "xdg-open"
|
|
24
|
+
notice("opening #{what}")
|
|
25
|
+
in_background { Subprocess.capture(opener, url) }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeInbox
|
|
4
|
+
class App
|
|
5
|
+
# The `/` filter line and the sections that match it.
|
|
6
|
+
module Filtering
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def filter_editing? = @filter_editing
|
|
10
|
+
|
|
11
|
+
def filter = @filter
|
|
12
|
+
|
|
13
|
+
def filtered(sections = @store.sections) = sections.matching(@filter&.to_s)
|
|
14
|
+
|
|
15
|
+
def start_filter
|
|
16
|
+
@filter ||= TextBuffer.new
|
|
17
|
+
@filter_editing = true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def clear_filter
|
|
21
|
+
@filter = nil
|
|
22
|
+
@filter_editing = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def handle_line_key(name, key)
|
|
26
|
+
case name
|
|
27
|
+
when :escape then clear_filter
|
|
28
|
+
when :return, :enter then @filter_editing = false
|
|
29
|
+
when :backspace, :ctrl_h
|
|
30
|
+
@filter.empty? ? clear_filter : @filter.press(name, key)
|
|
31
|
+
else
|
|
32
|
+
@filter.press(name, key)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeInbox
|
|
4
|
+
class App
|
|
5
|
+
# Where the cursor is in the sectioned, foldable list and how it moves.
|
|
6
|
+
# The only writer of the selection; the rest of App reads it.
|
|
7
|
+
module Navigating
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Remembers where the frame put the rows, so a click can find one.
|
|
11
|
+
def placed(frame)
|
|
12
|
+
@row_items = frame.items
|
|
13
|
+
@top = frame.top
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The row to land on once a poll lists it, as after a spawn.
|
|
17
|
+
def select_later(id)
|
|
18
|
+
@pending_select = id
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ensure_selection(sections)
|
|
22
|
+
stops = sections.selections(@expanded)
|
|
23
|
+
pending = @pending_select && Store::Selection.row(@pending_select)
|
|
24
|
+
if pending && stops.include?(pending)
|
|
25
|
+
select(pending)
|
|
26
|
+
@pending_select = nil
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
return if stops.include?(@selected)
|
|
30
|
+
select(stops.first)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def select(selection)
|
|
34
|
+
@selected = selection
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def session_for(key) = @store.sessions.find { |s| s.key == key }
|
|
38
|
+
|
|
39
|
+
def selected_session = session_for(@selected&.key)
|
|
40
|
+
|
|
41
|
+
def page = [Renderer.body_height(@terminal.size[1]), 1].max
|
|
42
|
+
|
|
43
|
+
def move(delta)
|
|
44
|
+
stops = filtered.selections(@expanded)
|
|
45
|
+
return if stops.empty?
|
|
46
|
+
idx = stops.index(@selected) || 0
|
|
47
|
+
select(stops[(idx + delta).clamp(0, stops.size - 1)])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Tab / Shift-Tab: first selectable row of the next / previous section.
|
|
51
|
+
def jump_section(dir)
|
|
52
|
+
sections = filtered
|
|
53
|
+
heads = sections.heads(@expanded)
|
|
54
|
+
return if heads.empty?
|
|
55
|
+
current = sections.section_of(@selected)
|
|
56
|
+
idx = heads.index { |name, _| name == current } || -1
|
|
57
|
+
select(heads[(idx + dir) % heads.size].last)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The foldable section the cursor is currently on or inside, if any.
|
|
61
|
+
def current_fold_section
|
|
62
|
+
name = filtered.section_of(@selected)
|
|
63
|
+
name if Store::FOLDABLE_SECTIONS.include?(name)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def set_expanded(value)
|
|
67
|
+
name = current_fold_section
|
|
68
|
+
@expanded[name] = value if name
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def toggle_expanded = set_expanded(!@expanded[current_fold_section])
|
|
72
|
+
|
|
73
|
+
# Clicking a row selects it and attaches, same as landing on it with
|
|
74
|
+
# j/k and pressing Enter — activate already knows how to expand a fold
|
|
75
|
+
# or refuse a terminal/remote row, so this doesn't repeat that.
|
|
76
|
+
def click_row(row)
|
|
77
|
+
item = row_item_at(row)
|
|
78
|
+
return unless item
|
|
79
|
+
select(item)
|
|
80
|
+
activate
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The wrapped detail line under a two-line row ("↳ ~/code/x") carries
|
|
84
|
+
# no item of its own; a click there resolves to the row above it.
|
|
85
|
+
def row_item_at(row)
|
|
86
|
+
idx = row - 1
|
|
87
|
+
@row_items[idx] || (@row_items[idx - 1] if idx > 0 && @row_items[idx - 1]&.row?)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeInbox
|
|
4
|
+
class App
|
|
5
|
+
# Starting a session, from the `n` form here or from another device.
|
|
6
|
+
module Spawning
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def open_new_session
|
|
10
|
+
cwd = selected_session&.cwd || Dir.pwd
|
|
11
|
+
dirs = SessionRequest.directories(@store.sessions, @trust.call)
|
|
12
|
+
@modal = NewSessionForm.new(cwd: SessionRequest.strip_worktree(cwd), color: @color, dirs: dirs)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# `:start`/`:start_and_attach` leave the form as `@modal`, now busy: it
|
|
16
|
+
# closes itself once the spawn is confirmed to have started (`:form_started`)
|
|
17
|
+
# or reopens with the failure and the prompt intact (`:form_failed`).
|
|
18
|
+
def handle_form_key(name, key)
|
|
19
|
+
form = @modal
|
|
20
|
+
case form.press(name, key)
|
|
21
|
+
when :cancel then @modal = nil
|
|
22
|
+
when :start then start_session(form, attach: false)
|
|
23
|
+
when :start_and_attach then start_session(form, attach: true)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# `attach:` hands the terminal over as soon as the session starts. Without
|
|
28
|
+
# it we stay in the inbox and poll, so the new row shows up right away
|
|
29
|
+
# rather than at the next tick.
|
|
30
|
+
def start_session(form, attach:)
|
|
31
|
+
v = form.values
|
|
32
|
+
notice("starting session…")
|
|
33
|
+
in_background do
|
|
34
|
+
id = @client.spawn(**v)
|
|
35
|
+
@queue << [:notice, "started #{id}"]
|
|
36
|
+
@queue << [:form_started, form]
|
|
37
|
+
@queue << [:select, id]
|
|
38
|
+
attach ? @queue << [:attach, id] : @poller.soon
|
|
39
|
+
rescue AgentsClient::Error => e
|
|
40
|
+
@queue << [:form_failed, form, e.message]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def form_started(form)
|
|
45
|
+
@modal = nil if @modal.equal?(form)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# A spawn failure hands the form back rather than just logging it, so
|
|
49
|
+
# the composed prompt survives (see NewSessionForm#submission_failed).
|
|
50
|
+
def form_failed(form, message)
|
|
51
|
+
@modal.equal?(form) ? form.submission_failed(message) : (@error = message)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Someone at the desk may be mid-thought, so a start from another
|
|
55
|
+
# device never moves the cursor, attaches or closes what is open.
|
|
56
|
+
def remote_started(id, via)
|
|
57
|
+
notice("started #{id} from #{via}")
|
|
58
|
+
@poller.soon
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeInbox
|
|
4
|
+
class App
|
|
5
|
+
# The edits of the selected row that live in our own store, and the two
|
|
6
|
+
# guards they share.
|
|
7
|
+
module Triaging
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Guard for snooze/wake/alias: anything with a key, since those live in
|
|
11
|
+
# our own store. A terminal you are sitting in is the one exception.
|
|
12
|
+
def require_storable
|
|
13
|
+
s = selected_session
|
|
14
|
+
return true if @selected&.row? && !s&.terminal?
|
|
15
|
+
notice("you're in that terminal right now — nothing to snooze") if s&.terminal?
|
|
16
|
+
false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The edits with no request form, which a second inbox cannot keep.
|
|
20
|
+
def require_writable(verb)
|
|
21
|
+
return true if @store.writable?
|
|
22
|
+
notice("another claude-inbox owns state.json — #{verb} there")
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def wake_selected
|
|
27
|
+
@store.wake(@selected.key) if require_storable
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def toggle_pin_selected
|
|
31
|
+
@store.toggle_pin(@selected.key) if require_storable
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def settle_selected
|
|
35
|
+
return unless require_storable
|
|
36
|
+
@store.settle(@selected.key)
|
|
37
|
+
notice("settled — u brings it back")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def open_snooze_menu
|
|
41
|
+
return unless require_storable
|
|
42
|
+
@modal = Dialog::Snooze.new(@selected.key)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def snooze_chosen
|
|
46
|
+
@store.snooze(@modal.id, @modal.choice)
|
|
47
|
+
@modal = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def open_alias_editor
|
|
51
|
+
return unless require_storable && require_writable("rename")
|
|
52
|
+
current = @store.alias_for(@selected.key) || ""
|
|
53
|
+
@modal = Dialog::Prompt.new(:alias, @selected.key, current)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def open_pr_editor
|
|
57
|
+
return unless require_storable && require_writable("link a pull request")
|
|
58
|
+
current = @store.pr_for(@selected.key) || selected_session&.pr&.url || ""
|
|
59
|
+
@modal = Dialog::Prompt.new(:pr, @selected.key, current)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def save_prompt
|
|
63
|
+
value = @modal.value.strip
|
|
64
|
+
if @modal.kind == :alias
|
|
65
|
+
@store.set_alias(@modal.id, value)
|
|
66
|
+
elsif value.empty? || PullRequests.valid_url?(value)
|
|
67
|
+
@store.set_pr(@modal.id, value)
|
|
68
|
+
@poller.soon
|
|
69
|
+
else
|
|
70
|
+
return notice("that's not a github.com pull request url")
|
|
71
|
+
end
|
|
72
|
+
@modal = nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeInbox
|
|
4
|
+
class App
|
|
5
|
+
# The newer-release check, the header's word on it, and U to install
|
|
6
|
+
# and relaunch.
|
|
7
|
+
module Updating
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Once, on the first step: a stale cache means a network round trip, so
|
|
11
|
+
# it runs off the main thread. A laptop that is offline stays quiet
|
|
12
|
+
# rather than show an error at every launch; U says what went wrong.
|
|
13
|
+
def ensure_update_checked
|
|
14
|
+
return if @update_status
|
|
15
|
+
@update_status = @updater ? "checking for updates…" : "update check is off — claude-inbox update installs the latest"
|
|
16
|
+
return unless @updater
|
|
17
|
+
in_background do
|
|
18
|
+
@queue << [:update, @updater.check]
|
|
19
|
+
rescue Updater::Error => e
|
|
20
|
+
@queue << [:update, nil, e.message]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def update_checked(update, failure = nil)
|
|
25
|
+
@update = update
|
|
26
|
+
@snapshot.update = update&.notice
|
|
27
|
+
@update_status = failure ? "couldn't check for updates: #{failure}" : "up to date"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The header's line once a newer release is known; nil until then.
|
|
31
|
+
def update_notice
|
|
32
|
+
"#{@update.notice} · U updates" if @update
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The install prints on the real screen, and the relaunch replaces this
|
|
36
|
+
# process. A failed install relaunches this version instead, once its
|
|
37
|
+
# error has been read, rather than leave the shell showing.
|
|
38
|
+
def update_inbox
|
|
39
|
+
return notice(@update_status) unless @update
|
|
40
|
+
@terminal.release do
|
|
41
|
+
@out.puts @update.installing
|
|
42
|
+
applied = @updater.apply
|
|
43
|
+
@out.puts applied.output unless applied.output.empty?
|
|
44
|
+
@terminal.pause unless applied.success?
|
|
45
|
+
relaunch
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def relaunch
|
|
50
|
+
@updater.relaunch(@argv)
|
|
51
|
+
rescue SystemCallError => e
|
|
52
|
+
@error = "relaunch failed: #{e.message}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|