proctolog 0.1.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 +7 -0
- data/CHANGELOG.md +23 -0
- data/README.md +178 -0
- data/app/controllers/proctolog/acceptances_controller.rb +29 -0
- data/app/controllers/proctolog/application_controller.rb +32 -0
- data/app/controllers/proctolog/bursts_controller.rb +34 -0
- data/app/controllers/proctolog/choices_controller.rb +30 -0
- data/app/controllers/proctolog/code_reviews_controller.rb +26 -0
- data/app/controllers/proctolog/decisions_controller.rb +33 -0
- data/app/controllers/proctolog/entries_controller.rb +14 -0
- data/app/controllers/proctolog/evidence_controller.rb +23 -0
- data/app/controllers/proctolog/freshness_controller.rb +27 -0
- data/app/controllers/proctolog/good_to_go_controller.rb +29 -0
- data/app/controllers/proctolog/memberships_controller.rb +20 -0
- data/app/controllers/proctolog/mentions_controller.rb +16 -0
- data/app/controllers/proctolog/moments_controller.rb +26 -0
- data/app/controllers/proctolog/note_params.rb +24 -0
- data/app/controllers/proctolog/notes_controller.rb +56 -0
- data/app/controllers/proctolog/open_pull_requests_controller.rb +59 -0
- data/app/controllers/proctolog/pictures_controller.rb +27 -0
- data/app/controllers/proctolog/prompts_controller.rb +28 -0
- data/app/controllers/proctolog/pull_request_scoped.rb +21 -0
- data/app/controllers/proctolog/pull_requests_controller.rb +34 -0
- data/app/controllers/proctolog/put_away_controller.rb +19 -0
- data/app/controllers/proctolog/recordings_controller.rb +28 -0
- data/app/controllers/proctolog/rendered_pieces_controller.rb +11 -0
- data/app/controllers/proctolog/review_data_controller.rb +12 -0
- data/app/controllers/proctolog/rules_controller.rb +20 -0
- data/app/controllers/proctolog/scenario_notes_controller.rb +26 -0
- data/app/controllers/proctolog/scenario_scoped.rb +21 -0
- data/app/controllers/proctolog/scenarios_controller.rb +67 -0
- data/app/controllers/proctolog/sitting_servers_controller.rb +30 -0
- data/app/controllers/proctolog/waiting_controller.rb +14 -0
- data/app/controllers/proctolog/with_decisions.rb +28 -0
- data/app/helpers/proctolog/bursts_helper.rb +52 -0
- data/app/helpers/proctolog/decisions_helper.rb +102 -0
- data/app/helpers/proctolog/desk_helper.rb +61 -0
- data/app/helpers/proctolog/mentions_helper.rb +27 -0
- data/app/helpers/proctolog/open_pull_requests_helper.rb +31 -0
- data/app/helpers/proctolog/prompts_helper.rb +21 -0
- data/app/helpers/proctolog/pull_requests_helper.rb +67 -0
- data/app/helpers/proctolog/recordings_helper.rb +30 -0
- data/app/helpers/proctolog/rounds_helper.rb +27 -0
- data/app/helpers/proctolog/scenarios_helper.rb +123 -0
- data/app/helpers/proctolog/standings_helper.rb +80 -0
- data/app/jobs/proctolog/application_job.rb +8 -0
- data/app/jobs/proctolog/ask_for_release_job.rb +9 -0
- data/app/jobs/proctolog/fetch_pictures_ahead_job.rb +36 -0
- data/app/jobs/proctolog/prune_fresh_profiles_job.rb +9 -0
- data/app/jobs/proctolog/read_mentions_ahead_job.rb +12 -0
- data/app/models/proctolog/acceptance.rb +47 -0
- data/app/models/proctolog/app.rb +92 -0
- data/app/models/proctolog/browser.rb +42 -0
- data/app/models/proctolog/burst.rb +268 -0
- data/app/models/proctolog/burst_notices.rb +143 -0
- data/app/models/proctolog/chapters.rb +42 -0
- data/app/models/proctolog/check.rb +122 -0
- data/app/models/proctolog/choices.rb +112 -0
- data/app/models/proctolog/code_review.rb +251 -0
- data/app/models/proctolog/decision_record.rb +12 -0
- data/app/models/proctolog/decisions.rb +199 -0
- data/app/models/proctolog/description.rb +165 -0
- data/app/models/proctolog/difference.rb +138 -0
- data/app/models/proctolog/doctor.rb +127 -0
- data/app/models/proctolog/evidence.rb +29 -0
- data/app/models/proctolog/failure.rb +7 -0
- data/app/models/proctolog/focus.rb +48 -0
- data/app/models/proctolog/fresh_sitting.rb +35 -0
- data/app/models/proctolog/github.rb +353 -0
- data/app/models/proctolog/handover/brief.rb +138 -0
- data/app/models/proctolog/handover/layers.rb +128 -0
- data/app/models/proctolog/handover/places.rb +98 -0
- data/app/models/proctolog/handover/taken.rb +41 -0
- data/app/models/proctolog/handover/worklist.rb +35 -0
- data/app/models/proctolog/handover.rb +123 -0
- data/app/models/proctolog/housekeeping.rb +23 -0
- data/app/models/proctolog/kit.rb +36 -0
- data/app/models/proctolog/marks.rb +130 -0
- data/app/models/proctolog/membership.rb +91 -0
- data/app/models/proctolog/mentions.rb +117 -0
- data/app/models/proctolog/note.rb +94 -0
- data/app/models/proctolog/outside.rb +45 -0
- data/app/models/proctolog/overlay/brief.rb +52 -0
- data/app/models/proctolog/overlay/written.rb +33 -0
- data/app/models/proctolog/overlay.rb +87 -0
- data/app/models/proctolog/owner.rb +16 -0
- data/app/models/proctolog/pick.rb +47 -0
- data/app/models/proctolog/picture.rb +138 -0
- data/app/models/proctolog/pieces_rendering.rb +40 -0
- data/app/models/proctolog/project.rb +118 -0
- data/app/models/proctolog/pull_request.rb +89 -0
- data/app/models/proctolog/recording.rb +194 -0
- data/app/models/proctolog/release.rb +59 -0
- data/app/models/proctolog/rendering.rb +303 -0
- data/app/models/proctolog/reports.rb +66 -0
- data/app/models/proctolog/response_files.rb +55 -0
- data/app/models/proctolog/rounds.rb +305 -0
- data/app/models/proctolog/route.rb +151 -0
- data/app/models/proctolog/rules.rb +121 -0
- data/app/models/proctolog/scenario.rb +126 -0
- data/app/models/proctolog/server.rb +273 -0
- data/app/models/proctolog/since.rb +108 -0
- data/app/models/proctolog/size.rb +30 -0
- data/app/models/proctolog/standing.rb +132 -0
- data/app/models/proctolog/theme.rb +40 -0
- data/app/models/proctolog/unopened/numbers.rb +48 -0
- data/app/models/proctolog/unopened/opening.rb +88 -0
- data/app/models/proctolog/unopened.rb +203 -0
- data/app/models/proctolog/visit.rb +26 -0
- data/app/views/layouts/proctolog/application.html.erb +58 -0
- data/app/views/proctolog/acceptances/new.html.erb +34 -0
- data/app/views/proctolog/bursts/_line.html.erb +28 -0
- data/app/views/proctolog/bursts/_merged.html.erb +23 -0
- data/app/views/proctolog/bursts/_moved.html.erb +16 -0
- data/app/views/proctolog/bursts/_notices.html.erb +39 -0
- data/app/views/proctolog/bursts/_put_away.html.erb +11 -0
- data/app/views/proctolog/bursts/_rules.html.erb +27 -0
- data/app/views/proctolog/bursts/_side.html.erb +49 -0
- data/app/views/proctolog/bursts/_side_stop.html.erb +10 -0
- data/app/views/proctolog/bursts/show.html.erb +54 -0
- data/app/views/proctolog/code_reviews/show.html.erb +33 -0
- data/app/views/proctolog/decisions/_alternative.html.erb +5 -0
- data/app/views/proctolog/decisions/_beneath.html.erb +21 -0
- data/app/views/proctolog/decisions/_box.html.erb +27 -0
- data/app/views/proctolog/decisions/_choices.html.erb +9 -0
- data/app/views/proctolog/decisions/_draft_tools.html.erb +47 -0
- data/app/views/proctolog/decisions/_part.html.erb +61 -0
- data/app/views/proctolog/decisions/_round_data.html.erb +1 -0
- data/app/views/proctolog/decisions/_theme.html.erb +13 -0
- data/app/views/proctolog/decisions/index.html.erb +75 -0
- data/app/views/proctolog/notes/_card.html.erb +19 -0
- data/app/views/proctolog/notes/_template.html.erb +41 -0
- data/app/views/proctolog/open_pull_requests/_side.html.erb +30 -0
- data/app/views/proctolog/open_pull_requests/index.html.erb +106 -0
- data/app/views/proctolog/prompts/show.html.erb +41 -0
- data/app/views/proctolog/pull_requests/_connections.html.erb +65 -0
- data/app/views/proctolog/pull_requests/_finish.html.erb +42 -0
- data/app/views/proctolog/pull_requests/_margin.html.erb +33 -0
- data/app/views/proctolog/pull_requests/_note.html.erb +4 -0
- data/app/views/proctolog/pull_requests/_outline.html.erb +26 -0
- data/app/views/proctolog/pull_requests/_paragraphs.html.erb +9 -0
- data/app/views/proctolog/pull_requests/_scenarios.html.erb +16 -0
- data/app/views/proctolog/pull_requests/_since.html.erb +42 -0
- data/app/views/proctolog/pull_requests/_standing.html.erb +51 -0
- data/app/views/proctolog/pull_requests/_status.html.erb +5 -0
- data/app/views/proctolog/pull_requests/_try.html.erb +122 -0
- data/app/views/proctolog/pull_requests/show.html.erb +145 -0
- data/app/views/proctolog/recordings/_dialog.html.erb +26 -0
- data/app/views/proctolog/recordings/_list.html.erb +8 -0
- data/app/views/proctolog/recordings/_note.html.erb +22 -0
- data/app/views/proctolog/recordings/index.html.erb +33 -0
- data/app/views/proctolog/recordings/show.html.erb +118 -0
- data/app/views/proctolog/scenarios/_dialog.html.erb +22 -0
- data/app/views/proctolog/scenarios/_note.html.erb +18 -0
- data/app/views/proctolog/scenarios/_open.html.erb +9 -0
- data/app/views/proctolog/scenarios/_scenario.html.erb +48 -0
- data/app/views/proctolog/scenarios/_server.html.erb +20 -0
- data/app/views/proctolog/scenarios/index.html.erb +62 -0
- data/bin/proctolog +330 -0
- data/completions/_proctolog +143 -0
- data/config/application.rb +94 -0
- data/config/database.yml +16 -0
- data/config/environment.rb +6 -0
- data/config/initializers/grouse.rb +113 -0
- data/config/initializers/grouse_notes.rb +9 -0
- data/config/keys.yml +6 -0
- data/config/routes.rb +71 -0
- data/config/storage.yml +4 -0
- data/db/migrate/20261019213542_create_grouse_tables.grouse.rb +128 -0
- data/db/migrate/20261019213550_create_active_storage_tables.active_storage.rb +58 -0
- data/docs/manual/app.md +157 -0
- data/docs/manual/bursts.md +129 -0
- data/docs/manual/deciding.md +89 -0
- data/docs/manual/desk.png +0 -0
- data/docs/manual/films.md +20 -0
- data/docs/manual/handover.md +93 -0
- data/docs/manual/pr-format.md +75 -0
- data/docs/manual/reading.md +233 -0
- data/docs/manual/unopened.md +80 -0
- data/docs/manual/updates.md +55 -0
- data/lib/proctolog/installation.rb +125 -0
- data/lib/proctolog/version.rb +7 -0
- data/public/asking.js +50 -0
- data/public/code-review.js +40 -0
- data/public/cursor.js +292 -0
- data/public/decision-screenshots.js +97 -0
- data/public/decisions.css +949 -0
- data/public/decisions.js +1289 -0
- data/public/desk.css +582 -0
- data/public/desk.js +126 -0
- data/public/favicon.png +0 -0
- data/public/keys.js +228 -0
- data/public/mentions.js +389 -0
- data/public/messages.js +103 -0
- data/public/notes.js +731 -0
- data/public/pick.css +99 -0
- data/public/plaque.png +0 -0
- data/public/pr-number.js +11 -0
- data/public/prompt.css +46 -0
- data/public/prompt.js +16 -0
- data/public/pull_request.css +805 -0
- data/public/recording.css +379 -0
- data/public/recording.js +394 -0
- data/public/release.js +25 -0
- data/public/respond.css +260 -0
- data/public/respond.js +95 -0
- data/public/review-drafts.js +949 -0
- data/public/review-rounds.js +1447 -0
- data/public/scenarios.css +253 -0
- data/public/scenarios.js +36 -0
- data/public/schemas/proctolog-response-v1.json +101 -0
- data/public/side.js +133 -0
- data/public/standing.css +478 -0
- data/public/terminal-apl.svg +6 -0
- data/public/terminal-cuneiform.svg +3 -0
- data/public/terminal-grain-dots.svg +6 -0
- data/public/terminal-grain-weave.svg +6 -0
- data/public/terminal-grain-wedges.svg +6 -0
- data/public/terminal-shades.svg +3 -0
- data/public/terminal.css +4426 -0
- data/public/toast.js +26 -0
- data/public/try.js +124 -0
- data/public/viewer.js +808 -0
- data/public/words.js +81 -0
- data/skills/proctolog-handover/SKILL.md +77 -0
- data/skills/proctolog-pr-writing/SKILL.md +228 -0
- data/skills/proctolog-pr-writing/references/example-description.md +33 -0
- metadata +366 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Before anything is opened
|
|
2
|
+
|
|
3
|
+
Work still being shaped can be reviewed before any pull request is opened for
|
|
4
|
+
it. Nothing reaches GitHub: no pull request, no picture, no push. Once the
|
|
5
|
+
branches are opened, the review goes with them.
|
|
6
|
+
|
|
7
|
+
## What the agent writes
|
|
8
|
+
|
|
9
|
+
An unopened pull request is what `gh pr create` would be told, written down
|
|
10
|
+
before anyone runs it: a branch, the branch it goes into, a title and a
|
|
11
|
+
description. Each lives in a folder of your store named after its branch,
|
|
12
|
+
which `proctolog unopened folder BRANCH` prints:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
~/.local/share/proctolog/<owner>/<repo>/unopened/recovery/emails/
|
|
16
|
+
pull_request.md
|
|
17
|
+
reset-mail-before.png
|
|
18
|
+
reset-mail-after.png
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```markdown
|
|
22
|
+
---
|
|
23
|
+
title: A reset mail says which account it is for
|
|
24
|
+
base: recovery/fields
|
|
25
|
+
---
|
|
26
|
+
## Summary
|
|
27
|
+
…
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The description is written as any other the desk reads
|
|
31
|
+
([how](pr-format.md)). Its pictures sit beside it, linked by their file names:
|
|
32
|
+
``. Tell your agent to "write these
|
|
33
|
+
for PRoctolog without opening PRs"; the `proctolog-pr-writing` skill knows the
|
|
34
|
+
rest. A missing title is the branch's name, and a missing base is origin's
|
|
35
|
+
default branch.
|
|
36
|
+
|
|
37
|
+
`proctolog unopened` lists what the desk reads, each with its number. GitHub
|
|
38
|
+
never writes a number with a leading zero, so the desk numbers these 01, 02
|
|
39
|
+
and on, and a branch keeps its number. `proctolog check recovery/emails`, or
|
|
40
|
+
`proctolog check 01`, checks one, pictures and all.
|
|
41
|
+
|
|
42
|
+
## Reviewing them
|
|
43
|
+
|
|
44
|
+
The open pull requests page lists them under "Not opened", each with "Review
|
|
45
|
+
alone", and "Review the chain" for one that sits on another's branch. A burst
|
|
46
|
+
file names one by its branch, in the lists it already has:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
name: Account recovery
|
|
50
|
+
repository: sartoria-llc/sartoria
|
|
51
|
+
tops: [recovery/emails] # followed down by base, as any chain
|
|
52
|
+
pull_requests: [recovery/copy] # stands alone
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
From there it is a pull request like any other: read, decided, tried, handed
|
|
56
|
+
over. Its line says "not opened, on `recovery/emails`", and `G` says GitHub
|
|
57
|
+
has nothing of it. A chain followed down from it can end on an open pull
|
|
58
|
+
request's branch, and sits on it.
|
|
59
|
+
|
|
60
|
+
What differs is where things are kept:
|
|
61
|
+
|
|
62
|
+
- Its head and files are read from the branch on this Mac, each time a page
|
|
63
|
+
is shown: there is nothing to read again.
|
|
64
|
+
- Ticking accepted decisions writes its file, not GitHub.
|
|
65
|
+
- The desk keeps each text it reads in the folder's `.revisions/`, so a "good
|
|
66
|
+
to go" that the description moved past says how it changed.
|
|
67
|
+
- The handover tells the agent where its file is. An agent rewrites the file
|
|
68
|
+
in place, which needs no word from you, so a whole round stays on this Mac.
|
|
69
|
+
|
|
70
|
+
## Opening them
|
|
71
|
+
|
|
72
|
+
When you're ready, tell the agent to open them. That is a GitHub write, and
|
|
73
|
+
waits for your word: it pushes the branches, uploads the pictures and opens
|
|
74
|
+
each pull request with its title, base and description.
|
|
75
|
+
|
|
76
|
+
On its next reading the desk finds the open pull request on each branch and
|
|
77
|
+
carries the review over to its new number: notes, choices, good-to-go marks,
|
|
78
|
+
code comments, handovers and films. The route says "opened, was #01" until
|
|
79
|
+
you dismiss it with `x`. From then on GitHub is the truth, and the folder is
|
|
80
|
+
left alone.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Versions and updates
|
|
2
|
+
|
|
3
|
+
PRoctolog is a gem, `proctolog`, numbered the semantic way: `MAJOR.MINOR.PATCH`.
|
|
4
|
+
|
|
5
|
+
- A **patch** (0.1.0 → 0.1.1) fixes something. Nothing you do changes.
|
|
6
|
+
- A **minor** (0.1.1 → 0.2.0) adds something. Nothing you do changes either.
|
|
7
|
+
- A **major** (0.2.0 → 1.0.0) asks something new of you: a command renamed or
|
|
8
|
+
gone, a file that moved, a format your agent writes differently.
|
|
9
|
+
[CHANGELOG.md](../../CHANGELOG.md) says what.
|
|
10
|
+
|
|
11
|
+
While the number starts with 0, a minor may ask something of you too, and the
|
|
12
|
+
changelog says so when it does.
|
|
13
|
+
|
|
14
|
+
`proctolog version` says which one you run.
|
|
15
|
+
|
|
16
|
+
## Being told
|
|
17
|
+
|
|
18
|
+
Once a day while the desk serves, and once as it starts, it asks RubyGems which
|
|
19
|
+
PRoctolog is the latest. That's the only thing it sends anywhere besides
|
|
20
|
+
GitHub: a request for `rubygems.org/api/v1/versions/proctolog/latest.json`,
|
|
21
|
+
with nothing of yours in it. It keeps the answer in
|
|
22
|
+
`~/.cache/proctolog/latest_release.json`.
|
|
23
|
+
|
|
24
|
+
When the latest is newer than yours, you're told three ways:
|
|
25
|
+
|
|
26
|
+
- the next page you open says so in the toast at the foot, once in each
|
|
27
|
+
browser for each version, and `m` shows it again after;
|
|
28
|
+
- `proctolog serve` says so as it starts;
|
|
29
|
+
- `proctolog doctor` says so, on a `--` line.
|
|
30
|
+
|
|
31
|
+
None of them waits on RubyGems: they read what the desk last kept, and say
|
|
32
|
+
nothing before the first answer.
|
|
33
|
+
|
|
34
|
+
## Updating
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
proctolog update
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
It asks RubyGems, installs the newer version the way yours was installed
|
|
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.
|
|
46
|
+
|
|
47
|
+
The old version stays installed. `gem cleanup proctolog` removes the ones
|
|
48
|
+
you no longer run; with rv, `rv tool list` shows them and
|
|
49
|
+
`rv tool uninstall proctolog` removes them all.
|
|
50
|
+
|
|
51
|
+
A checkout is pulled only when that's all it takes: no changes of your own in
|
|
52
|
+
it, on the branch origin calls its own, and only forward (`git pull
|
|
53
|
+
--ff-only`). Anything else, `update` says what's in the way and leaves it to
|
|
54
|
+
you. A plain `git pull` works too; run `bundle install` after it when the
|
|
55
|
+
Gemfile.lock changed.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "rbconfig"
|
|
6
|
+
require "shellwords"
|
|
7
|
+
|
|
8
|
+
module Proctolog
|
|
9
|
+
# Where this PRoctolog lives, and what it puts on this Mac for zsh and the
|
|
10
|
+
# agents to find it: zsh's completion and the agent skills, as links into
|
|
11
|
+
# it, so they never fall behind it. It lives in one of three places:
|
|
12
|
+
#
|
|
13
|
+
# - a checkout, with its own bundle and a launcher in ~/.local/bin;
|
|
14
|
+
# - an rv tool, on a Ruby and gems of its own whatever Ruby the shell has,
|
|
15
|
+
# with a launcher in ~/.local/bin that asks rvx for it;
|
|
16
|
+
# - a gem among the others, run by RubyGems' own launcher.
|
|
17
|
+
#
|
|
18
|
+
# Plain Ruby, as bin/install runs it before any bundle.
|
|
19
|
+
class Installation
|
|
20
|
+
# Something of the same name is someone else's, and stays.
|
|
21
|
+
Refused = Class.new(StandardError)
|
|
22
|
+
|
|
23
|
+
SKILL_FOLDERS = %w[.claude/skills .agents/skills].freeze
|
|
24
|
+
|
|
25
|
+
def self.here = new(File.expand_path("../..", __dir__))
|
|
26
|
+
|
|
27
|
+
attr_reader :root
|
|
28
|
+
|
|
29
|
+
def initialize(root, home: Dir.home, env: ENV)
|
|
30
|
+
@root, @home, @env = Pathname(root).realpath, Pathname(home), env
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A gem ships no Gemfile: a checkout runs in a bundle of its own.
|
|
34
|
+
def checkout? = root.join("Gemfile").exist?
|
|
35
|
+
|
|
36
|
+
# rv gives each tool a gem home of its own, named for it and its version,
|
|
37
|
+
# and runs it with GEM_HOME set there.
|
|
38
|
+
def rv_tool? = !checkout? && gem_home&.basename.to_s.start_with?("proctolog@") && root.to_s.start_with?("#{gem_home}/")
|
|
39
|
+
|
|
40
|
+
def gem_home = @env["GEM_HOME"]&.then { Pathname(it) }
|
|
41
|
+
|
|
42
|
+
# Brings a checkout up to its origin, as `proctolog update` does: only a
|
|
43
|
+
# clean one, on the branch origin calls its own, and only forward.
|
|
44
|
+
def pull
|
|
45
|
+
raise Refused, "#{root} has changes of its own: commit or stash them, then run `proctolog update` again." unless git("status", "--porcelain").empty?
|
|
46
|
+
|
|
47
|
+
branch, main = git("branch", "--show-current"), origin_branch
|
|
48
|
+
raise Refused, "#{root} is on #{branch}, not #{main}: switch to #{main}, or pull #{branch} yourself." unless branch == main
|
|
49
|
+
|
|
50
|
+
git("pull", "--ff-only")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def launcher = @home.join(".local/bin/proctolog")
|
|
54
|
+
|
|
55
|
+
def completion = @home.join(".zsh/completions/_proctolog")
|
|
56
|
+
|
|
57
|
+
# What it did, a line each. Refuses before it writes anything.
|
|
58
|
+
def install
|
|
59
|
+
refuse_strangers
|
|
60
|
+
[*(script ? launch : shadowed), complete, *link_skills]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# What git printed, or Refused with what it said.
|
|
66
|
+
def git(*arguments)
|
|
67
|
+
output, status = Open3.capture2e("git", "-C", root.to_s, *arguments)
|
|
68
|
+
status.success? ? output.strip : raise(Refused, "git #{arguments.join(" ")} failed in #{root}:\n#{output}")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# origin's own branch, as this Mac last fetched it.
|
|
72
|
+
def origin_branch
|
|
73
|
+
git("symbolic-ref", "--short", "refs/remotes/origin/HEAD").delete_prefix("origin/")
|
|
74
|
+
rescue Refused
|
|
75
|
+
"main"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The launcher's, where this PRoctolog needs one of its own on the PATH.
|
|
79
|
+
def script
|
|
80
|
+
if checkout? then "#!/bin/sh\nexec #{RbConfig.ruby.shellescape} #{root.join("bin/proctolog").to_s.shellescape} \"$@\"\n"
|
|
81
|
+
elsif rv_tool? then "#!/bin/sh\nexec rvx proctolog \"$@\"\n"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def refuse_strangers
|
|
86
|
+
if script && launcher.exist? && launcher.read != script
|
|
87
|
+
raise Refused, "#{launcher} runs another PRoctolog; leave it in place or remove it before installing."
|
|
88
|
+
end
|
|
89
|
+
# A copy an earlier install left is replaced by the link; anything else is someone else's.
|
|
90
|
+
if completion.exist? && !completion.symlink? && !completion.read.start_with?("#compdef proctolog")
|
|
91
|
+
raise Refused, "#{completion} is not PRoctolog's completion; leave it in place or remove it before installing."
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def launch
|
|
96
|
+
return "#{launcher} runs this PRoctolog." if launcher.exist?
|
|
97
|
+
|
|
98
|
+
launcher.dirname.mkpath
|
|
99
|
+
launcher.write(script, perm: 0o755)
|
|
100
|
+
"Installed #{launcher}."
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# A checkout's launcher, left from before the gem, may come first on the PATH.
|
|
104
|
+
def shadowed = launcher.exist? ? ["#{launcher} runs another PRoctolog: remove it, or it may answer for proctolog instead of this one."] : []
|
|
105
|
+
|
|
106
|
+
def complete
|
|
107
|
+
completion.dirname.mkpath
|
|
108
|
+
FileUtils.ln_sf(root.join("completions/_proctolog"), completion)
|
|
109
|
+
"Linked #{completion}."
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# A skill folder of the same name that is no link is someone else's: it stays.
|
|
113
|
+
def link_skills
|
|
114
|
+
SKILL_FOLDERS.product(root.glob("skills/*/")).map do |folder, skill|
|
|
115
|
+
link = @home.join(folder, skill.basename)
|
|
116
|
+
next "#{link} is not a link to PRoctolog's skill; left as it is." if link.exist? && !link.symlink?
|
|
117
|
+
|
|
118
|
+
link.dirname.mkpath
|
|
119
|
+
FileUtils.rm_f(link)
|
|
120
|
+
link.make_symlink(skill.to_s.chomp("/"))
|
|
121
|
+
"Linked #{link}."
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
data/public/asking.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// A question the owner asks at once, in a Claude Code session of its own,
|
|
2
|
+
// rather than in the handover. The prompt quotes each decision as the desk
|
|
3
|
+
// holds it, so the session needs nothing else to know what is meant, and
|
|
4
|
+
// puts the question last, where the owner goes on typing.
|
|
5
|
+
|
|
6
|
+
import { inventory } from "./review-drafts.js";
|
|
7
|
+
|
|
8
|
+
const { checkout, pictures } = document.querySelector("[data-review-draft]")
|
|
9
|
+
.dataset;
|
|
10
|
+
|
|
11
|
+
// The decision's words as a block quote, without its theme marker.
|
|
12
|
+
const quoted = (text) =>
|
|
13
|
+
text
|
|
14
|
+
.replace(/\s*<!-- proctolog:theme [\w-]+ -->/g, "")
|
|
15
|
+
.trim()
|
|
16
|
+
.split("\n")
|
|
17
|
+
.map((line) => `> ${line}`.trimEnd())
|
|
18
|
+
.join("\n");
|
|
19
|
+
|
|
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 }) => {
|
|
23
|
+
const here = shots.filter((picture) => picture.file);
|
|
24
|
+
return [
|
|
25
|
+
`#${pr}, "${title}" (${url}), on ${branch} at ${head.slice(0, 7)}:`,
|
|
26
|
+
quoted(text),
|
|
27
|
+
here.length &&
|
|
28
|
+
`Its pictures, on this Mac:\n${here.map((picture) => `- ${picture.name}: ${pictures}/${picture.file}`).join("\n")}`,
|
|
29
|
+
]
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join("\n\n");
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const askingPrompt = (ids, question) => {
|
|
35
|
+
const decisions = ids.map((id) => inventory.find((each) => each.id === id));
|
|
36
|
+
const which =
|
|
37
|
+
decisions.length === 1
|
|
38
|
+
? "one of their decisions"
|
|
39
|
+
: `${decisions.length} of their decisions`;
|
|
40
|
+
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.`,
|
|
42
|
+
...decisions.map(about),
|
|
43
|
+
`My question:\n${question.trim()}`,
|
|
44
|
+
].join("\n\n");
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Claude's own address for a new Code session in the checkout under review,
|
|
48
|
+
// with the prompt typed in. Nothing is sent until the owner sends it there.
|
|
49
|
+
export const sessionAddress = (prompt) =>
|
|
50
|
+
`claude://code/new?folder=${encodeURIComponent(checkout)}&q=${encodeURIComponent(prompt)}`;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// What Diffle holds of this pull request, on the outline: the comments nobody
|
|
2
|
+
// resolved and the files marked viewed. Diffle goes on in a tab of its own,
|
|
3
|
+
// and closing it tells this one nothing, so the page asks the desk again
|
|
4
|
+
// whenever the owner comes back to it. The desk reads Diffle's own file:
|
|
5
|
+
// asking costs nothing of GitHub.
|
|
6
|
+
|
|
7
|
+
const comments = document.querySelector("[data-outline] [data-code-tally]");
|
|
8
|
+
const viewing = document.querySelector("[data-outline] [data-files-viewed]");
|
|
9
|
+
const count = (line) => line.querySelector(".count");
|
|
10
|
+
|
|
11
|
+
const say = ({ open, viewed, files }) => {
|
|
12
|
+
count(comments).textContent = open || "";
|
|
13
|
+
count(comments).dataset.waits = open > 0;
|
|
14
|
+
viewing.hidden = files == null;
|
|
15
|
+
count(viewing).textContent = `${viewed}/${files}`;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Coming back to a tab is both shown and focused: one question answers both.
|
|
19
|
+
let asking;
|
|
20
|
+
const ask = () => {
|
|
21
|
+
asking ??= fetch(comments.dataset.codeTally, {
|
|
22
|
+
headers: { Accept: "application/json" },
|
|
23
|
+
})
|
|
24
|
+
.then((response) => (response.ok ? response.json() : undefined))
|
|
25
|
+
.then((tally) => tally && say(tally))
|
|
26
|
+
.catch(() => {})
|
|
27
|
+
.finally(() => {
|
|
28
|
+
asking = undefined;
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (comments) {
|
|
33
|
+
document.addEventListener("visibilitychange", () => {
|
|
34
|
+
if (!document.hidden) ask();
|
|
35
|
+
});
|
|
36
|
+
addEventListener("focus", ask);
|
|
37
|
+
addEventListener("pageshow", (event) => {
|
|
38
|
+
if (event.persisted) ask();
|
|
39
|
+
});
|
|
40
|
+
}
|
data/public/cursor.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// The cursor of a pull request's page. The reading rests on one block at a
|
|
2
|
+
// time, and notes.js and decisions.js move it; this script only shows where:
|
|
3
|
+
// a mark in the gutter, the keys of that block beside it and at the head of
|
|
4
|
+
// the docked list, and on the outline the section the block is in. Nothing
|
|
5
|
+
// else on the page carries keys, so the page stays quiet until it is asked.
|
|
6
|
+
|
|
7
|
+
import { thisBlock } from "./keys.js";
|
|
8
|
+
import { prNumber } from "./pr-number.js";
|
|
9
|
+
import { progress } from "./review-drafts.js";
|
|
10
|
+
|
|
11
|
+
const main = document.querySelector("body.shell > .main");
|
|
12
|
+
const sections = [...document.querySelectorAll("details.section")];
|
|
13
|
+
// The outline lists the page's sections in the page's own order.
|
|
14
|
+
const folds = [...document.querySelectorAll("[data-outline] [data-fold]")];
|
|
15
|
+
|
|
16
|
+
const made = (tag, className, text = "") =>
|
|
17
|
+
Object.assign(document.createElement(tag), {
|
|
18
|
+
className,
|
|
19
|
+
textContent: text,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// The keys of a block, as pairs like the bar's. A third member is what a
|
|
23
|
+
// press of the pointer does, where the key alone would not reach it.
|
|
24
|
+
const deciding = () => document.body.dataset.decisionMode === "true";
|
|
25
|
+
const folded = (summary) => !summary.parentElement.open;
|
|
26
|
+
const fold = (summary) => () => {
|
|
27
|
+
summary.parentElement.open = folded(summary);
|
|
28
|
+
};
|
|
29
|
+
const pictured = (decision) =>
|
|
30
|
+
decision.querySelectorAll('a[href*="#screenshot-"]').length;
|
|
31
|
+
|
|
32
|
+
// Where f begins deciding, by its place among the blocking decisions: the first
|
|
33
|
+
// still to review, which decisions.js marks, and the page marks in its turn
|
|
34
|
+
// while the reading rests on their heading.
|
|
35
|
+
const beginsAt = () => {
|
|
36
|
+
const all = [
|
|
37
|
+
...document.querySelectorAll(
|
|
38
|
+
'li[data-decision]:not([data-grade="non_blocking"])',
|
|
39
|
+
),
|
|
40
|
+
];
|
|
41
|
+
const first = all.findIndex((box) => box.hasAttribute("data-first"));
|
|
42
|
+
if (first >= 0) return `decide, from ${first + 1} of ${all.length}`;
|
|
43
|
+
return progress(pullRequest).left
|
|
44
|
+
? "blocking reviewed: what waits"
|
|
45
|
+
: "all reviewed: what now";
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const keysOf = (block) => {
|
|
49
|
+
// What can be done with the whole pull request stands beside its title itself.
|
|
50
|
+
if (block.matches("h1")) return [];
|
|
51
|
+
if (block.matches("li[data-decision]")) {
|
|
52
|
+
if (!deciding())
|
|
53
|
+
return [
|
|
54
|
+
["Enter", "decide on this"],
|
|
55
|
+
["n", "note on this"],
|
|
56
|
+
];
|
|
57
|
+
const pictures = pictured(block);
|
|
58
|
+
return [
|
|
59
|
+
...(pictures ? [["s", `its pictures · ${pictures}`]] : []),
|
|
60
|
+
["u", "undo"],
|
|
61
|
+
[
|
|
62
|
+
"Esc",
|
|
63
|
+
document.body.dataset.returnsTo
|
|
64
|
+
? "back to the list"
|
|
65
|
+
: "back to reading",
|
|
66
|
+
],
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
// A decision's line where they end unfolds to its words, and opens it.
|
|
70
|
+
if (block.matches("[data-line-of]"))
|
|
71
|
+
return [
|
|
72
|
+
block.getAttribute("aria-expanded") === "true"
|
|
73
|
+
? ["←", "less"]
|
|
74
|
+
: ["→", "more"],
|
|
75
|
+
["Enter", "decide on this"],
|
|
76
|
+
["Esc", "the whole page"],
|
|
77
|
+
];
|
|
78
|
+
// A picture an answer rests on is looked at, and takes no note.
|
|
79
|
+
if (block.matches("figure.evidence")) return [["Space", "look closely"]];
|
|
80
|
+
if (block.matches("figure.picture"))
|
|
81
|
+
return [
|
|
82
|
+
["Space", "look closely"],
|
|
83
|
+
["n", "note on this"],
|
|
84
|
+
];
|
|
85
|
+
// The condition on its decisions goes where they are decided.
|
|
86
|
+
if (block.matches('.conditions > [data-about="decisions"]'))
|
|
87
|
+
return document.querySelector("li[data-decision]")
|
|
88
|
+
? [
|
|
89
|
+
["f", beginsAt()],
|
|
90
|
+
["Enter", "the same"],
|
|
91
|
+
]
|
|
92
|
+
: [];
|
|
93
|
+
if (block.matches("#decisions > summary"))
|
|
94
|
+
return [
|
|
95
|
+
["f", beginsAt()],
|
|
96
|
+
["Enter", folded(block) ? "open" : "close", fold(block)],
|
|
97
|
+
];
|
|
98
|
+
// The non-blocking decisions' heading: one gesture leaves those that wait
|
|
99
|
+
// for nothing as they are; decisions.js says how many it would.
|
|
100
|
+
if (block.matches("details[data-non-blocking] > summary"))
|
|
101
|
+
return [
|
|
102
|
+
...(Number(block.dataset.leaves)
|
|
103
|
+
? [["A", `leave ${block.dataset.leaves} as is`]]
|
|
104
|
+
: []),
|
|
105
|
+
["Enter", folded(block) ? "open" : "close", fold(block)],
|
|
106
|
+
];
|
|
107
|
+
if (block.matches("details.section > summary"))
|
|
108
|
+
return [
|
|
109
|
+
["Enter", folded(block) ? "open" : "close", fold(block)],
|
|
110
|
+
["]", "next section"],
|
|
111
|
+
];
|
|
112
|
+
if (block.matches("summary"))
|
|
113
|
+
return [["Enter", folded(block) ? "open" : "close", fold(block)]];
|
|
114
|
+
if (block.closest("[data-quotable]")) return [["n", "note on this"]];
|
|
115
|
+
return [];
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// A press of the pointer on a block's key is the key itself, pressed where
|
|
119
|
+
// the reading rests.
|
|
120
|
+
const NAMED = {
|
|
121
|
+
Space: " ",
|
|
122
|
+
Esc: "Escape",
|
|
123
|
+
"→": "ArrowRight",
|
|
124
|
+
"←": "ArrowLeft",
|
|
125
|
+
};
|
|
126
|
+
const press = (key, on) =>
|
|
127
|
+
on.dispatchEvent(
|
|
128
|
+
new KeyboardEvent("keydown", {
|
|
129
|
+
key: NAMED[key] ?? key,
|
|
130
|
+
// keys.js reads a capital by Shift, as a keyboard sends it.
|
|
131
|
+
shiftKey: /^[A-Z]$/.test(key),
|
|
132
|
+
bubbles: true,
|
|
133
|
+
cancelable: true,
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const mark = made("span", "mark", ">");
|
|
138
|
+
mark.setAttribute("aria-hidden", "true");
|
|
139
|
+
// A rule down the gutter as far as the block goes, so that where the reading
|
|
140
|
+
// rests is seen whole, however many lines it takes.
|
|
141
|
+
const reach = made("span", "reach");
|
|
142
|
+
const beside = made("div", "beside");
|
|
143
|
+
const cursor = made("div", "cursor");
|
|
144
|
+
cursor.setAttribute("data-desk", "");
|
|
145
|
+
cursor.hidden = true;
|
|
146
|
+
cursor.append(mark, reach, beside);
|
|
147
|
+
main.append(cursor);
|
|
148
|
+
|
|
149
|
+
const hint = (block, [keys, does, pressed]) => {
|
|
150
|
+
const button = made("button", "");
|
|
151
|
+
button.type = "button";
|
|
152
|
+
button.append(
|
|
153
|
+
...keys.split(" ").map((key) => made("kbd", "", key)),
|
|
154
|
+
` ${does}`,
|
|
155
|
+
);
|
|
156
|
+
// The reading keeps its place while the pointer presses.
|
|
157
|
+
button.addEventListener("mousedown", (event) => event.preventDefault());
|
|
158
|
+
button.addEventListener("click", () => {
|
|
159
|
+
if (pressed) pressed();
|
|
160
|
+
else press(keys.split(" ")[0], block);
|
|
161
|
+
place();
|
|
162
|
+
});
|
|
163
|
+
return button;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// What can be done with the whole pull request shows while the title holds the
|
|
167
|
+
// focus, and a press on a link takes none: the title keeps it, or the panel
|
|
168
|
+
// would go before the press ends and the click land on the page behind.
|
|
169
|
+
document
|
|
170
|
+
.querySelector(".pull-request .whole")
|
|
171
|
+
?.addEventListener("mousedown", (event) => event.preventDefault());
|
|
172
|
+
|
|
173
|
+
const onOutline = (block) => {
|
|
174
|
+
const section = block?.closest("details.section");
|
|
175
|
+
folds.forEach((line, index) => {
|
|
176
|
+
if (section && sections[index] === section)
|
|
177
|
+
line.setAttribute("aria-current", "true");
|
|
178
|
+
else line.removeAttribute("aria-current");
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// The block the reading last rested on. A press of the pointer on bare page
|
|
183
|
+
// takes the focus from it; the reading goes back there, so that the cursor is
|
|
184
|
+
// never lost and j and k go on from where they were. Not while the press may
|
|
185
|
+
// be the start of a selection, nor while words are selected: taking the focus
|
|
186
|
+
// then would undo the selection under the owner's hand.
|
|
187
|
+
let rested;
|
|
188
|
+
let pointerDown = false;
|
|
189
|
+
const comeBack = () => {
|
|
190
|
+
if (document.activeElement !== document.body) return;
|
|
191
|
+
if (document.querySelector("dialog[open], :popover-open")) return;
|
|
192
|
+
if (pointerDown || !getSelection().isCollapsed) return;
|
|
193
|
+
if (rested?.isConnected && rested.getClientRects().length)
|
|
194
|
+
rested.focus({ preventScroll: true });
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const place = () => {
|
|
198
|
+
const block = document.activeElement?.closest?.("[data-reading]");
|
|
199
|
+
const shown =
|
|
200
|
+
!!block && main.contains(block) && block.getClientRects().length > 0;
|
|
201
|
+
if (shown) rested = block;
|
|
202
|
+
cursor.hidden = !shown;
|
|
203
|
+
onOutline(shown ? block : null);
|
|
204
|
+
if (!shown) return thisBlock([]);
|
|
205
|
+
|
|
206
|
+
const keys = keysOf(block);
|
|
207
|
+
beside.replaceChildren(...keys.map((key) => hint(block, key)));
|
|
208
|
+
thisBlock(keys);
|
|
209
|
+
|
|
210
|
+
// The mark stands at the block's first line, whatever the block's own face.
|
|
211
|
+
const style = getComputedStyle(block);
|
|
212
|
+
const top =
|
|
213
|
+
block.getBoundingClientRect().top -
|
|
214
|
+
main.getBoundingClientRect().top +
|
|
215
|
+
Number.parseFloat(style.paddingTop) +
|
|
216
|
+
Number.parseFloat(style.borderTopWidth);
|
|
217
|
+
cursor.style.top = `${top}px`;
|
|
218
|
+
const line = Number.parseFloat(style.lineHeight);
|
|
219
|
+
cursor.style.setProperty(
|
|
220
|
+
"--line",
|
|
221
|
+
`${Number.isNaN(line) ? Number.parseFloat(style.fontSize) * 1.6 : line}px`,
|
|
222
|
+
);
|
|
223
|
+
cursor.style.setProperty(
|
|
224
|
+
"--height",
|
|
225
|
+
`${
|
|
226
|
+
block.getBoundingClientRect().height -
|
|
227
|
+
Number.parseFloat(style.paddingTop) -
|
|
228
|
+
Number.parseFloat(style.paddingBottom) -
|
|
229
|
+
Number.parseFloat(style.borderTopWidth) -
|
|
230
|
+
Number.parseFloat(style.borderBottomWidth)
|
|
231
|
+
}px`,
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// The outline goes to a section at a press. From a decision that is being
|
|
236
|
+
// decided, the page comes back first.
|
|
237
|
+
folds.forEach((line, index) => {
|
|
238
|
+
line.addEventListener("click", () => {
|
|
239
|
+
const heading = sections[index]?.querySelector("summary");
|
|
240
|
+
if (!heading) return;
|
|
241
|
+
if (document.body.dataset.focus) press("Escape", document.activeElement);
|
|
242
|
+
heading.focus({ preventScroll: true });
|
|
243
|
+
heading.scrollIntoView({ block: "start" });
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// How many decisions still wait, on the outline: a number that goes when none do.
|
|
248
|
+
const pullRequest = prNumber(
|
|
249
|
+
document.querySelector("[data-review-draft]")?.dataset.pr,
|
|
250
|
+
);
|
|
251
|
+
const count = () => {
|
|
252
|
+
const said = document.querySelector(
|
|
253
|
+
'[data-outline] [data-waits-for="decisions"]',
|
|
254
|
+
);
|
|
255
|
+
if (!said) return;
|
|
256
|
+
const { left } = progress(pullRequest);
|
|
257
|
+
said.textContent = left || "";
|
|
258
|
+
said.dataset.waits = left > 0;
|
|
259
|
+
};
|
|
260
|
+
count();
|
|
261
|
+
|
|
262
|
+
// The page moves under the cursor when a section folds, a picture arrives or
|
|
263
|
+
// the list of keys docks: the mark follows its block.
|
|
264
|
+
const soon = () => requestAnimationFrame(place);
|
|
265
|
+
document.addEventListener("focusin", soon);
|
|
266
|
+
document.addEventListener("focusout", () =>
|
|
267
|
+
setTimeout(() => {
|
|
268
|
+
comeBack();
|
|
269
|
+
place();
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
document.addEventListener("pointerdown", () => {
|
|
273
|
+
pointerDown = true;
|
|
274
|
+
});
|
|
275
|
+
document.addEventListener("pointerup", () => {
|
|
276
|
+
pointerDown = false;
|
|
277
|
+
setTimeout(comeBack);
|
|
278
|
+
});
|
|
279
|
+
document.addEventListener("toggle", soon, true);
|
|
280
|
+
document.addEventListener("keys-docked", soon);
|
|
281
|
+
document.addEventListener("drafts-changed", () => {
|
|
282
|
+
count();
|
|
283
|
+
soon();
|
|
284
|
+
});
|
|
285
|
+
addEventListener("resize", soon);
|
|
286
|
+
addEventListener("load", soon);
|
|
287
|
+
new ResizeObserver(soon).observe(main);
|
|
288
|
+
new MutationObserver(soon).observe(document.body, {
|
|
289
|
+
attributes: true,
|
|
290
|
+
attributeFilter: ["data-focus", "data-decision-mode"],
|
|
291
|
+
});
|
|
292
|
+
soon();
|