Skip to content

Add git-tree: local Git commit graph viewer - #8

Open
Microbiosis wants to merge 5 commits into
MiniMax-AI:mainfrom
Microbiosis:add-git-tree
Open

Microbiosis wants to merge 5 commits into
MiniMax-AI:mainfrom
Microbiosis:add-git-tree

Conversation

@Microbiosis

@Microbiosis Microbiosis commented Sep 22, 2026

Copy link
Copy Markdown

Summary

Adds git-tree — a MiniApp that inspects a local Git repository's commit history.

Plugin directory: plugins/microbiosis/git-tree/
Plugin ID (name in .minimax-plugin/plugin.json): git-tree
Author (plugin.json): redmingwei

This is a re-submission of the previously closed PR #7. The five bugs below
were found by running the plugin for real — live server plus a real browser
driven by Puppeteer — and are fixed in this branch. All five were invisible to
syntax/import smoke tests; each one was reproduced first, then fixed, then
re-verified through the same interfaces.

Bugs fixed since #7

  1. Paginated graph layout was per-page, not per-window. buildGraph() is
    stateful across rows (lane colour reuse, merge-path detection,
    lockedFirst), so laying out page N in isolation disagreed with the pages
    around it: a branch opened on page 1 lost its lane on page 2, and merges
    stopped resolving once scrolled past the first page. Now the whole
    accumulated window is laid out. git log -n offset+limit+1 already fetched
    those rows, so this costs no extra git call; only the current page's commits
    go over the wire.
  2. Client concatenated the graph geometry arrays. They are absolute-indexed
    and describe the whole accumulated list, so concat duplicated every page's
    rows and left the canvas sized to the first page only. They now replace,
    followed by applyGraphSize() + renderCanvas() so rows past page 1 get
    lanes and dots.
  3. A cleared filter could not be persisted. sanitizePrefs() dropped empty
    strings, so an empty ref — how the client says the filter was cleared —
    was silently discarded and the merge resurrected the old value on reload.
  4. An oversized prefs body reset the connection instead of answering.
    req.destroy() tore the socket down before sendJson could write the
    response, so the client saw a bare connection reset rather than the designed
    error body. Now the stream drains and the caller returns a real
    413 body_too_large.
  5. Two normalisation bugs. A repos.json written by a Windows editor
    carries a UTF-8 BOM that JSON.parse rejects, which made an explicitly
    configured allowlist fail silently and fall back to discovery. And
    buildRegistry() pushed raw D:/foo paths while defaultRepo handed the
    client resolve()d ones, so the repo dropdown could not match its own
    option list and rendered as "nothing selected".

Features

  • Swim-lane commit graph rendered as a single SVG canvas (lanes, paths, dots, hover highlight, selection ring); the lane-assignment algorithm in miniapp/node/git-graph.mjs is a JavaScript port of zai-org/ZCode's layoutAlgorithm.ts / layout.ts, which are Apache-2.0. Attribution is preserved in LICENSE and README.md.
  • Stats panel: total commits, branches, tags, working-tree changes (7 parallel git commands, cached 30 s per repo).
  • Graph pane: git log <ref> --topo-order + optional --grep / --author; up to 400 rows per call; result server-validated, client renders one SVG.
  • Commit detail: subject, refs, file changes table, body. Two independent views — inline (collapsible below the list) and a right-pane (always on).
  • Filter preferences (theme, repo, ref, q, author, interval) persisted to <dataDir>/prefs.json; merged writes via /api/prefs (POST never wipes siblings), 400 ms client-side debounce.
  • Optional auto-refresh (5 / 10 / 30 / 60 s) with manual refresh still available; preserves selected commit, scroll position, and inline-detail state across ticks; countdown chip next to the refresh button.
  • Per-command git timeouts: 10 s quick / 15 s medium / 30 s heavy; client api() caps at 35 s with AbortController. API responses ≥ 256 bytes are gzipped when Accept-Encoding: gzip is sent.
  • Portability fallback for repos.json empty installs: when the user has not authored repos.json and the plugin is not installed inside any git tree, the registry also scans ~/Code, ~/Projects, ~/repos, ~/workspace, ~/src, ~/source, ~/dev, ~/work, ~/Documents, ~/git (case-insensitive on Windows/macOS), plus on Windows every mounted drive root (A:\Z:\). Windows system hives (Program Files, Windows, Users, ProgramData, $Recycle.Bin, …) and macOS resource dirs (Library, Applications, System) are filtered so widening to drive roots cannot recurse into %ProgramFiles%. The fallback only activates when no other discovery source is present, so users with an explicit repos.json are unaffected.

File layout

plugins/microbiosis/git-tree/
├── .minimax-plugin/plugin.json     # manifest (name, displayName, version, license)
├── docs/preview.jpg                # preview image (synthetic data)
├── icon.png
├── miniapp/
│   ├── client/index.html          # single file, single SVG canvas, inline script
│   ├── miniapp.json
│   └── node/
│       ├── git-graph.mjs         # lane-assignment algorithm (port from ZCode)
│       ├── repos.json            # ships empty by design
│       └── server.mjs            # entry; binds the host listen port
├── package.json
├── LICENSE                        # Apache-2.0 with ZCode attribution block
├── README.md                      # English
└── README.zh-CN.md                # 简体中文

What was tested

  • MiniMax Code desktop 3.0.73.166, Windows 10.0.26200 (x64).
  • Synthetic git repository with 5 commits, 1 merge, 2 branches — preview screenshot in docs/preview.jpg is generated from this synthetic data.
  • End-to-end check of the portability fallback on this Windows host confirmed discovery of git repositories under D:\ and E:\ without any repos.json configuration.
  • Full end-to-end run after the five fixes, against a real server bound to the host listen port and driven by a real Chrome via Puppeteer:
    • API harness: 47/47 passing — repo discovery, overview, graph pagination (including the multi-page lane-continuity cases that motivated fix 1), commit detail, and prefs read/write (including the empty-string and oversize-body cases behind fixes 3 and 4).
    • Browser harness: 24/24 passing — real DOM/canvas interaction, pagination, filters, detail panes, refresh, persistence across reload.
    • Additionally verified through the installed Host copy (not just the source tree): the process is really listening, GET /git-tree returns 200, and the fixed endpoints return the corrected payloads.

Not verified: macOS and Linux. The portability fallback is implemented for both platforms (Unix home parent + common dev directories; Windows drive roots), but only the Windows path has live confirmation.

Submission checklist

  • Plugin directory placed under plugins/<github-username>/<plugin-id>/.
  • Plugin ID is unique in the repository (no git-tree collision).
  • .minimax-plugin/, package.json, miniapp/, and runtime assets preserved.
  • English README.md (with mutual link to README.zh-CN.md); documented install path, verified client version + OS, configuration, and required file / network access.
  • LICENSE (Apache-2.0) with explicit attribution to zai-org/ZCode for the ported layout algorithm.
  • No node_modules/, no secrets, no real session data. Screenshot uses synthetic data.
  • Plugin can be copied and used independently; no dependency on other directories in this repository.
  • New row added to both root READMEs (English and 简体中文) and a preview block.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

redmingwei and others added 5 commits September 22, 2026 18:13
MiniApp that inspects a local Git repository's commit history:
- Swim-lane commit graph (single SVG canvas, ported from zai-org/ZCode)
- Branch / tag / working-tree status from parallel git calls
- Inline (collapsible) and right-pane (always-on) commit detail
- Filter state, theme, and refresh cadence persisted to <dataDir>/prefs.json
- Portability fallback: on a fresh install, scans home dev directories
  and every mounted drive root on Windows to discover repos without
  any repos.json configuration

Tested environment: MiniMax Code desktop 3.0.73.166 on Windows 10.0.26200
(x64). macOS and Linux not verified.
When the portability fallback kicks in (no `repos.json`, no walk-up match),
the registry now recurses one level into first-level entries that look like
a dev parent (`github`, `code`, `projects`, `workspace`, `src`, `dev`,
`work`, `git`, `repos`, plus CJK variants `代码` / `项目` / `工程` / `源码`).

This catches the common layout `D:\Github\<repo>\` where the user's
projects live two levels deep under the drive root, which the previous
single-level scan missed. On the maintainer's Windows machine this lifts
the discovery count from 14 to 27 (the 13 new entries are all under
`D:\Github\`).

Cost is bounded: one extra `readdir` per matching dev-parent entry, and
we only recurse when the immediate child does not already carry `.git`.
The dev-parent name set is intentionally narrow (no fuzzy heuristics like
`play` / `stuff`) so the recursion cannot walk into arbitrary folders.

- Update `miniapp/node/server.mjs`: add `DEV_PARENT_HINTS` set and
  `isLikelyDevParentName`, recurse one level inside `buildRegistry`'s
  scan loop when the entry matches.
- Update `README.md` and `README.zh-CN.md` Data sources section to
  describe the recursive step.
The previous `slice(0, 60)` was applied to *every* scan-base entry before
the `.git` check, which silently dropped legitimate repos that happened
to land past index 60 in the alphabetical listing.

Concrete failure on the maintainer's Windows host: `D:\` has ~110
non-skipped entries; `D:\synthetic-git-repo` sat at index 81 and never
reached the `.git` stat. After this change, all 27 repos are discovered
(arcreel-connect, synthetic-git-repo, plus the 13 `D:\Github\<repo>\`
projects and 12 on `E:\`).

- Direct `.git` check is now uncapped. One stat per non-skipped entry
  on a local SSD is cheap enough that the cap was never justified.
  Stat calls run in parallel via `Promise.all` to keep wall time flat.
- The dev-parent recursion (which costs an extra `readdir` per matching
  entry) keeps the 60-entry cap to bound worst-case fan-out.
- Set `repos[0].isDefault = true` and `defaultRepo = repos[0].path`
  in the fallback path so the client dropdown opens with a selection
  instead of forcing the user to pick one manually when no walk-up
  produced a preferred repo.

Verified end-to-end against `D:\synthetic-git-repo`: 5 commits, 2
lanes, merge commit correctly identified at depth 1 with parent edges
on both lanes.
The port had two deviations from upstream's
`packages/ui/src/git-graph/layout.ts`:

- `curveOffset = Math.max(8, Math.abs(toY - fromY) * 0.5)` →
  `Math.max(14, Math.abs(toY - fromY) * 0.38)`. The minimum offset
  of 14 and the 0.38 factor produce gentler Bezier curves at
  short row distances and match what ZCode renders. The previous
  values were tighter curves that diverged from the upstream
  visual.
- File header comment claimed the port emits per-row primitives
  (stale from an earlier draft that the client has since
  replaced with a single SVG canvas). Updated to describe the
  actual divergence (default pixel sizes reduced for the Mini App
  surface) and to claim byte-equivalence for the algorithm and
  curve constants.

Also dropped two unused fields (`laneIndices`, `laneByHash`) that
were computed by `createGitGraphLayoutModel` but never read by
any consumer. The returned shape now matches upstream exactly.

Verified end-to-end against a synthetic merge graph (HEAD at
index 0, oldest at the tail — the order `git log --topo-order`
produces). Output paths include both straight-line edges on
the same lane and Bezier curves on cross-lane connections, with
`curveOffset = max(14, |dy| * 0.38)` exactly matching the
upstream formula.
Every fix below was reproduced against a running server and a real
browser before being written; none were visible to a syntax or import
smoke test.

1. Paginated graph layout was per-page, not per-window. buildGraph() is
   stateful across rows (lane colour reuse, merge-path detection,
   lockedFirst), so laying out page N in isolation disagreed with the
   pages around it: a branch opened on page 1 lost its lane on page 2,
   and merges stopped resolving once scrolled past the first page. Lay
   out the whole accumulated window instead - git log already fetched
   those rows, so this costs no extra call. Only the current page
   commits go over the wire.

2. The client concatenated the graph geometry arrays on each page. They
   are absolute-indexed and describe the whole accumulated list, so
   concat duplicated every page rows and left the canvas sized to the
   first page only. Replace them, then re-run applyGraphSize() +
   renderCanvas() so rows past page 1 get lanes and dots.

3. A cleared filter could not be persisted. sanitizePrefs() dropped
   empty strings, so an empty ref - how the client says the filter was
   cleared - was silently discarded and the merge resurrected the old
   value on reload. Accept the empty string as a meaningful value; for
   the ref key it normalises to the default all scope.

4. An oversized prefs body reset the connection instead of answering.
   req.destroy() tore the socket down before sendJson could write the
   response, so the client saw a bare reset rather than the designed
   error. Stop accumulating, let the stream drain, and reject with a
   sentinel that becomes a real 413 body_too_large response.

5. Two path/config normalisation bugs. repos.json written by a Windows
   editor carries a UTF-8 BOM that JSON.parse rejects, which made an
   explicitly configured allowlist fail silently and fall back to
   discovery - parse it with the BOM stripped. And buildRegistry()
   pushed raw D:/foo paths while defaultRepo handed the client
   resolve()d ones, so the repo dropdown could not match its own option
   list and rendered as nothing selected.
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.

1 participant