proctolog 0.1.0 → 0.2.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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +87 -3
  3. data/README.md +15 -4
  4. data/app/controllers/proctolog/application_controller.rb +1 -1
  5. data/app/controllers/proctolog/bursts_controller.rb +1 -1
  6. data/app/controllers/proctolog/choices_controller.rb +3 -1
  7. data/app/controllers/proctolog/decisions_controller.rb +1 -1
  8. data/app/controllers/proctolog/notes_controller.rb +2 -2
  9. data/app/controllers/proctolog/prompts_controller.rb +1 -1
  10. data/app/controllers/proctolog/pull_requests_controller.rb +2 -4
  11. data/app/controllers/proctolog/review_data_controller.rb +3 -1
  12. data/app/controllers/proctolog/shares_controller.rb +27 -0
  13. data/app/helpers/proctolog/decisions_helper.rb +6 -2
  14. data/app/helpers/proctolog/desk_helper.rb +48 -8
  15. data/app/helpers/proctolog/rounds_helper.rb +4 -2
  16. data/app/helpers/proctolog/shares_helper.rb +11 -0
  17. data/app/helpers/proctolog/standings_helper.rb +5 -4
  18. data/app/models/proctolog/burst.rb +17 -8
  19. data/app/models/proctolog/choices.rb +8 -0
  20. data/app/models/proctolog/doctor.rb +9 -1
  21. data/app/models/proctolog/github.rb +1 -1
  22. data/app/models/proctolog/marks.rb +1 -1
  23. data/app/models/proctolog/note.rb +4 -0
  24. data/app/models/proctolog/owner.rb +3 -0
  25. data/app/models/proctolog/pick.rb +12 -0
  26. data/app/models/proctolog/rounds.rb +37 -0
  27. data/app/models/proctolog/share/ngrok.rb +120 -0
  28. data/app/models/proctolog/share/settings.rb +23 -0
  29. data/app/models/proctolog/share/tailscale.rb +103 -0
  30. data/app/models/proctolog/share.rb +101 -0
  31. data/app/models/proctolog/standing.rb +24 -7
  32. data/app/views/proctolog/bursts/_line.html.erb +2 -2
  33. data/app/views/proctolog/bursts/_side.html.erb +6 -5
  34. data/app/views/proctolog/bursts/_side_stop.html.erb +4 -4
  35. data/app/views/proctolog/notes/_card.html.erb +7 -3
  36. data/app/views/proctolog/open_pull_requests/_side.html.erb +4 -3
  37. data/app/views/proctolog/open_pull_requests/_start.html.erb +8 -0
  38. data/app/views/proctolog/open_pull_requests/index.html.erb +4 -5
  39. data/app/views/proctolog/open_pull_requests/start/_first_time.html.erb +3 -0
  40. data/app/views/proctolog/open_pull_requests/start/_nothing_open.html.erb +3 -0
  41. data/app/views/proctolog/open_pull_requests/start/_nothing_written.html.erb +3 -0
  42. data/app/views/proctolog/shares/_line.html.erb +5 -0
  43. data/app/views/proctolog/shares/show.html.erb +48 -0
  44. data/app/views/proctolog/shares/ways/_ngrok.html.erb +6 -0
  45. data/app/views/proctolog/shares/ways/_tailscale.html.erb +2 -0
  46. data/bin/proctolog +92 -6
  47. data/completions/_proctolog +28 -4
  48. data/config/application.rb +23 -4
  49. data/config/initializers/grouse.rb +1 -1
  50. data/config/routes.rb +3 -0
  51. data/docs/manual/bursts.md +7 -2
  52. data/docs/manual/deciding.md +4 -1
  53. data/docs/manual/handover.md +6 -0
  54. data/docs/manual/reading.md +54 -27
  55. data/docs/manual/sharing.md +77 -0
  56. data/lib/proctolog/changelog.rb +50 -0
  57. data/lib/proctolog/version.rb +1 -1
  58. data/public/asking.js +31 -4
  59. data/public/decisions.js +4 -4
  60. data/public/desk.js +10 -6
  61. data/public/keys.js +6 -0
  62. data/public/notes.js +164 -58
  63. data/public/pick.css +21 -0
  64. data/public/respond.css +6 -0
  65. data/public/respond.js +51 -7
  66. data/public/review-drafts.js +47 -9
  67. data/public/review-rounds.js +27 -3
  68. data/public/share.css +58 -0
  69. data/public/side.js +11 -20
  70. data/public/terminal.css +451 -77
  71. data/public/viewer.js +700 -173
  72. metadata +18 -1
@@ -0,0 +1,48 @@
1
+ <% content_for :title, "Sharing the desk" %>
2
+ <% content_for :head do %>
3
+ <link rel="stylesheet" href="/share.css">
4
+ <% end %>
5
+
6
+ <header class="burst">
7
+ <h1>Sharing the desk</h1>
8
+ <p class="lede">
9
+ Review from your phone, or a computer elsewhere, while this Mac keeps the desk. Only you get in.
10
+ <%= link_to "Back", @back %>
11
+ </p>
12
+ </header>
13
+
14
+ <main class="route share">
15
+ <% if @share %>
16
+ <p class="now">Shared at <%= link_to @share.address, @share.address %> through <%= @share.way.name %>: <%= @share.who %>.</p>
17
+ <p class="hint">It stays shared until you stop it, the desk running or not.<% if Proctolog::Share.carries?(request) %> This page came through it: stopping it closes this page too.<% end %></p>
18
+ <%= button_to "Stop sharing", share_path, method: :delete, params: {back: @back}, class: "quiet" %>
19
+ <% else %>
20
+ <p class="now">Only on this Mac, at <%= link_to Proctolog::Desk.address, Proctolog::Desk.address %>.</p>
21
+
22
+ <h2>Share it through</h2>
23
+ <div class="ways">
24
+ <% @ways.each do |way| %>
25
+ <section class="way" aria-label="<%= way.name %>">
26
+ <h3><%= way.name %></h3>
27
+ <% if way.ready? %>
28
+ <%= form_with url: share_path, method: :post do |form| %>
29
+ <%= form.hidden_field :via, value: way.to_param %>
30
+ <%= form.hidden_field :back, value: @back %>
31
+ <%= render "proctolog/shares/ways/#{way.to_param}", way:, form: %>
32
+ <%= form.button "Share through #{way.name}", class: "quiet" %>
33
+ <% end %>
34
+ <% else %>
35
+ <p class="facts"><%= linked_sentence(way.unready) %>.</p>
36
+ <% end %>
37
+ </section>
38
+ <% end %>
39
+ </div>
40
+ <% end %>
41
+
42
+ <h2>From a terminal</h2>
43
+ <p class="hint">
44
+ <code>proctolog share</code> shares it the way you named last, or the first one ready, and prints the address;
45
+ <code>proctolog share stop</code> stops it, and <code>proctolog share status</code> says what each way needs.
46
+ An agent can run them too, on a Mac you left working.
47
+ </p>
48
+ </main>
@@ -0,0 +1,6 @@
1
+ <%# locals: (way:, form:) %>
2
+ <p class="facts">At your ngrok account's address, on the internet, behind ngrok's sign-in with Google and a warning page of its own. Its free plan carries 1 GB a month, and every picture and film you open passes through it.</p>
3
+ <p class="allow">
4
+ <%= form.label :allow, "Who may sign in" %>
5
+ <%= form.text_field :allow, value: way.allowed.join(", "), spellcheck: false, autocomplete: "off" %>
6
+ </p>
@@ -0,0 +1,2 @@
1
+ <%# locals: (way:, form:) %>
2
+ <p class="facts">At this Mac's name on your tailnet, and nowhere on the internet. Your phone needs the Tailscale app, signed in to <%= way.tailnet ? "the tailnet #{way.tailnet}" : "the same tailnet" %>: one account can be in more than one.</p>
data/bin/proctolog CHANGED
@@ -17,6 +17,7 @@ end
17
17
  require_relative "../config/application"
18
18
  require_relative "../app/models/proctolog/project"
19
19
  require "rbconfig"
20
+ require "socket"
20
21
  require "thor"
21
22
 
22
23
  module Proctolog
@@ -26,9 +27,15 @@ module Proctolog
26
27
 
27
28
  # A command prints what it was asked for and nothing else, and asks
28
29
  # 1Password for no keys. The desk's log, with what Grouse says at boot it
29
- # is missing, and the keys are for `serve`, which says so in the block.
30
- def boot!
30
+ # is missing, and the keys are for the owner's `serve`, which says so in
31
+ # the block. A trial desk serves without them.
32
+ def boot!(&)
31
33
  Project.current = Project.discover(options[:project] || Dir.pwd)
34
+ boot_desk!(&)
35
+ end
36
+
37
+ # The desk alone, for a command of this Mac's rather than a checkout's.
38
+ def boot_desk!
32
39
  Desk.config.log_level = :error
33
40
  Desk.config.x.reads_keys = false
34
41
  yield Desk.config if block_given?
@@ -128,6 +135,69 @@ module Proctolog
128
135
  def tree = super
129
136
  end
130
137
 
138
+ class ShareCommands < Thor
139
+ include Booting
140
+
141
+ def self.exit_on_failure? = true
142
+
143
+ class_option :port, type: :numeric, default: 4040, aliases: "-p", desc: "The port of the desk"
144
+ default_command :start
145
+
146
+ desc "start", "Share the desk through Tailscale or ngrok, and print its address"
147
+ long_desc <<~TEXT
148
+ Lets you reach the desk on this Mac from your phone, or a computer elsewhere. Through Tailscale, only
149
+ the devices signed in to your tailnet reach it, at this Mac's name there, and nothing goes on the
150
+ internet. Through ngrok, it is on the internet behind ngrok's sign-in with Google, which lets in only
151
+ the addresses --allow names, your Git email when none is named. Without --via, it shares the way named
152
+ last, or the first one ready, Tailscale first. The share stays open until `proctolog share stop`, the
153
+ desk running or not; one already open is only printed. `proctolog share status` says what each way
154
+ needs first.
155
+ TEXT
156
+ option :via, type: :string, enum: %w[tailscale ngrok], desc: "The way to share it, remembered for the next time"
157
+ option :allow, type: :string, repeatable: true, desc: "An email that may sign in through ngrok, in place of your Git email"
158
+ def start
159
+ boot_desk!
160
+ abort "No desk answers on :#{port}. Start one with the share: proctolog serve --share#{port_option}" unless answers?
161
+ puts Share.open(port, via: options[:via], allow: options[:allow])
162
+ puts "Stop it with: proctolog share stop#{port_option}"
163
+ rescue Share::Refused => e
164
+ abort e.message
165
+ end
166
+
167
+ desc "stop", "Stop sharing the desk: it is only on this Mac again"
168
+ def stop
169
+ boot_desk!
170
+ share = Share.at(port)
171
+ share&.close
172
+ puts share ? "The desk on :#{port} is only on this Mac again." : "The desk on :#{port} was not shared."
173
+ rescue Share::Refused => e
174
+ abort e.message
175
+ end
176
+
177
+ desc "status", "Say whether the desk is shared, and what Tailscale and ngrok each need to share it"
178
+ def status
179
+ boot_desk!
180
+ puts Share.at(port) || "The desk on :#{port} is only on this Mac, at #{Desk.address(port)}."
181
+ Share.ways.each { puts it.ready? ? "ok #{it.name}: ready, and #{it.who}" : "-- #{it.unready}" }
182
+ end
183
+
184
+ desc "tree", "Print a tree of all available commands", hide: true
185
+ def tree = super
186
+
187
+ no_commands do
188
+ def port = options[:port]
189
+
190
+ def port_option = (" --port #{port}" unless port == 4040)
191
+
192
+ def answers?
193
+ Socket.tcp("localhost", port, connect_timeout: 1, &:close)
194
+ true
195
+ rescue SystemCallError
196
+ false
197
+ end
198
+ end
199
+ end
200
+
131
201
  class CLI < Thor
132
202
  include Booting
133
203
 
@@ -141,19 +211,26 @@ module Proctolog
141
211
  desc "unopened SUBCOMMAND", "List the pull requests nobody has opened yet, or say where one is written"
142
212
  subcommand "unopened", UnopenedCommands
143
213
 
144
- desc "serve", "Open the desk on http://localhost:4040"
214
+ desc "share SUBCOMMAND", "Share the desk with your phone or another computer, through Tailscale or ngrok"
215
+ subcommand "share", ShareCommands
216
+
217
+ desc "serve", "Open the desk on http://proctolog.localhost:4040"
145
218
  option :port, type: :numeric, default: 4040, aliases: "-p", desc: "The port to listen on"
219
+ option :share, type: :boolean, default: false, desc: "Share it too, as `proctolog share` does"
146
220
  def serve
147
221
  boot! do |config|
148
222
  config.log_level = :warn
149
- config.x.reads_keys = true
223
+ config.x.reads_keys = !Desk.trial?
224
+ config.x.port = options[:port]
150
225
  end
151
226
  require "rackup"
152
227
  Release.newer&.then { puts it.news }
153
228
  Housekeeping.start
154
- Rackup::Handler.get("puma").run(Desk, Host: "localhost", Port: options[:port])
229
+ Rackup::Handler.get("puma").run(Desk, Host: "localhost", Port: options[:port]) do |launcher|
230
+ launcher.events.after_booted { opened }
231
+ end
155
232
  rescue Errno::EADDRINUSE
156
- abort "Port #{options[:port]} is taken. If that is the desk, it is open at http://localhost:#{options[:port]}; " \
233
+ abort "Port #{options[:port]} is taken. If that is the desk, it is open at #{Desk.address(options[:port])}; " \
157
234
  "for a second one: proctolog serve --port #{options[:port] + 1}"
158
235
  rescue Project::Missing => e
159
236
  abort e.message
@@ -275,6 +352,15 @@ module Proctolog
275
352
  end
276
353
 
277
354
  no_commands do
355
+ # Once the desk listens: where it is, and where else, when it is shared.
356
+ # A share that cannot open leaves the desk open on this Mac.
357
+ def opened
358
+ puts "The desk is open at #{Desk.address}."
359
+ puts(options[:share] ? Share.open(Desk.port) : Share.at(Desk.port) || return)
360
+ rescue Share::Refused => e
361
+ warn "The desk is not shared. #{e.message}"
362
+ end
363
+
278
364
  # rv runs as the shell would, not on the Ruby and gems it gave this one.
279
365
  def take_with_rv(latest)
280
366
  Outside.run do |environment|
@@ -5,7 +5,7 @@
5
5
  # where burst files were kept before; for check and burst new, the open pull
6
6
  # requests of the desk's last reading, with their titles, and the branches
7
7
  # whose pull requests nobody has opened yet; for unopened folder, every
8
- # branch of the checkout.
8
+ # branch of the checkout; for share, its commands and the ways to share.
9
9
 
10
10
  # "owner/name" of the checkout's GitHub origin.
11
11
  _proctolog_repository() {
@@ -47,9 +47,10 @@ _proctolog_parts() {
47
47
 
48
48
  _proctolog() {
49
49
  local root own named
50
- local -a commands burst_commands unopened_commands bursts pull_requests unopened branches parts project files
50
+ local -a commands burst_commands unopened_commands share_commands bursts pull_requests unopened branches parts project port files
51
51
  commands=(
52
- 'serve:Open the desk on http://localhost:4040'
52
+ 'serve:Open the desk on http://proctolog.localhost:4040'
53
+ 'share:Share the desk with your phone or another computer, through Tailscale or ngrok'
53
54
  'route:Print the order to take a burst in'
54
55
  'handover:Hand what the owner decided so far to the agent that runs it'
55
56
  'burst:Start a burst of your own, or list the bursts there are'
@@ -73,7 +74,14 @@ _proctolog() {
73
74
  'folder:Print the folder a pull request nobody has opened yet is written in'
74
75
  'help:Describe the unopened commands, or one of them'
75
76
  )
77
+ share_commands=(
78
+ 'start:Share the desk through Tailscale or ngrok, and print its address'
79
+ 'stop:Stop sharing the desk: it is only on this Mac again'
80
+ 'status:Say whether the desk is shared, and what Tailscale and ngrok each need'
81
+ 'help:Describe the share commands, or one of them'
82
+ )
76
83
  project=('(-C --project)'{-C,--project}'[Git checkout]:directory:_directories')
84
+ port=('(-p --port)'{-p,--port}'[The port of the desk]:port:')
77
85
  root=$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null)
78
86
  own=${PROCTOLOG_DATA:-$HOME/.local/share/proctolog}/$(_proctolog_repository "$root")/bursts
79
87
  bursts=($own/*.yml(N:t:r) $root/.proctolog/{bursts,reviews}/*.yml(N:t:r))
@@ -85,7 +93,23 @@ _proctolog() {
85
93
  command) _describe -t commands 'command' commands ;;
86
94
  argument)
87
95
  case $words[1] in
88
- serve) _arguments '(-p --port)'{-p,--port}'[The port to listen on]:port:' $project ;;
96
+ serve) _arguments $port '--share[Share it too, as proctolog share does]' $project ;;
97
+ share)
98
+ # Without a subcommand, share is start: its options come at once.
99
+ if (( CURRENT == 2 )) && [[ $words[2] != -* ]]; then
100
+ _describe -t commands 'share command' share_commands
101
+ else
102
+ case $words[2] in
103
+ start|-*)
104
+ _arguments $port \
105
+ '--via[The way to share it, remembered for the next time]:way:(tailscale ngrok)' \
106
+ '*--allow[An email that may sign in through ngrok]:email:' \
107
+ '1:: :' ;;
108
+ stop|status) _arguments $port '1:: :' ;;
109
+ help) _describe -t commands 'share command' share_commands ;;
110
+ esac
111
+ fi
112
+ ;;
89
113
  route) _arguments $project "1:burst:(${bursts})" ;;
90
114
  handover)
91
115
  named=${${words[2,-1]:#-*}[1]}
@@ -53,6 +53,18 @@ module Proctolog
53
53
 
54
54
  def self.scratch = @scratch ||= Pathname(Dir.mktmpdir("proctolog")).tap { |made| at_exit { made.rmtree } }
55
55
 
56
+ def self.port = config.x.port
57
+
58
+ # Where the desk is on this Mac: a name of its own, which a browser's
59
+ # history completes from "procto" where localhost:4040 is one of many.
60
+ def self.address(port = self.port) = "http://proctolog.localhost:#{port}"
61
+
62
+ # A desk on marks of its own (PROCTOLOG_MARKS): a test's, or one an agent
63
+ # starts to try a change, often while the owner is away. It writes nothing
64
+ # to GitHub, and asks 1Password for no keys, which would wait on the owner
65
+ # to unlock it.
66
+ def self.trial? = ENV.key?("PROCTOLOG_MARKS")
67
+
56
68
  config.root = File.expand_path("..", __dir__)
57
69
  config.load_defaults 8.1
58
70
  config.eager_load = false
@@ -61,8 +73,14 @@ module Proctolog
61
73
  # Kept outside the installed app so an update does not sign out an open desk.
62
74
  config.secret_key_base = kept("secret_key_base") { SecureRandom.hex(64) }
63
75
  # Whatever the environment: a page of another site that points a name of
64
- # its own at this Mac is turned away.
65
- config.hosts = ["localhost", IPAddr.new("127.0.0.0/8"), IPAddr.new("::1")]
76
+ # its own at this Mac is turned away. Names under localhost are this
77
+ # Mac's alone, as browsers send them nowhere else: proctolog.localhost is
78
+ # the desk's own. While the desk is shared, it answers to the share's
79
+ # name too (Share).
80
+ config.hosts = [".localhost", IPAddr.new("127.0.0.0/8"), IPAddr.new("::1")]
81
+ config.host_authorization = {exclude: ->(request) { Proctolog::Share.carries?(request) }}
82
+ # The port the desk serves on, which `serve` names.
83
+ config.x.port = 4040
66
84
  config.logger = ActiveSupport::Logger.new($stdout)
67
85
  # Grouse says at boot what is not set up yet, which its tests need not hear.
68
86
  config.log_level = Rails.env.test? ? :error : :warn
@@ -80,8 +98,9 @@ module Proctolog
80
98
  config.active_job.queue_adapter = Rails.env.test? ? :test : :async
81
99
  # Whether Grouse is given the owner's keys from 1Password (config/keys.yml):
82
100
  # when the desk serves, and for Rails' own commands such as grouse:doctor.
83
- # A `proctolog` command that only prints asks 1Password for nothing.
84
- config.x.reads_keys = Rails.env.development?
101
+ # A `proctolog` command that only prints asks 1Password for nothing, and
102
+ # nor does a trial desk.
103
+ config.x.reads_keys = Rails.env.development? && !trial?
85
104
  config.active_storage.service = :local
86
105
  config.active_storage.variant_processor = :disabled
87
106
  # The migrations are the record; the desk brings its database up to them
@@ -86,7 +86,7 @@ Grouse.configure do |c|
86
86
  # The owner's keys, read from 1Password by config/keys.yml's references where the desk
87
87
  # reads keys at all (config.x.reads_keys). Without one, a voice note arrives as audio only
88
88
  # and a note is not written up; `op` says on the terminal why it could not read it. Tests
89
- # read none, and send nothing to a vendor.
89
+ # and trial desks (PROCTOLOG_MARKS) read none, and send nothing to a vendor.
90
90
  c.transcriber = Grouse::Transcribers::ElevenLabs.new(api_key: key.call("elevenlabs"))
91
91
  c.structurer = Grouse::Structurers::Anthropic.new(api_key: key.call("anthropic"))
92
92
  # c.transcriber = nil # turns a recording into words; with none, voice notes arrive as audio only
data/config/routes.rb CHANGED
@@ -17,6 +17,9 @@ Proctolog::Desk.routes.draw do
17
17
  # The pull requests, of every state, that a # typed in any box may mean.
18
18
  resources :mentions, only: :index
19
19
 
20
+ # The desk shared with the owner's other devices, through Tailscale or ngrok.
21
+ resource :share, only: %i[show create destroy]
22
+
20
23
  resources :bursts, only: %i[show create] do
21
24
  resource :put_away, only: %i[create destroy], controller: :put_away
22
25
  resources :pull_requests, only: :show, param: :number do
@@ -7,8 +7,8 @@ the one waiting for you; `proctolog route` and `proctolog handover` take it too
7
7
  when no burst is named. With no burst yet, it opens on the repository's open
8
8
  pull requests, at `/pull_requests`. "bursts & PRs" in the pane of every page
9
9
  goes there: every burst at its top, the newest first, then the pull requests.
10
- Its own pane says no burst is open, and lists your bursts with how far each
11
- one's decisions are. A burst put away says so beside its name in its pane.
10
+ Its own pane says no burst is open, and lists your bursts with how many of
11
+ each one's decisions wait: a bead of each grade, and how many of it. A burst put away says so beside its name in its pane.
12
12
  GitHub's stacks come first, each whole from the bottom up, then the pull requests in no
13
13
  stack: yours first, then those written for PRoctolog (they have decisions),
14
14
  then the newest. Each line says whose it is, what it sits on when that is not
@@ -27,6 +27,11 @@ Under "Not opened" come the branches with a description written for them that
27
27
  nobody has opened yet, each with "Review alone", and "Review the chain" for one
28
28
  that sits on another's branch ([how](unopened.md)).
29
29
 
30
+ "Start here" heads the page until you have a burst of your own, and whenever
31
+ there is nothing to decide: it says how many pull requests are written for
32
+ PRoctolog and how to open one, that none is yet and how to have them
33
+ rewritten, or that nothing is open and how to have some written.
34
+
30
35
  ## When a burst is done
31
36
 
32
37
  A pull request that merges leaves the route, and goes under "Merged" at its
@@ -17,7 +17,10 @@ question's text, “Ask Claude now” opens a new Claude Code session in the
17
17
  Claude app, in the checkout under review, with a prompt typed in: the decision
18
18
  quoted with its pull request, branch and head, the pictures it rests on where
19
19
  the desk keeps them on this Mac, and your question last, where you can go on
20
- typing. It asks the session to answer and change nothing. Nothing is sent
20
+ typing. It asks the session to answer and change nothing. A pull request
21
+ nobody has opened yet is named by its branch, its base and the file its
22
+ description is kept in, and the session is told to read it with git, since
23
+ GitHub has nothing of it. Nothing is sent
21
24
  until you send it there. “Copy prompt” copies the same prompt, for a session
22
25
  already open or another agent. With decisions selected, the prompt quotes each
23
26
  of them. Asking leaves the question here a draft and the decision open: once
@@ -56,6 +56,12 @@ to response, and Esc goes back to where `v` was pressed. Wherever you are in a
56
56
  response, `a` confirms it, `q` replies or asks, `c` requests a correction, and
57
57
  `l` defers. Ctrl/⌘ + Enter saves the reply; `e` exports the next pass.
58
58
 
59
+ A note of yours that an agent answers follows its answer in the marks, where
60
+ the page's Notes and an agent reading the files both see it: addressed once
61
+ the answer is in, confirmed once you confirm it here, open again once you
62
+ answer back. Its card says to judge it under Returned work and offers only
63
+ Withdraw. The pane's `↩` follows as you go, without the page loading again.
64
+
59
65
  What the agent wrote is drawn as GitHub draws markdown. Under Evidence, the
60
66
  pictures it names are shown, read from where they are on this Mac: two named
61
67
  alike but for `before` and `after` stand side by side as one, and Space looks
@@ -42,18 +42,23 @@ server goes on in the background until it is stopped. When it has stopped
42
42
  without the desk, the end of its log shows while the pointer is on that line.
43
43
  `t` goes to Try it. In a narrower window it waits at the foot of the page.
44
44
 
45
- On the Route, `j` / `k` select a PR and Enter opens it. Each line shows how many
46
- of its decisions you have reviewed, and whose it is when it is not yours; the
47
- line the cursor is on opens out with the rest: its layer, what is open, its
48
- pictures and words, and when it last changed. `N` opens the next PR with
49
- decisions left. Clicking a title opens that PR. Clicking elsewhere on a row,
45
+ On the Route, `j` / `k` select a PR and Enter opens it. Each line ends with its
46
+ decisions as beads, a bead to each, as deciding draws them: a full-size bead
47
+ for a blocking decision, a small one for a non-blocking, solid once reviewed,
48
+ hollow while it waits. Before them it says **back to you** once the agent's
49
+ answers have come back for you to confirm, or answer, or once it addressed one
50
+ of your notes; otherwise whose it is when it is not yours: with the agent,
51
+ settled or waiting. The line the cursor is on opens out with the rest: its
52
+ layer, what is open (“4 returned, to confirm”), its pictures and words, and
53
+ when it last changed. `N` opens the next PR that asks for you: one with a
54
+ blocking decision left, or something back from the agent. Clicking a title opens that PR. Clicking elsewhere on a row,
50
55
  or tabbing to one of its links, selects it and shows its actions. In a narrow
51
56
  window those actions wrap under the selected row. On
52
57
  a PR's page, `j` / `k` or the up/down arrows move through headings, description
53
58
  blocks, pictures, decisions and returned work, as below. `[` / `]` jump between
54
59
  sections; Enter or Right opens a section, Left closes it. Home / End reach the
55
60
  first / last item. Tab / Shift+Tab reach links, buttons and fields. `J` / `K` open the
56
- next / previous PR, `N` the next one with decisions to review, and `h` returns
61
+ next / previous PR, `N` the next one that asks for you, and `h` returns
57
62
  to the Route. `G`, on the Route or a PR's page, opens the PR on GitHub in a tab
58
63
  of its own.
59
64
 
@@ -93,12 +98,14 @@ part stands apart in the pane. A handover of one part says the rest of the
93
98
  burst is outside it, and names the chains that sit on it, so a change to it is
94
99
  carried up through them.
95
100
 
96
- A pane at the left of every page lists the route, each PR with its count
97
- (`x` all reviewed, `~` some, no mark for none yet, `-` when it has none), and
98
- marks the one you are on. A `*` before a title means GitHub moved on since the
99
- desk read it. Hold the pointer on a row and a card beside the pane gives its
100
- title in full, says each of its marks in words, and links to it on GitHub
101
- (and to “read again” when it moved).
101
+ A pane at the left of every page lists the route, and marks the one you are
102
+ on. Each PR ends with the same beads as its line on the Route, smaller, and
103
+ they follow your choices as you make them. A `↩` before it means the agent's
104
+ answers came back on it for you. A `*` before a title means GitHub moved on
105
+ since the desk read it. Hold the pointer on a row and a card beside the pane
106
+ gives its title in full, says each of its marks in words (“back to you: 4
107
+ returned, to confirm”, “1 of 4 blocking decisions reviewed”), and links to it
108
+ on GitHub (and to “read again” when it moved).
102
109
 
103
110
  The desk has no colour. What you are on and what waits for you is white and
104
111
  bold; what is settled is grey. A bead says the same of anything that is
@@ -120,23 +127,43 @@ beside its box with its words: ⌘ Enter saves it written over, in the same
120
127
  place, and Withdraw takes it back off the picture. Escape puts it away while
121
128
  nothing in it is changed. Escape, or the ✕ at the top
122
129
  right, closes the viewer and nothing else: you are back on the block, or the
123
- decision, you opened it from. Once it closes, the page comes back at the last
124
- note written; while deciding, it stays on the decision, and the note shows the
125
- next time the page loads.
130
+ decision, you opened it from, and the page draws the notes written there
131
+ where it stands. Once the viewer shows only part of the picture, a map in its
132
+ corner shows the whole, with the part on the stage framed; a press on the map
133
+ goes there.
134
+
135
+ A note on a picture of the page itself, a click or a drag on it, is written
136
+ under the picture and saved where it stands too: the page does not load
137
+ again or move, zoomed in or not, and draws the note's box and its card.
126
138
 
127
139
  Two pictures named `NAME-before` and `NAME-after` are one picture of a change:
128
140
  the description shows them side by side under one name, and a decision links
129
- them as `#screenshot-NAME`. In the viewer, `s` shows them side by side, `w`
130
- swipes (the after picture over the before, up to a divider that a drag or the
131
- arrows move), `b` blinks from one to the other at each press, `o` lays the
132
- after picture half seen through over the before, and `d` lights only what
133
- differs. `n` goes from one changed region to the next, each outlined on the
134
- pictures. The caption says “Comparing…” until the regions are found, then how
135
- many there are, “Identical”, “Different sizes: compared from the top left”, or
136
- “Regions need ImageMagick” on a Mac without it. A mark goes on the half it is
137
- made on side by side, and on the one showing in blink; swipe, onion skin and
138
- difference take none. Comparisons are kept in the cache, by the pictures'
139
- pixels.
141
+ them as `#screenshot-NAME`. In the viewer a quiet panel beside them lists the
142
+ five ways to look at them, each a button with its key; the one you are in
143
+ says what it shows. `s` side by side, `w` swipe (the after picture over the
144
+ before, up to a divider that a drag or the arrows move), `b` blink (one, then
145
+ the other), `o` onion skin (the after picture half seen through over the
146
+ before) and `d` difference (only what differs is lit). A view's key again, or
147
+ a press on its button, goes back to side by side. Blink is the exception: `b`
148
+ again shows the other half, when you want it, and Shift B sets it blinking by
149
+ itself, or stops it.
150
+
151
+ Under the views the panel counts the changed regions. They are outlined on
152
+ the pictures in black dashed with white, at the same place on each half; a
153
+ note's box is a heavier solid frame, so the two are told apart at a glance.
154
+ `n` and `p` focus on the next region and the one before, as does a press on
155
+ its number: it is tagged with its number and its half. Focusing keeps your
156
+ zoom: seen whole, the region is only outlined; zoomed in, the stage goes near
157
+ enough to see each region whole. `z` goes near to the one in focus, and `0`
158
+ back to the whole. The panel shows the region focused on as the before has it
159
+ and as the after does; Enter writes a note on it in the after, and a press on
160
+ either writes one there, in any view. Until the regions are found the panel
161
+ says “Comparing…”, then how many there are, “Nothing: the same pixels.”,
162
+ “Sizes differ: laid from the top left.”, or that finding them needs
163
+ ImageMagick. A click or a drag marks the half it is made on side by side, and
164
+ the one showing in blink, which holds still while you write; swipe, onion skin
165
+ and difference take a note only on a region the panel shows. Comparisons are
166
+ kept in the cache, by the pictures' pixels.
140
167
 
141
168
  A run of pictures that no decision or other line of the description links
142
169
  folds where it stood, with its captions, into one “More pictures (N)”, opened
@@ -162,7 +189,7 @@ discarding it, then Escape returns to the section heading. `u` undoes the last
162
189
  choice, wherever you are on the page. Once every blocking decision is
163
190
  reviewed, the page shows where the decisions end: what is reviewed and how many
164
191
  non-blocking still wait; what to do next, where `g` marks the PR good to go,
165
- `N` opens the next PR with decisions to review, `e` hands over and
192
+ `N` opens the next PR that asks for you, `e` hands over and
166
193
  `h` goes back to the Route; and every decision on a line under its grade, with
167
194
  what you chose on it or what holds it. Each grade says what it asks of you: a
168
195
  blocking decision needs your answer before the work goes on, and a non-blocking
@@ -0,0 +1,77 @@
1
+ # The desk's address, and sharing it
2
+
3
+ ## On this Mac
4
+
5
+ The desk is at <http://proctolog.localhost:4040>. Arc, Chrome and the other
6
+ Chromium browsers send any name under `localhost` to this Mac without asking
7
+ anyone, so there is nothing to set up; after the first visit, typing `procto`
8
+ in the address bar finds it. `http://localhost:4040` still works too. Safari asks macOS, which
9
+ doesn't know these names, so use `localhost` there.
10
+
11
+ A bare `proctolog`, with no port, would need the desk on port 80, which macOS
12
+ gives a user only on every network at once, Wi-Fi included. The desk listens
13
+ on this Mac alone, so it stays on its port.
14
+
15
+ ## From your phone, or a computer elsewhere
16
+
17
+ Sharing the desk lets you reach it from your other devices while this Mac
18
+ keeps it: a review on your phone, say, while an agent works on. Only you get
19
+ in. It goes through whichever of these this Mac has:
20
+
21
+ - **Tailscale.** The desk is at this Mac's name on your tailnet,
22
+ `https://your-mac.your-tailnet.ts.net`. Only the devices signed in to your
23
+ tailnet reach it, so your phone needs the Tailscale app, signed in to the
24
+ same tailnet as this Mac. One account can be in more than one tailnet, one
25
+ of its own and others it was invited to, so check the tailnet's name, not
26
+ only the account: `proctolog share status` and the sharing page say it.
27
+ Nothing goes on the internet, and nothing limits how much you open. Install it from <https://tailscale.com/download/mac>; the desk asks
28
+ for HTTPS certificates to be on for your tailnet, which they are unless you
29
+ turned them off, at <https://login.tailscale.com/admin/dns>.
30
+ - **ngrok.** The desk is on the internet, at the address ngrok gives your
31
+ account, behind ngrok's sign-in with Google, which lets in only your Git
32
+ email unless you name others. Install it (`brew install ngrok`), then copy
33
+ your authtoken from <https://dashboard.ngrok.com/get-started/your-authtoken>
34
+ and run `ngrok config add-authtoken TOKEN`. On the free plan, ngrok shows a
35
+ warning page of its own before the desk, and carries 1 GB a month.
36
+
37
+ Neither is installed for you. `proctolog doctor` and `proctolog share status`
38
+ say what each one still needs.
39
+
40
+ ### Turning it on
41
+
42
+ On any page with the pane at the left, its foot says whether the desk is
43
+ shared; **share it** opens the page that shares it, through either way,
44
+ and stops it again. From a terminal:
45
+
46
+ ```sh
47
+ proctolog share # the way you named last, or the first one ready
48
+ proctolog share --via ngrok --allow you@gmail.com
49
+ proctolog share status # shared or not, and what each way needs
50
+ proctolog share stop
51
+ proctolog serve --share # open the desk, shared
52
+ ```
53
+
54
+ An agent can run them too, on a Mac you left working, and send you the
55
+ address it prints. A share stays open until it is stopped, whether the desk
56
+ is running or not, and a share already open is only printed again. A trial
57
+ desk, on marks of its own, is never shared.
58
+
59
+ ### What passes through it
60
+
61
+ Everything a page shows comes from this Mac, so everything you open travels
62
+ through the share. That counts for ngrok's 1 GB: a pull request's pictures
63
+ are on GitHub, but GitHub gives them only to someone signed in, so the desk
64
+ fetches each one once, with your `gh`, keeps it, and serves it itself. Its
65
+ films are the same. Keeping them saves asking GitHub again, not the trip to
66
+ your phone.
67
+
68
+ ### What stays on this Mac
69
+
70
+ - **The app under review.** A scenario's button and the Try it rail open the
71
+ app at `localhost`, which on a phone is the phone. They work on this Mac
72
+ only for now.
73
+ - **Grouse's overlay.** The desk shows it to a request from this Mac, and one
74
+ through the share isn't one.
75
+
76
+ The desk is drawn for a keyboard. On a phone it reads, and its buttons and
77
+ links work, but the keys have no touch of their own yet.
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ # CHANGELOG.md as a release reads it: a section to each version, newest
5
+ # first, under ## Unreleased what main has that no gem has yet. Plain Ruby,
6
+ # as bin/release and the Release workflow read it before any bundle.
7
+ class Changelog
8
+ PATH = File.expand_path("../../CHANGELOG.md", __dir__)
9
+
10
+ # One ## section, its lines under ### Added, Changed and Fixed.
11
+ Section = Data.define(:name, :text) do
12
+ def adds? = text.match?(/^### Added$/)
13
+ def lines? = text.match?(/^- /)
14
+ def notes = text.strip
15
+ end
16
+
17
+ # It writes ⌘ and ↩, so it is read as UTF-8 whatever the locale.
18
+ def self.read(path = PATH) = new(File.read(path, encoding: "UTF-8"))
19
+
20
+ def initialize(text)
21
+ @sections = text.split(/^## /).drop(1).map do
22
+ name, body = it.split("\n", 2)
23
+ Section.new(name: name.strip, text: body.to_s)
24
+ end
25
+ end
26
+
27
+ def [](name) = @sections.find { it.name == name }
28
+
29
+ def unreleased = self["Unreleased"]
30
+
31
+ # Adding something makes a minor (docs/manual/updates.md): the minor this
32
+ # version owes when it adds something but is only a patch on the one
33
+ # before it.
34
+ def minor_owed(version)
35
+ owed = released_before(version)&.then { Gem::Version.new("#{it.bump}.0") }
36
+ owed if owed && adds?(version) && Gem::Version.new(version) < owed
37
+ end
38
+
39
+ private
40
+
41
+ # Under its own heading, or still under Unreleased, since main has that
42
+ # too.
43
+ def adds?(version) = [self[version], unreleased].compact.any?(&:adds?)
44
+
45
+ def released_before(version)
46
+ older = @sections.drop_while { it.name != version }.drop(1)
47
+ older.map(&:name).find { Gem::Version.correct?(it) }&.then { Gem::Version.new(it) }
48
+ end
49
+ end
50
+ end
@@ -3,5 +3,5 @@
3
3
  module Proctolog
4
4
  # Semantic: a patch fixes, a minor adds, a major asks something new of the
5
5
  # owner (a command gone, a file moved). docs/manual/updates.md says more.
6
- VERSION = "0.1.0"
6
+ VERSION = "0.2.0"
7
7
  end