Skip to content

A sidebar on Ctrl+B: drill's first plugin, clickable to the last gesture - #18

Open
jainal09 wants to merge 11 commits into
feat/macos-cmdfrom
feat/explorer-sidebar
Open

A sidebar on Ctrl+B: drill's first plugin, clickable to the last gesture#18
jainal09 wants to merge 11 commits into
feat/macos-cmdfrom
feat/explorer-sidebar

Conversation

@jainal09

@jainal09 jainal09 commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Stacked on #17 (feat/macos-cmd) — merge that first; this PR retargets main cleanly after.

What this is

An optional file-explorer sidebar on Ctrl+B / Cmd+B — drill's first and only plugin-powered feature, built to stay an exception:

  • nvim-tree draws the tree, nvzone/volt + nvzone/menu draw the right-click menu — three checkouts pinned to exact SHAs by the new vendor.sh (a pin table and a loop, not a plugin manager).
  • Nothing loads at startup. The checkouts aren't on the runtime path until the first toggle; never press Ctrl+B and you run the config drill always had.
  • The same branch seals the packpath leak: user plugins in ~/.local/share/nvim/site/pack/*/start loaded inside drill despite the "no plugins" promise (measured). Both site dirs now come off runtimepath and packpath, with a suite assert.

The gestures (all mouse, all pty-tested)

  • One click opens a file (never into the REPL — window_picker excludes terminals) or folds a folder.
  • [+ File] / [+ Folder] — real clickable winbar buttons (%@handler@ regions). The folder button does mkdir -p, not the trailing-slash typing convention.
  • Right-click menu — plain-ASCII items (New file/folder, Open, Open in split, Rename, Cut, Copy, Paste, Delete), global-but-late mapping (mouse events route to the focused buffer's maps, so tree-local could never fire); outside the tree the stock right-click is re-fed untouched.
  • Ctrl+click multi-select (marks, rendered *), and drag-and-drop: drop a file on a folder to move it in; drag a marked row and the whole selection moves. A one-cell jitter is a click, not a drag — same trackpad rule as the rest of drill.

Bugs found on the way

  • A mouse press from insert focuses the tree while staying in insert (measured: mode "i", filetype NvimTree), landing releases in a mode no tree mapping owns — every click silently ate itself. The tree now forces Normal on entry.
  • <D-b> stops being the docs' example of a floored chord; the floor test re-spells with <D-g>.

Tests & plumbing

  • New tests/suite_tree.sh + tests/tree_drive.py: 45 cases on a real pty — both toggle chords (Cmd+B as raw CSI-u bytes), focus that never leaves your code, toolbar clicks all the way to files on disk, a menu item clicked by its float coordinates, marked-pair drag moves, and the 3-window REPL layout.
  • tests/run.sh gains a vendor.sh preflight (CI needs zero workflow changes); gate grows 558 → 719 cases, all green locally (2 full runs).
  • install.sh: manifest + vendor fetch (FEATURE-level; offline installs lose only the sidebar), nvim gate 0.9 → 0.10 (what the pinned nvim-tree requires). Verified in a sandboxed HOME.
  • Docs: README section, KEYS.md gestures, new docs/explorer.md, honesty pass on every "no plugins" claim.

🤖 Generated with Claude Code


Summary by cubic

Adds an optional, vendored file-explorer sidebar on Ctrl+B/Cmd+B that loads only on first toggle. Old behavior: no plugins and the sidebar (when present) rooted at the shell’s working directory with possible user plugin leakage. New behavior: one opt‑in plugin (nvim-tree.lua + nvzone/volt + nvzone/menu) loads lazily, the tree roots at the current file’s folder on open, it keeps the existing root when toggled from the REPL or a scratch buffer, filesystem watchers stay disabled to avoid EMFILE, and both runtimepath and packpath seals block third‑party loads. Hiding the REPL now returns focus to a real typing buffer instead of the tree.

  • Focus stays in the file window; single‑click opens/folds; real winbar “[+ File]” / “[+ Folder]”; right‑click menu only in the tree; Ctrl+click marks; drag‑and‑drop moves (marked sets move together); jitter is a click; entering the tree forces Normal; window picker excludes terminals. Netrw roots the tree at the directory it lists. Docs call out iTerm2’s right‑click Pointer binding that must be removed.
  • Code review: explorer.lua lazy setup, drag/drop, menu, toolbar, rooting and watcher settings; nvimrc.lua path sealing, Ctrl+B/Cmd+B toggles, and repl_hide focus fix; vendor.sh pinned checkouts; install.sh runs vendor.sh, adds a git dep, and enforces Neovim 0.10+; tests/run.sh preflights vendor.sh; gate is 729 cases with 55 real‑PTY sidebar tests; docs updated.

Rollout

  • Required: run ./vendor.sh after pulling; ensure Neovim 0.10+ and git are installed.
  • macOS: enable Cmd forwarding; in iTerm2 remove the default right‑click Pointer binding so the tree menu works.
  • No change if the sidebar is never toggled; netrw remains for directory listing.

Written for commit 9a1a3c2. Summary will update on new commits.

Review in cubic

Greptile Summary

The PR adds an optional, lazily loaded file-explorer sidebar and vendors its pinned plugin dependencies.

  • Adds mouse-driven tree navigation, creation, context-menu, selection, and drag-and-drop behavior.
  • Seals user site directories out of Neovim runtime and package paths.
  • Extends installation, documentation, and PTY-based test coverage for the sidebar.

Confidence Score: 3/5

The PR is not yet safe to merge because vendor refresh failures can remove working sidebar dependencies and the installer rejects a still-documented supported Neovim version.

The existing vendor replacement path deletes each nonmatching checkout before fetching its replacement, and the installation contract still disagrees between the hard Neovim 0.10 gate and README’s 0.9+ prerequisite.

Files Needing Attention: vendor.sh, install.sh, README.md

Important Files Changed

Filename Overview
explorer.lua Implements lazy sidebar setup, tree rooting, mouse gestures, toolbar actions, context menus, and drag-and-drop.
nvimrc.lua Seals user plugin paths and adds lazy sidebar toggle mappings while preserving the isolated editor setup.
vendor.sh Fetches three exact plugin revisions, but the previously reported destructive replacement sequence remains.
install.sh Installs sidebar assets and dependencies, but its Neovim hard gate remains inconsistent with the documented prerequisite.
tests/suite_tree.sh Adds real-PTY coverage for sidebar toggles, focus, mouse interactions, filesystem operations, and REPL layouts.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  K[Ctrl+B or Cmd+B] --> L[Load explorer.lua]
  L --> V[Add pinned vendor checkouts to runtimepath]
  V --> T[Initialize nvim-tree and menu]
  T --> U[Mouse-driven sidebar]
  I[install.sh or tests/run.sh] --> F[vendor.sh]
  F --> V
Loading

Reviews (3): Last reviewed commit: "Ctrl+E out of the REPL means the code, n..." | Re-trigger Greptile

jainal09 and others added 8 commits August 16, 2026 22:23
…to a fault

drill promised "no plugins" and mostly still keeps it: nothing here touches
startup. Ctrl+B (n/i) and Cmd+B (n/i/t -- ^B belongs to readline in the REPL,
Cmd+B rides CSI-u past python) lazily dofile explorer.lua, which only then
puts three pinned checkouts on 'runtimepath': nvim-tree for the tree, volt and
menu for the right-click work to come. vendor.sh fetches them by bare SHA --
shallow, detached, reproducible -- so there is still no plugin manager, just a
table with three rows. Never press Ctrl+B and you run the config you always
ran, byte for byte.

The toggle opens with focus=false: the sidebar appears, the caret stays in
your code, and type_here() keeps the one promise on both directions. ASCII
glyphs on purpose (stock fonts have no nerd runes), netrw untouched for bare
`d`, window_picker excludes ensure a file opened from the tree can never land
in the interpreter.

<D-b> had to stop being the example of a floored chord:
suite_config re-spells its floor case with <D-g>, and suite_options now
asserts the toggle is bound exactly where claimed -- and NOT in cmdline,
where ^B is cursor-to-start at the / prompt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"No plugins" removed the site dir from 'runtimepath' and called it isolation,
but startup packages load from 'packpath', which still held it -- measured: a
plugin dropped in ~/.local/share/nvim/site/pack/x/start/ loaded happily inside
drill. The day drill gains its one deliberate plugin is the day the accidental
ones should stop: site and site/after come off both options, and suite_options
now asserts the seal so it cannot quietly reopen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The deal-breaker was "press a to make a file" -- keyboard folklore where a
button belongs. The tree window now carries a winbar toolbar with real
clickable [+ File] and [+ Folder] regions (%@handler@ click support, core
since 0.8). Both seed the name prompt from the row the cursor is on -- a
folder means "in here", a file means "next to me" -- and the folder button
does NOT ride api.fs.create's trailing-slash convention: that is the same
folklore, one key deeper. mkdir -p, reload, done.

One click opens a file and one click folds a folder -- no double-click. The
release acts on the node the press already parked the cursor on, and a guard
on getmousepos().line == 0 keeps winbar clicks with the buttons they hit.

The bug worth the commit: drill lives in insert mode, and a mouse press from
insert FOCUSES the tree while STAYING in insert -- measured, mode "i" inside
the NvimTree buffer -- so the release arrived in a mode no tree mapping owns
and every click silently ate itself. Entering the tree now lands you in
Normal, by any route, and every gesture can assume it.

suite_tree is the proof: 19 cases on a real pty writing real SGR sequences --
toggle both chords, focus that never leaves your code, window-local
virtualedit, toolbar clicks all the way to files appearing on disk. run.sh
gains a vendor.sh preflight so the gate fetches the pins before it needs them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
menu+volt draw it and own the item clicks; the items are drill's own -- plain
ASCII labels (the upstream set is nerd-font glyphs, tofu on a stock font, and
its "New folder" is the trailing-slash convention wearing a menu costume).
New file, New folder, Open, Open in split, Rename, Cut, Copy, Paste, Delete,
every one acting on the row the right-click parked the cursor on.

The mapping is global, and that is forced, not chosen: a right-click ON the
tree while you are typing in the file is delivered to the FILE buffer's maps,
so a tree-local mapping would never fire. It is still late -- registered only
once the sidebar has ever been toggled -- and everywhere outside the tree it
re-feeds the unmapped key, so the file buffer keeps stock right-click
behavior, asserted on the pty from both sides: drill's menu drawn in the
tree, the built-in one (and not ours) in the file.

The suite also clicks an ITEM by its float coordinates -- border corner one
row below the mouse, "Open" four text lines in -- and watches d.py open, the
float close, and insert mode return. Menus are drawn, not queryable, so
these cases read the escape-stripped pty stream, the quit_drive technique.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…moves

No tree plugin ships drag-and-drop, but the events were always there: the
press parks the cursor on the source row, the first <LeftDrag> report
snapshots that node, and the release resolves the row under the mouse into a
destination. Cut, one paste, done -- onto a folder means INTO it, onto a file
means NEXT TO it. A release back on the source row is not a drag: that is a
trackpad click that jittered, the gesture mouse_drive.py already defends, and
it falls through to the ordinary click.

Ctrl-click toggles a mark (rendered *), and dragging any MARKED row takes the
whole marked set -- cut accumulates, one paste moves them all, marks clear.
Dragging an unmarked row moves it alone and leaves your selection standing.
The mapping is global-but-late like right-click, and for the same routing
reason; outside the tree it re-feeds a plain press, which is byte-for-byte
what the mouse section's Ctrl-stripping maps already did (stock <C-LeftMouse>
is a tag jump that wedges a tagless editor behind E426).

Eight new pty cases drive it for real: mark two, drag the unmarked third and
watch it travel alone, drag a marked one and watch the pair follow, and a
one-cell jitter that must open a file and move nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ng a click

Eleven cases for the layout the sidebar creates. <C-e> still opens the REPL
botright -- full width, under the tree, the IDE shape -- and the tree
survives it. A file clicked in the tree lands in the FILE window, never the
interpreter (the window_picker excludes terminals, and the case would catch
it lying). And the netrw contract holds in the tree too: Ctrl+S and Ctrl+E
pressed there wedge nothing, spawn nothing, and Ctrl+B from inside the tree
still closes it and leaves you typing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…0.10

The manifest gains explorer.lua and vendor.sh (both hardcoded lists, as
before), and after the copy the installer runs vendor.sh to bring the three
pinned checkouts down. FEATURE-level on purpose: an offline install costs
Ctrl+B and nothing else, the message says exactly what to rerun, and at the
pins the fetch is an offline no-op so reinstalls pay nothing. git joins the
dependency probe the same way -- a note, never a bail.

The version gate moves 0.9 -> 0.10, which is what the pinned nvim-tree
actually requires; explorer.lua carries its own has("nvim-0.10") guard for
the git-pull upgrader who never reruns the installer.

Verified in a sandboxed HOME (mktemp -d): full run, files + vendor/ landed,
4-line rc block added, exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ional

"Two files. No plugins" stopped being the whole truth the moment Ctrl+B
learned to grow a tree, so every place that said it now says what IS true:
three checkouts pinned to exact commits, fetched by vendor.sh, loaded only on
the first press, and the accidental-plugin door (packpath) closed in the same
change. README gets the sidebar its own section; KEYS.md documents every
gesture and the modes Ctrl+B deliberately stays out of; docs/explorer.md is
the full story; docs/macos-cmd.md gains the Cmd+B row and stops citing Cmd+B
as the example of a floored chord (Cmd+N takes that job).

The gate counts move 558 -> 719: options 172, config 52, mouse 66 (the table
had gone stale before this branch), and suite_tree's 45 gesture cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 17, 2026 02:30
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 64be8e83-84b6-4f38-b598-b797897b2618

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

codescene-access[bot]

This comment was marked as outdated.

Comment thread vendor.sh
Comment on lines +40 to +45
rm -rf "$dest"
mkdir -p "$dest"
git -C "$dest" init -q
# GitHub serves fetches of a bare SHA (allow-any-sha1-in-want), so this
# needs no branch name and survives upstream force-pushes.
git -C "$dest" fetch -q --depth 1 "$url" "$sha"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Failed fetch destroys working checkout

When an existing checkout differs from the configured pin and the replacement fetch fails, vendor.sh deletes the working checkout before downloading its replacement, leaving an incomplete vendor directory and disabling the entire sidebar.

Knowledge Base Used: Installation and launch flow

Comment thread install.sh

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

11 issues found across 17 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="README.md">

<violation number="1" location="README.md:116">
P3: The README still tells users 'Requires Neovim 0.9+', but this PR bumps install.sh's Neovim gate to 0.10+ (the sidebar's pinned plugins need 0.10). Update the Install details line to match so the documented minimum matches the enforced minimum.</violation>
</file>

<file name="tests/suite_tree.sh">

<violation number="1" location="tests/suite_tree.sh:45">
P3: Running with a filter that matches nothing (e.g. `./suite_tree.sh clik` with a typo) prints `PASS=0 FAIL=0` and exits 0, so the run is reported as green even though no case executed. When FILTER is non-empty, guard the success path on at least one matched case (or print the filter in the summary) so a mistyped filter cannot silently pass.</violation>
</file>

<file name="docs/explorer.md">

<violation number="1" location="docs/explorer.md:33">
P3: The doc promises that `Ctrl+E` and `Ctrl+R` "do nothing" inside the tree, but the global mappings still act there: `repl_show` and `run_once` only gate on `source_state()`, which, because the NvimTree buffer fails `typing_buffer()` (it is not modifiable and is not the netrw listing), simply returns the stale non-nil `last_file`. So once any file has been opened, pressing `Ctrl+E` in the tree focuses/opens the interpreter and `Ctrl+R` runs the file, neither of which is "nothing" — the same applies in the netrw listing the doc cites as the comparison. The suite only asserts "no wedge / no extra window" (tests/tree_drive.py), not a true no-op, so the tests don't back the doc claim. Either narrow the sentence to what actually holds (only `Ctrl+S`, which `save()` guards on `typing_buffer()`, is a no-op) or add a buffer-type guard to `repl_show`/`run_once` if the intent is genuinely inert keys in the tree.</violation>

<violation number="2" location="docs/explorer.md:57">
P3: install.sh now hard-requires Neovim 0.10+ (HARD abort), but README.md still states "Requires Neovim 0.9+" and this new line implies the rest of drill runs fine on 0.9. Align the minimum version in README.md (and soften this bullet) so users don't rely on 0.9 support that the installer refuses.</violation>
</file>

<file name="install.sh">

<violation number="1" location="install.sh:187">
P2: When apt's candidate is Neovim 0.9.x, this new gate rejects it, but `nvim_pkg` still offers `neovim` because its filter stops at 0.8. The installer therefore suggests an apt command that installs the same rejected version, then aborts; extend the candidate filter through 0.9.</violation>
</file>

<file name="vendor.sh">

<violation number="1" location="vendor.sh:35">
P2: When a pinned checkout is dirty, this fast path skips cleanup, so modified or extra plugin files remain loaded by the sidebar. Check the worktree is clean before declaring the pin ready, or recreate it.</violation>

<violation number="2" location="vendor.sh:40">
P2: When a pin update cannot fetch, this deletes the existing checkout first and leaves the sidebar unavailable. Fetch into a temporary checkout and replace the old directory only after checkout and verification succeed.</violation>
</file>

<file name="explorer.lua">

<violation number="1" location="explorer.lua:204">
P2: The stale `<LeftMouse>` auto-close map left by nvzone/menu is only deleted at the start of the *next* `tree_menu()` call. When a menu closes by item click (the common path), the plugin's auto_close map never deletes itself, so a leftover global `<LeftMouse>` mapping stays installed in the file buffer until the user right-clicks again. It re-feeds the built-in press so it usually doesn't visibly break anything, but the cleanup is in the wrong place. Clear it in the menu's after_close path (or have the store delete it as soon as the menu closes) instead of relying on the next open.</violation>

<violation number="2" location="explorer.lua:261">
P1: When a drag leaves the tree and releases over a file window, the tree’s buffer-local `<LeftRelease>` mapping does not clear `drag_src`. The next ordinary tree click can therefore call `drop` and move the stale file; clear drag state on release or window leave regardless of the receiving window.</violation>
</file>

<file name="nvimrc.lua">

<violation number="1" location="nvimrc.lua:11">
P1: Users with `~/.config/nvim/plugin` or `after/plugin` files can still execute those plugins in drill, despite the new no-plugin seal. Remove `stdpath("config")` and its `/after` entry from both paths, along with any other non-bundled site entries, or disable startup plugin loading.</violation>

<violation number="2" location="nvimrc.lua:1341">
P2: When the sidebar is unavailable, pressing Ctrl+B from insert mode leaves the user in Normal mode because `stopinsert` runs before the error return. Check the module error before stopping insert and call `type_here()` on this failure path.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread explorer.lua

local function tree_click()
local mp = vim.fn.getmousepos()
local src = drag_src

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a drag leaves the tree and releases over a file window, the tree’s buffer-local <LeftRelease> mapping does not clear drag_src. The next ordinary tree click can therefore call drop and move the stale file; clear drag state on release or window leave regardless of the receiving window.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At explorer.lua, line 261:

<comment>When a drag leaves the tree and releases over a file window, the tree’s buffer-local `<LeftRelease>` mapping does not clear `drag_src`. The next ordinary tree click can therefore call `drop` and move the stale file; clear drag state on release or window leave regardless of the receiving window.</comment>

<file context>
@@ -0,0 +1,313 @@
+
+local function tree_click()
+  local mp = vim.fn.getmousepos()
+  local src = drag_src
+  drag_src = nil
+  if mp.line == 0 then return end
</file context>

Comment thread nvimrc.lua
-- packages load from 'packpath', which still held it -- measured, a plugin in
-- ~/.local/share/nvim/site/pack/x/start/ loaded inside drill. All three
-- doors close here, so "no plugins" means the machine's plugins too.
for _, p in ipairs({ vim.fn.stdpath("data") .. "/site",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Users with ~/.config/nvim/plugin or after/plugin files can still execute those plugins in drill, despite the new no-plugin seal. Remove stdpath("config") and its /after entry from both paths, along with any other non-bundled site entries, or disable startup plugin loading.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At nvimrc.lua, line 11:

<comment>Users with `~/.config/nvim/plugin` or `after/plugin` files can still execute those plugins in drill, despite the new no-plugin seal. Remove `stdpath("config")` and its `/after` entry from both paths, along with any other non-bundled site entries, or disable startup plugin loading.</comment>

<file context>
@@ -1,6 +1,18 @@
+-- packages load from 'packpath', which still held it -- measured, a plugin in
+-- ~/.local/share/nvim/site/pack/x/start/ loaded inside drill. All three
+-- doors close here, so "no plugins" means the machine's plugins too.
+for _, p in ipairs({ vim.fn.stdpath("data") .. "/site",
+                     vim.fn.stdpath("data") .. "/site/after" }) do
+  vim.opt.runtimepath:remove(p)
</file context>

Comment thread install.sh
maj="$(printf '%s' "${cand%%.*}" | tr -cd '0-9')"
rest="${cand#*.}"; min="$(printf '%s' "${rest%%.*}" | tr -cd '0-9')"
if [ "${maj:-0}" -eq 0 ] && [ "${min:-0}" -lt 9 ]; then
if [ "${maj:-0}" -eq 0 ] && [ "${min:-0}" -lt 10 ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When apt's candidate is Neovim 0.9.x, this new gate rejects it, but nvim_pkg still offers neovim because its filter stops at 0.8. The installer therefore suggests an apt command that installs the same rejected version, then aborts; extend the candidate filter through 0.9.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At install.sh, line 187:

<comment>When apt's candidate is Neovim 0.9.x, this new gate rejects it, but `nvim_pkg` still offers `neovim` because its filter stops at 0.8. The installer therefore suggests an apt command that installs the same rejected version, then aborts; extend the candidate filter through 0.9.</comment>

<file context>
@@ -184,10 +184,10 @@ check_deps() {
     maj="$(printf '%s' "${cand%%.*}" | tr -cd '0-9')"
     rest="${cand#*.}"; min="$(printf '%s' "${rest%%.*}" | tr -cd '0-9')"
-    if [ "${maj:-0}" -eq 0 ] && [ "${min:-0}" -lt 9 ]; then
+    if [ "${maj:-0}" -eq 0 ] && [ "${min:-0}" -lt 10 ]; then
       NOTES="$NOTES
-    - neovim $cand is too old, need 0.9+ (the config is Lua and uses nvim_win_hide).
</file context>

Comment thread vendor.sh
continue
fi
echo "vendor.sh: fetching $name @ ${sha:0:12}"
rm -rf "$dest"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a pin update cannot fetch, this deletes the existing checkout first and leaves the sidebar unavailable. Fetch into a temporary checkout and replace the old directory only after checkout and verification succeed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vendor.sh, line 40:

<comment>When a pin update cannot fetch, this deletes the existing checkout first and leaves the sidebar unavailable. Fetch into a temporary checkout and replace the old directory only after checkout and verification succeed.</comment>

<file context>
@@ -0,0 +1,49 @@
+    continue
+  fi
+  echo "vendor.sh: fetching $name @ ${sha:0:12}"
+  rm -rf "$dest"
+  mkdir -p "$dest"
+  git -C "$dest" init -q
</file context>

Comment thread vendor.sh
[ -z "$name" ] && continue
dest="$VENDOR/$name"
have="$(git -C "$dest" rev-parse HEAD 2>/dev/null || true)"
if [ "$have" = "$sha" ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a pinned checkout is dirty, this fast path skips cleanup, so modified or extra plugin files remain loaded by the sidebar. Check the worktree is clean before declaring the pin ready, or recreate it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vendor.sh, line 35:

<comment>When a pinned checkout is dirty, this fast path skips cleanup, so modified or extra plugin files remain loaded by the sidebar. Check the worktree is clean before declaring the pin ready, or recreate it.</comment>

<file context>
@@ -0,0 +1,49 @@
+  [ -z "$name" ] && continue
+  dest="$VENDOR/$name"
+  have="$(git -C "$dest" rev-parse HEAD 2>/dev/null || true)"
+  if [ "$have" = "$sha" ]; then
+    echo "vendor.sh: $name already at ${sha:0:12}"
+    continue
</file context>

Comment thread explorer.lua
-- menu leaves its auto-close <LeftMouse> map behind when a menu is closed
-- by an item click rather than an outside click; clear the stale one so
-- it cannot stack
pcall(vim.keymap.del, "n", "<LeftMouse>")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The stale <LeftMouse> auto-close map left by nvzone/menu is only deleted at the start of the next tree_menu() call. When a menu closes by item click (the common path), the plugin's auto_close map never deletes itself, so a leftover global <LeftMouse> mapping stays installed in the file buffer until the user right-clicks again. It re-feeds the built-in press so it usually doesn't visibly break anything, but the cleanup is in the wrong place. Clear it in the menu's after_close path (or have the store delete it as soon as the menu closes) instead of relying on the next open.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At explorer.lua, line 204:

<comment>The stale `<LeftMouse>` auto-close map left by nvzone/menu is only deleted at the start of the *next* `tree_menu()` call. When a menu closes by item click (the common path), the plugin's auto_close map never deletes itself, so a leftover global `<LeftMouse>` mapping stays installed in the file buffer until the user right-clicks again. It re-feeds the built-in press so it usually doesn't visibly break anything, but the cleanup is in the wrong place. Clear it in the menu's after_close path (or have the store delete it as soon as the menu closes) instead of relying on the next open.</comment>

<file context>
@@ -0,0 +1,313 @@
+  -- menu leaves its auto-close <LeftMouse> map behind when a menu is closed
+  -- by an item click rather than an outside click; clear the stale one so
+  -- it cannot stack
+  pcall(vim.keymap.del, "n", "<LeftMouse>")
+  require("menu").open(menu_items(), { mouse = true, border = true })
+end
</file context>

Comment thread README.md
@@ -84,7 +84,11 @@ And underneath it is still real Neovim: `hjkl`, `dd`, `ciw`, macros and

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The README still tells users 'Requires Neovim 0.9+', but this PR bumps install.sh's Neovim gate to 0.10+ (the sidebar's pinned plugins need 0.10). Update the Install details line to match so the documented minimum matches the enforced minimum.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 116:

<comment>The README still tells users 'Requires Neovim 0.9+', but this PR bumps install.sh's Neovim gate to 0.10+ (the sidebar's pinned plugins need 0.10). Update the Install details line to match so the documented minimum matches the enforced minimum.</comment>

<file context>
@@ -109,6 +113,22 @@ t -k      # stop
 
 Non-blocking, with a sound and a desktop notification at zero.
 
+## `Ctrl+B` — a file sidebar, when you want one
+
+The one optional extra: press `Ctrl+B` (or `Cmd+B` on a Mac that forwards it,
</file context>

Comment thread tests/suite_tree.sh

PASS=0; FAIL=0
declare -a BAD=()
while IFS=$'\t' read -r verdict name got; do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Running with a filter that matches nothing (e.g. ./suite_tree.sh clik with a typo) prints PASS=0 FAIL=0 and exits 0, so the run is reported as green even though no case executed. When FILTER is non-empty, guard the success path on at least one matched case (or print the filter in the summary) so a mistyped filter cannot silently pass.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/suite_tree.sh, line 45:

<comment>Running with a filter that matches nothing (e.g. `./suite_tree.sh clik` with a typo) prints `PASS=0 FAIL=0` and exits 0, so the run is reported as green even though no case executed. When FILTER is non-empty, guard the success path on at least one matched case (or print the filter in the summary) so a mistyped filter cannot silently pass.</comment>

<file context>
@@ -0,0 +1,59 @@
+
+PASS=0; FAIL=0
+declare -a BAD=()
+while IFS=$'\t' read -r verdict name got; do
+  case "$verdict" in PASS|FAIL) ;; *) continue ;; esac
+  if [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]]; then continue; fi
</file context>

Comment thread docs/explorer.md
handling lives by, so nothing ever moves by accident.

Inside the tree, drill's file keys say no the same way they do in the
directory listing: `Ctrl+S`, `Ctrl+E`, `Ctrl+R` do nothing there.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The doc promises that Ctrl+E and Ctrl+R "do nothing" inside the tree, but the global mappings still act there: repl_show and run_once only gate on source_state(), which, because the NvimTree buffer fails typing_buffer() (it is not modifiable and is not the netrw listing), simply returns the stale non-nil last_file. So once any file has been opened, pressing Ctrl+E in the tree focuses/opens the interpreter and Ctrl+R runs the file, neither of which is "nothing" — the same applies in the netrw listing the doc cites as the comparison. The suite only asserts "no wedge / no extra window" (tests/tree_drive.py), not a true no-op, so the tests don't back the doc claim. Either narrow the sentence to what actually holds (only Ctrl+S, which save() guards on typing_buffer(), is a no-op) or add a buffer-type guard to repl_show/run_once if the intent is genuinely inert keys in the tree.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/explorer.md, line 33:

<comment>The doc promises that `Ctrl+E` and `Ctrl+R` "do nothing" inside the tree, but the global mappings still act there: `repl_show` and `run_once` only gate on `source_state()`, which, because the NvimTree buffer fails `typing_buffer()` (it is not modifiable and is not the netrw listing), simply returns the stale non-nil `last_file`. So once any file has been opened, pressing `Ctrl+E` in the tree focuses/opens the interpreter and `Ctrl+R` runs the file, neither of which is "nothing" — the same applies in the netrw listing the doc cites as the comparison. The suite only asserts "no wedge / no extra window" (tests/tree_drive.py), not a true no-op, so the tests don't back the doc claim. Either narrow the sentence to what actually holds (only `Ctrl+S`, which `save()` guards on `typing_buffer()`, is a no-op) or add a buffer-type guard to `repl_show`/`run_once` if the intent is genuinely inert keys in the tree.</comment>

<file context>
@@ -0,0 +1,60 @@
+handling lives by, so nothing ever moves by accident.
+
+Inside the tree, drill's file keys say no the same way they do in the
+directory listing: `Ctrl+S`, `Ctrl+E`, `Ctrl+R` do nothing there.
+
+## What it costs when you don't use it
</file context>

Comment thread docs/explorer.md
- *"sidebar plugins are not fetched"* — run `./vendor.sh` in your drill
directory (`~/drill` unless you moved it). `install.sh` does this for you;
a `git pull` upgrade does not.
- *"the sidebar needs nvim 0.10+"* — everything else in drill still works;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: install.sh now hard-requires Neovim 0.10+ (HARD abort), but README.md still states "Requires Neovim 0.9+" and this new line implies the rest of drill runs fine on 0.9. Align the minimum version in README.md (and soften this bullet) so users don't rely on 0.9 support that the installer refuses.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/explorer.md, line 57:

<comment>install.sh now hard-requires Neovim 0.10+ (HARD abort), but README.md still states "Requires Neovim 0.9+" and this new line implies the rest of drill runs fine on 0.9. Align the minimum version in README.md (and soften this bullet) so users don't rely on 0.9 support that the installer refuses.</comment>

<file context>
@@ -0,0 +1,60 @@
+- *"sidebar plugins are not fetched"* — run `./vendor.sh` in your drill
+  directory (`~/drill` unless you moved it). `install.sh` does this for you;
+  a `git pull` upgrade does not.
+- *"the sidebar needs nvim 0.10+"* — everything else in drill still works;
+  the pinned plugins are what need 0.10.
+- `Cmd+B` does nothing — your terminal is not forwarding Cmd. One setting:
</file context>

…ight-click

`d lld-prac main` runs nvim from wherever the shell happened to be, and the
sidebar rooted there -- a tree of your home directory is a tree of everything
except your project. The toggle now roots at the folder of the file under the
caret, freshly on every open, with the file's row highlighted (find_file);
no real file -- the netrw listing, a scratch buffer -- falls back to the cwd
as before. Three new pty cases pin it: open from sub/c.py roots at sub/,
the file is listed, and reopening from a root file moves the root back.

The right-click half was never drill's to fix, but it is drill's to name:
iTerm2 ships a Pointer binding (right button, single click -> Open Context
Menu) that wins over mouse reporting, so nvim never sees the click and the
menu that appears is iTerm2's. docs/explorer.md now walks through removing
it -- Settings -> Pointer -> Bindings, minus button -- after which the click
reaches drill inside the editor while the shell prompt keeps its own menu.

Gate: 722.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
codescene-access[bot]

This comment was marked as outdated.

jainal09 and others added 2 commits August 16, 2026 23:05
The screenshot that reported this showed the quit prompt buried under
"[NvimTree] File system watcher failed (EMFILE)": nvim-tree registers one
filesystem watcher per directory, the pre-rooting sidebar had rooted at the
shell's cwd -- the user's home -- and the fd limit ran out mid-session, with
the error spray eating every Enter meant for the dialog. Watchers are now off
outright: drill's files change from inside the editor, the tree refreshes on
its own actions, and the glue reloads after every drop and mkdir.

The second half of the same failure: a toggle from the REPL or any nameless
buffer passed no better path and fell back to the cwd, yanking a perfectly
good project root back to ~. Now the toggle re-roots only when the buffer
knows better -- a real file (its folder) or the netrw listing (the directory
it shows) -- and keeps the previous root otherwise. Four regression cases:
watchers stay off, netrw roots the tree at its dir, a Cmd+B from the REPL
keeps the root, and the quit suite stays green alongside.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hiding a split hands focus to a neighbor, and with the sidebar open that
neighbor is the tree -- measured: <C-e> from the interpreter parked the caret
in the sidebar in Normal mode, and type_here() correctly refused to insert
there. repl_hide now walks to the first window holding a typing buffer before
landing you; no such window anywhere (a bare listing plus the tree) leaves
focus where it fell, which type_here already treats as nothing-to-do. Three
pty cases pin it, with a real file up top and the tree open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gates Failed
New code is healthy (1 new file with code health below 10.00)
Enforce critical code health rules (1 file with Bumpy Road Ahead)
Enforce advisory code health rules (1 file with Complex Method)

Our agent can fix these. Install it.

Gates Passed
1 Quality Gates Passed

Reason for failure
New code is healthy Violations Code Health Impact
tree_drive.py 2 rules 8.49 Suppress
Enforce critical code health rules Violations Code Health Impact
tree_drive.py 1 critical rule 8.49 Suppress
Enforce advisory code health rules Violations Code Health Impact
tree_drive.py 1 advisory rule 8.49 Suppress

See analysis details in CodeScene

Quality Gate Profile: Clean Code Collective
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants