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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -3
  3. data/README.md +32 -18
  4. data/app/controllers/proctolog/application_controller.rb +2 -2
  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 +23 -12
  21. data/app/models/proctolog/github.rb +1 -1
  22. data/app/models/proctolog/localhost.rb +17 -0
  23. data/app/models/proctolog/marks.rb +1 -1
  24. data/app/models/proctolog/note.rb +4 -0
  25. data/app/models/proctolog/owner.rb +3 -0
  26. data/app/models/proctolog/pick.rb +12 -0
  27. data/app/models/proctolog/rounds.rb +37 -0
  28. data/app/models/proctolog/server.rb +1 -7
  29. data/app/models/proctolog/setup.rb +190 -0
  30. data/app/models/proctolog/share/ngrok.rb +120 -0
  31. data/app/models/proctolog/share/settings.rb +23 -0
  32. data/app/models/proctolog/share/tailscale.rb +103 -0
  33. data/app/models/proctolog/share.rb +101 -0
  34. data/app/models/proctolog/standing.rb +24 -7
  35. data/app/views/proctolog/bursts/_line.html.erb +2 -2
  36. data/app/views/proctolog/bursts/_side.html.erb +6 -5
  37. data/app/views/proctolog/bursts/_side_stop.html.erb +4 -4
  38. data/app/views/proctolog/notes/_card.html.erb +7 -3
  39. data/app/views/proctolog/open_pull_requests/_side.html.erb +4 -3
  40. data/app/views/proctolog/open_pull_requests/_start.html.erb +8 -0
  41. data/app/views/proctolog/open_pull_requests/index.html.erb +4 -5
  42. data/app/views/proctolog/open_pull_requests/start/_first_time.html.erb +3 -0
  43. data/app/views/proctolog/open_pull_requests/start/_nothing_open.html.erb +3 -0
  44. data/app/views/proctolog/open_pull_requests/start/_nothing_written.html.erb +3 -0
  45. data/app/views/proctolog/shares/_line.html.erb +5 -0
  46. data/app/views/proctolog/shares/show.html.erb +48 -0
  47. data/app/views/proctolog/shares/ways/_ngrok.html.erb +6 -0
  48. data/app/views/proctolog/shares/ways/_tailscale.html.erb +2 -0
  49. data/bin/proctolog +154 -27
  50. data/completions/_proctolog +31 -6
  51. data/config/application.rb +23 -4
  52. data/config/initializers/grouse.rb +1 -1
  53. data/config/routes.rb +3 -0
  54. data/docs/manual/bursts.md +7 -2
  55. data/docs/manual/deciding.md +4 -1
  56. data/docs/manual/handover.md +6 -0
  57. data/docs/manual/pr-format.md +1 -1
  58. data/docs/manual/reading.md +54 -27
  59. data/docs/manual/setup.md +59 -0
  60. data/docs/manual/sharing.md +77 -0
  61. data/docs/manual/updates.md +11 -4
  62. data/lib/proctolog/changelog/news.rb +67 -0
  63. data/lib/proctolog/changelog.rb +75 -0
  64. data/lib/proctolog/installation.rb +19 -0
  65. data/lib/proctolog/version.rb +1 -1
  66. data/public/asking.js +31 -4
  67. data/public/decisions.js +4 -4
  68. data/public/desk.js +10 -6
  69. data/public/keys.js +6 -0
  70. data/public/notes.js +164 -58
  71. data/public/pick.css +21 -0
  72. data/public/respond.css +6 -0
  73. data/public/respond.js +51 -7
  74. data/public/review-drafts.js +47 -9
  75. data/public/review-rounds.js +27 -3
  76. data/public/share.css +58 -0
  77. data/public/side.js +11 -20
  78. data/public/terminal.css +295 -34
  79. data/public/viewer.js +700 -173
  80. 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.
@@ -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 install`, so zsh's completion and the agent
43
- skills link to it. A checkout of PRoctolog is pulled forward instead, and its
44
- bundle installed. A desk that's open keeps running the old one: stop it and run
45
- `proctolog serve` again.
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")
@@ -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.1"
6
+ VERSION = "0.3.0"
7
7
  end
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
- const about = ({ pr, title, url, branch, head, text, pictures: shots }) => {
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}" (${url}), on ${branch} at ${head.slice(0, 7)}:`,
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. Read a pull request with \`gh pr view\` and \`gh pr diff\`; if one has moved on since the head named here, say so.`,
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 panel over the page, a picture looked at closely, takes the focus
1096
- // without leaving the decision: its Esc comes back to it.
1097
- if (event.target.closest("dialog")) return;
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 with decisions still to review: the one the
31
- // cursor rests on when it has some, else the first after it, going round to
32
- // the top of the route. With none left anywhere, the cursor's row says so.
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
- // The non-blocking wait for one gesture on their pull request's page.
36
- const next = ahead.find((stop) => Number(stop.dataset.blockingLeft) > 0);
37
- if (!next) return said("Every decision of this burst is reviewed.");
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 = []) => {