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
+9 -1
View File
@@ -450,6 +450,7 @@ Object.assign(CodemanApp.prototype, {
document.getElementById('appSettingsShowSessionButton').checked = settings.showSessionButton ?? defaults.showSessionButton ?? false;
document.getElementById('appSettingsShowAwayDigestButton').checked = settings.showAwayDigestButton ?? defaults.showAwayDigestButton ?? false;
document.getElementById('appSettingsShowCronButton').checked = settings.showCronButton ?? defaults.showCronButton ?? false;
document.getElementById('appSettingsShowGitStatus').checked = settings.showGitStatus ?? defaults.showGitStatus ?? false;
// Gesture control lives in the Input section (alongside Local Echo / CJK Input)
// but is only available when the instance runs with CODEMAN_GESTURE=1 (server sets
// window.__codemanGestureAvailable). Hide just this item otherwise so the toggle
@@ -2422,6 +2423,7 @@ Object.assign(CodemanApp.prototype, {
showSessionButton: document.getElementById('appSettingsShowSessionButton').checked,
showAwayDigestButton: document.getElementById('appSettingsShowAwayDigestButton').checked,
showCronButton: document.getElementById('appSettingsShowCronButton').checked,
showGitStatus: document.getElementById('appSettingsShowGitStatus').checked,
gestureControlEnabled: document.getElementById('appSettingsGestureControl').checked,
subagentTrackingEnabled: document.getElementById('appSettingsSubagentTracking').checked,
subagentActiveTabOnly: document.getElementById('appSettingsSubagentActiveTabOnly').checked,
@@ -2674,6 +2676,8 @@ Object.assign(CodemanApp.prototype, {
showSessionButton: _ssb,
showAwayDigestButton: _adb,
showCronButton: _crb,
// Per-device bottom-bar indicator, absent from SettingsUpdateSchema (.strict()): it must not reach the PUT.
showGitStatus: _sgs,
showTabDetachButton: _tdb,
// Phone-only home surface, and absent from SettingsUpdateSchema (.strict()).
mobileOverviewEnabled: _mov,
@@ -3613,6 +3617,10 @@ Object.assign(CodemanApp.prototype, {
cronBtn.classList.toggle('btn-cron--hidden', !showCronButton);
}
// Bottom-bar Git indicator (git-status-ui.js): opt-in, per-device. Starts or stops its poll to
// match the setting, so a live toggle needs no reload.
this.applyGitStatusVisibility?.();
// Notification bell is retired (notifications live in Settings → Notifications
// + the drawer); keep it hidden regardless of the notification-enabled state.
const notifBtn = document.querySelector('.btn-notifications');
@@ -3961,7 +3969,7 @@ Object.assign(CodemanApp.prototype, {
'language',
'terminalWheelLocalScrollback',
'autoCopySelection', 'copyStripMargin',
'showSessionButton', 'showAwayDigestButton', 'showCronButton',
'showSessionButton', 'showAwayDigestButton', 'showCronButton', 'showGitStatus',
'showTabDetachButton',
'mobileOverviewEnabled',
'sessionLineageLines',