Compare commits

...

9 Commits

Author SHA1 Message Date
tgrosinger 419b4ca6bc Neovim: Fix <C-e> hotkey in Octo buffers 2026-07-06 14:33:08 -07:00
tgrosinger 243b9810c8 Revert "tmux: Extended keys"
This reverts commit 3e521b1b90.
2026-07-06 08:54:29 -07:00
tgrosinger d6bed7a5f5 Neovim: Enable claude-review and disable code-review
I was not really using code-review and the keybindings conflicted with
my new claude-review extension. Claude review is installed from a local
directory.
2026-07-06 08:54:24 -07:00
tgrosinger 5a839c4051 Claude: Remove pr-walkthrough skill
The skill I am using for pr-walkthrough now lives in the claude-review
repo and has been adjusted to output structured json that can be viewed
in my Neovim plugin.
2026-07-06 08:54:24 -07:00
tgrosinger 590c2ce2e7 Pi: Install 2026-07-06 08:54:24 -07:00
tgrosinger a2915b9ffe Neovim: Install updates 2026-07-06 08:54:24 -07:00
tgrosinger e52b60547b Neovim: Show hidden and ignored files in snacks explorer by default 2026-07-06 08:54:24 -07:00
tgrosinger 3e521b1b90 tmux: Extended keys 2026-07-06 08:54:24 -07:00
tgrosinger 71454f4aae Ghostty: Switch from Alacritty 2026-07-06 08:54:24 -07:00
19 changed files with 629 additions and 1844 deletions
@@ -1,3 +0,0 @@
.DS_Store
*-walkthrough.html
!templates/reference-walkthrough.html
@@ -1,88 +0,0 @@
# PR Walkthrough
Retrieved from https://github.com/houshuang/pr-walkthrough
A [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skill that generates beautiful, self-contained HTML walkthroughs of pull requests.
Instead of reading diffs line by line, get a narrative explanation of *what* changed, *why* it was designed that way, and *how* the pieces connect — presented as a polished technical article.
<!-- TODO: Add screenshot -->
![Example walkthrough output](examples/screenshot.png)
## What It Generates
A single self-contained HTML file with:
- **Narrative structure** — Sections build understanding incrementally, from big picture to implementation details
- **Real code excerpts** — Actual code from the PR with syntax highlighting, not pseudocode
- **Architecture diagrams** — Flow diagrams, layer stacks, and sequence diagrams showing how components connect
- **Annotated tradeoffs** — Why this approach was chosen over alternatives
- **Extension recipes** — How to add a similar feature following the same patterns
The design aesthetic is editorial — think Stripe engineering blog post, not GitHub diff view.
## Design System
The walkthrough uses a warm paper theme with an editorial typography stack:
- **Instrument Serif** — Display headings
- **Source Serif 4** — Body text
- **DM Mono** — Code, labels, metadata
Components include flow diagrams, layer stacks, callout boxes (insight/warning/pattern/tradeoff), comparison tables, file trees, and syntax-highlighted code blocks. See `templates/reference-walkthrough.html` for the full component library.
## Install
Copy the skill into your Claude Code skills directory:
```bash
# Clone the repo
git clone https://github.com/houshuang/pr-walkthrough.git
# Copy to your Claude Code skills directory
cp -r pr-walkthrough ~/.claude/skills/pr-walkthrough
```
Or if you prefer to keep it as a symlink:
```bash
ln -s /path/to/pr-walkthrough ~/.claude/skills/pr-walkthrough
```
## Usage
From any git repository with a PR or feature branch checked out:
```
> walk me through this PR
> explain the changes on this branch
> create a walkthrough of the authentication feature
> deep dive into how the new caching layer works
```
The skill will:
1. Research all changed files and git history
2. Plan a narrative structure
3. Generate a self-contained HTML file
4. Open it in your browser
The output file lands in the project root as `{topic}-walkthrough.html`.
## How It Works
The skill instructs Claude Code to follow a structured workflow:
1. **Research** — Read all changed files (not just diffs), trace data flow, understand the full context
2. **Plan** — Structure the walkthrough as a teaching document with sections for architecture, implementation, wiring, tradeoffs, and extension patterns
3. **Write** — Generate a self-contained HTML page using the design system from the reference template
4. **Deliver** — Save and open in the browser
The reference template (`templates/reference-walkthrough.html`) serves as both a design system reference and a content example. Claude Code reads it before generating each walkthrough to match the aesthetic and component patterns.
## Examples
See the `examples/` directory for sample walkthrough outputs.
## License
MIT
-163
View File
@@ -1,163 +0,0 @@
---
name: pr-walkthrough
description: Generate a detailed, pedagogical HTML walkthrough of a PR or feature branch. Use when the user wants to understand, document, or share how a PR works — covering architecture, data flow, design decisions, tradeoffs, and extension patterns. Triggers on requests like "walk me through this PR", "explain this branch", "write up how this feature works", "create a walkthrough".
---
# PR Walkthrough Generator
Generate a self-contained HTML page that deeply explains a PR or feature branch — not just what changed, but *why*, *how it connects*, and *what patterns it establishes* for future work.
## When to Use
- User asks to understand or explain a PR / branch / feature
- User wants a shareable document about how something works
- User wants to document architecture decisions for the team
- User says "walkthrough", "deep dive", "explain this PR", "write up how this works"
## Workflow
### 1. Research the PR Thoroughly
Read **everything** before writing a single line of HTML. The quality of the walkthrough depends entirely on depth of understanding.
**Gather context in parallel:**
- `git log --oneline main..HEAD` — commit history and narrative arc
- `git diff --stat main..HEAD` — scope and affected areas
- `git diff --name-only main..HEAD` — file list for categorization
- `gh pr view --json title,body,url` — PR description if available
**Read all changed files that matter.** Not just the diff — read the full files to understand context. Prioritize:
1. New files (these ARE the feature)
2. Interface/type definitions (these define the contracts)
3. Wiring/glue code (how things connect)
4. UI components (what users see)
5. Configuration and infrastructure changes
**Understand the threading.** Trace data flow from entry point to leaf. For backend features, trace from route handler through service layers to the actual operation. For AI/tool features, trace from registration through execution context to the tool itself.
### 2. Plan the Narrative
Structure as a **teaching document**, not a changelog. The reader should understand:
1. **The big picture** — What problem does this solve? What's the before/after?
2. **The architecture** — What are the layers? How do they relate?
3. **The key abstraction** — What's the central interface or pattern?
4. **The implementation** — Walk through each component, building understanding incrementally
5. **The wiring** — How do the pieces connect at runtime? Trace the full path.
6. **The UX** — What does the user see and do?
7. **The extension story** — How would you add a similar feature? What's the recipe?
8. **The tradeoffs** — What alternatives existed? Why was this approach chosen?
9. **The limitations** — What doesn't it do yet? What are natural next steps?
Not every section applies to every PR. Adapt the structure. A pure refactoring PR needs different sections than a new feature PR.
### 3. Write the HTML
Read the reference template at `./templates/reference-walkthrough.html` before generating. It demonstrates the exact aesthetic, component library, and patterns to use.
**Aesthetic direction: Editorial / Technical Paper.** The walkthrough should feel like a well-typeset technical article — a mix of long-form explanation, annotated code, and clear diagrams. Think: a Stripe engineering blog post or a well-written RFC.
#### Design System
**Typography:**
- Display font: `Instrument Serif` (headings) — elegant, editorial feel
- Body font: `Source Serif 4` — readable long-form text
- Mono font: `DM Mono` — code, labels, metadata
- Load all from Google Fonts
**Color palette (warm paper theme):**
```css
--ink: #1a1a18; /* primary text */
--paper: #f5f0e8; /* page background */
--paper-warm: #ede7db; /* secondary surfaces */
--accent: #c23616; /* emphasis, new items, key callouts */
--blue: #2d5f8a; /* insights, links */
--green: #3a7d44; /* patterns, success, strings */
--purple: #6a4c93; /* tradeoffs */
--orange: #d4820a; /* warnings, keywords */
--gray: #8a8578; /* secondary text, metadata */
```
Each semantic color has a `-dim` variant at ~15% opacity for backgrounds.
#### Component Library
Use these components (all demonstrated in the reference template):
**Section structure:**
- Section number as small mono kicker (`01`, `02`, ...)
- `h2` with display font and bottom border
- `h3` for subsections, `h4` for small mono labels
**Code blocks (`<pre>`):**
- Dark background (`var(--ink)`), left accent border
- File label badge in top-right corner (`.file-label`)
- Syntax highlighting via span classes: `.keyword`, `.string`, `.type`, `.fn`, `.comment`
- Keep code excerpts focused — show the essential 5-15 lines, not the whole file
**Diagrams (`.diagram-container`):**
- White background with thin border
- Rainbow gradient top strip (accent → blue → green → purple)
- Mono label at top
**Flow diagrams (`.flow`):**
- Vertical flow with `.flow-row` containing `.flow-box` elements
- Boxes color-coded by role: `.accent`, `.blue`, `.green`, `.purple`, `.orange`, `.filled`
- Arrow separators (`.flow-arrow`) and notes (`.flow-note`) between rows
- Each box can have a `<small>` subtitle
**Layer diagrams (`.layer-stack`):**
- Horizontal rows with label + items
- Items tagged `.new` get a red "NEW" badge
**Callouts (`.callout`):**
- Four types: `.insight` (blue), `.warning` (orange), `.pattern` (green), `.tradeoff` (purple)
- Mono label at top, then explanation text
- Use for key takeaways that deserve visual emphasis
**Comparison tables (`.comparison`):**
- Mono uppercase headers
- First column as `.label-cell` (bold mono)
- Clean bottom-border rows
**File trees (`.file-tree`):**
- Mono text with `.dir`, `.file`, `.new-file` classes
- Indent levels via `.indent`, `.indent-2`
**Table of contents (`.toc`):**
- Two-column layout with numbered entries
- "Contents" label as positioned pseudo-element
#### Content Principles
**Show real code, not pseudocode.** Extract actual code from the PR files. Trim to the essential lines. Add syntax highlighting spans.
**Annotate, don't just describe.** After a code block, explain *why* it's designed that way, not just *what* it does.
**Use diagrams for flow, text for reasoning.** Flow diagrams show *how things connect*. Prose explains *why they connect that way*.
**Callouts for key insights.** If you find yourself writing "importantly" or "note that" in prose, extract it into a callout box instead.
**Be opinionated about tradeoffs.** Don't just list alternatives — explain why the chosen approach was right for this context, and when the alternative would be better.
### 4. Deliver
**Output location:** Write to the project root as `{topic}-walkthrough.html` (e.g., `github-integration-walkthrough.html`).
**Open in browser:**
- macOS: `open path/to/walkthrough.html`
**Tell the user:**
- The file path
- A summary of sections covered
- That the file is in the repo root and they may want to gitignore or delete it
## Quality Checks
Before delivering, verify:
- **Completeness:** Does every new file/concept get explained? Did you miss any layer?
- **Flow:** Can someone unfamiliar with the codebase follow the narrative from section 1 to the end?
- **Code accuracy:** Are code excerpts real (from the actual files), not invented?
- **Diagram clarity:** Do flow diagrams actually trace the real runtime path?
- **Tradeoff depth:** Did you go beyond "we could have done X instead" to explain *why* the choice was made?
- **Extension recipe:** Could a developer follow your recipe to add a similar feature?
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
theme = Catppuccin Latte
font-family = JetBrainsMono Nerd Font Mono
# https://pi.dev/docs/latest/terminal-setup
keybind = alt+backspace=text:\x1b\x7f
+1
View File
@@ -22,3 +22,4 @@ pnpm = "latest"
"github:DarthSim/overmind" = "latest" "github:DarthSim/overmind" = "latest"
"github:F1bonacc1/process-compose" = "latest" "github:F1bonacc1/process-compose" = "latest"
"github:modem-dev/hunk" = "latest" "github:modem-dev/hunk" = "latest"
"npm:@earendil-works/pi-coding-agent" = "latest"
+15 -16
View File
@@ -1,45 +1,44 @@
{ {
"LazyVim": { "branch": "main", "commit": "c10948c50b18fae7f256433afdef09e432410480" }, "LazyVim": { "branch": "main", "commit": "c10948c50b18fae7f256433afdef09e432410480" },
"SchemaStore.nvim": { "branch": "main", "commit": "735540a7602bffd1b07deab7d508affb57b78c64" }, "SchemaStore.nvim": { "branch": "main", "commit": "6ff1f21b2e2b77ec59f7433ce2d9fbc052d908ac" },
"blink.cmp": { "branch": "main", "commit": "78336bc89ee5365633bcf754d93df01678b5c08f" }, "blink.cmp": { "branch": "main", "commit": "78336bc89ee5365633bcf754d93df01678b5c08f" },
"bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" }, "bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" },
"catppuccin": { "branch": "main", "commit": "49a926655a2f5579e9c276470fc300baaa49e524" }, "catppuccin": { "branch": "main", "commit": "e068ab5f8261f23f6f71ffd8791ae40315b77b9c" },
"code-review.nvim": { "branch": "main", "commit": "ed91462e20bd08c3be71efb11a4a7d00459f0b47" },
"conform.nvim": { "branch": "master", "commit": "619363c30309d29ffa631e67c8183f2a72caa373" }, "conform.nvim": { "branch": "master", "commit": "619363c30309d29ffa631e67c8183f2a72caa373" },
"diffview.nvim": { "branch": "main", "commit": "c65ddb4724cc772a77e9eca1478bff451867b2e2" }, "diffview.nvim": { "branch": "main", "commit": "bcf4b62b4acc36a7c3d19e423713a220c838a668" },
"flash.nvim": { "branch": "main", "commit": "fcea7ff883235d9024dc41e638f164a450c14ca2" }, "flash.nvim": { "branch": "main", "commit": "fcea7ff883235d9024dc41e638f164a450c14ca2" },
"friendly-snippets": { "branch": "main", "commit": "6cd7280adead7f586db6fccbd15d2cac7e2188b9" }, "friendly-snippets": { "branch": "main", "commit": "6cd7280adead7f586db6fccbd15d2cac7e2188b9" },
"gitsigns.nvim": { "branch": "main", "commit": "42d6aed4e94e0f0bbced16bbdcc42f57673bd75e" }, "gitsigns.nvim": { "branch": "main", "commit": "42d6aed4e94e0f0bbced16bbdcc42f57673bd75e" },
"grug-far.nvim": { "branch": "main", "commit": "c995bbacf8229dc096ec1c3d60f8531059c86c1b" }, "grug-far.nvim": { "branch": "main", "commit": "c69859c1d5427ab5fc7ed12380ab521b4e336691" },
"lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" }, "lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" },
"lazydev.nvim": { "branch": "main", "commit": "ff2cbcba459b637ec3fd165a2be59b7bbaeedf0d" }, "lazydev.nvim": { "branch": "main", "commit": "ff2cbcba459b637ec3fd165a2be59b7bbaeedf0d" },
"lualine.nvim": { "branch": "master", "commit": "221ce6b2d999187044529f49da6554a92f740a96" }, "lualine.nvim": { "branch": "master", "commit": "221ce6b2d999187044529f49da6554a92f740a96" },
"markdown-preview.nvim": { "branch": "master", "commit": "a923f5fc5ba36a3b17e289dc35dc17f66d0548ee" }, "markdown-preview.nvim": { "branch": "master", "commit": "a923f5fc5ba36a3b17e289dc35dc17f66d0548ee" },
"mason-lspconfig.nvim": { "branch": "main", "commit": "21c5b3ebeaa0412e28096bb0701434c51c1fbf76" }, "mason-lspconfig.nvim": { "branch": "main", "commit": "47059d71b42d74b0a1e9f61c1d99d301039c3b5b" },
"mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" }, "mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" },
"mini.ai": { "branch": "main", "commit": "4511b3481707c1d021485475d34f2ed2a50bf47b" }, "mini.ai": { "branch": "main", "commit": "cb20f298ebf5ae91924cd0c6c310712de2ef4086" },
"mini.diff": { "branch": "main", "commit": "05be51814a718e74244829754a2a900a430a8d8b" }, "mini.diff": { "branch": "main", "commit": "0743d26bd858ebe32efcf5c86a91a422a000f273" },
"mini.icons": { "branch": "main", "commit": "ac38c983aed0a2bd32a65ca3e2348e12e58ca292" }, "mini.icons": { "branch": "main", "commit": "24dbea2195c477e57d581215839a6ab915f34b14" },
"mini.pairs": { "branch": "main", "commit": "30cf2f01c4aaa2033db67376b9924fa2442c05d6" }, "mini.pairs": { "branch": "main", "commit": "fd150ac39b78e6a2286f5138e472b7dc7eba43b9" },
"mini.surround": { "branch": "main", "commit": "d401c3856585473693e5ce9ec4c8e5b608b4c0fe" }, "mini.surround": { "branch": "main", "commit": "a2f644f3759edd3d3f8b6a6d55378408bfe6d290" },
"noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" }, "noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" },
"nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" }, "nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" },
"nvim-ansible": { "branch": "main", "commit": "c7f595d568b588942d4d0c37b5cd6cae3764a148" }, "nvim-ansible": { "branch": "main", "commit": "c7f595d568b588942d4d0c37b5cd6cae3764a148" },
"nvim-lint": { "branch": "master", "commit": "4b7957daf4b81eb578114bd6fcf20b6f5a2b59e8" }, "nvim-lint": { "branch": "master", "commit": "a219b2c9e5b4765e5c845aba119dad55806fcaf1" },
"nvim-lspconfig": { "branch": "master", "commit": "a683e0ddf0cf64c6cd689e18ffb480ade3c162b7" }, "nvim-lspconfig": { "branch": "master", "commit": "292f44408498103c47996ff5c18fd366293840d8" },
"nvim-treesitter": { "branch": "main", "commit": "4916d6592ede8c07973490d9322f187e07dfefac" }, "nvim-treesitter": { "branch": "main", "commit": "4916d6592ede8c07973490d9322f187e07dfefac" },
"nvim-treesitter-textobjects": { "branch": "main", "commit": "851e865342e5a4cb1ae23d31caf6e991e1c99f1e" }, "nvim-treesitter-textobjects": { "branch": "main", "commit": "851e865342e5a4cb1ae23d31caf6e991e1c99f1e" },
"nvim-ts-autotag": { "branch": "main", "commit": "88c1453db4ba7dd24131086fe51fdf74e587d275" }, "nvim-ts-autotag": { "branch": "main", "commit": "88c1453db4ba7dd24131086fe51fdf74e587d275" },
"octo.nvim": { "branch": "master", "commit": "7fed87415c401954f73401bbed0fd736b9611e7c" }, "octo.nvim": { "branch": "master", "commit": "b9a73e167f851a98d8f29d62658d3640bb8a7314" },
"persistence.nvim": { "branch": "main", "commit": "b20b2a7887bd39c1a356980b45e03250f3dce49c" }, "persistence.nvim": { "branch": "main", "commit": "b20b2a7887bd39c1a356980b45e03250f3dce49c" },
"plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" }, "plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" },
"render-markdown.nvim": { "branch": "main", "commit": "5adf0895310c1904e5abfaad40a2baad7fe44a07" }, "render-markdown.nvim": { "branch": "main", "commit": "f422cb5c6855f150e2ddcfaf44e7157b98b34f6a" },
"sidekick.nvim": { "branch": "main", "commit": "208e1c5b8170c01fd1d07df0139322a76479b235" }, "sidekick.nvim": { "branch": "main", "commit": "208e1c5b8170c01fd1d07df0139322a76479b235" },
"snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" }, "snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" },
"todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" }, "todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" },
"tokyonight.nvim": { "branch": "main", "commit": "cdc07ac78467a233fd62c493de29a17e0cf2b2b6" }, "tokyonight.nvim": { "branch": "main", "commit": "cdc07ac78467a233fd62c493de29a17e0cf2b2b6" },
"trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" }, "trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" },
"ts-comments.nvim": { "branch": "main", "commit": "123a9fb12e7229342f807ec9e6de478b1102b041" }, "ts-comments.nvim": { "branch": "main", "commit": "a59d6092213447450191122c9346f309161504cb" },
"vim-tmux-navigator": { "branch": "master", "commit": "e41c431a0c7b7388ae7ba341f01a0d217eb3a432" }, "vim-tmux-navigator": { "branch": "master", "commit": "e41c431a0c7b7388ae7ba341f01a0d217eb3a432" },
"which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" } "which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" }
} }
@@ -0,0 +1,18 @@
return {
{
"folke/which-key.nvim",
opts = {
spec = {
{ "<leader>r", group = "review" },
},
},
},
{
dir = "/home/tgrosinger/code/claude-review",
cmd = "ClaudeReview",
dependencies = {
"dlyongemallo/diffview.nvim",
},
opts = {},
},
}
+56 -54
View File
@@ -16,57 +16,59 @@
-- Inside the comment input window: <C-s> submits, <Esc> or q cancels. -- Inside the comment input window: <C-s> submits, <Esc> or q cancels.
-- (Terminals can't distinguish <C-CR> from <CR>, so we bind <C-s> instead.) -- (Terminals can't distinguish <C-CR> from <CR>, so we bind <C-s> instead.)
return { return {}
{
"folke/which-key.nvim", -- return {
opts = { -- {
spec = { -- "folke/which-key.nvim",
{ "<leader>r", group = "review" }, -- opts = {
}, -- spec = {
}, -- { "<leader>r", group = "review" },
}, -- },
{ -- },
"choplin/code-review.nvim", -- },
cmd = { -- {
"CodeReviewComment", -- "choplin/code-review.nvim",
"CodeReviewShowComment", -- cmd = {
"CodeReviewList", -- "CodeReviewComment",
"CodeReviewPreview", -- "CodeReviewShowComment",
"CodeReviewSave", -- "CodeReviewList",
"CodeReviewCopy", -- "CodeReviewPreview",
"CodeReviewClear", -- "CodeReviewSave",
"CodeReviewDeleteComment", -- "CodeReviewCopy",
}, -- "CodeReviewClear",
keys = { -- "CodeReviewDeleteComment",
{ "<leader>rc", mode = { "n", "v" }, desc = "Code review: add comment" }, -- },
{ "<leader>rp", desc = "Code review: preview" }, -- keys = {
{ "<leader>ry", desc = "Code review: copy to clipboard" }, -- { "<leader>rc", mode = { "n", "v" }, desc = "Code review: add comment" },
{ "<leader>rs", desc = "Code review: show at cursor" }, -- { "<leader>rp", desc = "Code review: preview" },
{ "<leader>rl", desc = "Code review: list comments" }, -- { "<leader>ry", desc = "Code review: copy to clipboard" },
{ "<leader>rd", desc = "Code review: delete at cursor" }, -- { "<leader>rs", desc = "Code review: show at cursor" },
{ "<leader>rx", desc = "Code review: clear all" }, -- { "<leader>rl", desc = "Code review: list comments" },
{ "<leader>rw", desc = "Code review: save to file" }, -- { "<leader>rd", desc = "Code review: delete at cursor" },
}, -- { "<leader>rx", desc = "Code review: clear all" },
opts = { -- { "<leader>rw", desc = "Code review: save to file" },
ui = { -- },
input_window = { -- opts = {
title = " Add Comment (C-s to submit) ", -- ui = {
}, -- input_window = {
}, -- title = " Add Comment (C-s to submit) ",
output = { -- },
-- Flat format optimized for pasting into AI assistants like Claude Code: -- },
-- path/to/file.lua:L42: comment text -- output = {
format = "minimal", -- -- Flat format optimized for pasting into AI assistants like Claude Code:
}, -- -- path/to/file.lua:L42: comment text
}, -- format = "minimal",
init = function() -- },
vim.api.nvim_create_autocmd("User", { -- },
pattern = "CodeReviewInputEnter", -- init = function()
callback = function(ev) -- vim.api.nvim_create_autocmd("User", {
local funcs = require("code-review").get_input_buffer_functions(ev.data.buf) -- pattern = "CodeReviewInputEnter",
vim.keymap.set({ "i", "n" }, "<C-s>", funcs.submit, { buffer = ev.data.buf }) -- callback = function(ev)
end, -- local funcs = require("code-review").get_input_buffer_functions(ev.data.buf)
}) -- vim.keymap.set({ "i", "n" }, "<C-s>", funcs.submit, { buffer = ev.data.buf })
end, -- end,
}, -- })
} -- end,
-- },
-- }
+17
View File
@@ -0,0 +1,17 @@
-- Restore <C-e> to its default scroll behavior in Octo review/PR buffers.
-- Octo binds <C-e> to copy_sha, which errors "Not a pull request buffer" in
-- diff buffers instead of scrolling. Blanking the lhs makes Octo skip the
-- mapping (see octo.utils.is_blank / apply_mappings), so <C-e> scrolls again.
return {
"pwntester/octo.nvim",
opts = {
mappings = {
review_diff = {
copy_sha = { lhs = "" },
},
pull_request = {
copy_sha = { lhs = "" },
},
},
},
}
+13
View File
@@ -0,0 +1,13 @@
return {
"folke/snacks.nvim",
opts = {
picker = {
sources = {
explorer = {
hidden = true,
ignored = true,
},
},
},
},
}
+2 -2
View File
@@ -11,7 +11,7 @@ set $down j
set $up k set $up k
set $right l set $right l
# Your preferred terminal emulator # Your preferred terminal emulator
set $term alacritty set $term ghostty
# Your preferred application launcher # Your preferred application launcher
# Note: pass the final command to swaymsg so that the resulting window can be opened # Note: pass the final command to swaymsg so that the resulting window can be opened
# on the original workspace that the command was run on. # on the original workspace that the command was run on.
@@ -93,7 +93,7 @@ input "2362:628:PIXA3854:00_093A:0274_Touchpad" {
bindsym $mod+Equal exec /home/tgrosinger/.config/rofi/scripts/qalc.sh bindsym $mod+Equal exec /home/tgrosinger/.config/rofi/scripts/qalc.sh
# Lazygit # Lazygit
bindsym $mod+g exec alacritty --title Floating-Lazygit --command /home/linuxbrew/.linuxbrew/bin/lazygit; grab_focus; floating enable bindsym $mod+g exec $term --title Floating-Lazygit --command /home/linuxbrew/.linuxbrew/bin/lazygit; grab_focus; floating enable
for_window [title="Floating-Lazygit"] floating enable for_window [title="Floating-Lazygit"] floating enable
for_window [title="Floating-Lazygit"] resize set 1800 1200 for_window [title="Floating-Lazygit"] resize set 1800 1200
+3
View File
@@ -0,0 +1,3 @@
{
"bashAllowPatterns": []
}
+393
View File
@@ -0,0 +1,393 @@
/**
* Approval Gate Extension
*
* Prompts before mutating/dangerous tool calls execute.
*
* For `edit` calls, renders a rich diff preview (with surrounding file
* context) by reusing pi's built-in edit tool renderer inside a custom
* approval modal. For `bash`/`write`, falls back to a text confirm dialog.
*
* Config: ~/.pi/agent/approval-gate.json
* {
* "bashAllowPatterns": ["^pwd$"]
* }
*/
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import {
createEditToolDefinition,
getAgentDir,
type EditToolInput,
} from "@earendil-works/pi-coding-agent";
import {
Box,
type Component,
Key,
matchesKey,
Spacer,
Text,
} from "@earendil-works/pi-tui";
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
interface ApprovalGateConfig {
bashAllowPatterns?: unknown;
}
interface LoadedConfig {
bashAllowPatterns: RegExp[];
warnings: string[];
}
const BLOCKED_REASON = "Blocked by approval-gate";
const STATUS_KEY = "approval-gate";
const CONFIG_FILE = "approval-gate.json";
function truncate(value: string, maxLength = 1200): string {
if (value.length <= maxLength) return value;
return `${value.slice(0, maxLength)}\n… truncated ${value.length - maxLength} character(s)`;
}
function formatBytes(value: string): string {
const bytes = Buffer.byteLength(value, "utf8");
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
function loadConfig(): LoadedConfig {
const configPath = join(getAgentDir(), CONFIG_FILE);
const warnings: string[] = [];
if (!existsSync(configPath)) {
return { bashAllowPatterns: [], warnings };
}
let parsed: ApprovalGateConfig;
try {
parsed = JSON.parse(readFileSync(configPath, "utf8")) as ApprovalGateConfig;
} catch (error) {
warnings.push(`Failed to parse ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
return { bashAllowPatterns: [], warnings };
}
if (parsed.bashAllowPatterns === undefined) {
return { bashAllowPatterns: [], warnings };
}
if (!Array.isArray(parsed.bashAllowPatterns)) {
warnings.push(`${configPath}: bashAllowPatterns must be an array of regex pattern strings`);
return { bashAllowPatterns: [], warnings };
}
const bashAllowPatterns: RegExp[] = [];
for (const pattern of parsed.bashAllowPatterns) {
if (typeof pattern !== "string") {
warnings.push(`${configPath}: skipped non-string bash allow pattern`);
continue;
}
try {
bashAllowPatterns.push(new RegExp(pattern));
} catch (error) {
warnings.push(
`${configPath}: skipped invalid regex ${JSON.stringify(pattern)}: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
return { bashAllowPatterns, warnings };
}
function updateStatus(ctx: ExtensionContext, enabled: boolean): void {
if (!ctx.hasUI) return;
ctx.ui.setStatus(STATUS_KEY, `approvals: ${enabled ? "✓" : "✗"}`);
}
function isAllowedBashCommand(command: string, patterns: RegExp[]): boolean {
const trimmed = command.trim();
return patterns.some((pattern) => pattern.test(trimmed));
}
function summarizeToolCall(toolName: string, input: Record<string, unknown>): string {
if (toolName === "bash") {
const command = typeof input.command === "string" ? input.command : JSON.stringify(input, null, 2);
return `Command:\n\n${truncate(command)}`;
}
if (toolName === "edit") {
const path = typeof input.path === "string" ? input.path : "unknown";
const edits = Array.isArray(input.edits) ? input.edits : [];
const editSummaries = edits.slice(0, 3).map((edit, index) => {
if (!edit || typeof edit !== "object") return `Edit ${index + 1}: <unrecognized edit block>`;
const block = edit as { oldText?: unknown; newText?: unknown };
const oldText = typeof block.oldText === "string" ? block.oldText : "<missing oldText>";
const newText = typeof block.newText === "string" ? block.newText : "<missing newText>";
return [
`Edit ${index + 1}:`,
` old (${formatBytes(oldText)}): ${truncate(oldText, 500)}`,
` new (${formatBytes(newText)}): ${truncate(newText, 500)}`,
].join("\n");
});
const omitted = edits.length > editSummaries.length ? `\n\n… omitted ${edits.length - editSummaries.length} edit block(s)` : "";
return [`Path: ${path}`, `Edit blocks: ${edits.length}`, "", ...editSummaries].join("\n") + omitted;
}
if (toolName === "write") {
const path = typeof input.path === "string" ? input.path : "unknown";
const content = typeof input.content === "string" ? input.content : undefined;
if (content === undefined) {
return `Path: ${path}\nContent size: unknown`;
}
return `Path: ${path}\nContent size: ${formatBytes(content)}\n\nContent preview:\n${truncate(content)}`;
}
return truncate(JSON.stringify(input, null, 2));
}
interface EditRenderState {
callComponent?: Box & {
preview?: unknown;
previewArgsKey?: string;
previewPending?: boolean;
settledError?: boolean;
};
}
interface EditRenderContext {
args: EditToolInput;
toolCallId: string;
invalidate: () => void;
lastComponent: Component | undefined;
state: EditRenderState;
cwd: string;
executionStarted: boolean;
argsComplete: boolean;
isPartial: boolean;
expanded: boolean;
showImages: boolean;
isError: boolean;
}
/**
* Custom approval component for `edit` tool calls.
*
* Reuses pi's built-in edit tool `renderCall` to render the same contextual
* diff preview the user would see in the normal tool-execution row, including
* surrounding file context and colored intra-line changes. The preview is
* computed asynchronously (reads the target file), so we re-render on
* invalidate until it settles.
*/
class EditApprovalComponent implements Component {
private readonly tui: { requestRender: () => void };
private readonly theme: { fg: (color: string, text: string) => string; bold: (text: string) => string };
private readonly editDef: ReturnType<typeof createEditToolDefinition>;
private readonly input: EditToolInput;
private readonly cwd: string;
private readonly toolCallId: string;
private readonly state: EditRenderState = {};
private lastComponent: Component | undefined;
private readonly header: Text;
private readonly footer: Text;
private readonly topBorder: DynamicBorder;
private readonly bottomBorder: DynamicBorder;
private settled = false;
public onApprove?: () => void;
public onReject?: () => void;
constructor(opts: {
tui: { requestRender: () => void };
theme: { fg: (color: string, text: string) => string; bold: (text: string) => string };
cwd: string;
input: EditToolInput;
toolCallId: string;
}) {
this.tui = opts.tui;
this.theme = opts.theme;
this.cwd = opts.cwd;
this.input = opts.input;
this.toolCallId = opts.toolCallId;
this.editDef = createEditToolDefinition(opts.cwd);
const editCount = Array.isArray(this.input.edits) ? this.input.edits.length : 0;
const headerText = `${this.theme.fg("toolTitle", this.theme.bold("edit"))} ${this.theme.fg("accent", this.input.path)}${this.theme.fg("dim", ` · ${editCount} block(s) · approve?`)}`;
this.header = new Text(headerText, 1, 0);
this.footer = new Text(this.theme.fg("dim", "y / enter approve · n / esc reject"), 1, 0);
const borderFn = (s: string) => this.theme.fg("accent", s);
this.topBorder = new DynamicBorder(borderFn);
this.bottomBorder = new DynamicBorder(borderFn);
}
private buildContext(): EditRenderContext {
return {
args: this.input,
toolCallId: this.toolCallId,
invalidate: () => this.tui.requestRender(),
lastComponent: this.lastComponent,
state: this.state,
cwd: this.cwd,
executionStarted: false,
argsComplete: true,
isPartial: false,
expanded: true,
showImages: false,
isError: false,
};
}
private renderEditPreview(width: number): string[] {
const renderCall = this.editDef.renderCall;
if (!renderCall) return [this.theme.fg("warning", "(diff preview unavailable)")];
const component = renderCall(this.input, this.theme as never, this.buildContext() as never);
this.lastComponent = component;
return component.render(width);
}
render(width: number): string[] {
const lines: string[] = [];
lines.push(...this.topBorder.render(width));
lines.push(...this.header.render(width));
lines.push(...this.renderEditPreview(width));
lines.push(...new Spacer(1).render(width));
lines.push(...this.footer.render(width));
lines.push(...this.bottomBorder.render(width));
return lines;
}
handleInput(data: string): void {
if (this.settled) return;
if (matchesKey(data, Key.enter) || data === "y" || data === "Y") {
this.settled = true;
this.onApprove?.();
} else if (matchesKey(data, Key.escape) || data === "n" || data === "N") {
this.settled = true;
this.onReject?.();
}
}
invalidate(): void {
this.header.invalidate();
this.footer.invalidate();
this.topBorder.invalidate();
this.bottomBorder.invalidate();
this.lastComponent?.invalidate?.();
}
}
function isEditInput(value: Record<string, unknown>): value is EditToolInput {
return (
typeof value.path === "string" &&
Array.isArray(value.edits) &&
value.edits.length > 0 &&
value.edits.every(
(e) => e && typeof e === "object" && typeof (e as { oldText?: unknown }).oldText === "string" && typeof (e as { newText?: unknown }).newText === "string",
)
);
}
async function approveEditCall(input: Record<string, unknown>, ctx: ExtensionContext, toolCallId: string): Promise<boolean> {
if (ctx.mode !== "tui" || !isEditInput(input)) {
// Non-TUI or malformed input: fall back to text summary confirm.
const message = `${summarizeToolCall("edit", input)}\n\nAllow this tool call?`;
return ctx.ui.confirm("Approve edit?", message);
}
return ctx.ui.custom<boolean>((tui, theme, _keybindings, done) => {
const component = new EditApprovalComponent({
tui: tui as { requestRender: () => void },
theme: theme as { fg: (color: string, text: string) => string; bold: (text: string) => string },
cwd: ctx.cwd,
input,
toolCallId,
});
component.onApprove = () => done(true);
component.onReject = () => done(false);
return component;
});
}
export default function approvalGateExtension(pi: ExtensionAPI) {
let enabled = true;
const config = loadConfig();
pi.registerCommand("approval-gate", {
description: "Turn mutating/dangerous tool approval prompts on or off",
handler: async (args, ctx) => {
const action = args.trim().toLowerCase();
if (action === "on") {
enabled = true;
updateStatus(ctx, enabled);
ctx.ui.notify("approval-gate enabled", "info");
return;
}
if (action === "off") {
enabled = false;
updateStatus(ctx, enabled);
ctx.ui.notify("approval-gate disabled", "info");
return;
}
if (action === "" || action === "status") {
updateStatus(ctx, enabled);
ctx.ui.notify(`approval-gate is ${enabled ? "enabled" : "disabled"}`, "info");
return;
}
ctx.ui.notify("Usage: /approval-gate [on|off|status]", "warning");
},
});
pi.on("session_start", async (_event, ctx) => {
updateStatus(ctx, enabled);
for (const warning of config.warnings) {
ctx.ui.notify(`approval-gate: ${warning}`, "warning");
}
});
pi.on("tool_call", async (event, ctx) => {
if (!enabled) return undefined;
const input = event.input as Record<string, unknown>;
const isBash = event.toolName === "bash";
const isEdit = event.toolName === "edit";
const isWrite = event.toolName === "write";
if (!isBash && !isEdit && !isWrite) return undefined;
if (isBash) {
const command = typeof input.command === "string" ? input.command : "";
if (command && isAllowedBashCommand(command, config.bashAllowPatterns)) {
return undefined;
}
}
if (!ctx.hasUI) {
return { block: true, reason: `${BLOCKED_REASON} (no UI available for confirmation)` };
}
let approved: boolean;
if (isEdit) {
approved = await approveEditCall(input, ctx, event.toolCallId);
} else {
approved = await ctx.ui.confirm(
`Approve ${event.toolName}?`,
`${summarizeToolCall(event.toolName, input)}\n\nAllow this tool call?`,
);
}
if (!approved) {
return { block: true, reason: BLOCKED_REASON };
}
return undefined;
});
}
+12
View File
@@ -0,0 +1,12 @@
{
"lastChangelogVersion": "0.79.8",
"theme": "catppuccin-latte",
"defaultProvider": "openai-codex",
"defaultModel": "gpt-5.5",
"defaultThinkingLevel": "high",
"skills": [
"~/.claude/skills"
],
"hideThinkingBlock": true,
"enableInstallTelemetry": false
}
@@ -0,0 +1,90 @@
{
"$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
"name": "catppuccin-tui-latte",
"vars": {
"rosewater": "#dc8a78",
"flamingo": "#dd7878",
"pink": "#ea76cb",
"mauve": "#8839ef",
"red": "#d20f39",
"maroon": "#e64553",
"peach": "#fe640b",
"yellow": "#df8e1d",
"green": "#40a02b",
"teal": "#179299",
"sky": "#04a5e5",
"sapphire": "#209fb5",
"blue": "#1e66f5",
"lavender": "#7287fd",
"text": "#4c4f69",
"subtext1": "#5c5f77",
"subtext0": "#6c6f85",
"overlay2": "#7c7f93",
"overlay1": "#8c8fa1",
"overlay0": "#9ca0b0",
"surface2": "#acb0be",
"surface1": "#bcc0cc",
"surface0": "#ccd0da",
"base": "#eff1f5",
"mantle": "#e6e9ef",
"crust": "#dce0e8"
},
"colors": {
"accent": "mauve",
"border": "surface2",
"borderAccent": "lavender",
"borderMuted": "surface1",
"success": "green",
"error": "red",
"warning": "yellow",
"muted": "subtext0",
"dim": "overlay1",
"text": "text",
"thinkingText": "subtext1",
"selectedBg": "surface0",
"userMessageBg": "surface0",
"userMessageText": "text",
"customMessageBg": "mantle",
"customMessageText": "text",
"customMessageLabel": "mauve",
"toolPendingBg": "mantle",
"toolSuccessBg": "surface0",
"toolErrorBg": "surface0",
"toolTitle": "blue",
"toolOutput": "overlay2",
"mdHeading": "mauve",
"mdLink": "blue",
"mdLinkUrl": "sapphire",
"mdCode": "peach",
"mdCodeBlock": "subtext1",
"mdCodeBlockBorder": "surface2",
"mdQuote": "subtext0",
"mdQuoteBorder": "surface2",
"mdHr": "surface2",
"mdListBullet": "mauve",
"toolDiffAdded": "green",
"toolDiffRemoved": "red",
"toolDiffContext": "subtext0",
"syntaxComment": "overlay2",
"syntaxKeyword": "mauve",
"syntaxFunction": "blue",
"syntaxVariable": "maroon",
"syntaxString": "green",
"syntaxNumber": "peach",
"syntaxType": "yellow",
"syntaxOperator": "sky",
"syntaxPunctuation": "overlay2",
"thinkingOff": "surface1",
"thinkingMinimal": "surface2",
"thinkingLow": "blue",
"thinkingMedium": "sapphire",
"thinkingHigh": "mauve",
"thinkingXhigh": "red",
"bashMode": "peach"
},
"export": {
"pageBg": "#eff1f5",
"cardBg": "#e6e9ef",
"infoBg": "#ccd0da"
}
}
+1 -1
View File
@@ -46,7 +46,7 @@ bind-key -T copy-mode-vi 'C-l' select-pane -R
# Improved support for Claude Code CLI. # Improved support for Claude Code CLI.
set -g allow-passthrough on set -g allow-passthrough on
set -as terminal-features 'alacritty*:RGB' set -as terminal-features 'xterm-ghostty*:RGB'
# change windows # change windows
bind -n S-Right next-window bind -n S-Right next-window
+3
View File
@@ -66,6 +66,9 @@ sudo dnf install \
socat \ socat \
wf-recorder wf-recorder
sudo dnf copr enable scottames/ghostty
sudo dnf install ghostty
# Install global packages managed by mise. # Install global packages managed by mise.
mise install mise install