Bounded local observation
apps/backend/internal/agentctl/server/process/git_contribution_history.go ↗The operator checks reflogs and counts with timeouts and limits and never fetches or rewrites refs.
Result type
type ContributionHistoryExplanationResult struct {
Repo string `json:"repo,omitempty"`
Branch string `json:"branch"`
ExpectedLocalHead string `json:"expected_local_head"`
ExpectedRemoteHead string `json:"expected_remote_head"`
Kind string `json:"kind"`
Reason string `json:"reason"`
OntoHead string `json:"onto_head,omitempty"`
TaskCommitCount *int `json:"task_commit_count,omitempty"`
PublishedCommitCount *int `json:"published_commit_count,omitempty"`
NewBaseCommitCount *int `json:"new_base_commit_count,omitempty"`
}
Bounded checks
func (g *GitOperator) ExplainContributionHistory(ctx context.Context, branch, expectedLocalHead, expectedRemoteHead string) (*ContributionHistoryExplanationResult, error) {
result := newContributionHistoryResult(g.repoName, branch, expectedLocalHead, expectedRemoteHead)
observeCtx, cancel := context.WithTimeout(ctx, contributionHistoryObservationTimeout)
defer cancel()
if err := g.requireNoContributionHistoryRebase(observeCtx); err != nil {
result.Reason = contributionHistoryFailureReason(err, contributionHistoryReasonAmbiguous)
return result, nil
}
if err := g.requireContributionHistoryIdentity(observeCtx, branch, expectedLocalHead); err != nil {
result.Reason = contributionHistoryFailureReason(err, contributionHistoryReasonUnavailable)
return result, nil
}
ontoHead, err := g.findContributionHistoryRebase(observeCtx, branch, expectedLocalHead, expectedRemoteHead)
if err != nil {
result.Reason = contributionHistoryFailureReason(err, contributionHistoryReasonNoMatch)
return result, nil
}
result.Kind = contributionHistoryKindLocalRebase
result.Reason = contributionHistoryReasonMatched
result.OntoHead = ontoHead
return result, nil
}
Reflog match
func matchContributionHistoryBranchReflog(entries []contributionHistoryReflogEntry, branch, localHead, remoteHead string) (string, error) {
if len(entries) < 2 || !strings.EqualFold(entries[0].sha, localHead) || !strings.EqualFold(entries[1].sha, remoteHead) {
return "", errors.New("no matching branch reflog")
}
prefix := "rebase (finish): refs/heads/" + branch + " onto "
if !strings.HasPrefix(entries[0].subject, prefix) {
return "", errors.New("no matching branch reflog")
}
ontoHead := strings.TrimSpace(strings.TrimPrefix(entries[0].subject, prefix))
if !isFullCommitSHA(ontoHead) {
return "", errContributionHistoryAmbiguous
}
return ontoHead, nil
}
WebSocket handler
apps/backend/internal/agent/handlers/git_handlers.go ↗The handler validates branch and heads and forwards the read-only request to agentctl.
Request type
type GitContributionHistoryExplanationRequest struct {
SessionID string `json:"session_id"`
Repo string `json:"repo,omitempty"`
Branch string `json:"branch"`
ExpectedLocalHead string `json:"expected_local_head"`
ExpectedRemoteHead string `json:"expected_remote_head"`
}
Handler
func (h *GitHandlers) wsContributionHistoryExplanation(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
var req GitContributionHistoryExplanationRequest
if err := msg.ParsePayload(&req); err != nil {
return nil, fmt.Errorf("invalid payload: %w", err)
}
if req.SessionID == "" {
return nil, fmt.Errorf("session_id is required")
}
for field, value := range map[string]string{
"branch": req.Branch, "expected_local_head": req.ExpectedLocalHead, "expected_remote_head": req.ExpectedRemoteHead,
} {
if value == "" {
return nil, fmt.Errorf("%s is required", field)
}
}
agentClient, releaseClient, err := h.getAgentCtlClient(ctx, req.SessionID)
defer releaseClient()
if err != nil {
return nil, err
}
result, err := agentClient.GitContributionHistoryExplanation(ctx, req.Branch, req.ExpectedLocalHead, req.ExpectedRemoteHead, req.Repo)
if err != nil {
return nil, fmt.Errorf("contribution history explanation failed: %w", err)
}
return ws.NewResponse(msg.ID, msg.Action, result)
}
Registration
func (h *GitHandlers) RegisterHandlers(d *ws.Dispatcher) {
d.RegisterFunc(ws.ActionWorktreePull, h.wsPull)
d.RegisterFunc(ws.ActionWorktreePush, h.wsPush)
d.RegisterFunc(ws.ActionWorktreeReplaceContribution, h.wsReplaceContribution)
d.RegisterFunc(ws.ActionWorktreeUseContribution, h.wsUseContribution)
d.RegisterFunc(ws.ActionWorktreeContributionHistoryExplanation, h.wsContributionHistoryExplanation)
d.RegisterFunc(ws.ActionWorktreeRebase, h.wsRebase)
}
Agentctl client
apps/backend/internal/agent/runtime/agentctl/git.go ↗The client posts the branch and heads to agentctl and decodes the bounded result.
Client call
func (c *Client) GitContributionHistoryExplanation(ctx context.Context, branch, expectedLocalHead, expectedRemoteHead, repo string) (*ContributionHistoryExplanationResult, error) {
payload := struct {
Branch string `json:"branch"`
ExpectedLocalHead string `json:"expected_local_head"`
ExpectedRemoteHead string `json:"expected_remote_head"`
Repo string `json:"repo,omitempty"`
}{
Branch: branch, ExpectedLocalHead: expectedLocalHead, ExpectedRemoteHead: expectedRemoteHead, Repo: repo,
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/v1/git/contribution/history-explanation", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
responseBody, err := readResponseBody(resp)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
var result ContributionHistoryExplanationResult
if err := json.Unmarshal(responseBody, &result); err != nil {
return nil, fmt.Errorf("failed to parse response (status %d, body: %s): %w", resp.StatusCode, truncateBody(responseBody), err)
}
if resp.StatusCode >= 400 {
return &result, fmt.Errorf("git contribution history explanation failed with status %d", resp.StatusCode)
}
return &result, nil
}
Agentctl HTTP endpoint
apps/backend/internal/agentctl/server/api/git.go ↗The endpoint validates required fields and delegates to the Git operator.
Request type
type GitContributionHistoryExplanationRequest struct {
Branch string `json:"branch"`
ExpectedLocalHead string `json:"expected_local_head"`
ExpectedRemoteHead string `json:"expected_remote_head"`
Repo string `json:"repo,omitempty"`
}
Handler
func (s *Server) handleGitContributionHistoryExplanation(c *gin.Context) {
var req GitContributionHistoryExplanationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, process.GitOperationResult{Success: false, Operation: "contribution_history_explanation", Error: "invalid request: " + err.Error()})
return
}
for _, required := range []struct{ name, value string }{
{name: "branch", value: req.Branch},
{name: "expected_local_head", value: req.ExpectedLocalHead},
{name: "expected_remote_head", value: req.ExpectedRemoteHead},
} {
if required.value == "" {
c.JSON(http.StatusBadRequest, process.GitOperationResult{Success: false, Operation: "contribution_history_explanation", Error: required.name + " is required"})
return
}
}
gitOp := s.gitOpForRepo(c, "contribution_history_explanation", req.Repo)
if gitOp == nil {
return
}
result, err := gitOp.ExplainContributionHistory(c.Request.Context(), req.Branch, req.ExpectedLocalHead, req.ExpectedRemoteHead)
if err != nil {
s.handleGitError(c, "contribution_history_explanation", err)
return
}
c.JSON(http.StatusOK, result)
}
Frontend shared hook
apps/web/hooks/domains/session/use-contribution-history-explanation.ts ↗The hook shares one in-flight request per identity and discards stale responses.
Key and validation
export function contributionHistoryExplanationKey(target: ContributionHistoryExplanationTarget | null): string | null {
if (!targetIsRequestable(target)) return null;
return JSON.stringify([
target.workspaceId,
target.sessionId,
target.repositoryScope,
target.branch,
target.expectedLocalHead,
target.expectedRemoteHead,
]);
}
function targetIsRequestable(target: ContributionHistoryExplanationTarget | null): target is ContributionHistoryExplanationTarget {
return Boolean(
target?.sessionId && target.workspaceId && target.repositoryScope !== undefined &&
target.branch && target.selectedPRKey &&
isFullCommitSHA(target.expectedLocalHead) && isFullCommitSHA(target.expectedRemoteHead),
);
}
Shared request
function startEntry(key: string, target: ContributionHistoryRequestTarget): ExplanationEntry {
const entry: ExplanationEntry = { ...emptySnapshot(), status: "loading", listeners: new Set() };
entries.set(key, entry);
const client = getWebSocketClient();
if (!client) { entry.status = "unavailable"; return entry; }
const request = client.request<ContributionHistoryExplanation>(ACTION, {
session_id: target.sessionId, repo: target.repositoryScope, branch: target.branch,
expected_local_head: target.expectedLocalHead, expected_remote_head: target.expectedRemoteHead,
}, REQUEST_TIMEOUT_MS);
void request.then((response) => {
if (entries.get(key) !== entry) return;
if (!responseMatchesTarget(response, target)) {
entry.status = "unavailable";
entry.error = new Error("contribution history explanation identity changed");
} else {
entry.status = "ready"; entry.explanation = response;
}
notify(entry);
}, (error) => { if (entries.get(key) !== entry) return; entry.status = "unavailable"; entry.error = error; notify(entry); });
return entry;
}
Hook
export function useContributionHistoryExplanation(target: ContributionHistoryExplanationTarget | null, enabled: boolean) {
const key = enabled ? contributionHistoryExplanationKey(requestTarget) : null;
const [boundSnapshot, setBoundSnapshot] = useState<BoundExplanationSnapshot>(() => ({ key: null, snapshot: emptySnapshot() }));
const snapshot = boundSnapshot.key === key ? boundSnapshot.snapshot : emptySnapshot();
useEffect(() => {
if (!key) { setBoundSnapshot({ key: null, snapshot: emptySnapshot() }); return; }
const entry = getOrStartEntry(key, requestFields);
const sync = () => { if (active) setBoundSnapshot({ key, snapshot: snapshotOf(entry) }); };
entry.listeners.add(sync); sync();
return () => { entry.listeners.delete(sync); if (entry.listeners.size === 0) entries.delete(key); };
}, [key, requestFields]);
return { ...snapshot, isLoading: snapshot.status === "loading" };
}
Header menu and Compare
apps/web/components/task/remote-contribution-header-actions.tsx ↗The menu shows local-rebase copy and counts and makes Compare the primary action.
Explanation body
function HistoryExplanationBody({ status, explanation }: { status: string; explanation: ContributionHistoryExplanation | null }) {
const { t } = useTranslation();
const localRebase = isConfirmedLocalRebase(explanation);
return (
<div className="space-y-2">
<p className="font-normal leading-relaxed text-muted-foreground">
{localRebase ? t("task:remoteContributionHistoryLocalRebase") : t("task:remoteContributionHistoryBody")}
</p>
{status === "loading" && <p data-testid="history-explanation-loading">{t("task:remoteContributionHistoryLoading")}</p>}
{localRebase && explanation && (
<div data-testid="history-counts">
{typeof explanation.task_commit_count === "number" && <p>{t("task:remoteContributionTaskCommits", { count: explanation.task_commit_count })}</p>}
{typeof explanation.published_commit_count === "number" && <p>{t("task:remoteContributionPublishedCommits", { count: explanation.published_commit_count })}</p>}
{typeof explanation.new_base_commit_count === "number" && <p>{t("task:remoteContributionNewBaseCommits", { count: explanation.new_base_commit_count })}</p>}
</div>
)}
{localRebase && <p>{t("task:remoteContributionHistoryGuidance")}</p>}
</div>
);
}
Compare wiring
export function requestContributionComparison(key: string) {
currentRequest = { key, token: ++nextToken };
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("switch-to-changes-tab"));
}
notify();
}
Menu trigger
const history = useContributionHistoryExplanation(contributionHistoryTarget ?? null, menu.open);
if (relation?.kind !== "diverged" || !resolution || !resolutionTarget || !policy) return null;
const comparisonKey = contributionHistoryExplanationKey(contributionHistoryTarget ?? null);
// menu.handleCompare closes the menu and calls requestContributionComparison(comparisonKey)
// Changes panel expands on token and focuses the heading