Files
awesome-copilot/extensions/git-worktree-explorer/extension.mjs
T
ca87445cb2 Add Git Worktree Explorer canvas (#2344)
* Add Git Worktree Explorer canvas

Add an interactive repository, worktree, branch, and commit graph with GitHub PR enrichment, safe inspection actions, and shared lane visualization.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 69dd5824-7094-4b82-8cfa-83c2fbe53307

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Address review feedback for Git Worktree Explorer

Security
- Quote copied shell commands (git -C / git log) with shell-safe single quoting and --end-of-options via new public/shell-quote.mjs
- Move repository path into the untrusted data block of the commit Ask Copilot prompt

Performance
- Compute branch divergence with a single `git for-each-ref %(ahead-behind:...)` query (Git 2.41+), falling back to rev-list with bounded concurrency (8) instead of one process per branch
- Drop the unused `git show --stat` summary from commit details

Correctness
- Only attach same-repository pull requests to local branches (filter isCrossRepository / headRepositoryOwner)
- Resolve the default branch by full name or upstream instead of a suffix match (server + client)
- Preserve already-loaded commit pages when a load-more request fails; only reset requests show the error state
- Size the lane graph from the widest row (lanes + branch badges + text) and collapse >3 branch tips into a "+N more" badge

Accessibility / UX
- Add aria-pressed to view toggles and graph nodes; replace the unimplemented ARIA tree with group/button semantics
- preventDefault on Space/Enter for branch badges so keyboard activation does not scroll
- Use a lane palette that meets 3:1 contrast in both light and dark themes (tested)
- Mobile inspector no longer auto-opens on snapshot load and gains a close button

Tests: 19 -> 29

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 18350039-5e2b-40f0-b537-bc22cabcbb1b

* Migrate git-worktree-explorer to plugins/ manifest layout

Move the manifest from extensions/git-worktree-explorer/.github/plugin/plugin.json to plugins/git-worktree-explorer/plugin.json with README and copilot-extension.json, matching the extensions-container migration (#2334). Regenerated marketplace.json and docs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 18350039-5e2b-40f0-b537-bc22cabcbb1b

* Address follow-up review: shell-specific quoting and sibling row controls

- quoteShellArg/formatShellCommand take a target shell; PowerShell doubles apostrophes while POSIX uses '\\''. app.js detects the platform and reports which syntax was copied.
- Commit rows no longer nest branch badge buttons inside the row button; the row button and badges are sibling controls.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 18350039-5e2b-40f0-b537-bc22cabcbb1b

* Make the closed mobile inspector inert and restore focus to the graph

When the inspector overlay is dismissed on narrow viewports it is now marked inert/aria-hidden so its controls leave the tab order and accessibility tree, focus returns to the selected graph control, Escape closes it, and viewport changes re-sync the state.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 18350039-5e2b-40f0-b537-bc22cabcbb1b

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Aaron Powell <me@aaron-powell.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot-Session: 69dd5824-7094-4b82-8cfa-83c2fbe53307
Copilot-Session: 18350039-5e2b-40f0-b537-bc22cabcbb1b
2026-08-26 10:25:00 +10:00

95 lines
4.3 KiB
JavaScript

import { CanvasError, createCanvas, joinSession } from "@github/copilot-sdk/extension";
import { getServerEntry, refreshServer, startServer, stopServer } from "./server.mjs";
const session = await joinSession({
canvases: [
createCanvas({
id: "git-worktree-explorer",
displayName: "Git Worktree Explorer",
description: "Explore the active Git repository through worktrees, branches, commits, and related GitHub pull requests.",
inputSchema: {
type: "object",
additionalProperties: false,
properties: {
startAt: {
type: "string",
enum: ["repository"],
description: "Initial topology level.",
},
},
},
actions: [
{
name: "refresh",
description: "Refresh Git and GitHub information shown by an open explorer.",
inputSchema: {
type: "object",
additionalProperties: false,
properties: {},
},
handler: async (ctx) => {
try {
const snapshot = await refreshServer(ctx.instanceId);
return {
gatheredAt: snapshot.gatheredAt,
worktrees: snapshot.worktrees.length,
branches: snapshot.branches.length,
};
} catch (error) {
throw new CanvasError("git_refresh_failed", error.message);
}
},
},
{
name: "focus_node",
description: "Ask an open explorer to focus a repository, worktree, or branch node by its canvas node ID.",
inputSchema: {
type: "object",
additionalProperties: false,
properties: {
nodeId: { type: "string", minLength: 1 },
},
required: ["nodeId"],
},
handler: async (ctx) => {
const entry = getServerEntry(ctx.instanceId);
if (!entry) throw new CanvasError("canvas_not_open", "Canvas instance is not open.");
const nodeId = ctx.input?.nodeId;
const snapshot = entry.snapshot;
const exists = nodeId === "repository"
|| snapshot.worktrees.some((item) => item.id === nodeId)
|| snapshot.branches.some((item) => item.id === nodeId);
if (!exists) throw new CanvasError("git_node_not_found", `Git node not found: ${nodeId}`);
for (const client of entry.clients) {
client.write(`event: focus\ndata: ${JSON.stringify({ nodeId })}\n\n`);
}
return { nodeId };
},
},
],
open: async (ctx) => {
const cwd = ctx.session?.workingDirectory;
if (!cwd) {
throw new CanvasError("workspace_unavailable", "The active session working directory is unavailable.");
}
try {
const entry = await startServer(ctx.instanceId, {
cwd,
sendPrompt: async (prompt) => session.send({ prompt }),
});
return {
title: "Git Worktree Explorer",
status: `${entry.snapshot.worktrees.length} worktrees · ${entry.snapshot.branches.length} branches`,
url: entry.url,
};
} catch (error) {
throw new CanvasError("git_repository_unavailable", error.message);
}
},
onClose: async (ctx) => {
await stopServer(ctx.instanceId);
},
}),
],
});