Problem or motivation
When a workspace directory is a git repo, neither the Dashboard workspace view nor the IM /status command surfaces any VCS context. To answer "what branch am I on, am I ahead/behind the remote, how many files are dirty" you currently have to drop into a terminal.
Since pikiclaw exists to drive coding agents that constantly mutate the working tree, this is exactly the context an operator wants at a glance — and from IM there's no terminal at all. A quick "you're on feature/x, 2 ahead of origin, 5 files changed" massively improves situational awareness before you fire off another agent run.
Proposed solution
1. One shared core helper (single source of truth)
New src/core/git.ts exposing readGitStatus(dir): GitStatus | null:
interface GitStatus {
branch: string | null; // null when detached
detached: boolean;
shortSha: string | null; // for the detached case
upstream: string | null; // e.g. "origin/main", null when no tracking branch
ahead: number;
behind: number;
staged: number;
unstaged: number;
untracked: number;
changed: number; // staged + unstaged + untracked — the headline count
}
- One git invocation per call:
git status --porcelain=v2 --branch, parsed for # branch.head, # branch.upstream, # branch.ab +A -B, and counting the 1/2/u/? entry lines. Porcelain v2 is machine-stable across git versions and locales — no parsing of human output.
- Reuse the safe-spawn pattern already in
/api/git-changes (src/dashboard/routes/config.ts:472): spawnSync('git', …, { cwd, timeout: 5000, env: { …process.env, GIT_OPTIONAL_LOCKS: '0' } }).
- Returns
null for not-a-repo / git-not-installed / timeout. Never throws into the status path.
2. IM /status
StatusData already carries workdir. Compute git once in the async getStatusDataAsync (src/bot/commands.ts:636) and add git?: GitStatus to StatusData (commands.ts:619). Add a shared one-line formatter formatGitStatusLine(git) in src/bot/render-shared.ts, so all 7 channels render identical text instead of each reinventing it — each channel's cmdStatus (telegram bot.ts:448, slack bot.ts:199, plus feishu / discord / dingtalk / wecom / weixin) just pushes that one line.
Friendly output, omitted entirely when the workdir isn't a repo:
Git: main ↑2 ↓1 · 5 changed (3 staged · 2 untracked)
Git: feature/x · clean · (no upstream)
Git: (detached a1b2c3d) · 1 changed
3. Dashboard workspace view
- New
GET /api/workspace-git?path=<workdir> (next to the existing /api/git-changes), returning { ok, isGit, git } from the same readGitStatus.
- Frontend
WorkspaceGroup header (dashboard/src/pages/sessions/SessionWorkspace.tsx:1379) renders a compact git badge beside the workspace name: branch pill + ahead/behind arrows + a dirty-count dot, full breakdown on hover. Lazy-fetch when the workspace expands and on the existing refresh action; small TTL cache so polling doesn't hammer git.
Design notes
- Single git read feeds both surfaces — no logic duplication, per-channel code stays thin.
- Bounded (5s timeout, optional-locks off) so a huge repo or a stuck
.git/index.lock never blocks /status or the workspace poll.
- Graceful degrade: not-a-repo / git-missing / detached / no-upstream are all first-class states, never an error toast.
Alternatives considered
- Extend the existing
/api/git-changes: it only lists changed files vs HEAD (no branch, no ahead/behind, excludes untracked) and is per-file. The header wants a cheap summary, so a dedicated summary endpoint backed by the shared helper is cleaner — both can call readGitStatus.
- Inline git in each channel / the SPA: rejected — would duplicate spawn+parse in 8+ places and drift over time.
Area
Dashboard (also touches Channels /status and Session management)
Problem or motivation
When a workspace directory is a git repo, neither the Dashboard workspace view nor the IM
/statuscommand surfaces any VCS context. To answer "what branch am I on, am I ahead/behind the remote, how many files are dirty" you currently have to drop into a terminal.Since pikiclaw exists to drive coding agents that constantly mutate the working tree, this is exactly the context an operator wants at a glance — and from IM there's no terminal at all. A quick "you're on
feature/x, 2 ahead of origin, 5 files changed" massively improves situational awareness before you fire off another agent run.Proposed solution
1. One shared core helper (single source of truth)
New
src/core/git.tsexposingreadGitStatus(dir): GitStatus | null:git status --porcelain=v2 --branch, parsed for# branch.head,# branch.upstream,# branch.ab +A -B, and counting the1/2/u/?entry lines. Porcelain v2 is machine-stable across git versions and locales — no parsing of human output./api/git-changes(src/dashboard/routes/config.ts:472):spawnSync('git', …, { cwd, timeout: 5000, env: { …process.env, GIT_OPTIONAL_LOCKS: '0' } }).nullfor not-a-repo / git-not-installed / timeout. Never throws into the status path.2. IM
/statusStatusDataalready carriesworkdir. Computegitonce in the asyncgetStatusDataAsync(src/bot/commands.ts:636) and addgit?: GitStatustoStatusData(commands.ts:619). Add a shared one-line formatterformatGitStatusLine(git)insrc/bot/render-shared.ts, so all 7 channels render identical text instead of each reinventing it — each channel'scmdStatus(telegrambot.ts:448, slackbot.ts:199, plus feishu / discord / dingtalk / wecom / weixin) just pushes that one line.Friendly output, omitted entirely when the workdir isn't a repo:
3. Dashboard workspace view
GET /api/workspace-git?path=<workdir>(next to the existing/api/git-changes), returning{ ok, isGit, git }from the samereadGitStatus.WorkspaceGroupheader (dashboard/src/pages/sessions/SessionWorkspace.tsx:1379) renders a compact git badge beside the workspace name: branch pill + ahead/behind arrows + a dirty-count dot, full breakdown on hover. Lazy-fetch when the workspace expands and on the existing refresh action; small TTL cache so polling doesn't hammer git.Design notes
.git/index.locknever blocks/statusor the workspace poll.Alternatives considered
/api/git-changes: it only lists changed files vsHEAD(no branch, no ahead/behind, excludes untracked) and is per-file. The header wants a cheap summary, so a dedicated summary endpoint backed by the shared helper is cleaner — both can callreadGitStatus.Area
Dashboard (also touches Channels
/statusand Session management)