acstack 0.0.0-stage → 0.47.0

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