proctolog 0.1.1 → 0.3.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/CHANGELOG.md +111 -3
- data/README.md +32 -18
- data/app/controllers/proctolog/application_controller.rb +2 -2
- data/app/controllers/proctolog/bursts_controller.rb +1 -1
- data/app/controllers/proctolog/choices_controller.rb +3 -1
- data/app/controllers/proctolog/decisions_controller.rb +1 -1
- data/app/controllers/proctolog/notes_controller.rb +2 -2
- data/app/controllers/proctolog/prompts_controller.rb +1 -1
- data/app/controllers/proctolog/pull_requests_controller.rb +2 -4
- data/app/controllers/proctolog/review_data_controller.rb +3 -1
- data/app/controllers/proctolog/shares_controller.rb +27 -0
- data/app/helpers/proctolog/decisions_helper.rb +6 -2
- data/app/helpers/proctolog/desk_helper.rb +48 -8
- data/app/helpers/proctolog/rounds_helper.rb +4 -2
- data/app/helpers/proctolog/shares_helper.rb +11 -0
- data/app/helpers/proctolog/standings_helper.rb +5 -4
- data/app/models/proctolog/burst.rb +17 -8
- data/app/models/proctolog/choices.rb +8 -0
- data/app/models/proctolog/doctor.rb +23 -12
- data/app/models/proctolog/github.rb +1 -1
- data/app/models/proctolog/localhost.rb +17 -0
- data/app/models/proctolog/marks.rb +1 -1
- data/app/models/proctolog/note.rb +4 -0
- data/app/models/proctolog/owner.rb +3 -0
- data/app/models/proctolog/pick.rb +12 -0
- data/app/models/proctolog/rounds.rb +37 -0
- data/app/models/proctolog/server.rb +1 -7
- data/app/models/proctolog/setup.rb +190 -0
- data/app/models/proctolog/share/ngrok.rb +120 -0
- data/app/models/proctolog/share/settings.rb +23 -0
- data/app/models/proctolog/share/tailscale.rb +103 -0
- data/app/models/proctolog/share.rb +101 -0
- data/app/models/proctolog/standing.rb +24 -7
- data/app/views/proctolog/bursts/_line.html.erb +2 -2
- data/app/views/proctolog/bursts/_side.html.erb +6 -5
- data/app/views/proctolog/bursts/_side_stop.html.erb +4 -4
- data/app/views/proctolog/notes/_card.html.erb +7 -3
- data/app/views/proctolog/open_pull_requests/_side.html.erb +4 -3
- data/app/views/proctolog/open_pull_requests/_start.html.erb +8 -0
- data/app/views/proctolog/open_pull_requests/index.html.erb +4 -5
- data/app/views/proctolog/open_pull_requests/start/_first_time.html.erb +3 -0
- data/app/views/proctolog/open_pull_requests/start/_nothing_open.html.erb +3 -0
- data/app/views/proctolog/open_pull_requests/start/_nothing_written.html.erb +3 -0
- data/app/views/proctolog/shares/_line.html.erb +5 -0
- data/app/views/proctolog/shares/show.html.erb +48 -0
- data/app/views/proctolog/shares/ways/_ngrok.html.erb +6 -0
- data/app/views/proctolog/shares/ways/_tailscale.html.erb +2 -0
- data/bin/proctolog +154 -27
- data/completions/_proctolog +31 -6
- data/config/application.rb +23 -4
- data/config/initializers/grouse.rb +1 -1
- data/config/routes.rb +3 -0
- data/docs/manual/bursts.md +7 -2
- data/docs/manual/deciding.md +4 -1
- data/docs/manual/handover.md +6 -0
- data/docs/manual/pr-format.md +1 -1
- data/docs/manual/reading.md +54 -27
- data/docs/manual/setup.md +59 -0
- data/docs/manual/sharing.md +77 -0
- data/docs/manual/updates.md +11 -4
- data/lib/proctolog/changelog/news.rb +67 -0
- data/lib/proctolog/changelog.rb +75 -0
- data/lib/proctolog/installation.rb +19 -0
- data/lib/proctolog/version.rb +1 -1
- data/public/asking.js +31 -4
- data/public/decisions.js +4 -4
- data/public/desk.js +10 -6
- data/public/keys.js +6 -0
- data/public/notes.js +164 -58
- data/public/pick.css +21 -0
- data/public/respond.css +6 -0
- data/public/respond.js +51 -7
- data/public/review-drafts.js +47 -9
- data/public/review-rounds.js +27 -3
- data/public/share.css +58 -0
- data/public/side.js +11 -20
- data/public/terminal.css +295 -34
- data/public/viewer.js +700 -173
- metadata +22 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Setting up, a step at a time
|
|
2
|
+
|
|
3
|
+
`proctolog setup` is the one command to remember. Run it the first time, with
|
|
4
|
+
`rvx proctolog setup` or after `gem install proctolog`, and again whenever
|
|
5
|
+
you're unsure something is in place. It takes you from wherever you are to an
|
|
6
|
+
open desk, one step at a time. `proctolog install` is another name for it, so
|
|
7
|
+
whichever you remember works.
|
|
8
|
+
|
|
9
|
+
1. **PRoctolog on this Mac**: setup links the `proctolog` command, zsh's
|
|
10
|
+
completion and the agent skills to this PRoctolog, then checks Ruby, the
|
|
11
|
+
command on your PATH, and whether a newer PRoctolog is out.
|
|
12
|
+
2. **GitHub, through gh**: installed and signed in.
|
|
13
|
+
3. **The checkout you review**: a Git checkout whose origin is on GitHub,
|
|
14
|
+
and which your `gh` can read. Outside one, type its folder here.
|
|
15
|
+
4. **A first burst**: the pull requests you review together.
|
|
16
|
+
5. **What turns features on**: Chrome, Diffle, ImageMagick, ffmpeg and the
|
|
17
|
+
checkout's `.proctolog/app.yml`.
|
|
18
|
+
6. **Your phone, through a share**: Tailscale or ngrok, what each gives, and
|
|
19
|
+
where to get them ([sharing](sharing.md)).
|
|
20
|
+
7. **The desk**: Enter opens it with `proctolog serve`.
|
|
21
|
+
|
|
22
|
+
Each step says why it matters and shows doctor's lines for it, each with the
|
|
23
|
+
fix. Do what it says in another terminal, then press Enter there: the step
|
|
24
|
+
checks itself again. A step that is met is stamped `[ DONE ]` and the next
|
|
25
|
+
one begins, so a second run passes over what you already have, stamping each
|
|
26
|
+
step as it goes. The links in the first step are PRoctolog's own, made again
|
|
27
|
+
each time; anything of the same name that is someone else's stays, and is
|
|
28
|
+
said. Nothing else is installed for you: a step explains, links and checks.
|
|
29
|
+
|
|
30
|
+
Some steps can wait. Type `l` and Enter to leave one for later, and it is
|
|
31
|
+
stamped `[ LATER ]` with what does its work meanwhile:
|
|
32
|
+
|
|
33
|
+
- A step whose lines are only `--` is a nicety: the desk opens without it.
|
|
34
|
+
- The first step, when the command is linked but your PATH doesn't have it
|
|
35
|
+
yet: setup goes on as it is, and a new terminal finds `proctolog` once
|
|
36
|
+
`~/.zshrc` puts it there.
|
|
37
|
+
- A first burst: **Review** on the desk's first page starts one, so the step
|
|
38
|
+
needn't hold you up.
|
|
39
|
+
- The features step: each feature waits for its own program, and the rest of
|
|
40
|
+
the desk works.
|
|
41
|
+
|
|
42
|
+
gh and the checkout can't wait, as the desk opens nothing without them;
|
|
43
|
+
there, `l` only checks again. `q` stops anywhere, and `proctolog setup`
|
|
44
|
+
starts again from the first step.
|
|
45
|
+
|
|
46
|
+
If a desk already answers on port 4040, the last step says so instead of
|
|
47
|
+
opening a second one.
|
|
48
|
+
|
|
49
|
+
## After an update
|
|
50
|
+
|
|
51
|
+
`proctolog update` runs the new version's `setup`, which then only links it
|
|
52
|
+
and says what's new: you asked for an update, not to go through setup again.
|
|
53
|
+
|
|
54
|
+
## For an agent
|
|
55
|
+
|
|
56
|
+
Outside a terminal, in a pipe or an agent's shell, `setup` asks nothing. It
|
|
57
|
+
links, prints every step as it stands, stamping the unfinished ones
|
|
58
|
+
`[ TO DO ]`, and exits 0 when the desk can open, 1 when a step still waits on
|
|
59
|
+
something the desk needs.
|
|
@@ -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`, `proctolog setup` 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.
|
data/docs/manual/updates.md
CHANGED
|
@@ -39,10 +39,17 @@ proctolog update
|
|
|
39
39
|
|
|
40
40
|
It asks RubyGems, installs the newer version the way yours was installed
|
|
41
41
|
(`rv tool install` for a PRoctolog that `rvx` runs, `gem install` otherwise),
|
|
42
|
-
and runs that version's `proctolog
|
|
43
|
-
skills
|
|
44
|
-
bundle installed.
|
|
45
|
-
`proctolog
|
|
42
|
+
and runs that version's `proctolog setup`, which after an update only links
|
|
43
|
+
zsh's completion and the agent skills to it, and walks no steps. A checkout of PRoctolog is pulled forward instead, and its
|
|
44
|
+
bundle installed. When there's nothing newer, it says so and changes nothing.
|
|
45
|
+
A desk that's open keeps running the old one: stop it and run `proctolog
|
|
46
|
+
serve` again.
|
|
47
|
+
|
|
48
|
+
Then it says what's new since the version you had, from the changelog: each
|
|
49
|
+
change by its first sentence, under Added, Changed and Fixed, and in full
|
|
50
|
+
what you must do. Past a dozen changes it counts the rest, and it ends with
|
|
51
|
+
where the whole changelog is. An update from 0.2.0 or earlier says none of
|
|
52
|
+
this, as the version you leave is the one that runs the update.
|
|
46
53
|
|
|
47
54
|
The old version stays installed. `gem cleanup proctolog` removes the ones
|
|
48
55
|
you no longer run; with rv, `rv tool list` shows them and
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
|
|
5
|
+
module Proctolog
|
|
6
|
+
class Changelog
|
|
7
|
+
# What an update brings, as the console says it once the new version is
|
|
8
|
+
# in: each change by the first sentence of its line, under its heading,
|
|
9
|
+
# and what to do in full. Plain text, as a terminal reads no markdown.
|
|
10
|
+
class News
|
|
11
|
+
# Past this many changes, the rest are counted rather than shown.
|
|
12
|
+
SHOWN = 12
|
|
13
|
+
|
|
14
|
+
# As wide as the terminal it is said in, within reason.
|
|
15
|
+
def self.columns = (IO.console&.winsize&.last || 80).clamp(40, 100)
|
|
16
|
+
|
|
17
|
+
def initialize(since:, sections:, path:)
|
|
18
|
+
@since, @sections, @path = since, sections, path
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_s(width: self.class.columns)
|
|
22
|
+
shown = changes.first(SHOWN)
|
|
23
|
+
more = changes.size - shown.size
|
|
24
|
+
told = @sections.flat_map { told(it, shown:, width:) }
|
|
25
|
+
["What's new since PRoctolog #{@since}:", *told, "", more.positive? ? "#{more} more, and all of it: #{@path}" : "All of it: #{@path}"].join("\n")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Every change, in the order written: its section, its heading and its line.
|
|
31
|
+
def changes = @sections.flat_map { |section| section.changes.flat_map { |heading, lines| lines.map { [section, heading, it] } } }
|
|
32
|
+
|
|
33
|
+
# A section's changes that are shown, and what it asks, which always is.
|
|
34
|
+
def told(section, shown:, width:)
|
|
35
|
+
headed = section.changes.flat_map do |heading, lines|
|
|
36
|
+
kept = lines.select { shown.include?([section, heading, it]) }
|
|
37
|
+
kept.any? ? [" #{heading}", *kept.map { " - #{headline(it, room: width - 6)}" }] : []
|
|
38
|
+
end
|
|
39
|
+
asked = section.you_do&.then { laid("You do#{plain(it)}", width:) } || []
|
|
40
|
+
(headed.any? || asked.any?) ? ["", section.name, *headed, *asked] : []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The first sentence of a line. One longer than the room ends at its last
|
|
44
|
+
# clause that fits, when that keeps half the room, or else at a word.
|
|
45
|
+
def headline(line, room:)
|
|
46
|
+
sentence = plain(line).then { it[/\A.+?[.!?](?=\s|\z)/] || it }
|
|
47
|
+
return sentence if sentence.size <= room
|
|
48
|
+
|
|
49
|
+
cut = sentence[0, room]
|
|
50
|
+
clause = cut[/\A.*(?=[,;:]\s)/]
|
|
51
|
+
"#{(clause && clause.size >= room / 2) ? clause : cut[/\A.*(?=\s)/]}…"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# In lines no wider than the console, the first two spaces in, the rest four.
|
|
55
|
+
def laid(text, width:)
|
|
56
|
+
text.split.each_with_object([+" "]) do |word, lines|
|
|
57
|
+
lines << +" " if lines.last.size + word.size >= width && !lines.last.strip.empty?
|
|
58
|
+
lines.last << (lines.last.strip.empty? ? word : " #{word}")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Without markdown's marks: **bold**, <links> and [text](links) keep
|
|
63
|
+
# their words. Backticks stay, as the console writes commands in them.
|
|
64
|
+
def plain(text) = text.gsub(/\*\*(.+?)\*\*/, '\1').gsub(/<(\S+?)>/, '\1').gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "changelog/news"
|
|
4
|
+
|
|
5
|
+
module Proctolog
|
|
6
|
+
# CHANGELOG.md as a release and an update read it: a section to each
|
|
7
|
+
# version, newest first, under ## Unreleased what main has that no gem has
|
|
8
|
+
# yet. Plain Ruby, as bin/release, the Release workflow and bin/install
|
|
9
|
+
# read it before any bundle.
|
|
10
|
+
class Changelog
|
|
11
|
+
PATH = File.expand_path("../../CHANGELOG.md", __dir__)
|
|
12
|
+
|
|
13
|
+
# One ## section, its lines under ### Added, Changed and Fixed, and what
|
|
14
|
+
# to do after **You do**.
|
|
15
|
+
Section = Data.define(:name, :text) do
|
|
16
|
+
def adds? = text.match?(/^### Added$/)
|
|
17
|
+
def lines? = text.match?(/^- /)
|
|
18
|
+
def notes = text.strip
|
|
19
|
+
|
|
20
|
+
def unreleased? = name == "Unreleased"
|
|
21
|
+
def version = (Gem::Version.new(name) if Gem::Version.correct?(name))
|
|
22
|
+
|
|
23
|
+
# Its lines by heading, in the order written, each on one line.
|
|
24
|
+
def changes
|
|
25
|
+
text.scan(/^### ([^\n]+)\n(.*?)(?=^### |^\*\*You do\*\*|\z)/m).to_h do |heading, lines|
|
|
26
|
+
[heading, lines.split(/^- /).drop(1).map { it.split.join(" ") }]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# What it asks, on one line, as it goes on after **You do**: ": nothing",
|
|
31
|
+
# or ", coming from a checkout: …".
|
|
32
|
+
def you_do = text[/^\*\*You do\*\*(.*)/m, 1]&.split&.join(" ")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# It writes ⌘ and ↩, so it is read as UTF-8 whatever the locale.
|
|
36
|
+
def self.read(path = PATH) = new(File.read(path, encoding: "UTF-8"), path:)
|
|
37
|
+
|
|
38
|
+
def initialize(text, path: PATH)
|
|
39
|
+
@path = path
|
|
40
|
+
@sections = text.split(/^## /).drop(1).map do
|
|
41
|
+
name, body = it.split("\n", 2)
|
|
42
|
+
Section.new(name: name.strip, text: body.to_s)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def [](name) = @sections.find { it.name == name }
|
|
47
|
+
|
|
48
|
+
def unreleased = self["Unreleased"]
|
|
49
|
+
|
|
50
|
+
# What an update from this version brings: every section newer than it,
|
|
51
|
+
# Unreleased too in a checkout. Nil when it brings nothing written here.
|
|
52
|
+
def since(version)
|
|
53
|
+
return unless Gem::Version.correct?(version)
|
|
54
|
+
|
|
55
|
+
newer = @sections.select { it.unreleased? || it.version&.>(Gem::Version.new(version)) }
|
|
56
|
+
News.new(since: version, sections: newer, path: @path) if newer.any?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Adding something makes a minor (docs/manual/updates.md): the minor this
|
|
60
|
+
# version owes when it adds something but is only a patch on the one
|
|
61
|
+
# before it.
|
|
62
|
+
def minor_owed(version)
|
|
63
|
+
owed = released_before(version)&.then { Gem::Version.new("#{it.bump}.0") }
|
|
64
|
+
owed if owed && adds?(version) && Gem::Version.new(version) < owed
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Under its own heading, or still under Unreleased, since main has that
|
|
70
|
+
# too.
|
|
71
|
+
def adds?(version) = [self[version], unreleased].compact.any?(&:adds?)
|
|
72
|
+
|
|
73
|
+
def released_before(version) = @sections.drop_while { it.name != version }.drop(1).find(&:version)&.version
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "open3"
|
|
5
5
|
require "rbconfig"
|
|
6
6
|
require "shellwords"
|
|
7
|
+
require_relative "changelog"
|
|
7
8
|
|
|
8
9
|
module Proctolog
|
|
9
10
|
# Where this PRoctolog lives, and what it puts on this Mac for zsh and the
|
|
@@ -22,8 +23,20 @@ module Proctolog
|
|
|
22
23
|
|
|
23
24
|
SKILL_FOLDERS = %w[.claude/skills .agents/skills].freeze
|
|
24
25
|
|
|
26
|
+
# What a pull did: the commit the checkout was at, and the one it is at.
|
|
27
|
+
Pull = Data.define(:from, :to) do
|
|
28
|
+
def moved? = from != to
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# `proctolog update` hands the new version's install the version it
|
|
32
|
+
# leaves, in the environment, for that install to say what is new since.
|
|
33
|
+
UPDATED_FROM = "PROCTOLOG_UPDATED_FROM"
|
|
34
|
+
|
|
25
35
|
def self.here = new(File.expand_path("../..", __dir__))
|
|
26
36
|
|
|
37
|
+
# The environment update starts the new version's install in.
|
|
38
|
+
def self.updating_from(version) = {UPDATED_FROM => version.to_s}
|
|
39
|
+
|
|
27
40
|
attr_reader :root
|
|
28
41
|
|
|
29
42
|
def initialize(root, home: Dir.home, env: ENV)
|
|
@@ -47,9 +60,15 @@ module Proctolog
|
|
|
47
60
|
branch, main = git("branch", "--show-current"), origin_branch
|
|
48
61
|
raise Refused, "#{root} is on #{branch}, not #{main}: switch to #{main}, or pull #{branch} yourself." unless branch == main
|
|
49
62
|
|
|
63
|
+
from = git("rev-parse", "HEAD")
|
|
50
64
|
git("pull", "--ff-only")
|
|
65
|
+
Pull.new(from:, to: git("rev-parse", "HEAD"))
|
|
51
66
|
end
|
|
52
67
|
|
|
68
|
+
# What is new in this PRoctolog since the one an update left, or nil when
|
|
69
|
+
# no update brought it here.
|
|
70
|
+
def news = @env[UPDATED_FROM]&.then { Changelog.read(root.join("CHANGELOG.md")).since(it) }
|
|
71
|
+
|
|
53
72
|
def launcher = @home.join(".local/bin/proctolog")
|
|
54
73
|
|
|
55
74
|
def completion = @home.join(".zsh/completions/_proctolog")
|
data/lib/proctolog/version.rb
CHANGED
data/public/asking.js
CHANGED
|
@@ -18,11 +18,25 @@ const quoted = (text) =>
|
|
|
18
18
|
.join("\n");
|
|
19
19
|
|
|
20
20
|
// Where a decision stands, its words, and the pictures it rests on that
|
|
21
|
-
// the desk holds on this Mac.
|
|
22
|
-
|
|
21
|
+
// the desk holds on this Mac. One nobody has opened yet has no address on
|
|
22
|
+
// GitHub: its branch and the file its description is kept in stand there.
|
|
23
|
+
const about = ({
|
|
24
|
+
pr,
|
|
25
|
+
title,
|
|
26
|
+
url,
|
|
27
|
+
branch,
|
|
28
|
+
base,
|
|
29
|
+
file,
|
|
30
|
+
head,
|
|
31
|
+
text,
|
|
32
|
+
pictures: shots,
|
|
33
|
+
}) => {
|
|
23
34
|
const here = shots.filter((picture) => picture.file);
|
|
35
|
+
const where = file
|
|
36
|
+
? `not opened, on ${branch} at ${head.slice(0, 7)}, into ${base}, described in ${file}`
|
|
37
|
+
: `(${url}), on ${branch} at ${head.slice(0, 7)}`;
|
|
24
38
|
return [
|
|
25
|
-
`#${pr}, "${title}"
|
|
39
|
+
`#${pr}, "${title}" ${where}:`,
|
|
26
40
|
quoted(text),
|
|
27
41
|
here.length &&
|
|
28
42
|
`Its pictures, on this Mac:\n${here.map((picture) => `- ${picture.name}: ${pictures}/${picture.file}`).join("\n")}`,
|
|
@@ -31,6 +45,19 @@ const about = ({ pr, title, url, branch, head, text, pictures: shots }) => {
|
|
|
31
45
|
.join("\n\n");
|
|
32
46
|
};
|
|
33
47
|
|
|
48
|
+
// How to read the pull requests asked about: GitHub has the opened ones,
|
|
49
|
+
// git and the store the rest.
|
|
50
|
+
const reading = (decisions) =>
|
|
51
|
+
[
|
|
52
|
+
decisions.some((each) => !each.file) &&
|
|
53
|
+
"Read a pull request with `gh pr view` and `gh pr diff`.",
|
|
54
|
+
decisions.some((each) => each.file) &&
|
|
55
|
+
"A pull request not opened yet is not on GitHub: read its code with `git log` and `git diff` from its base to its branch, and its description in the file named here.",
|
|
56
|
+
"If one has moved on since the head named here, say so.",
|
|
57
|
+
]
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.join(" ");
|
|
60
|
+
|
|
34
61
|
export const askingPrompt = (ids, question) => {
|
|
35
62
|
const decisions = ids.map((id) => inventory.find((each) => each.id === id));
|
|
36
63
|
const which =
|
|
@@ -38,7 +65,7 @@ export const askingPrompt = (ids, question) => {
|
|
|
38
65
|
? "one of their decisions"
|
|
39
66
|
: `${decisions.length} of their decisions`;
|
|
40
67
|
return [
|
|
41
|
-
`I am reviewing pull requests in PRoctolog and have a question on ${which} before I choose. Answer it plainly, with evidence. Change nothing while you do: no edits, no commits, no checkout, nothing on GitHub.
|
|
68
|
+
`I am reviewing pull requests in PRoctolog and have a question on ${which} before I choose. Answer it plainly, with evidence. Change nothing while you do: no edits, no commits, no checkout, nothing on GitHub. ${reading(decisions)}`,
|
|
42
69
|
...decisions.map(about),
|
|
43
70
|
`My question:\n${question.trim()}`,
|
|
44
71
|
].join("\n\n");
|
data/public/decisions.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
prepareScreenshots,
|
|
10
10
|
toggleScreenshots,
|
|
11
11
|
} from "./decision-screenshots.js";
|
|
12
|
-
import { helping, keyOf, mode } from "./keys.js";
|
|
12
|
+
import { helping, keyOf, mode, overThePage } from "./keys.js";
|
|
13
13
|
import { withMentions } from "./mentions.js";
|
|
14
14
|
import { prNumber } from "./pr-number.js";
|
|
15
15
|
import {
|
|
@@ -1092,9 +1092,9 @@ document.addEventListener("click", (event) => {
|
|
|
1092
1092
|
});
|
|
1093
1093
|
|
|
1094
1094
|
document.addEventListener("focusin", (event) => {
|
|
1095
|
-
// A
|
|
1096
|
-
// without leaving the decision: its Esc comes back to it.
|
|
1097
|
-
if (event.target
|
|
1095
|
+
// A picture looked at closely, or a note written in Grouse's overlay, takes
|
|
1096
|
+
// the focus without leaving the decision: its Esc comes back to it.
|
|
1097
|
+
if (overThePage(event.target)) return;
|
|
1098
1098
|
const box = event.target.closest("[data-decision]");
|
|
1099
1099
|
if (inline) {
|
|
1100
1100
|
// Deciding begins only when it is asked for (f, Enter, a click, a choice's
|
data/public/desk.js
CHANGED
|
@@ -27,14 +27,18 @@ const follow = (key) => {
|
|
|
27
27
|
if (link) location.assign(link.href + location.hash);
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
// Opens the next pull request
|
|
31
|
-
//
|
|
32
|
-
// the
|
|
30
|
+
// Opens the next pull request that asks for the owner, with blocking
|
|
31
|
+
// decisions to review or something back from the agent (Standing#asks_for_owner?):
|
|
32
|
+
// the one the cursor rests on when it asks, else the first after it, going
|
|
33
|
+
// round to the top of the route. With none left anywhere, the cursor's row
|
|
34
|
+
// says so.
|
|
33
35
|
const openUnreviewed = () => {
|
|
34
36
|
const ahead = [...stops.slice(at), ...stops.slice(0, at)];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
const next = ahead.find((stop) => stop.dataset.asks === "true");
|
|
38
|
+
if (!next)
|
|
39
|
+
return said(
|
|
40
|
+
"Nothing asks for you: every blocking decision is reviewed, and nothing is back from the agent.",
|
|
41
|
+
);
|
|
38
42
|
restOn(stops.indexOf(next));
|
|
39
43
|
location.assign(next.querySelector("a").href);
|
|
40
44
|
};
|
data/public/keys.js
CHANGED
|
@@ -201,6 +201,12 @@ export const thisBlock = (hints = []) => {
|
|
|
201
201
|
export const helping = () =>
|
|
202
202
|
!!document.querySelector("dialog[open], :popover-open");
|
|
203
203
|
|
|
204
|
+
// Whether the focus went over the page rather than along it: into a picture
|
|
205
|
+
// looked at closely, or into Grouse's overlay to write a note on the desk.
|
|
206
|
+
// Either leaves the page where it was, a decision being decided included.
|
|
207
|
+
export const overThePage = (target) =>
|
|
208
|
+
!!target.closest?.("dialog, grouse-widget");
|
|
209
|
+
|
|
204
210
|
// `hints` are pairs of keys and what they do: [["j k", "move"], ["a", "leave as is"]].
|
|
205
211
|
let shown;
|
|
206
212
|
export const mode = (name, hints = []) => {
|