Skip to content

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

Merged
MyPrototypeWhat merged 7 commits into
MiniMax-AI:mainfrom
Microbiosis:add-git-tree
Sep 25, 2026
Merged

MyPrototypeWhat merged 7 commits into
MiniMax-AI:mainfrom
Microbiosis:add-git-tree

Conversation

@Microbiosis

@Microbiosis Microbiosis commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

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.

@MyPrototypeWhat MyPrototypeWhat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the resubmission — the five e2e fixes all check out against the code, and the package structure, ZCode attribution, and synthetic preview all look good.

Since this is a community repo we mostly gate on the submission rules and on what users are told, not on internal code quality. Three things to sort out before merge:

1. Author attribution is inconsistent

The directory is plugins/microbiosis/ and the PR is from Microbiosis, but the root README.md / README.zh-CN.md rows link to github.com/redmingwei, .minimax-plugin/plugin.json has "author": "redmingwei", and the plugin README says Author: Microbiosis (unlinked). Per CONTRIBUTING the author column should point at the GitHub username that owns the directory — please pick one and use it everywhere. The other plugins use Author: [name](https://github.com/name) in their README.

2. README needs to cover file access and network requests

CONTRIBUTING asks each plugin README to explain "file access, and network requests". Network isn't mentioned at all — a one-liner like "makes no network requests" is enough if that's the case. On file access, the fallback scan on macOS/Linux also walks the parent of $HOME (server.mjs:252-257, i.e. /Users or /home, so other users' home directories), and the Windows A:\–Z:\ scan will touch mapped network drives. Users should be able to see that scope before installing.

3. "Users with an explicit repos.json are unaffected" isn't true as written

server.mjs:310 only checks scanBases.size === 0, and entries from config.repos never go into scanBases, so a repos.json with only repos (no scanRoots) still triggers the full home/drive scan. Either also gate the fallback on config.repos being empty, or adjust the README/PR wording — whichever you prefer, as long as the doc matches the behaviour.

Non-blocking, in case you want to tidy up while you're in there: the README's "refs chip with is-head/is-tag" never renders (client reads commit.refs at index.html:1209, server only sends tags); "first 200 commits (configurable via repos.json)" is actually a hard-coded PAGE_LIMIT; and the 35 s client timeout is 60 s for graph calls (LONG_TIMEOUT_MS).

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.
The repo contract added by upstream now expects these headings in every
plugin README. Fill them in with the real facts: local git reads only,
no network egress, and the exact tested desktop version + Windows build.
- Attribute the plugin to Microbiosis (gh api users/redmingwei returns 404: redmingwei is the GitHub profile display name, not a login, so the README link github.com/redmingwei was dead). Unified across both root indexes, plugin.json and both plugin READMEs.
- Drop the unused refs chip row: the server never sends a per-commit refs field, so refsHtml() always returned an empty string and .row-refs / .ref-chip.is-head were dead code. Branches/tags still render in the detail panels via .ref-chip.is-tag.
- Gate the whole-machine scan fallback on config.repos.length === 0 as well, so users who declared repos explicitly never trigger a drive-root scan.
- Correct docs: 200 commits is a fixed client page size, not a repos.json setting; client timeout is 35s default and 60s for graph requests; expand Data & access into read / discovery / write / network sections.
@Microbiosis
Microbiosis force-pushed the add-git-tree branch 2 times, most recently from 3bb5e69 to ba9457f Compare September 24, 2026 12:50
@Microbiosis

Copy link
Copy Markdown
Contributor Author

Thanks — all six points are addressed in ba9457f (rebase onto current main shifted some line numbers).

Blocking

  1. Attribution — unified on Microbiosis in both root indexes, plugin.json, and both plugin READMEs, now using the same linked form as the other plugins: Author: [Microbiosis](https://github.com/Microbiosis).
  2. File access & network — both plugin READMEs gain a Data & access section covering reads, the full discovery scan surface (home dev dirs + walk-up; Windows A:\–Z:\ incl. mapped network drives; macOS/Linux parent of $HOME, i.e. other accounts' home directory names), the single write <dataDir>/prefs.json, and an explicit "no outbound network requests".
  3. Scan gate — took the code option: the fallback now also requires config.repos.length === 0, and the README wording matches.

Non-blocking

  • refs-chip claim and its dead code (refsHtml(), .row-refs, .ref-chip.is-head) removed.
  • 200 now documented as a fixed client page size, not a repos.json setting.
  • Client timeout documented as 35 s default, 60 s for graph calls.

@MyPrototypeWhat
MyPrototypeWhat merged commit d9d9523 into MiniMax-AI:main Sep 25, 2026
2 checks passed
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