feat(ui): git status indicator in the bottom bar, with a panel of uncommitted and unpushed work

Optional and per-device (showGitStatus, default off). GET /api/sessions/:id/git-status is
read-only and offline (no fetch, --no-optional-locks), skips remote and Docker sessions, caps its
lists, and single-flights concurrent polls. The toolbar indicator shows uncommitted files,
commits not pushed, or a check; clicking opens a draggable panel in the style of the Files window.

Which repositories: the enclosing one when there is one; otherwise every repository up to two
levels below the working directory (capped, skipping dot-folders and node_modules, never
following symlinks), each in a collapsible section, with the indicator summing them. A repository
that merely sits above the workspace and is the home folder or higher (a dotfiles repo) is
ignored. Git-supplied text is only ever written with textContent.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS
This commit is contained in:
Devvyn
2026-10-05 07:19:11 +08:00
co-authored by Claude Sonnet 5.5
parent ffaa5ee80c
commit bfc164a262
14 changed files with 2528 additions and 1 deletions
+33
View File
@@ -0,0 +1,33 @@
/**
* @fileoverview `GET /api/sessions/:id/git-status`: what the session's workspace has not committed or
* pushed (src/git-workspace-status.ts), for the bottom-bar Git indicator and its panel. The answer is
* an overview: the enclosing repository, or each repository found below a folder that holds several
* projects (see `getGitWorkspaceOverview` for exactly which).
*
* Read-only and offline: it never fetches and never runs a git write command. A remote (SSH) or
* Docker session is not inspected and answers `state: 'unsupported'`: a Docker workspace is writable
* from inside the sandbox, and git here would run on the host. Ownership goes through
* `findSessionOrFail`, like every session-scoped route.
*/
import type { FastifyInstance } from 'fastify';
import type { ApiResponse } from '../../types.js';
import { findSessionOrFail } from '../route-helpers.js';
import {
emptyOverview,
getGitWorkspaceOverview,
type GitRunner,
type GitWorkspaceOverview,
} from '../../git-workspace-status.js';
import type { SessionPort } from '../ports/index.js';
export function registerGitStatusRoutes(app: FastifyInstance, ctx: SessionPort, git?: GitRunner): void {
app.get('/api/sessions/:id/git-status', async (req): Promise<ApiResponse<GitWorkspaceOverview>> => {
const { id } = req.params as { id: string };
const { fresh } = req.query as { fresh?: string };
const session = findSessionOrFail(ctx, id, req);
if (session.remote) return { success: true, data: emptyOverview('unsupported', { reason: 'remote' }) };
if (session.docker) return { success: true, data: emptyOverview('unsupported', { reason: 'docker' }) };
return { success: true, data: await getGitWorkspaceOverview(session.workingDir, { git, fresh: fresh === '1' }) };
});
}