acstack 0.0.0-stage → 0.47.1

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 (143) hide show
  1. package/.claude-plugin/plugin.json +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +389 -2
  4. package/THIRD_PARTY_NOTICES +14 -0
  5. package/TROUBLESHOOTING.md +225 -0
  6. package/agents/codex/frontend-verifier.toml +352 -0
  7. package/agents/codex/task-checker.toml +345 -0
  8. package/agents/codex/task-executor.toml +157 -0
  9. package/agents/codex/test-writer.toml +149 -0
  10. package/agents/frontend-verifier.md +351 -0
  11. package/agents/task-checker.md +344 -0
  12. package/agents/task-executor.md +156 -0
  13. package/agents/test-writer.md +148 -0
  14. package/assets/playwriter-bridge/build-launcher.ps1 +32 -0
  15. package/assets/playwriter-bridge/doctor.ps1 +450 -0
  16. package/assets/playwriter-bridge/launcher-src.cs +131 -0
  17. package/assets/playwriter-bridge/opener.js +330 -0
  18. package/assets/playwriter-bridge/opener.ps1 +63 -0
  19. package/assets/playwriter-bridge/register-tasks.ps1 +328 -0
  20. package/assets/playwriter-bridge/relay.ps1 +50 -0
  21. package/assets/playwriter-bridge/stop.ps1 +125 -0
  22. package/assets/playwriter-bridge/sync.ps1 +271 -0
  23. package/assets/playwriter-bridge/tunnel.ps1 +325 -0
  24. package/assets/playwriter-bridge/vm-free-port.sh +166 -0
  25. package/assets/terminal-workspace/client-keybindings.md +87 -0
  26. package/assets/terminal-workspace/config.example.yaml +334 -0
  27. package/assets/terminal-workspace/setup.sh +4404 -0
  28. package/cli/README.md +47 -0
  29. package/cli/bin/acstack.js +3 -0
  30. package/cli/dist/index.js +25984 -0
  31. package/cli/dist/index.js.map +7 -0
  32. package/cli/package.json +59 -0
  33. package/docs/adr/0001-ste-writing-standard.md +16 -0
  34. package/docs/adr/0002-version-gate-not-auto-bump.md +35 -0
  35. package/docs/adr/0003-handoff-ack-command.md +19 -0
  36. package/docs/adr/0004-dev-setup-per-host.md +13 -0
  37. package/docs/adr/0005-two-orchestrators-per-host.md +25 -0
  38. package/docs/adr/0006-document-opener-service.md +89 -0
  39. package/docs/adr/0007-lead-check-reads-the-transcript.md +87 -0
  40. package/docs/adr/0008-cli-audits-skill-interviews.md +37 -0
  41. package/docs/adr/0009-verify-the-write-not-the-writer.md +51 -0
  42. package/docs/adr/0010-promote-dev-to-main-by-fast-forward.md +115 -0
  43. package/docs/adr/0011-node-header-is-the-edge-encoding.md +20 -0
  44. package/docs/adr/0012-node-kind-is-a-closed-set-of-five.md +31 -0
  45. package/docs/adr/0013-the-diagram-renderer-is-pure.md +23 -0
  46. package/docs/adr/0014-reading-a-node-hides-its-generated-diagram.md +29 -0
  47. package/docs/adr/0015-a-node-id-links-to-its-issue-through-an-anchor.md +37 -0
  48. package/docs/adr/0016-the-map-root-carries-no-status-of-its-own.md +21 -0
  49. package/docs/adr/0017-call-impeccable-copy-only-its-questions.md +13 -0
  50. package/docs/adr/index.md +17 -0
  51. package/docs/config-reference.md +572 -0
  52. package/docs/dev-setups.md +164 -0
  53. package/docs/migrate-0.8.md +28 -0
  54. package/docs/playwriter-bridge-windows.md +15 -0
  55. package/docs/terminal-workspace-keys.md +137 -0
  56. package/docs/terminal-workspace.md +1780 -0
  57. package/docs/workflows.md +309 -0
  58. package/hooks/ac-writing-style-reminder.sh +86 -0
  59. package/hooks/context-pressure.sh +583 -0
  60. package/hooks/hooks.json +50 -0
  61. package/hooks/session-start-handoff.sh +165 -0
  62. package/hooks/session-start.sh +214 -0
  63. package/orchestrator/AGENTS.md +532 -0
  64. package/output-styles/ac-writing-style.md +286 -0
  65. package/package.json +50 -4
  66. package/pi/agents/frontend-verifier.md +357 -0
  67. package/pi/agents/task-checker.md +348 -0
  68. package/pi/agents/task-executor.md +160 -0
  69. package/pi/agents/test-writer.md +154 -0
  70. package/pi/extension.ts +723 -0
  71. package/profiles/fullstack-python.yaml +29 -0
  72. package/profiles/node-typescript.yaml +23 -0
  73. package/profiles/python.yaml +24 -0
  74. package/skills/archive/SKILL.md +160 -0
  75. package/skills/bootstrap/SKILL.md +109 -0
  76. package/skills/bridge/HANDOFF.md +93 -0
  77. package/skills/bridge/SKILL-VM.md +292 -0
  78. package/skills/bridge/SKILL-WINDOWS.md +526 -0
  79. package/skills/bridge/SKILL.md +121 -0
  80. package/skills/browser-verify/SKILL.md +229 -0
  81. package/skills/code-review/SKILL.md +44 -0
  82. package/skills/commit/SKILL.md +106 -0
  83. package/skills/compact-and-continue/SKILL.md +241 -0
  84. package/skills/explore/SKILL.md +149 -0
  85. package/skills/grilling/SKILL.md +134 -0
  86. package/skills/handoff/SKILL.md +250 -0
  87. package/skills/handoff/launch-herdr.md +197 -0
  88. package/skills/handoff/launch-without-herdr.md +30 -0
  89. package/skills/handoff/lead-check.md +59 -0
  90. package/skills/handoff/outcomes.md +162 -0
  91. package/skills/handoff/running-agent.md +50 -0
  92. package/skills/handoff/watch.md +115 -0
  93. package/skills/host/SKILL.md +247 -0
  94. package/skills/host/detect-host.sh +297 -0
  95. package/skills/host/herdr.md +79 -0
  96. package/skills/host/install-herdr-block.sh +467 -0
  97. package/skills/host/install-orca.sh +512 -0
  98. package/skills/host/notes.md +97 -0
  99. package/skills/host/orca.md +160 -0
  100. package/skills/host/questions.md +163 -0
  101. package/skills/host/report.md +42 -0
  102. package/skills/implement/SKILL.md +230 -0
  103. package/skills/implement/decision-log.md +62 -0
  104. package/skills/implement/design-pass.md +32 -0
  105. package/skills/implement/relaxed-tdd.md +28 -0
  106. package/skills/map/SKILL.md +250 -0
  107. package/skills/map/frontend.md +227 -0
  108. package/skills/map/trackers/files.md +74 -0
  109. package/skills/map/trackers/github.md +180 -0
  110. package/skills/map/visuals.md +387 -0
  111. package/skills/new-worktree/SKILL.md +155 -0
  112. package/skills/new-worktree/detect-orchestrator.sh +328 -0
  113. package/skills/new-worktree/detect-orchestrator.test.sh +894 -0
  114. package/skills/new-worktree/herdr.md +84 -0
  115. package/skills/new-worktree/orca.md +101 -0
  116. package/skills/pre-commit-review/SKILL.md +242 -0
  117. package/skills/pre-commit-review/auto-fix.md +19 -0
  118. package/skills/pre-commit-review/interactive.md +35 -0
  119. package/skills/quick-fix/SKILL.md +125 -0
  120. package/skills/record-context/SKILL.md +106 -0
  121. package/skills/setup/SKILL.md +250 -0
  122. package/skills/setup/browser.md +123 -0
  123. package/skills/setup/design-review.md +97 -0
  124. package/skills/setup/first-run.md +114 -0
  125. package/skills/setup/report.md +68 -0
  126. package/skills/setup/row-fixes.md +196 -0
  127. package/skills/setup/terminal-workspace.md +153 -0
  128. package/skills/sync/SKILL.md +171 -0
  129. package/skills/teardown-worktree/SKILL.md +245 -0
  130. package/skills/teardown-worktree/detector.md +39 -0
  131. package/skills/teardown-worktree/herdr.md +113 -0
  132. package/skills/teardown-worktree/orca.md +61 -0
  133. package/skills/terminal-workspace/SKILL.md +249 -0
  134. package/skills/terminal-workspace/components.md +36 -0
  135. package/skills/terminal-workspace/config-choices.md +69 -0
  136. package/skills/terminal-workspace/emulator-keys.md +48 -0
  137. package/skills/terminal-workspace/headless-clipboard.md +175 -0
  138. package/skills/terminal-workspace/turn-off-and-update.md +104 -0
  139. package/skills/test-conventions/SKILL.md +59 -0
  140. package/skills/visual-docs/SKILL.md +246 -0
  141. package/skills/writing/SKILL.md +231 -0
  142. package/skills/writing/examples.md +153 -0
  143. package/skills/writing/vocabulary.md +96 -0
@@ -0,0 +1,1780 @@
1
+ # Terminal workspace: herdr, gh-dash, diffnav, lazygit
2
+
3
+ This is an optional setup for running several coding agents on a remote machine, and reviewing their work without leaving the terminal.
4
+
5
+ - **Your code lives on a machine you reach over the network**
6
+ - You run agents on it
7
+ - **A plain terminal session dies with its connection**, taking every running agent with it
8
+ - **The tools worth reviewing code in assume a desktop**, meaning a clipboard, a browser, and a display
9
+ - **That remote machine has none of them**
10
+ - A copy or an "open in browser" goes nowhere
11
+ - **ACStack needs none of this setup**
12
+ - Skip the whole document if you work locally in an editor
13
+
14
+ - Run herdr on the dev machine, a terminal multiplexer that holds many terminal sessions
15
+ - herdr keeps every session running after you disconnect
16
+ - Attach to it from your own machine with `herdr --remote`
17
+ - A pane is one terminal window inside herdr
18
+ - The server keeps every pane alive when your connection drops
19
+ - The local client is what lets your own clipboard and browser reach the session, which an SSH shell cannot do
20
+ - Run `/acstack:terminal-workspace` to install and configure all of it
21
+ - One file, `~/.config/acstack/terminal-workspace.yaml`, turns each part on or off
22
+ - The rest of this document explains what that sets up and why
23
+ - Read it if you would rather configure it by hand or change the defaults
24
+
25
+ ```mermaid
26
+ flowchart TD
27
+ subgraph yours["Your machine, where you type"]
28
+ EM["Terminal emulator<br/>(Windows Terminal, iTerm2, kitty)"]
29
+ CL["herdr client<br/>(herdr --remote)"]
30
+ OUT["Clipboard and browser<br/>(acstack-clip, acstack-open)"]
31
+ end
32
+ subgraph dev["Dev machine, where the code is"]
33
+ SRV["Terminals that survive disconnect<br/>(herdr server and its panes)"]
34
+ RD["Reading code and markdown<br/>(yazi, acstack-md)"]
35
+ PR["Diffs and pull requests<br/>(gh-dash, diffnav)"]
36
+ DB["A SQL editor on one key<br/>(harlequin, acstack-db)"]
37
+ end
38
+ EM -->|"keys you press"| CL
39
+ CL -->|"SSH"| SRV
40
+ SRV --> RD
41
+ SRV --> PR
42
+ SRV --> DB
43
+ RD -->|"clickable links and copied text<br/>(OSC 8, OSC 52)"| OUT
44
+ PR -->|"clickable links and copied text<br/>(OSC 8, OSC 52)"| OUT
45
+ OUT -->|"no browser or clipboard reachable"| FB["A link on screen for you to click<br/>(acstack-open link route)"]
46
+ ```
47
+
48
+ | Part of the diagram | Section |
49
+ | --- | --- |
50
+ | herdr client | 4. How you attach |
51
+ | herdr server and its panes | 5. The herdr server |
52
+ | Terminal emulator | 6. Freeing your emulator's keys |
53
+ | Reading code and markdown | 7. Reading code, markdown, and diagrams |
54
+ | A SQL editor on one key | 8. Harlequin: a SQL editor on one key |
55
+ | Diffs and pull requests | 9. Diffs and pull requests |
56
+ | Clipboard and browser | 10. What crosses back to your machine |
57
+ | A link on screen for you to click | 10.2. acstack-open: the browser |
58
+
59
+ - Sections 1 and 2 cover what you get and the loop you run daily
60
+ - Section 3 installs it
61
+ - Section 11 explains why the parts work the way they do
62
+ - Section 12 lists the things that cost real time to find
63
+
64
+ ## 1. What you get, and its parts
65
+
66
+ *Five tools you drive day to day. herdr owns the terminals, gh-dash triages pull requests, diffnav reads diffs, lazygit commits them, harlequin queries the database. Five more tools and ACStack's own helpers sit behind them. Use this when your code lives on a dev machine, you drive more than one agent at a time, and you want a keyboard-only workflow.*
67
+
68
+ - Skip it if you work locally in a graphical editor
69
+ - A desktop tool serves you better
70
+ - **Agents that survive disconnect.** herdr runs a background server. Closing the terminal, dropping the network, or attaching from another machine never stops an agent mid-task
71
+ - **One screen that shows who needs you.** herdr marks every pane working, blocked, idle, or done
72
+ - You stop hunting for the stuck one
73
+ - **Review without a browser.** Pull request triage, diffs with a file tree, and comments, all from the terminal
74
+
75
+ The five you drive day to day:
76
+
77
+ | Tool | Role | Why this one |
78
+ |---|---|---|
79
+ | [herdr](https://herdr.dev) | Terminal multiplexer | Background server, per-pane agent state, one container of tabs and panes per git worktree |
80
+ | [gh-dash](https://github.com/dlvhdr/gh-dash) | Pull request dashboard | Saved searches per section, custom actions, `gh` underneath |
81
+ | [diffnav](https://github.com/dlvhdr/diffnav) | Diff reader | File tree beside the diff, the blast-radius view a plain pager lacks |
82
+ | [lazygit](https://github.com/jesseduffield/lazygit) | Git TUI | Stage, commit, amend and push without leaving the keyboard, and LazyVim already binds it |
83
+ | [harlequin](https://github.com/tconbeer/harlequin) | SQL editor | Query, results and schema tree in one window, with adapters for postgres and Snowflake |
84
+
85
+ Five more tools do the work those five ask for.
86
+
87
+ - You drive one of them yourself, tuicr, from the moment a review starts on gh-dash's `D` key
88
+
89
+ | Tool | Role | Why this one |
90
+ |---|---|---|
91
+ | [delta](https://github.com/dandavison/delta) | Pager | Syntax highlighting for every diff, and gh-dash uses it internally |
92
+ | [tuicr](https://github.com/agavra/tuicr) | Code review | Vim keys, and pull request review in the same tool gh-dash hands work to |
93
+ | [yazi](https://github.com/sxyazi/yazi) | File manager | Live preview per file type, so markdown renders as the cursor moves |
94
+ | [mermaid-ascii](https://github.com/AlexanderGrooff/mermaid-ascii) | Diagram renderer | Draws a mermaid diagram, meaning one written as text inside the file, as box-drawing art |
95
+ | [glow](https://github.com/charmbracelet/glow) | Markdown pager | Readable markdown in the terminal |
96
+
97
+ ACStack adds two helpers of its own beside those tools.
98
+
99
+ - `acstack-md` reads markdown and draws its mermaid diagrams
100
+ - `acstack-clip` and `acstack-open` carry your clipboard and your browser back across the link
101
+ - Section 10 covers that pair
102
+
103
+ ## 2. The daily loop
104
+
105
+ *Six steps, from dispatching an agent to tearing its worktree down. Four of them start with one key.*
106
+
107
+ Three terms, then the loop.
108
+
109
+ - **A workspace** is one herdr container of tabs and panes, usually one per repository or worktree
110
+ - **A worktree** is a second checkout of the same repository on its own branch
111
+ - **Every key below** is one this setup binds (see sections 3 to 10)
112
+
113
+ 1. **Dispatch.** Press `alt+r` to create a worktree workspace. Or ask an agent, and the `new-worktree` skill registers one with herdr for you
114
+ 2. **Monitor.** Press `alt+s` for the sidebar, the strip listing every workspace and every agent.
115
+
116
+ A blocked agent needs an answer from you. A done agent has finished and you have not looked yet. Leave a working agent alone
117
+
118
+ 3. **Review locally.** Press `alt+f` to see what the agent's branch adds to dev. Press `alt+x` to compare something else. Run the `pre-commit-review` skill in the agent's own pane for a deeper pass
119
+ 4. **Ship.** The agent commits, pushes, and opens the pull request from its worktree
120
+ 5. **Review the pull request.** Press `alt+i` for the dashboard. Press `p` then `]` to reach Files Changed. Press `D` to review it in tuicr, or `S` to hand it to an agent
121
+ 6. **Tear down.** Run `herdr worktree remove --workspace <id>`. Then delete the branch
122
+
123
+ ## 3. Install
124
+
125
+ *One command installs every tool and writes every config. The rest of this section is the same work done by hand.*
126
+
127
+ The `/acstack:terminal-workspace` skill runs `assets/terminal-workspace/setup.sh` for you, and that script takes six commands.
128
+
129
+ | Command | What it does |
130
+ | --- | --- |
131
+ | `setup.sh apply` | installs every enabled tool and writes its config |
132
+ | `setup.sh status` | reports what this machine ended up with |
133
+ | `setup.sh outdated` | reports which tools are behind and which components are new, and changes nothing |
134
+ | `setup.sh upgrade [tool...]` | reinstalls what `outdated` reported, and adds any new component's config block to your file |
135
+ | `setup.sh disable <component>` | removes one component's keys and config |
136
+ | `setup.sh uninstall` | removes everything `apply` wrote, in every config it touched |
137
+
138
+ What `outdated` answers in its exit code:
139
+
140
+ - Exit 0 means nothing to do, and 1 means a missing, new, or stale finding
141
+ - Exit 2 means it cannot read the config or the shipped example
142
+ - It reads herdr's stable release manifest, so a machine on the preview channel sees herdr reported stale
143
+ - Name a tool on `upgrade` to skip the version comparison, which is the only way to refresh `mermaid-ascii`
144
+ - `mermaid-ascii` has no version flag, so nothing can read the installed version
145
+
146
+ What `uninstall` removes:
147
+
148
+ - It strips the managed blocks from the herdr, tuicr, and yazi configs
149
+ - It unsets the delta git settings
150
+ - It deletes the helper scripts
151
+ - It removes the keybindings it wrote into gh-dash
152
+ - It deletes `~/.config/nvim/lua/plugins/acstack.lua`, a file it wrote whole
153
+ - It takes back what the `pi` component added to pi (section 3.6)
154
+ - Your own settings in those files survive, and so does every tool binary
155
+ - harlequin is the exception, because uv owns its whole environment rather than one file in `~/.local/bin`
156
+ - pi stays, because it holds your logins and sessions
157
+
158
+ What the config holds:
159
+
160
+ - The script reads `~/.config/acstack/terminal-workspace.yaml`, which the skill copies from `assets/terminal-workspace/config.example.yaml`
161
+ - The config holds twelve components, each with its own `enabled` key
162
+ - You turn a part off there and run `apply` again
163
+ - Eight components name a tool this setup installs: `herdr`, `gh_dash`, `diffnav`, `delta`, `tuicr`, `lazygit`, `yazi`, and `harlequin`
164
+ - `neovim` names an editor you installed yourself, and writes one file into its config
165
+ - It starts off, and section 3.5 says why
166
+ - `pi` names a coding agent, which npm installs, and ACStack then installs into
167
+ - It starts off too, and section 3.6 says why
168
+ - The other two name a feature
169
+ - `markdown` covers `acstack-md` with mermaid-ascii and glow
170
+ - `remote` covers the clipboard and browser helpers
171
+ - Config keys use underscores
172
+ - gh-dash's key is `gh_dash`
173
+
174
+ Every setting has a default, so `apply` works with no config file and says so when it finds none.
175
+
176
+ - The two gh-dash review keys used to be the exception
177
+ - That writer read `review` and `agent_review` straight from the yaml, with no default of its own
178
+ - A machine with no config had both pruned and neither written back
179
+ - `o` already defaulted
180
+ - It kept working, and the config looked healthy
181
+ - Both keys now default like everything else, and an empty string still turns one off
182
+ - A `gh_dash:` heading with nothing under it is what commenting the block out leaves behind
183
+ - That now falls back to the defaults too, where it used to abort the gh-dash step with a traceback
184
+ - The script edits between its own marker comments
185
+ - Hand-written config around it survives
186
+ - gh-dash is the exception
187
+ - Python rewrites that config whole
188
+ - No comment survives the round trip
189
+ - The script therefore tracks its own keybindings there by name
190
+
191
+ ### 3.1. What `apply` installs, and how to do it by hand
192
+
193
+ *`apply` installs eight tools into `~/.local/bin`, herdr through its own installer, gh-dash as a `gh` extension, and harlequin through uv. None of it needs root.*
194
+
195
+ Two things to do first:
196
+
197
+ - Put `~/.local/bin` on your `PATH`
198
+ - Install the `gh` command
199
+
200
+ Where each binary lands:
201
+
202
+ - Every binary except herdr lands in `~/.local/bin`, or in `$ACSTACK_TW_BIN` when you set that
203
+ - herdr's own installer always uses `~/.local/bin`
204
+
205
+ By hand, herdr and gh-dash install like this:
206
+
207
+ ```bash
208
+ # herdr
209
+ curl -fsSL https://herdr.dev/install.sh | sh
210
+
211
+ # gh-dash, as a gh extension
212
+ gh extension install dlvhdr/gh-dash
213
+ ```
214
+
215
+ Eight more are single static binaries.
216
+
217
+ - Download each project's Linux x86_64 release
218
+ - Put the binary in `~/.local/bin`, then `chmod +x` it
219
+
220
+ | Tool | GitHub repository | Release asset |
221
+ |---|---|---|
222
+ | delta | `dandavison/delta` | `x86_64-unknown-linux-gnu` |
223
+ | diffnav | `dlvhdr/diffnav` | `Linux_x86_64` |
224
+ | fzf | `junegunn/fzf` | `linux_amd64` |
225
+ | lazygit | `jesseduffield/lazygit` | `linux_x86_64` |
226
+ | tuicr | `agavra/tuicr` | `x86_64-unknown-linux-gnu` |
227
+ | yazi | `sxyazi/yazi` | `x86_64-unknown-linux-gnu`, which also carries `ya` |
228
+ | mermaid-ascii | `AlexanderGrooff/mermaid-ascii` | `Linux_x86_64` |
229
+ | glow | `charmbracelet/glow` | `Linux_x86_64` |
230
+
231
+ harlequin is the one tool uv installs, rather than a release tarball.
232
+
233
+ - `uv tool install harlequin --with harlequin-postgres --with harlequin-snowflake` installs the editor and both adapters
234
+ - A machine without `uv` skips the component, and `apply` prints the one command that installs uv
235
+
236
+ Three more steps `apply` handles that no release tarball covers:
237
+
238
+ - `ya pkg add yazi-rs/plugins:piper`, the yazi plugin that lets `acstack-md` draw the preview pane
239
+ - `pip install --user markdown`, which `acstack-md --serve` needs
240
+ - a copy of `mermaid.min.js` at `~/.local/share/acstack/mermaid.min.js`
241
+ - A served page fetches nothing from a content delivery network (CDN)
242
+
243
+ ### 3.2. Let herdr read agent state from the agent
244
+
245
+ *A hook reports what an agent is doing, rather than herdr guessing from the screen.*
246
+
247
+ ```bash
248
+ herdr integration install claude # also: codex, opencode, copilot, and others
249
+ ```
250
+
251
+ - Check what is available with `herdr integration status`
252
+ - The hook applies to a session that starts after you install it
253
+ - The `herdr.integration` key in the config picks which one `apply` installs
254
+
255
+ ### 3.3. The tuicr fork build is temporary
256
+
257
+ *Two of the config keys for tuicr exist only in a fork build, which also carries one bug fix. Leave `build_from_fork: false` unless you want them.*
258
+
259
+ Two upstream pull requests are still open:
260
+
261
+ - [agavra/tuicr#607](https://github.com/agavra/tuicr/pull/607) by
262
+ [antonio2368](https://github.com/antonio2368) – configurable per-file
263
+ `+added -removed` counts in the tree, and the `show_file_line_stats` key
264
+
265
+ - [agavra/tuicr#633](https://github.com/agavra/tuicr/pull/633), opened from ACStack's own
266
+ fork – move the file list boundary with `<leader>L` / `<leader>H`, and the
267
+ `file_list_width` key
268
+
269
+ The fork carries a third change of its own, with no upstream pull request yet.
270
+
271
+ - It fixes blank `+N -N` counts in pull request review mode, `tuicr pr <N>`
272
+ - That mode has no version control system (VCS) backend of its own
273
+ - It borrowed the `File` type, which `--file <path>` uses, as a stand-in
274
+ - The fork's whole-file gate hides counts for `--file` and `--all-files`, since every line there counts as added
275
+ - That gate matched a review session too
276
+ - The counts stayed blank even with `show_file_line_stats` on
277
+ - The fix gives a review session its own `PullRequest` VCS type
278
+ - The gate no longer matches it
279
+ - Leave `build_from_fork: false` unless you want one of those three changes
280
+ - The default installs the stock release and skips both config keys
281
+ - tuicr then never warns about an unknown key
282
+ - Setting `build_from_fork: true` clones the fork and builds `local/daily` with cargo
283
+ - It also writes the two keys plus `no_update_check = true`
284
+ - `tuicr update` cannot replace the build
285
+ - `apply` replaces a value you set by hand on any of those keys rather than joining it
286
+ - TOML forbids two of one key, and tuicr ignores a config it cannot parse
287
+
288
+ On a machine where you did set it true:
289
+
290
+ - `setup.sh status` reports the state of both pull requests
291
+ - After both merge, check whether anyone has submitted the counts fix above upstream too, then set `build_from_fork: false` again
292
+ - Stock tuicr 0.23.0 has no counts feature at all
293
+ - It never had this bug or the fix
294
+ - Switching back before that fix lands upstream brings the blank counts back to review mode
295
+ - Re-run `apply` once you do switch
296
+ - Check the merged key names first
297
+ - The review can rename them
298
+
299
+ ### 3.4. Pin tuicr's light or dark theme
300
+
301
+ *tuicr works out its own theme by asking the terminal what colour it paints. Nothing answers under `herdr --remote`, so set `tuicr.appearance` on any machine you reach over the network.*
302
+
303
+ - tuicr's own default asks the terminal for its background colour over OSC 11, an escape sequence a terminal answers with the colour it is painting
304
+ - Under `herdr --remote` nothing answers, and tuicr falls back to `gsettings get org.gnome.desktop.interface color-scheme`
305
+ - A headless machine answers that with `default`, which tuicr reads as light
306
+ - It then paints a light theme with a transparent background, leaving dark text on a black terminal
307
+ - Set `tuicr.appearance` to `dark` or `light`, and tuicr skips the whole detection path
308
+ - Leave it empty on a machine you sit at, where the terminal answers for itself
309
+
310
+ ### 3.5. LazyVim: a dot for unsaved changes, and copy over SSH
311
+
312
+ *Set `neovim.enabled: true` to get both. LazyVim hides unsaved changes, and over SSH it cannot copy to your clipboard.*
313
+
314
+ - LazyVim's `pretty_path` component sets `modified_sign = ""`, so the only signal is colour
315
+ - It sets `modified_hl = "MatchParen"`, which recolours the filename you are already looking at
316
+ - You cannot tell a saved buffer from an unsaved one at a glance
317
+ - `apply` writes `~/.config/nvim/lua/plugins/acstack.lua`, which appends a lualine component to `lualine_c`
318
+ - LazyVim loads every file under `lua/plugins`, so a file of our own is a complete plugin spec
319
+ - The dot is `●` in catppuccin red, and `neovim.unsaved_dot_color` changes the colour
320
+ - This is the only component that starts off, because a neovim config is one you built
321
+ - Every other component owns its files outright, or edits a marked region in a config that invites one
322
+ - It writes one whole file and never edits one of yours
323
+ - `setup.sh disable neovim` deletes that file, and so does `uninstall`
324
+ - Neither one touches a `acstack.lua` you wrote by hand
325
+ - `apply` skips a machine with no LazyVim config, because both parts of the file rely on LazyVim
326
+ - `apply` says so rather than writing a file that does nothing
327
+ - `apply` leaves the dot out when a lualine override you wrote already marks a modified buffer, and names that file
328
+ - LazyVim loads every file under `lua/plugins`, so yours does exactly what this component does
329
+ - The statusline would then carry two dots side by side
330
+ - Neither file looks wrong on its own, and the only place the collision shows is the statusline
331
+ - Delete your own file to hand the dot to this component
332
+
333
+ Copy over SSH:
334
+
335
+ - Over SSH, LazyVim sets `clipboard` empty and neovim finds no provider, so `:%y+` fails with `clipboard: No provider`
336
+ - The same file sets `vim.g.clipboard` to send every copy over OSC 52, which reaches the clipboard of the machine you sit at (section 10.1)
337
+ - It also sets `clipboard=unnamedplus` after LazyVim's own `VeryLazy` restore, so a plain `y` copies too
338
+ - `apply` leaves that option alone when a file of yours sets `clipboard`
339
+ - `p` pastes what neovim copied last, and never asks the terminal
340
+ - Windows Terminal refuses OSC 52 reads, and neovim's own OSC 52 paste then waits ten seconds
341
+ - Paste text copied elsewhere with your terminal's paste key, `ctrl+shift+v` in Windows Terminal
342
+ - The file does nothing on a machine with a display, or inside tmux, which each have a clipboard of their own
343
+ - `apply` leaves the clipboard out when a file of yours already sets `vim.g.clipboard`, and names that file
344
+ - `neovim.osc52_clipboard: false` turns it off
345
+ - `setup.sh status` prints a `neovim clipboard:` line naming which provider neovim uses
346
+
347
+ ### 3.6. pi: ACStack in a second coding agent
348
+
349
+ *Set `pi.enabled: true` to install pi, the coding agent, with ACStack and your pi settings. It starts off, because it writes into pi's own config.*
350
+
351
+ What `apply` does:
352
+
353
+ - It installs pi with `npm install -g @earendil-works/pi-coding-agent` when pi is missing
354
+ - It installs ACStack into pi, pinned to the version of this plugin
355
+ - Re-running `apply` after a plugin update moves the pin forward, and never back
356
+ - It installs `pi-subagents`, which ACStack on pi needs to launch a subagent
357
+ - It installs `billion-context` by default for context compression in pi
358
+ - When the `bili` command is available, setup runs native plugin setup for pi
359
+ - Setup leaves Claude Code and Codex alone, and removes bili from them if an older setup added it
360
+ - Set `pi.billion_context: false` to leave it out, or to remove it later if this setup installed it
361
+ - It installs `@juicesharp/rpiv-todo` and `pi-plugin-manager` by default
362
+ - Set `pi.todo: false` or `pi.plugin_manager: false` to leave either package out
363
+ - It installs `@juicesharp/rpiv-ask-user-question` as pi's question tool by default
364
+ - ACStack uses it because decisions can include markdown, code and artifact previews
365
+ - It accepts freeform answers and notes, and needs no full-event telemetry environment variable
366
+ - This setup still keeps pi's install telemetry disabled through `pi.settings.install_telemetry: false`
367
+ - Set `pi.question_tool: false` to leave it out, or to remove it later if this setup installed it
368
+ - It installs `pi-web-access` by default for Pi web search and web access
369
+ - The package is `pi-web-access` from `nicobailon/pi-web-access`
370
+ - It adds web search, URL fetching, GitHub repo cloning, PDF extraction and media understanding tools to pi
371
+ - This is separate from pi's anonymous install and update reporting; `pi.settings.install_telemetry: false` still controls that telemetry setting
372
+ - Set `pi.web_access: false` to leave it out, or to remove it later if this setup installed it
373
+ - It can install `pi-lens` for diagnostics and code-intelligence tools
374
+ - It is off by default because it is a heavier dev-tool layer
375
+ - Set `pi.lens: true` to install it, or `pi.lens: false` to remove it later if this setup installed it
376
+ - It installs `pi-slash-anywhere` by default, so slash commands work from any prompt
377
+ - Set `pi.slash_anywhere: false` to leave it out, or to remove it later if this setup installed it
378
+ - It installs `pi-compact-tools` and runs `agentic-retro install-pi`, each behind its own toggle
379
+ - The agentic-retro step needs the agentic-retro plugin installed first, and `apply` says so when it is not
380
+ - It writes the delegation line into pi's context file in `~/.pi/agent`, between HTML comment markers
381
+ - That is `AGENTS.md` by default, or the first of `AGENTS.override.md`, `AGENTS.md`, `AGENTS.MD`, `CLAUDE.md` and `CLAUDE.MD` that exists, which is the one pi reads
382
+ - `pi-subagents` delegates only when your request or your instructions allow it
383
+ - It sets `"toolActivation": "eager"` in `~/.pi/agent/extensions/subagent/config.json`
384
+ - It binds `"app.message.dequeue"` to `"ctrl+enter"` in `~/.pi/agent/keybindings.json`
385
+ - It writes the four keys under `pi.settings` into `~/.pi/agent/settings.json`
386
+ - It patches pi's bundled TUI so Windows Terminal Win32 input records paste as text
387
+ - `pi update` can replace the bundle, so `setup.sh upgrade pi` reapplies the patch
388
+
389
+ How it treats pi's files:
390
+
391
+ - pi's config is JSON, which has no comments to mark a managed region with
392
+ - `apply` claims only the keys it writes, and every other key stays
393
+ - `apply` installs and removes packages through `pi install` and `pi remove`, and never edits `packages` itself
394
+ - `apply` touches nothing in a file that does not parse, and names it
395
+ - A broken `settings.json` stops the whole component, because `pi install` rewrites that file
396
+ - `~/.config/acstack/terminal-workspace.pi.json` records what `apply` added
397
+ - `disable pi` and `uninstall` undo only what that file names
398
+ - A package or a key you had before `apply` stays yours
399
+ - A value of yours that `apply` replaced goes back, and so does an ACStack pin it moved forward
400
+ - A value you changed after `apply` stays as you set it
401
+ - `apply` never touches `auth.json`
402
+ - Run `pi`, then `/login`, yourself
403
+ - `setup.sh status` reports pi's version, its packages, the delegation line, `toolActivation`, and whether `auth.json` exists
404
+ - `setup.sh outdated` and `setup.sh upgrade` include `billion-context`, `@juicesharp/rpiv-todo`, `pi-plugin-manager`, `@juicesharp/rpiv-ask-user-question`, `pi-web-access`, `pi-lens`, and `pi-slash-anywhere` like pi's other npm packages
405
+
406
+ ## 4. How you attach
407
+
408
+ *herdr is a background server plus a terminal client. Where you put the client decides what the setup can do, and which key combinations reach herdr at all.*
409
+
410
+ - The server owns the panes and keeps them running whatever happens to your connection
411
+ - The client draws them
412
+ - Those are two separate machines in this setup
413
+ - Which one runs the client is the single decision the rest of this document depends on
414
+
415
+ Two terms:
416
+
417
+ - **A chord** is one key combination, such as `alt+f`
418
+ - **A keymap** is the file that says which chord runs which command
419
+
420
+ The chord family lives in the config as `herdr.chord_modifier`, so changing your mind costs one edit and one `setup.sh apply`, which rewrites the keymap and reloads it.
421
+
422
+ ### 4.1. From your own machine, with `herdr --remote`
423
+
424
+ *The recommended path. The client runs beside your clipboard and browser, so both can reach the session.*
425
+
426
+ Install herdr on the machine you type on as well as on the dev machine, then attach:
427
+
428
+ ```bash
429
+ herdr --remote dev-box --remote-keybindings server
430
+ ```
431
+
432
+ - The client connects over SSH, starts or attaches the server on the dev machine, and streams the session back
433
+ - Panes and agents still run on the dev machine, and only the drawing happens locally
434
+ - Section 4.4 lists four things worth setting up once
435
+ - You can attach without any of them
436
+
437
+ ### 4.2. By SSH first
438
+
439
+ *The fallback. Simpler, works from a phone, and gives up the local clipboard route.*
440
+
441
+ ```bash
442
+ ssh you@dev-box
443
+ herdr
444
+ ```
445
+
446
+ - Everything runs on the dev machine now, the client included
447
+ - This is the tmux-shaped path
448
+ - Detach with `prefix+q`, where `prefix` is `ctrl+b`, the key you press before a herdr command
449
+ - Then disconnect, SSH back, and run `herdr` again
450
+ - Use this path from a phone SSH app, where there is no local herdr to run
451
+ - Keep it as the fallback for when a remote attach misbehaves
452
+ - Both paths attach to the same server
453
+ - You can move between them freely
454
+
455
+ ### 4.3. What differs, and why it decides your keymap
456
+
457
+ *Each attach path can carry a different set of chords. Bind the family that survives both.*
458
+
459
+ | | `herdr --remote` | `ssh`, then `herdr` |
460
+ | --- | --- | --- |
461
+ | Where the client runs | your machine | the dev machine |
462
+ | Clipboard image paste into an agent | yes | no |
463
+ | Which config supplies chords | the client's, unless `--remote-keybindings server` | the server's |
464
+ | `alt+<key>` | works | works |
465
+ | `ctrl+alt+<key>` | dead | works |
466
+ | `ctrl+<digit>` | works | dead |
467
+ | Works from a phone SSH app | no | yes |
468
+ | Windows as the dev machine | not supported | not applicable |
469
+
470
+ - We measured the three key rows on herdr 0.8.2 with a Windows client rather than inferring them
471
+ - They are why this document binds plain `alt` throughout
472
+
473
+ The two paths deliver keys by different routes.
474
+
475
+ - Over SSH the emulator encodes a chord as an escape sequence, and herdr decodes it on the far side
476
+ - Under `herdr --remote` the local client reads native key events instead
477
+ - The two routes do not carry the same set
478
+ - The `ctrl+alt` family survives the escape-sequence route and dies on the native one
479
+ - `ctrl+<digit>` does the opposite
480
+
481
+ - Either way herdr accepts the binding and then never sees the key
482
+ - A chord that does not cross is therefore silently dead, and herdr reports no error
483
+ - `alt+<key>` is the only family that crossed both
484
+ - Test the wider family and use it, if you only ever attach one way
485
+ - Bind `alt` and do not spend the time, if you use both or expect to
486
+ - This affects only herdr's own chords
487
+ - gh-dash and diffnav read their own configs on the dev machine, where they run
488
+ - Their keys behave the same however you attached
489
+
490
+ ### 4.4. Worth setting up once, after you have attached
491
+
492
+ *An SSH host block, a loaded key, the server keymap flag, and a shortcut you can tab-complete.*
493
+
494
+ **An SSH host block**, so the target is a name rather than an address.
495
+
496
+ - Anything you put in it carries through, port forwards included
497
+ - herdr builds its own SSH config by including yours first
498
+
499
+ ```text
500
+ Host dev-box
501
+ HostName 203.0.113.10
502
+ User you
503
+ LocalForward 6419 localhost:6419
504
+ ```
505
+
506
+ **Key authentication through an agent.**
507
+
508
+ - herdr's connection reuse is Unix-only
509
+ - A Windows client authenticates more than once while it sets the session up
510
+ - With no key loaded that is a password prompt each time
511
+ - Load it once per boot with `ssh-add`
512
+
513
+ **`--remote-keybindings server`.**
514
+
515
+ - By default herdr reads its chords from the config on the machine running the client
516
+ - It then ignores the keymap on the dev machine
517
+ - Every chord looks broken
518
+ - This flag keeps the server config as the single source of truth
519
+ - Section 4.3 covers why that matters more than it sounds
520
+
521
+ **A shortcut, because you will type this many times a day.**
522
+
523
+ - On Windows use a PowerShell function rather than an alias or a symlink
524
+ - Both of those map one name to another
525
+ - The target arrives as an argument to herdr itself, and herdr rejects it
526
+
527
+ ```powershell
528
+ function herdr-dev {
529
+ & "$env:LOCALAPPDATA\Programs\Herdr\bin\herdr.exe" `
530
+ --remote dev-box --remote-keybindings server @args
531
+ }
532
+ ```
533
+
534
+ - A hyphenated name tab-completes, where the second word of a two-word command never will
535
+ - On macOS or Linux the same thing is a shell function in your profile
536
+ - Add `--handoff` if you like
537
+ - It asks a running server to pass its live panes to a replacement rather than restarting them
538
+ - That matters the next time you upgrade herdr and your client meets an older server
539
+ - It does nothing on an ordinary attach
540
+ - It is safe to leave in the shortcut
541
+
542
+ ## 5. The herdr server
543
+
544
+ *One config file, one keymap, and a numbering quirk that bites once you use worktrees.*
545
+
546
+ ### 5.1. Every key, and the three lists that answer to 1..9
547
+
548
+ *Plain `alt` chords for everything you press often, and the prefix for anything that closes a pane.*
549
+
550
+ Each tool lists its own keys.
551
+
552
+ - Press `prefix+?` in herdr, then `/` to filter
553
+ - Press `?` in gh-dash
554
+ - Read diffnav's footer
555
+ - Press `?` in lazygit and in tuicr
556
+ - Every key worth memorising is on one page: [terminal-workspace-keys.md](terminal-workspace-keys.md)
557
+ - Prefix is `ctrl+b`, and a chord without it is direct and needs no prefix
558
+ - A popup floats over your layout and puts you back where you were when you close it
559
+
560
+ | Key | Action |
561
+ |---|---|
562
+ | `alt+1`..`alt+9` | Focus agent by row |
563
+ | `prefix+1`..`prefix+9` | Switch workspace by row |
564
+ | `prefix+alt+1`..`prefix+alt+9` | Switch tab by position |
565
+ | `alt+n` / `alt+u` | Next / previous agent |
566
+ | `alt+left` / `alt+right` | Previous / next tab |
567
+ | `alt+up` / `alt+down` | Previous / next workspace |
568
+ | `alt+h` `alt+j` `alt+k` `alt+l` | Move between panes |
569
+ | `alt+a` | Back to the last pane |
570
+ | `alt+s` | Toggle sidebar |
571
+ | `alt+g` / `alt+shift+g` | lazygit in a popup / in a new tab |
572
+ | `prefix+t` | Goto picker |
573
+ | `alt+z` | Zoom pane |
574
+ | `alt+c` | New tab |
575
+ | `alt+w` | New workspace |
576
+ | `alt+r` | New worktree workspace |
577
+ | `alt+e` / `alt+minus` | Split right / down |
578
+ | `alt+y` / `alt+shift+y` | File tree in a popup / in a new tab |
579
+ | `alt+f` / `alt+shift+f` | Diff of your branch against dev, in a popup / in a new tab |
580
+ | `alt+x` / `alt+shift+x` | Pick what to compare, in a popup / in a new tab |
581
+ | `alt+i` / `alt+shift+i` | Dashboard in a popup / in a new tab |
582
+ | `prefix+q` | Detach, leaving everything running |
583
+ | `prefix+x` / `prefix+shift+x` / `prefix+shift+d` | Close pane / tab / workspace |
584
+ | `prefix+[` | Copy mode |
585
+ | `prefix+w` | Navigate mode, a persistent movement surface |
586
+
587
+ Three separate lists answer to `1..9`, and they are not the same list.
588
+
589
+ - `alt+N` walks agents
590
+ - `prefix+N` walks workspaces
591
+ - `prefix+alt+N` walks the tabs of the workspace you are in
592
+ - An agent row and a workspace row that share a number are a coincidence
593
+
594
+ The `ctrl` and `shift` modifiers are not options for a fourth list, and which one fails depends on how you attached.
595
+
596
+ - A `shift+<digit>` chord arrives as punctuation on both paths
597
+ - A `ctrl+<digit>` chord reaches herdr under `herdr --remote` but not over SSH
598
+ - A binding you make on one path is therefore silently dead on the other (see the table in section 4.3)
599
+ - The three chords above, `alt+N`, `prefix+N`, and `prefix+alt+N`, are what both attach paths carry
600
+
601
+ ### 5.2. One config file, validated without a restart
602
+
603
+ *Everything above lives in `~/.config/herdr/config.toml`. Three choices in that file are worth explaining.*
604
+
605
+ - Validate with `herdr config check`
606
+ - Apply without restarting with `herdr server reload-config`
607
+ - Undo with `herdr config reset-keys`
608
+
609
+ ```toml
610
+ [keys]
611
+ # Jump straight to an agent by its sidebar row.
612
+ focus_agent = "alt+1..9"
613
+
614
+ # Panes, on the same vim letters as the prefix bindings.
615
+ focus_pane_left = ["prefix+h", "alt+h"]
616
+ focus_pane_down = ["prefix+j", "alt+j"]
617
+ focus_pane_up = ["prefix+k", "alt+k"]
618
+ focus_pane_right = ["prefix+l", "alt+l"]
619
+
620
+ # Agents, tabs, workspaces.
621
+ next_agent = "alt+n"
622
+ previous_agent = "alt+u"
623
+ next_tab = ["prefix+n", "alt+right"]
624
+ previous_tab = ["prefix+p", "alt+left"]
625
+ next_workspace = ["alt+down"]
626
+ previous_workspace = ["alt+up"]
627
+
628
+ # By number. herdr leaves switch_workspace unbound and puts switch_tab on
629
+ # prefix+1..9, so without these there is no way to reach a workspace by number.
630
+ switch_workspace = "prefix+1..9"
631
+ switch_tab = "prefix+alt+1..9"
632
+
633
+ # Overlays and movement.
634
+ toggle_sidebar = ["prefix+b", "alt+s"]
635
+ # g belongs to git: alt+g is lazygit and alt+shift+g is its tab.
636
+ goto = "prefix+t"
637
+ zoom = ["prefix+z", "alt+z"]
638
+ last_pane = "alt+a"
639
+
640
+ # Creating things. Closing stays on the prefix so a mistyped chord cannot
641
+ # kill a pane with an agent running in it.
642
+ new_tab = ["prefix+c", "alt+c"]
643
+ new_workspace = ["prefix+shift+n", "alt+w"]
644
+ new_worktree = "alt+r"
645
+ split_vertical = ["prefix+v", "alt+e"]
646
+ split_horizontal = ["prefix+minus", "alt+minus"]
647
+
648
+ # A popup floats over the layout and returns you where you were. Each one has
649
+ # a tab variant on the shift chord, which goes through acstack-herdr-tab.
650
+ # herdr runs a popup with its server's PATH. That PATH may lack ~/.local/bin.
651
+ # Each popup goes through acstack-popup, which puts that folder back.
652
+ [[keys.command]]
653
+ key = "alt+f"
654
+ type = "popup"
655
+ command = "/home/you/.local/bin/acstack-popup acstack-diff"
656
+ description = "diff: branch vs dev"
657
+ width = "90%"
658
+ height = "90%"
659
+
660
+ [[keys.command]]
661
+ key = "alt+shift+f"
662
+ type = "shell"
663
+ command = "acstack-herdr-tab diff acstack-diff"
664
+ description = "diff: branch vs dev (tab)"
665
+
666
+ [[keys.command]]
667
+ key = "alt+g"
668
+ type = "popup"
669
+ command = "/home/you/.local/bin/acstack-popup lazygit"
670
+ description = "git: stage, commit, push"
671
+ width = "90%"
672
+ height = "90%"
673
+
674
+ [[keys.command]]
675
+ key = "alt+shift+g"
676
+ type = "shell"
677
+ command = "acstack-herdr-tab lazygit lazygit"
678
+ description = "git: stage, commit, push (tab)"
679
+
680
+ [[keys.command]]
681
+ key = "alt+x"
682
+ type = "popup"
683
+ command = "/home/you/.local/bin/acstack-popup acstack-diff pick"
684
+ description = "diff: pick what to compare"
685
+ width = "90%"
686
+ height = "90%"
687
+
688
+ [[keys.command]]
689
+ key = "alt+shift+x"
690
+ type = "shell"
691
+ command = "acstack-herdr-tab diffpick acstack-diff pick"
692
+ description = "diff: pick what to compare (tab)"
693
+
694
+ [[keys.command]]
695
+ key = "alt+i"
696
+ type = "popup"
697
+ command = "/home/you/.local/bin/acstack-popup acstack-dash"
698
+ description = "PR dashboard"
699
+ width = "95%"
700
+ height = "95%"
701
+
702
+ [[keys.command]]
703
+ key = "alt+shift+i"
704
+ type = "shell"
705
+ command = "acstack-herdr-tab dash acstack-dash"
706
+ description = "PR dashboard (tab)"
707
+
708
+ [[keys.command]]
709
+ key = "alt+y"
710
+ type = "popup"
711
+ command = "/home/you/.local/bin/acstack-popup yazi"
712
+ description = "file tree"
713
+ width = "95%"
714
+ height = "95%"
715
+
716
+ [[keys.command]]
717
+ key = "alt+shift+y"
718
+ type = "shell"
719
+ command = "acstack-herdr-tab yazi yazi"
720
+ description = "file tree (tab)"
721
+
722
+ [worktrees]
723
+ directory = "~/.herdr/worktrees"
724
+
725
+ [ui]
726
+ # A pane's scrollbar costs the pane a column that herdr does not subtract.
727
+ pane_scrollbars = false
728
+ show_agent_labels_on_pane_borders = true
729
+ ```
730
+
731
+ - A `shell` command carries no `width` or `height`
732
+ - herdr rejects both on anything that is not a popup
733
+
734
+ Four settings that are not keys sit alongside the block above. Each one is
735
+ written only when the config names it, so a machine that says nothing about a
736
+ setting keeps whatever herdr already does.
737
+
738
+ | Config key | Writes | Default |
739
+ | --- | --- | --- |
740
+ | `herdr.agent_labels_on_pane_borders` | `show_agent_labels_on_pane_borders` in `[ui]` | `true`, and written either way |
741
+ | `herdr.agent_panel_sort` | `agent_panel_sort` in `[ui]` | empty, so nothing is written |
742
+ | `herdr.toast_delivery` | `delivery` in `[ui.toast]` | empty, so nothing is written |
743
+ | `herdr.theme.name` and `herdr.theme.light` | `[theme]` | empty, so nothing is written |
744
+
745
+ **One server, two devices, two backgrounds.**
746
+
747
+ - `auto_switch` asks each attached client for its background colour over OSC 11, an escape sequence a terminal answers with the colour it is painting
748
+ - herdr then picks `dark_name` or `light_name` from the answer, per client
749
+ - A dark terminal on one device and a light one on another each get the right theme
750
+ - An e-ink panel is the case that makes this worth having
751
+ - A client that answers nothing falls back to `dark_name`
752
+ - So `apply` writes `herdr.theme.name` into both `name` and `dark_name`
753
+ - `herdr.theme.name` on its own writes the name and nothing else
754
+ - With no light theme, nothing has anything to switch to
755
+
756
+ Three choices worth explaining.
757
+
758
+ **Plain `alt` chords, not `ctrl+alt`.**
759
+
760
+ - herdr's own keyboard page recommends `ctrl+alt`
761
+ - It is free across most terminals
762
+ - Section 4.3 has the measurements showing it dies under `herdr --remote`
763
+ - Follow herdr's advice only if you attach exactly one way and have tested it
764
+
765
+ **Absolute `worktrees.directory`.**
766
+
767
+ - A relative value resolves against the herdr session's base directory, not the repository you point at
768
+ - Worktrees for a second repository then land inside the first
769
+ - Use an absolute path unless you only ever work in one repository
770
+
771
+ **`pane_scrollbars = false`, so a full-screen app gets the whole pane.**
772
+
773
+ - herdr draws a border and a scrollbar inside the pane rect, then reports the full rect width to the program in the pane
774
+ - A program that fills the pane then writes one column more than it has
775
+ - Every wrapped row starts one column left of the row above it
776
+ - Measured on one machine: `herdr pane layout` reported a 132-column rect at `x=26`, the PTY and neovim's `&columns` both reported 131, and 130 were usable
777
+ - The arithmetic is herdr's, so it should hit anyone running `pane_scrollbars = true`, and that is unproven on a second machine
778
+ - `alt+s` twice repairs the display, because toggling the sidebar recomputes the geometry from scratch
779
+ - The cost is that no pane draws a scrollbar
780
+ - Scrolling still works, because the mouse wheel and copy mode never used it
781
+ - Set `herdr.pane_scrollbars: true` in the workspace config to take the scrollbar back
782
+
783
+ ### 5.3. When the sidebar numbers stop matching
784
+
785
+ *Creating a worktree moves the sidebar rows and not the stored order. One helper realigns them.*
786
+
787
+ - Collapse the sidebar with `alt+s` and each workspace shows a number
788
+ - That number is its position in herdr's stored list
789
+ - The `prefix+N` chord uses the row's position in the grouped sidebar instead, where a worktree sits indented under its parent repository
790
+ - They agree until you touch a worktree
791
+ - A new one appends to the end of the stored list but appears mid-sidebar under its parent
792
+ - Everything below it shifts
793
+ - The number you read stops being the number you press
794
+
795
+ ```bash
796
+ acstack-herdr-renumber
797
+ ```
798
+
799
+ - That rewrites the stored order to match the grouped order
800
+ - Both agree again
801
+ - It prints the result and is safe to run repeatedly
802
+
803
+ You rarely have to run it.
804
+
805
+ - `apply` links a small herdr plugin, `acstack.herdr-numbers`, that runs the same helper on every event which can move a row
806
+ - Run the helper by hand when you configured herdr yourself, or when `apply` reported that the plugin link failed
807
+ - The `herdr.renumber_plugin: false` key turns the plugin off
808
+ - The `acstack-herdr-` prefix is the convention for a helper that only works under herdr
809
+ - A plain `acstack-` helper such as `acstack-diff` or `acstack-md` runs in any terminal
810
+ - The name tells you up front what a command depends on
811
+ - Both forms are reachable through the CLI, which dispatches an unknown subcommand to `acstack-<name>` on your PATH
812
+
813
+ Two things the helper cannot fix.
814
+
815
+ - Only rows 1 to 9 are reachable
816
+ - A tenth workspace needs `prefix+t` or `alt+up` and `alt+down`
817
+ - `prefix+N` stays positional
818
+ - It names a row rather than a workspace and still moves when the rows move
819
+
820
+ ## 6. Freeing your emulator's keys
821
+
822
+ *You bind the keymap on the dev machine, but the emulator on the machine you type at intercepts the chords. Fix that where the emulator runs.*
823
+
824
+ - Windows Terminal binds `alt+shift+d` to "duplicate pane" by default
825
+ - It never forwards the key
826
+ - A binding on it does nothing at all on the dev machine and the local tab splits instead
827
+ - That is why both diff keys sit on `f`: `alt+f` and `alt+shift+f`, and not `alt+d`
828
+ - Other emulators claim other chords
829
+ - This is true on both attach paths
830
+ - The emulator sees the key first either way
831
+
832
+ To confirm an emulator is eating a chord rather than herdr ignoring it, run `cat -v` in any pane and press the key.
833
+
834
+ - A chord that arrives prints an escape sequence such as `^[Z`
835
+ - One that prints nothing never left your machine
836
+ - [assets/terminal-workspace/client-keybindings.md](../assets/terminal-workspace/client-keybindings.md) lists every chord this setup uses and how to unbind them in the common emulators
837
+ - Follow it yourself, or hand it to an agent running on that machine
838
+ - Never try to edit a client-side emulator config from the dev machine
839
+ - Never paste your local config onto the dev machine so that something there can rewrite it
840
+
841
+ ## 7. Reading code, markdown, and diagrams
842
+
843
+ *A file tree with live preview. Markdown that draws its mermaid diagrams as text.*
844
+
845
+ ### 7.1. The file tree
846
+
847
+ *yazi opens over your layout on `alt+y`, and every review popup works the same way.*
848
+
849
+ - The file tree is yazi, a keyboard-driven file manager that previews whatever the cursor sits on
850
+ - Press `alt+y` for yazi in a popup, which floats and leaves the tab layout alone
851
+ - Press `alt+shift+y` for yazi in a new tab instead
852
+
853
+ Every popup works this way: `alt+f` and `alt+shift+f` for diffnav, `alt+i` and `alt+shift+i` for gh-dash.
854
+
855
+ - A popup is right for a glance
856
+ - A tab is right for something you will come back to
857
+ - Both come from the same command, and the tab variant goes through `acstack-herdr-tab`
858
+ - That helper creates the tab in the workspace and directory you pressed the key in
859
+ - Then it sends the command with `herdr pane run`
860
+ - Outside herdr it simply runs the command
861
+ - This setup leaves `alt+t` alone on purpose
862
+ - It is Claude Code's thinking toggle
863
+ - The one collision to know about is your terminal emulator claiming the same chords
864
+ - You fix that where the emulator runs, per section 6
865
+ - yazi previews each file type with its own command
866
+ - It routes markdown to `acstack-md`
867
+ - A document therefore renders **as the cursor moves over it** rather than needing a keypress
868
+ - The `Enter` key opens the same renderer full width, where more of a wide diagram fits than the preview pane allows
869
+ - The `~` and `F1` keys both open yazi's help
870
+ - Icons come from a Nerd Font, and without one every icon draws as an empty box
871
+ - Install any Nerd Font and select it in your terminal
872
+ - tuicr was the file tree before yazi
873
+ - yazi then took that job, and tuicr kept the one it is better at, reading diffs and reviewing pull requests
874
+ - It gets that work from gh-dash on the `D` key, which runs `tuicr pr <number>` in the local clone
875
+ - The command `tuicr --file .` still browses a tree if you want it
876
+
877
+ ### 7.2. Reading markdown and mermaid
878
+
879
+ *`acstack-md` draws a file with its diagrams, and two keys inside the pager change what you are looking at.*
880
+
881
+ ```bash
882
+ acstack-md NOTES.md # terminal, diagrams drawn as text
883
+ acstack-md --numbered NOTES.md # the source instead, with its line numbers
884
+ acstack-md --diagram 2 NOTES.md # one diagram alone, scrollable sideways
885
+ acstack-md --serve NOTES.md # browser, prints a clickable link
886
+ acstack-md --browser NOTES.md # the browser you are sitting at, wherever that is
887
+ acstack-md --html NOTES.md # that same page, as one document on stdout
888
+ ```
889
+
890
+ `--numbered` answers the one question a rendering cannot, which is what line of the file this is.
891
+
892
+ - A review comment names a line number, and a rendered heading has none
893
+ - `--numbered` therefore prints the source with a right-aligned gutter
894
+ - The gutter is as wide as the largest number in it
895
+ - The source stays on one column however long the file
896
+ - It pages through `less -S`, which chops rather than wraps
897
+ - The arrow keys pan across a long line
898
+ - The gutter keeps its column
899
+
900
+ Two keys work while you are reading.
901
+
902
+ - The `b` key opens the page in the browser you are sitting at and returns you to where you came from
903
+ - It uses the routes in section 7.4
904
+ - It is the same key whether you attached over `herdr --remote`, over SSH, or locally
905
+ - The `#` key switches the document between the rendered view and its source with line numbers, and switches it back
906
+ - That is the moment you usually want a line number
907
+ - You do not have to quit back to the tree
908
+
909
+ Both keys move.
910
+
911
+ - `ACSTACK_MD_LINE_NUMBERS_KEY` moves `#`, and `ACSTACK_MD_BROWSER_KEY` moves `b`
912
+ - In `less`, `b` is back-a-page
913
+ - The binding costs you nothing, and both `Ctrl-B` and `PageUp` still do that
914
+ - Both bindings need `less` 582 or newer, and an older version leaves the two keys alone
915
+ - Section 11.2 explains how they reach a running pager
916
+
917
+ A wide diagram cannot fit a narrow pane.
918
+
919
+ - `acstack-md` replaces anything wider than the terminal with a note
920
+ - The note gives its size and the two ways to see it
921
+ - The `--diagram N` flag prints that one diagram unwrapped through `less -S`, where the arrow keys scroll sideways
922
+ - `ACSTACK_MD_PAD` tunes the spacing passed to `mermaid-ascii`, defaulting to `-x 2 -y 2`
923
+ - Tighter padding buys roughly a third of the height back and very little width
924
+ - Section 11.3 explains why the note exists at all
925
+ - Terminal mode replaces each mermaid fence with a box-drawing rendering
926
+ - `mermaid-ascii` handles `graph`, `flowchart`, and `sequenceDiagram`
927
+ - Anything else keeps its source visible with a note rather than disappearing
928
+
929
+ Two things `acstack-md` normalises first, because `mermaid-ascii` handles
930
+ neither:
931
+
932
+ - `style`, `classDef`, `class`, `linkStyle`, and `click` lines, which it would
933
+ otherwise draw as if each were a node
934
+
935
+ - node shapes other than `[square]` – `{decision}`, `((circle))`, `([stadium])`,
936
+ `[(database)]`, `{{hexagon}}` – whose syntax would otherwise leak into the label
937
+
938
+ Serve mode renders the file with python-markdown and draws diagrams with a **locally vendored** mermaid.js, so the page fetches nothing from a CDN.
939
+
940
+ - It binds `127.0.0.1` only
941
+ - It re-reads the file on every request
942
+ - It prints an OSC 8 hyperlink, the escape sequence that marks a piece of text as a link the terminal draws itself
943
+ - herdr then rebuilds that link into the frame it sends the client
944
+ - The bare URL prints underneath for a terminal without OSC 8
945
+ - So ctrl+click opens the page in the browser on your own machine, as long as your SSH config forwards the port from there
946
+ - The `acstack-md --serve` command takes that port from `$ACSTACK_MD_PORT` and falls back to 6419
947
+
948
+ Only one server can hold that port at a time, and a second `--serve` names the process holding it and the command to stop it:
949
+
950
+ ```
951
+ port 6419 is busy: [Errno 98] Address already in use
952
+ held by pid 680666: acstack-md-serve - /path/to/NOTES.md 6419 serve
953
+ stop it with: kill 680666
954
+ ```
955
+
956
+ - Use that pid rather than `pkill -f acstack-md-serve`, which also matches any shell whose own command line mentions the name, including the one you type it in
957
+
958
+ ### 7.3. Reading markdown from the file tree
959
+
960
+ *Move the cursor onto a markdown file and yazi previews it rendered. `Enter` reads it full width.*
961
+
962
+ The file tree draws markdown twice over.
963
+
964
+ - Moving the cursor onto a `.md` file renders it straight into the preview pane, through the piper plugin running `acstack-md --preview`
965
+ - The pane is narrow
966
+ - A wide diagram shows a placeholder there rather than a chopped drawing
967
+ - `Enter` on the same file opens `acstack-md` full width, where the diagrams fit
968
+ - A rule in yazi's `[opener]` config table puts that ahead of the editor
969
+ - Reading is the default
970
+ - Press `O` to choose from the same menu instead, which offers **Read (acstack-md)**, **Read with line numbers**, then **Edit**
971
+ - Nothing shims `$EDITOR`
972
+ - Nothing changes your shell environment
973
+ - `b` on a file hands it to the browser you are sitting at, by the routes in section 7.4
974
+ - The key is free in yazi's file list
975
+ - Its only default binding is a word motion while you are typing into a prompt, which this does not touch
976
+ - Move it with `yazi.browser_key`
977
+ - `#` switches the preview pane between the rendered markdown and the source with line numbers, and switches it back
978
+ - Press it when you are about to quote a line in a review
979
+ - The pane redraws as `acstack-md --numbered` would print it, numbers down the left
980
+ - Every later `.md` file you move onto previews the same way until you press `#` again
981
+ - The key is unbound in yazi's file list, and `yazi.line_numbers_key` moves it
982
+ - The same key does the same job one level in, while you are reading a document full width after `Enter`
983
+ - That is the more common moment to want it
984
+ - The two are deliberately the same key
985
+ - There is nothing to remember about which of the two places you are standing in
986
+ - Section 7.2 describes the reader's half
987
+ - It is a binding in `lesskey`, the key-binding config file for the `less` pager, rather than a yazi one
988
+ - So `yazi.line_numbers_key` does not move it
989
+ - The switch is a file on disk, `$XDG_STATE_HOME/acstack/md-line-numbers`
990
+ - `acstack-md --toggle-line-numbers` creates and removes it
991
+ - That is what the key runs
992
+ - Only the preview pane reads it (see section 11.5)
993
+ - `setup.sh apply` reads a `yazi.toml` you wrote first and leaves alone anything you have set
994
+ - Add `acstack-md` to your own `[opener]` table to read markdown with it
995
+ - Add `show_hidden = true` to your own `mgr` table if you want hidden files shown
996
+ - Section 11.4 says what it concedes and why
997
+ - `D` sends the file under the cursor to the machine you sit at, over Tailscale Taildrop
998
+ - Taildrop is Tailscale's file send
999
+ - It beats `scp` back to a laptop, because it needs nothing installed at the other end and no inbound port there
1000
+ - The file lands in that machine's Tailscale receive folder
1001
+ - The key appears only when `yazi.download_target` names a Tailscale node, and `yazi.download_key` moves it
1002
+ - Taildrop sends only between devices one Tailscale account owns on one tailnet
1003
+ - On this machine `tailscale status` may report the target as `offline`
1004
+ - That means you signed the other machine in to another tailnet, whatever its own Tailscale client shows
1005
+ - A `502 Bad Gateway` from the send means the same thing
1006
+ - Read `tailscale status` on both ends and compare the account column, because two nodes can share a hostname across two tailnets
1007
+ - One step has no config key, because it is machine-level
1008
+ - `tailscale file cp` answers a non-root caller with `Access denied: file access denied` until the account owns the daemon
1009
+ - Run `sudo tailscale set --operator=$USER` once
1010
+
1011
+ Four keys and a cursor move cover the loop.
1012
+
1013
+ 1. Press `alt+y` for the tree.
1014
+ 2. Move the cursor onto a markdown file to preview it.
1015
+ 3. Press `#` to see its source with line numbers.
1016
+ 4. Press `Enter` to read it full width with its diagrams drawn.
1017
+ 5. Press `q` to go back to the tree.
1018
+
1019
+ ### 7.4. Getting the page to the browser you are sitting at
1020
+
1021
+ *`--browser` works out where your browser is and picks a delivery that reaches it.*
1022
+
1023
+ - `--browser` is the one to reach for
1024
+ - The same key works however you attached
1025
+ - It asks `acstack-open --why` where the browser is rather than deciding that a second time
1026
+
1027
+ | Where the browser is | What `--browser` does |
1028
+ | --- | --- |
1029
+ | Behind the opener on your laptop | Posts the whole document to it. The opener stores it, serves it, and opens your default browser |
1030
+ | A desktop on this machine, or the Windows Subsystem for Linux (WSL) | Serves it and opens it |
1031
+ | The far end of the Playwriter bridge, with no opener | Hands the whole document down the Chrome DevTools Protocol (CDP) link, into the tab the extension is armed on |
1032
+ | Anywhere else, over SSH | Serves it and prints a clickable link |
1033
+
1034
+ The [Playwriter bridge](../skills/bridge/SKILL.md) is the setup that lets a program on the dev machine drive Chrome on your laptop.
1035
+
1036
+ - It works one tab at a time
1037
+ - Arming means clicking the extension's icon on the tab you are willing to hand over
1038
+
1039
+ The opener is the route you want.
1040
+
1041
+ - It is a small service on your laptop that takes a page and opens it in your default browser
1042
+ - Section 10.5 covers how it gets installed
1043
+ - There is nothing to arm and no extension
1044
+ - The browser it reaches is your default one rather than a dedicated Chrome profile
1045
+ - Read one document after another and each simply appears
1046
+ - Re-render a file you are already looking at, and the tab you have open updates in place, scroll position kept
1047
+ - No second tab joins the first
1048
+
1049
+ The page itself decides that last part, rather than the opener remembering it.
1050
+
1051
+ - Every page the opener serves polls it for its own version
1052
+ - A tab that is still open says so by asking
1053
+ - Re-render that file and the opener sees a live tab and lets it reload itself
1054
+ - Close the tab and the asking stops
1055
+ - The next render opens a fresh one
1056
+ - Remembering that it once opened something would get the closed-tab case wrong every time
1057
+
1058
+ The bridge is the fallback, and it is the one that needs explaining.
1059
+
1060
+ - Under `herdr --remote` the reverse SSH tunnel to your laptop runs the *other way*
1061
+ - Nothing on the laptop can open a port on the dev machine
1062
+ - A link to `localhost:6419` names the laptop's own localhost, where nothing is listening
1063
+ - So there is no link to hand over, only a document
1064
+ - That is what `--html` is for
1065
+ - `--html` prints the page `--serve` would have served, once, to stdout, and starts nothing
1066
+ - A document you can capture in a variable travels
1067
+ - A port does not
1068
+ - The `--browser` flag is `--html` plus the delivery
1069
+ - The two share one renderer
1070
+ - The page cannot differ between them
1071
+ - They differ in exactly one place, which section 11.6 covers
1072
+ - On the bridge the page replaces what is in the armed tab
1073
+ - The helper opens no new tab
1074
+ - Arming the extension is how you nominate the tab this may take over
1075
+ - That is the tab it takes over
1076
+ - `--browser` says so and stops rather than falling back to serving, when the bridge is the route and the push fails
1077
+ - A link the laptop resolves to its own localhost is a wrong answer dressed as a working one
1078
+
1079
+ We measured one delivery rather than estimating it.
1080
+
1081
+ - This document, 39KB of markdown, renders to 50KB of HTML in under 200ms
1082
+ - It reaches a laptop browser, diagram drawn, in about two seconds
1083
+
1084
+ ### 7.5. Why text and not inline images
1085
+
1086
+ *No terminal you can reach from Windows or Android draws the graphics herdr emits.*
1087
+
1088
+ - herdr embeds libghostty and emits the **kitty graphics protocol**, one of the two ways a terminal draws an inline image
1089
+ - It emits no sixel, the other way
1090
+ - Windows Terminal renders sixel and has never supported kitty, and [microsoft/terminal#8389](https://github.com/microsoft/terminal/issues/8389) is still open
1091
+ - No Google Play terminal supports either protocol
1092
+ - The intersection is empty
1093
+ - An inline image never arrives no matter which terminal you pick
1094
+ - Text also suits e-ink, where the fast refresh modes an interactive terminal needs are the ones that discard the greyscale depth an image needs
1095
+ - `chafa` is worth adding by hand, with `apt install chafa`, if you want images drawn as text
1096
+ - It ships source-only so the setup script does not install it
1097
+
1098
+ ## 8. Harlequin: a SQL editor on one key
1099
+
1100
+ *`alt+q` opens harlequin on a connection profile. `alt+shift+q` opens it in a tab.*
1101
+
1102
+ - Query on the left, rows on the right, schema tree beside both
1103
+ - Profiles live in `~/.config/acstack/harlequin.toml`, which `acstack-db` writes once and never touches again
1104
+ - `acstack-db` with no argument launches the only profile, or opens an fzf menu when there is more than one
1105
+ - `acstack-db <profile>` skips the menu
1106
+
1107
+ ### 8.1. Why uv, and not a release binary
1108
+
1109
+ - harlequin is a python program, so there is no static binary to download
1110
+ - `uv tool install harlequin --with harlequin-postgres --with harlequin-snowflake` builds it an environment of its own and links one command into `~/.local/bin`
1111
+ - A machine without `uv` skips the component, and `apply` names the one command that installs uv
1112
+ - Adding an adapter to `harlequin.adapters` and re-running `apply` reinstalls with the new set
1113
+ - `uv tool install` on an already-installed tool does nothing, so `apply` compares the config against uv's own receipt and passes `--force` when they differ
1114
+
1115
+ ### 8.2. Postgres profiles
1116
+
1117
+ - Keep the password out of the profile file: write the line `host:5432:db:user:password` in `~/.pgpass`, `chmod 600` it, and leave the password out of `conn_str`
1118
+ - libpq reads that file on every connection, so harlequin never handles the secret
1119
+ - Put `read_only = true` on any production profile, which harlequin hands to the postgres adapter so every connection on that profile refuses a write
1120
+
1121
+ ```toml
1122
+ [profiles.prod]
1123
+ adapter = "postgres"
1124
+ conn_str = "postgresql://postgres@db.example.supabase.co:5432/postgres"
1125
+ read_only = true
1126
+ ```
1127
+
1128
+ ### 8.3. Connecting to Snowflake
1129
+
1130
+ - Put the credentials in `~/.snowflake/connections.toml` and name the connection from the profile, rather than writing a connection string into the acstack file
1131
+ - `externalbrowser` authentication does not work on a dev machine, because there is no browser for it to open
1132
+ - Use a key pair, or a password
1133
+ - `--read-only` exists for postgres and not for snowflake
1134
+
1135
+ ### 8.4. Keys inside harlequin
1136
+
1137
+ *A function key jumps straight to a pane. `tab` cycles them.*
1138
+
1139
+ | Key | Does |
1140
+ |---|---|
1141
+ | `f2` | the editor |
1142
+ | `f5` | the results |
1143
+ | `f6` | the catalog |
1144
+ | `tab` / `shift+tab` | next / previous pane |
1145
+ | `ctrl+enter` or `ctrl+j` | run the query |
1146
+ | `f4` | format the query |
1147
+ | `f8` | query history |
1148
+ | `ctrl+e` | export the results |
1149
+ | `f9` or `ctrl+b` | hide the catalog sidebar |
1150
+ | `f10` | full-screen the focused pane, and again to restore it |
1151
+ | `ctrl+p` | the command palette |
1152
+ | `f1` | every key |
1153
+ | `ctrl+q` | quit |
1154
+
1155
+ - `tab` cycles the panes from outside the editor, and inside it `tab` indents
1156
+ - In the catalog, the schema tree, `space` expands the node under the cursor and `ctrl+enter` puts its name in the editor at the cursor
1157
+ - Plain `enter` selects the node and leaves the editor alone
1158
+ - The footer writes `ctrl+q` as `^q`, and `f1` opens the full list
1159
+ - Custom keys go in a `[[keymaps.<name>]]` block in the profile file, named from `keymap_name` on a profile, which harlequin's own documentation covers
1160
+
1161
+ ## 9. Diffs and pull requests
1162
+
1163
+ *Diffs with a file tree, pull request triage with saved searches, and the two helpers that make one key always show something useful.*
1164
+
1165
+ ### 9.1. Five git settings that route every diff through delta
1166
+
1167
+ *One pager for `git diff`, `git show`, and gh-dash alike.*
1168
+
1169
+ ```bash
1170
+ git config --global core.pager delta
1171
+ git config --global interactive.diffFilter "delta --color-only"
1172
+ git config --global delta.navigate true
1173
+ git config --global delta.line-numbers true
1174
+ git config --global merge.conflictstyle zdiff3
1175
+ ```
1176
+
1177
+ - This does not change what agents see
1178
+ - The git command only pages to a terminal
1179
+ - A caller that pipes or captures the output still gets plain text
1180
+
1181
+ ### 9.2. Saved searches become dashboard tabs
1182
+
1183
+ *Any GitHub search string is a section, and two keys hand a pull request to tuicr or to an agent.*
1184
+
1185
+ `~/.config/gh-dash/config.yml`.
1186
+
1187
+ ```yaml
1188
+ prSections:
1189
+ - title: Mine
1190
+ filters: is:open author:@me
1191
+ - title: Needs review
1192
+ filters: is:open review-requested:@me
1193
+ - title: Failing
1194
+ filters: is:open author:@me status:failure
1195
+
1196
+ issuesSections:
1197
+ - title: Mine
1198
+ filters: is:open author:@me
1199
+ - title: Assigned
1200
+ filters: is:open assignee:@me
1201
+
1202
+ # Local clones. Required for checkout and for {{.RepoPath}} in keybindings.
1203
+ repoPaths:
1204
+ <you>/<repo>: ~/code/<repo>
1205
+
1206
+ keybindings:
1207
+ prs:
1208
+ - key: D
1209
+ name: review (tuicr)
1210
+ command: >
1211
+ cd {{.RepoPath}} && tuicr pr {{.PrNumber}}
1212
+ - key: S
1213
+ name: agent review
1214
+ command: >
1215
+ cd {{.RepoPath}} && claude "/code-review {{.PrNumber}}"
1216
+ ```
1217
+
1218
+ - Anything you can type into GitHub's search box becomes a tab
1219
+ - Avoid binding `R`, the built-in refresh-all
1220
+
1221
+ ### 9.3. The diff and dashboard helpers
1222
+
1223
+ *One key shows what your branch adds to dev. Another picks any comparison. A third opens the dashboard, scoped to where you are standing.*
1224
+
1225
+ - `alt+f` runs `acstack-diff`, which compares the branch you are on against `origin/dev`, committed work only
1226
+ - A repository with no dev branch falls back to the default branch, read from `origin/HEAD`
1227
+ - The comparison is `git diff <base>...HEAD`, three dots
1228
+ - You see the commits your branch added since the two diverged, and never the ones the base picked up meanwhile
1229
+ - A pane often sits in `<root>/<repo>/`, the parent directory herdr groups a repository's worktree workspaces under, rather than in a checkout
1230
+ - The helper then offers a numbered list of the checkouts it finds there
1231
+
1232
+ Every launch opens with a banner naming both sides, because diffnav renders whatever precedes the first `diff --git` line:
1233
+
1234
+ ```
1235
+ COMPARING origin/dev ==> deployment-map
1236
+ base origin/dev a84216b 16 hours ago Merge pull request #44 from
1237
+ compare deployment-map 549e94b 68 minutes ago chore: integration edits
1238
+ showing commits on deployment-map that origin/dev does not have
1239
+ command git diff origin/dev...HEAD
1240
+ ```
1241
+
1242
+ - Base first, then compare
1243
+ - That is the order `git diff` takes its arguments
1244
+ - It is also the order GitHub labels its two compare pickers
1245
+ - The `command` line is real git syntax
1246
+ - You can paste it into a shell
1247
+ - The comparison also reaches diffnav's status bar, which stays visible while you walk the file tree
1248
+ - That works because diffnav prints the line following a `commit <sha>` header as the commit subject
1249
+ - The helper synthesises that header for a plain diff
1250
+ - A single-commit view already carries git's own header and gets no synthetic one
1251
+ - No banner line may start with a space, because diffnav strips leading whitespace
1252
+ - Any indentation collapses and the columns stop lining up
1253
+
1254
+ #### A file diffnav cannot draw
1255
+
1256
+ *One committed source map used to abort the whole diff. The helper drops that file and names it.*
1257
+
1258
+ diffnav hands each file to delta, and delta dies on `SIGABRT` when one line runs to hundreds of kilobytes. diffnav reports its child's death and quits:
1259
+
1260
+ ```
1261
+ Error: signal: aborted (core dumped)
1262
+ 2026/08/24 02:04:25 FATA signal: aborted (core dumped)
1263
+ ```
1264
+
1265
+ - A committed build artifact is what does it
1266
+ - ACStack's own `cli/dist/index.js.map` carries a single 982,428-character line
1267
+ - `alt+f` on `main` aborted in 1.7 seconds and showed none of the other 51 files
1268
+
1269
+ So the file goes, and not the diff. The helper drops any file whose patch holds a line over 20,000 characters, and names it in the banner:
1270
+
1271
+ ```
1272
+ skipped cli/dist/index.js.map (one line is 982428 chars, and diffnav aborts on it)
1273
+ ```
1274
+
1275
+ - 20,000 is far above any line a person writes and far below where delta gives out
1276
+ - `ACSTACK_DIFF_LINE_LIMIT` changes it
1277
+ - The helper refuses anything that is not a positive whole number
1278
+ - awk compares each line length against that value
1279
+ - A value like `abc` makes every comparison a string comparison
1280
+ - A string comparison is never true here
1281
+ - A typo would otherwise switch the guard off and bring the abort back, with nothing on screen to explain it
1282
+ - Where the helper drops every changed file, it says so
1283
+ - The old code said "No difference between these two"
1284
+ - That claimed nothing had changed in a repository that had
1285
+
1286
+ #### Picking a different comparison
1287
+
1288
+ *`alt+x` opens an fzf menu of seven comparisons. Every row reads left to right as base, then compare.*
1289
+
1290
+ ```
1291
+ dev ==> my-branch committed work only
1292
+ dev ==> my-branch + uncommitted committed work plus what is not committed
1293
+ main ==> my-branch committed work only
1294
+ main ==> my-branch + uncommitted committed work plus what is not committed
1295
+ HEAD ==> my uncommitted changes staged and unstaged changes only
1296
+ its parent ==> one commit pick a commit from this branch's history
1297
+ any branch ==> any branch pick the base, then the branch to compare
1298
+ ```
1299
+
1300
+ - The dev rows appear only where a dev branch exists, and the branch named is the one you are on
1301
+ - Branch lists come from `git for-each-ref` over local and remote branches, newest commit first, with `git log` in the preview pane
1302
+ - The commit list is the last 300 commits, with `git show --stat` in the preview
1303
+ - Picking "any branch" asks twice, prompting `source (compare against)` and then `change (the new work)`
1304
+ - `apply` installs fzf alongside diffnav, and without fzf the picker says so and falls back to the automatic diff
1305
+
1306
+ `acstack-dash` adds a section for whichever repository you are standing in, then hands a generated config to gh-dash:
1307
+
1308
+ ```bash
1309
+ #!/usr/bin/env bash
1310
+ set -uo pipefail
1311
+ BASE="$HOME/.config/gh-dash/config.yml"
1312
+ REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null)"
1313
+ [ -z "$REPO" ] && exec gh dash "$@"
1314
+ GEN="$(mktemp)"; trap 'rm -f "$GEN"' EXIT
1315
+ REPO="$REPO" python3 - "$BASE" "$GEN" <<'PY'
1316
+ import os, sys, yaml
1317
+ cfg = yaml.safe_load(open(sys.argv[1])) or {}
1318
+ repo = os.environ["REPO"]; short = repo.split("/")[-1]
1319
+ s = [x for x in cfg.get("prSections", []) if x.get("title") != short]
1320
+ s.insert(0, {"title": short, "filters": f"repo:{repo} is:open"})
1321
+ cfg["prSections"] = s
1322
+ yaml.safe_dump(cfg, open(sys.argv[2], "w"), sort_keys=False)
1323
+ PY
1324
+ gh dash --config "$GEN" "$@"
1325
+ ```
1326
+
1327
+ - Because a herdr popup inherits the focused pane's directory, `alt+i` from an agent's worktree opens the dashboard already scoped to that repository
1328
+
1329
+ ### 9.4. acstack-gh: why an action failed
1330
+
1331
+ *gh-dash throws gh's stderr away, so a helper on `$PATH` turns a refusal into a notification.*
1332
+
1333
+ - gh-dash shells out to `gh` for everything it does to a pull request, and throws the command's stderr away
1334
+ - GitHub refusing one therefore arrives as `exit status 1` in the footer, for two seconds
1335
+ - Approving your own pull request is the case you meet daily
1336
+ - GitHub always refuses that
1337
+ - The input box closes, nothing else happens
1338
+ - The dashboard looks like it ignored the key
1339
+
1340
+ `acstack-dash` answers that without patching gh-dash.
1341
+
1342
+ - It links `acstack-gh` into a directory of its own at the front of `$PATH`, under the name `gh`
1343
+ - gh-dash therefore finds it before the real one
1344
+ - The real `gh` still does the work and still decides the exit code
1345
+ - The only thing added is a notification carrying gh's own words when a `pr` or `issue` action fails:
1346
+
1347
+ ```
1348
+ gh pr review failed
1349
+ Can not approve your own pull request.
1350
+ ```
1351
+
1352
+ - `acstack-gh` passes `gh dash` itself straight through
1353
+ - It also passes through every subcommand that is not an action, `repo view` and `api` among them
1354
+ - Those fail for reasons a notification cannot help with
1355
+
1356
+ ### 9.5. gh-dash keys
1357
+
1358
+ *Vim movement, `[` and `]` for the preview tabs, and `D` or `S` to hand the pull request on.*
1359
+
1360
+ | Key | Action |
1361
+ |---|---|
1362
+ | `j` / `k` | Move between rows |
1363
+ | `h` / `l` | Previous / next section |
1364
+ | `p` | Toggle the preview pane |
1365
+ | `[` / `]` | Previous / next preview tab: Overview, Activity, Commits, Checks, Files Changed |
1366
+ | `PageDown` / `PageUp` | Scroll the preview |
1367
+ | `ctrl+d` / `ctrl+u` | Scroll the preview, vim style |
1368
+ | `e` | Expand the description |
1369
+ | `d` | Built-in diff |
1370
+ | `D` | Open the pull request in tuicr and review it there |
1371
+ | `S` | Hand the pull request to an agent |
1372
+ | `C` or `space` | Check the branch out locally |
1373
+ | `w` | Watch checks |
1374
+ | `c` / `v` / `m` | Comment / approve / merge |
1375
+ | `y` / `Y` | Copy number / URL |
1376
+ | `r` / `R` | Refresh section / all |
1377
+ | `/` · `?` · `q` | Search · help · quit |
1378
+
1379
+ ### 9.6. diffnav keys
1380
+
1381
+ *Vim movement in the tree, and `Tab` to cross between the tree and the diff.*
1382
+
1383
+ | Key | Action |
1384
+ |---|---|
1385
+ | `j` / `k` | Move in the file tree |
1386
+ | `n` / `N` | Next / previous file |
1387
+ | `ctrl+d` / `ctrl+u` | Scroll the diff half a page |
1388
+ | `ctrl+e` / `ctrl+y` | Scroll the diff one line |
1389
+ | `Tab` | Switch focus between tree and diff |
1390
+ | `t` | Search and jump to a file |
1391
+ | `e` | Toggle the file tree |
1392
+ | `s` | Side-by-side or unified |
1393
+ | `o` | Open the file in Neovim, in a new herdr tab |
1394
+ | `q` | Quit |
1395
+
1396
+ `o` opens a tab rather than taking over the pane, and the reason is the popup:
1397
+
1398
+ - diffnav runs `$EDITOR` inside its own pane, and a herdr popup cannot host a second full-screen app
1399
+ - Neovim never drew in the `alt+f` and `alt+x` popups, so the key looked dead
1400
+ - `acstack-edit` stands in as `$EDITOR` and hands the file to `acstack-herdr-tab`, the same helper the tab keys use
1401
+ - The tab opens in the workspace and directory you pressed the key in
1402
+ - The tab carries the file's name, not its whole path
1403
+ - The popup keeps the diff, so close it to reach the tab
1404
+ - Outside herdr the helper runs your real editor in place
1405
+
1406
+ ## 10. What crosses back to your machine
1407
+
1408
+ *A dev machine has no display and no clipboard of its own. The `acstack-clip` and `acstack-open` helpers carry each one back across the link.*
1409
+
1410
+ Two gaps, and three gh-dash keys fall into them:
1411
+
1412
+ - `o`, open on GitHub, fails with `exit status 1`. gh-dash opens URLs through `$BROWSER`, falling back to `xdg-open`, and `xdg-open` with no `$DISPLAY` exits 1
1413
+ - `y` and `Y`, copy the URL and the number, fail with `Failed copying to clipboard`. The gh-dash tool copies through a Go library that shells out to `xclip`, `xsel`, `wl-copy` or `termux-clipboard-set`. A bare dev machine has none of them installed
1414
+ - An install would not make them work
1415
+ - Neither is a gh-dash bug, because the clipboard and the browser are on the machine you are typing at, several hops away
1416
+ - The two helpers below carry each one back across
1417
+
1418
+ ### 10.1. acstack-clip: the clipboard
1419
+
1420
+ *OSC 52 crosses SSH for free, so a copy on the dev machine lands on your own clipboard.*
1421
+
1422
+ - OSC 52 is the escape sequence that asks a terminal to put text on its own clipboard
1423
+ - It is bytes in the terminal stream
1424
+ - It crosses SSH for free
1425
+ - herdr forwards it from a pane to whatever terminal hosts it
1426
+ - Windows Terminal, iTerm2, kitty, and Ghostty all act on it
1427
+
1428
+ ```bash
1429
+ acstack-clip "some text" # copy the arguments
1430
+ git rev-parse HEAD | acstack-clip
1431
+ acstack-clip --out # print what was copied last
1432
+ ```
1433
+
1434
+ - Reading back is not symmetrical
1435
+ - Windows Terminal refuses OSC 52 clipboard *reads* on purpose
1436
+ - A program that can read your clipboard without asking is a security hole
1437
+ - So `--out` replays a local cache, and not the real clipboard
1438
+ - To reach a program that only knows how to shell out, `setup.sh apply` also writes an `xclip` onto `$PATH` backed by `acstack-clip`
1439
+ - That is what makes gh-dash's `y` and `Y` work unchanged, with no rebinding and no flicker
1440
+ - The `remote.clipboard_shim` key turns it off, and `setup.sh uninstall` removes it
1441
+ - The script skips it anyway on a machine with a real `xclip`, or with a display for one to talk to
1442
+
1443
+ ### 10.2. acstack-open: the browser
1444
+
1445
+ *Seven routes, tried in order, ending in a link you can click from any terminal.*
1446
+
1447
+ - `o` is a gh-dash keybinding rather than a `$BROWSER` setting
1448
+ - That is deliberate
1449
+ - The dashboard is still on screen when gh-dash runs `$BROWSER`
1450
+ - It discards that command's output
1451
+ - A route that needs to say anything, or to hand you a link to click, therefore has nowhere to put it
1452
+ - As a keybinding gh-dash steps aside and gives `acstack-open` the terminal
1453
+
1454
+ It tries, in order:
1455
+
1456
+ 1. `$ACSTACK_OPEN_CMD`, if you set one. The escape hatch
1457
+ 2. `xdg-open`, when this machine has a display after all
1458
+ 3. `wslview` or `explorer.exe`, when the Windows half of the machine holds the browser
1459
+ 4. The opener on your laptop, which puts the page in your default browser with nothing to click. See section 10.5
1460
+ 5. Chrome on your laptop through the [Playwriter bridge](../skills/bridge/SKILL.md), but only after it proves the browser is really reachable that way. See section 11.7
1461
+ 6. A clickable link. The terminal you are sitting at draws the URL as an OSC 8 hyperlink, so ctrl+click reaches your own browser. The URL goes on your clipboard too
1462
+ 7. With no terminal to draw on either, the URL still goes on the clipboard, and the command reports failure
1463
+
1464
+ - The link route needs nothing installed in between
1465
+ - That is why it is the last one that can still work
1466
+ - A terminal that ignores OSC 8 still shows the bare URL, and its own URL detection can catch that
1467
+ - Route 7 reports failure on purpose
1468
+ - Silent success is what left gh-dash claiming it had opened a page that never opened
1469
+
1470
+ The opener sits ahead of the bridge because the two are not competing for the same job.
1471
+
1472
+ - The bridge exists so an *agent* can drive a browser
1473
+ - It attaches one tab at a time, only after somebody clicks the extension icon
1474
+ - It does so in a dedicated Chrome profile
1475
+ - The opener exists so it can show a *person* a page
1476
+ - It needs no click at all
1477
+ - It reaches the browser you actually use
1478
+ - Both can be up at once
1479
+ - Each keeps its own job
1480
+ - `setup.sh status` prints which route a machine will take, and the last line of `~/.cache/acstack/open.log` says what the last press actually did
1481
+
1482
+ ### 10.3. Under `herdr --remote`
1483
+
1484
+ *We wrote both helpers for this shape, and they need no change. The clipboard crosses, and the browser falls to the link route, which is the one built for it.*
1485
+
1486
+ - `herdr --remote <target>` runs the server, and therefore every pane process, on the dev machine
1487
+ - The client is a thin attach
1488
+ - It sends input and draws what the server sends back
1489
+
1490
+ `acstack-clip` works, and we measured that rather than inferring it.
1491
+
1492
+ - A pane's OSC 52 write reaches the clipboard on the machine you attached from
1493
+ - So `acstack-clip "some text"` on the dev machine pastes on your own
1494
+ - Nothing in herdr's own documentation promises this
1495
+ - It mentions OSC 7 and OSC 8, and never OSC 52
1496
+ - We tested it on herdr 0.8.2 and it crossed
1497
+ - Terminals differ in what they act on
1498
+ - Copy something with `acstack-clip` after your first attach and check that it pasted
1499
+ - Clipboard *images* travel the other way, and only on this path
1500
+ - Copy a screenshot on your machine, focus an agent pane, and herdr stages the image on the dev machine and pastes its path
1501
+ - An SSH shell cannot do that at all
1502
+ - That is the single strongest reason to prefer a remote attach
1503
+ - `acstack-open` runs on the dev machine
1504
+ - That is the honest answer for routes 1 to 5
1505
+ - An override, a display, WSL, the opener or a bridge tunnel all have to be reachable *from there*
1506
+ - The opener and the bridge are reachable
1507
+ - The laptop opens reverse tunnels that carry them back
1508
+ - So on a machine set up for either, that is the route it takes
1509
+ - With neither, it falls to the link route, the one built for this
1510
+ - In that route herdr tracks hyperlinks per cell and re-emits them when it renders
1511
+ - The client therefore draws the link rather than shipping raw bytes
1512
+ - ctrl+click opens the browser on the machine you attached from
1513
+ - The link route also degrades further than the others
1514
+ - Even with no OSC 8 and no OSC 52 at all, the URL is on screen as plain text, which drag-select copies
1515
+ - That is why the link is its own text rather than a label over it
1516
+
1517
+ One trap belongs to your SSH config rather than to herdr.
1518
+
1519
+ - The `acstack-md --serve` command prints the URL it is listening on
1520
+ - That is the port *on the dev machine*, meaning `$ACSTACK_MD_PORT` or 6419
1521
+ - The printed link is wrong from where you are sitting, if your host block forwards it to a different local port
1522
+ - You want the local number instead
1523
+ - Forwarding `6419` to `6419` avoids the question entirely
1524
+
1525
+ ### 10.4. On a machine with none of this
1526
+
1527
+ *The link route needs only a terminal. One environment variable buys back a real one-key open.*
1528
+
1529
+ The link route works over SSH, through herdr, and under `herdr --remote`. To get a real one-key open instead, give `acstack-open` something that can do it:
1530
+
1531
+ ```bash
1532
+ export ACSTACK_OPEN_CMD="ssh laptop open" # or any command taking a URL
1533
+ ```
1534
+
1535
+ ### 10.5. The opener: a page in your own browser, with nothing to click
1536
+
1537
+ *A small service on your laptop. It takes a page from the dev machine, stores it, serves it back, and opens your default browser on it.*
1538
+
1539
+ - The dev machine has no browser and no way to reach yours
1540
+ - The bridge solved that for agents, and not for reading
1541
+ - It needs a tab armed by hand before every session
1542
+ - The tab it drives lives in a dedicated Chrome profile rather than your default browser
1543
+ - The opener is the answer for reading
1544
+ - It is a separate service on purpose, per [ADR 0006](adr/0006-document-opener-service.md)
1545
+
1546
+ What it does is deliberately small:
1547
+
1548
+ | The dev machine sends | The opener does |
1549
+ | --- | --- |
1550
+ | A URL | Hands it to your default browser |
1551
+ | A rendered document | Stores it, serves it at `http://127.0.0.1:19989/doc/<id>`, opens that |
1552
+ | A vendored `mermaid.min.js`, once | Keeps it, and answers `/mermaid.js` off it from then on |
1553
+ | A request to restart the relay or the tunnel | Restarts that scheduled task |
1554
+
1555
+ - The relay is the process on your laptop that the Playwriter extension connects to
1556
+ - Restarting it, and restarting the tunnel, are the two recovery actions the dev machine could never perform
1557
+ - They now go through the opener instead of arriving as a block for you to paste into PowerShell
1558
+ - Arming the extension is still yours
1559
+ - It is a click inside a third-party extension and nothing on either machine can press it
1560
+
1561
+ Installing the opener is the same command that registers the bridge, which now registers the opener too:
1562
+
1563
+ ```powershell
1564
+ cd $env:USERPROFILE\playwriter-bridge
1565
+ .\register-tasks.ps1 -VMs @("vm1.example.com")
1566
+ ```
1567
+
1568
+ - That generates a shared secret and stores it at `%LOCALAPPDATA%\playwriter-bridge\opener.token`
1569
+ - It copies the secret to each dev machine at `~/.config/acstack/opener.token`, over the same ssh the tunnel uses
1570
+ - Every request from the dev machine carries it
1571
+ - Loopback binding alone would not be enough
1572
+ - The reverse tunnel makes the port reachable by anything running on the dev machine
1573
+ - This is a service that puts pages on your screen
1574
+
1575
+ The opener rides the same machinery as the bridge.
1576
+
1577
+ - A scheduled task from `register-tasks.ps1` runs it, and a supervisor restarts it
1578
+ - The `sync.ps1` script reconciles the deployment on every Claude Code launch
1579
+ - Your laptop opens a reverse SSH tunnel
1580
+ - The opener gets its **own** tunnel task, `Playwriter-OpenerTunnel-VM<N>`, which carries port 19989
1581
+ - The bridge keeps port 19988 on a task of its own
1582
+ - Both tasks run the same `tunnel.ps1` script
1583
+ - They stay apart because ssh runs with `ExitOnForwardFailure=yes`
1584
+ - One wedged port fails the whole connection
1585
+ - Sharing one connection would let a stuck opener port take the bridge down with it
1586
+ - A document outlives the session that rendered it, which lets a page still work after the dev machine has gone away
1587
+ - The opener prunes a stored document after a week
1588
+
1589
+ ## 11. Design notes
1590
+
1591
+ *Why the parts work the way they do. Read this when you are changing the setup or debugging it, and skip it otherwise.*
1592
+
1593
+ ### 11.1. Why the markdown helpers are separate executables
1594
+
1595
+ *Node's startup would roughly double the cost of a preview that runs on every cursor move.*
1596
+
1597
+ - The `acstack-md` and `acstack-clip` helpers are their own executables rather than subcommands of the `acstack` CLI
1598
+ - The `acstack-md --preview` command runs on every cursor move in the file manager, and Node's startup would roughly double it
1599
+ - The CLI dispatches to them anyway, the way git does
1600
+ - So `acstack md` runs `acstack-md`, either form works
1601
+ - A config can keep the fast one
1602
+
1603
+ ### 11.2. How `b` and `#` reach a running pager
1604
+
1605
+ *A `lesskey` binding whose `quit` action carries an exit status, which `acstack-md` reads and acts on.*
1606
+
1607
+ - The `less` pager has no action that runs a fixed command
1608
+ - Its one shell escape would hand over the rendered temporary copy rather than the file you asked for
1609
+ - So each key quits `less` with an exit status of its own, and `acstack-md` reads that status and does the work
1610
+ - It needs `less` 582 or newer, and an older version ignores the bindings and leaves both keys alone
1611
+ - `setup.sh apply` writes the `#` binding as `\#` in the `lesskey` file
1612
+ - `lesskey` reads a line starting with `#` as a comment
1613
+ - It drops a bare binding without a word
1614
+ - The key then does nothing but ring the terminal bell
1615
+ - Only a leading `#` needs the escape
1616
+ - A backslash means something of its own to `lesskey`, and `\b` would bind backspace rather than the letter
1617
+
1618
+ ### 11.3. Why a wide diagram becomes a note
1619
+
1620
+ *Nothing can shrink a diagram. Wrapping box-drawing art destroys it.*
1621
+
1622
+ - A diagram's width comes from its node labels
1623
+ - Nothing can shrink a wide one into a narrow pane
1624
+ - Wrapping box-drawing art destroys it instead of fitting it
1625
+ - A note that names the size is the honest answer, and a badly drawn diagram is not
1626
+ - A wide diagram still appears in place when it fits
1627
+ - glow wraps whatever it renders
1628
+ - `acstack-md` holds the drawing back
1629
+ - It runs glow on the prose, then splices the full-width art into the output
1630
+ - The pager is `less -S`
1631
+ - glow already wrapped the prose to the pane
1632
+ - Only the diagram lines chop, and the arrow keys pan across them
1633
+
1634
+ ### 11.4. What yazi's config merge concedes
1635
+
1636
+ *Declaring anything twice would make yazi reject the whole file, so `apply` gives up whatever you already set.*
1637
+
1638
+ - `setup.sh apply` reads your `yazi.toml` first and leaves alone anything you have set
1639
+ - That covers your `mgr` settings, your markdown opener, your `plugin.prepend_previewers`, and your `open.prepend_rules`
1640
+ - Whichever of the four it skipped, it says so
1641
+ - Declaring any of them a second time would make yazi reject the whole config and fall back to presets
1642
+ - It concedes them instead
1643
+ - What it cannot read is a `yazi.toml` that does not parse
1644
+ - yazi is then already ignoring the file in favour of presets
1645
+ - Fix the error and re-run `apply`
1646
+ - The `b` and `#` bindings live in `keymap.toml` rather than `yazi.toml`, and `apply` writes them as `[[mgr.prepend_keymap]]`
1647
+ - That spelling is an array of tables
1648
+ - It stacks with a binding you have already written the same way
1649
+ - The one spelling it cannot sit beside is the inline `prepend_keymap = [...]` under `[mgr]`
1650
+ - That is a single key, and TOML forbids declaring it twice
1651
+ - The script detects that form, gives the bindings up rather than cost you the whole keymap, and says so
1652
+ - The `[opener]` templates say `%s` rather than `"$@"`
1653
+ - yazi runs a template as `sh -c '<template>'` with nothing after it
1654
+ - `$0` is `sh` and `$@` is empty
1655
+ - A template written with `"$@"` hands the helper no file at all
1656
+ - `%s` is the placeholder yazi substitutes, already quoted
1657
+ - Measured on yazi 26.8.15
1658
+ - The `keymap.toml` bindings say `%h` for the same reason, and never `%*`
1659
+ - `%h` is the hovered file
1660
+ - yazi quotes `%h` itself, so write it bare. Quotes around it end up inside the path.
1661
+ - `%*` belongs to `[opener]`, and a keybinding never expands it
1662
+ - The two characters go through untouched
1663
+ - The command reports them back as a filename, such as `open %*: no such file or directory`
1664
+ - So a binding that carries a file uses `%h`, and takes one file at a time
1665
+ - The `#` key runs two actions rather than one, flipping the flag, then forcing yazi to draw the pane again
1666
+ - Without the redraw the pane keeps showing whatever it drew before you pressed the key
1667
+ - The switch looks broken until you move the cursor
1668
+ - The flip blocks, though it has nothing to say
1669
+ - A detached one races the redraw and loses about half the time
1670
+
1671
+ One collision belongs to herdr rather than yazi.
1672
+
1673
+ - herdr hosts a `type = "shell"` command in a real pane for as long as the process runs
1674
+ - So `acstack-herdr-tab` creates the tab and then hands the wait to a detached child
1675
+ - It returns in about 100ms rather than three and a half seconds
1676
+ - Without that a stray pane sits in the current tab
1677
+
1678
+ ### 11.5. Why the line-number switch lives in a file
1679
+
1680
+ *The key and the preview pane are two processes that never meet, so the switch has to sit on disk between them.*
1681
+
1682
+ - The state file is `$XDG_STATE_HOME/acstack/md-line-numbers`, or `~/.local/state/acstack/md-line-numbers` when that variable is unset
1683
+ - Its presence is the whole setting
1684
+ - `acstack-md --toggle-line-numbers` only has to create or remove it
1685
+ - Only the preview pane reads that file
1686
+ - Every other route asks for a view by name
1687
+ - The full-width read has **Read with line numbers** as its own entry under `O`
1688
+ - The `--diagram N` flag outranks the file too, since asking for one drawing is a different question from which view the pane is on
1689
+
1690
+ ### 11.6. Why `--html` names a CDN and `--serve` does not
1691
+
1692
+ *A document standing on its own has no server behind it to answer for `/mermaid.js`.*
1693
+
1694
+ - `--serve` answers for `/mermaid.js` off the vendored copy
1695
+ - Its page fetches nothing from a CDN
1696
+ - A document standing on its own has no server behind it
1697
+ - `--html` names the CDN instead
1698
+ - The vendored file is 3.5MB, and inlining it would make the page offline-proof and far too big to hand anywhere
1699
+
1700
+ A document bound for the opener is the third case.
1701
+
1702
+ - Here `--browser` hands it over once, like `--html`
1703
+ - The document does end up behind a server, the opener's
1704
+ - It asks for `/mermaid.js` too
1705
+ - The 3.5MB goes up once, the first time the opener admits it has no copy
1706
+ - Every document after that draws its diagrams without reaching a CDN at all
1707
+
1708
+ ### 11.7. Why the bridge and the opener must prove themselves
1709
+
1710
+ *`agent-browser` launches a headless Chrome when it cannot attach. It then reports success to nobody.*
1711
+
1712
+ - `agent-browser` launches a headless Chrome on the dev machine when it cannot attach to the endpoint you gave it
1713
+ - That Chrome navigates perfectly happily, reports success, and shows nobody anything
1714
+ - The page opens on the dev machine, several hops from the screen you are looking at
1715
+ - Nothing about the relay answering on port 19988 rules that out either
1716
+ - Ours answered `/json/version` while refusing every CDP connection with `Multiple extensions connected. Specify extensionId.`
1717
+ - Every open landed in a headless Chrome for hours without one error message
1718
+ - So the bridge route asks the relay's `/json/list` what is on the far end, and takes the route only when something answers
1719
+ - The Playwriter extension attaches per tab, and `/json/list` is its own answer to that question
1720
+ - An empty `[]` means nobody has armed it on a tab
1721
+ - There is no browser to drive however healthy the tunnel underneath looks
1722
+
1723
+ The opener proves itself the same way and for the same reason.
1724
+
1725
+ - The `acstack-open` helper asks it for `/health`, carrying the shared token, and takes the route only if the opener answers
1726
+ - A token sitting on disk proves nothing about a service being up
1727
+ - A service being up proves nothing without the token it is going to demand
1728
+ - Success on the bridge is likewise not an exit status
1729
+ - The pushed script ends with the page title
1730
+ - The browser answers with what it is now holding
1731
+ - The `--browser` flag only reports success when that answer is the file you asked for
1732
+ - A command that exited 0 is not a page that arrived
1733
+ - The helper opens no new tab either
1734
+ - That is the second thing we measured rather than assumed
1735
+ - CDP creates a tab in the **background**
1736
+ - Nothing on the dev machine can bring it to the front
1737
+ - The document would land in it, the helper would report success
1738
+ - You would never see it
1739
+
1740
+ ## 12. Traps
1741
+
1742
+ *Each of these cost real time to find.*
1743
+
1744
+ - **`ctrl+b` is both herdr's prefix and Claude Code's "background this task"**
1745
+ - Press it twice inside a Claude pane to reach Claude, or rebind herdr's prefix
1746
+ - **Never submit a prompt to a blocked agent**
1747
+ - A blocked agent waits on a permission prompt
1748
+ - Injected text answers that prompt instead of giving an instruction
1749
+ - Wait for idle
1750
+ - **`--cwd` for `herdr worktree create` must be the main checkout**, and never a nested worktree
1751
+ - herdr stores it as the workspace's repository root
1752
+ - It also groups worktree workspaces under it in the sidebar
1753
+ - **Read the created path from the command output**
1754
+ - Never assume where a worktree landed
1755
+ - The configured root decides
1756
+ - **Worktree workspaces nest, and plain workspaces do not**
1757
+ - A workspace made with `alt+w` always sits at the top level, whatever directory it points at
1758
+ - **Sidebar actions act on the selected workspace**, and not on the focused pane
1759
+ - Open the sidebar and select before creating a worktree or closing a workspace
1760
+ - **In-process teammates are invisible to herdr**
1761
+ - A Claude teammate shares its parent's pane and session
1762
+ - It never appears as its own agent
1763
+ - Two agents in one worktree means two real sessions
1764
+ - **gh-dash never says why an action failed**
1765
+ - It discards gh's stderr
1766
+ - A refusal shows as `exit status 1` for two seconds
1767
+ - `acstack-gh`, which `acstack-dash` puts on `$PATH` as `gh`, turns that into a notification quoting gh
1768
+ - Approving your own pull request is the one you will hit
1769
+ - **A dev machine has no clipboard and no browser**
1770
+ - The `o`, `y`, and `Y` keys in gh-dash all fail there until `apply` installs `acstack-clip` and `acstack-open`
1771
+ - The `setup.sh status` command says which browser route a machine ended up with
1772
+ - **Snowflake's browser login cannot work on a dev machine**
1773
+ - `externalbrowser` authentication opens a browser, and that machine has none
1774
+ - Use a key pair or a password, held in `~/.snowflake/connections.toml`
1775
+ - `--read-only` guards a postgres connection only, so a snowflake profile has no such switch
1776
+ - **diffnav aborts on one very long line**
1777
+ - delta dies on `SIGABRT`, and diffnav quits with `FATA signal: aborted (core dumped)`, showing none of the diff
1778
+ - `acstack-diff` drops any file whose patch holds a line over 20,000 characters and names it in the banner
1779
+ - A committed source map is the usual culprit
1780
+ - The file is worth keeping out of git in the first place