fix: align sidebar PR badges and expose run scope (#3020)

This commit is contained in:
Carlos Florêncio
2026-08-25 22:57:20 +00:00
committed by GitHub
parent f0e070a8d2
commit 4dbb8a7811
8 changed files with 125 additions and 28 deletions
@@ -421,31 +421,26 @@ func TestClient_FirstBootMissesGracefully(t *testing.T) {
dir := t.TempDir()
cachePath := filepath.Join(dir, "models-dev.json")
// A cold-boot lookup schedules a background refresh against the
// configured URL. Block that refresh at the server so it can't
// populate the cache before the lookup reads it — otherwise the
// "miss" we're asserting races a fast background fetch and flakes
// into a hit. The request exits when the lookup context is canceled,
// before TempDir cleanup can race a cache write.
ctx, cancel := context.WithCancel(context.Background())
srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
<-r.Context().Done()
}))
t.Cleanup(func() {
cancel()
srv.Close()
})
// A cold-boot lookup schedules a detached background refresh. Hold that
// request at the gate so it cannot populate the cache before the lookup
// reads it, then release and join the refresh before TempDir cleanup.
gate := newRequestGate(t, sampleDataset)
c := modelsdev.New(modelsdev.Config{
CachePath: cachePath,
URL: srv.URL,
URL: gate.server.URL,
TTL: time.Hour,
HTTPClient: srv.Client(),
HTTPClient: gate.server.Client(),
}, logger.Default())
// No Refresh — simulating cold boot before any HTTP fetch.
if _, ok := c.LookupForModel(ctx, "claude-opus-4-7"); ok {
if _, ok := c.LookupForModel(context.Background(), "claude-opus-4-7"); ok {
t.Error("expected miss on cold-boot lookup")
}
gate.waitForFirstRequest(t)
gate.releaseAll()
if err := c.Refresh(context.Background()); err != nil {
t.Fatalf("join background refresh: %v", err)
}
}
type requestGate struct {
@@ -306,6 +306,10 @@ type RunDetailResponse struct {
// context the run dispatcher built at claim time. Surfaced for
// debugging.
ContextSnapshot string `json:"context_snapshot,omitempty"`
// ContinuationScope is the persisted summary scope selected when
// the run was created. It lets run inspection show which
// continuation-summary chain the run reads and updates.
ContinuationScope string `json:"continuation_scope,omitempty"`
// OutputSummary mirrors runs.output_summary — the free-form
// agent output captured at run finish. Kept alongside ResultJSON
// because legacy adapters populate this and not result_json.
@@ -276,16 +276,17 @@ func GetRunDetail(
CachedTokens: costs.CachedTokens,
CostSubcents: costs.CostSubcents,
},
Session: sessionDTO,
Invocation: invocation,
Runtime: runtimeDTO,
TasksTouched: taskIDs,
Events: eventDTOs,
AssembledPrompt: run.AssembledPrompt,
SummaryInjected: run.SummaryInjected,
ResultJSON: run.ResultJSON,
ContextSnapshot: run.ContextSnapshot,
OutputSummary: run.OutputSummary,
Session: sessionDTO,
Invocation: invocation,
Runtime: runtimeDTO,
TasksTouched: taskIDs,
Events: eventDTOs,
AssembledPrompt: run.AssembledPrompt,
SummaryInjected: run.SummaryInjected,
ResultJSON: run.ResultJSON,
ContextSnapshot: run.ContextSnapshot,
ContinuationScope: run.ContinuationScope,
OutputSummary: run.OutputSummary,
}
if run.CancelReason != nil && *run.CancelReason != "" {
v := *run.CancelReason
@@ -2,6 +2,7 @@ package dashboard_test
import (
"context"
"encoding/json"
"testing"
"time"
@@ -395,4 +396,16 @@ func TestGetRunDetail_PromptArtifactsAndSnapshots(t *testing.T) {
if resp.ContextSnapshot == "" {
t.Errorf("expected context_snapshot to round-trip from default '{}', got empty string")
}
raw, err := json.Marshal(resp)
if err != nil {
t.Fatalf("marshal run detail response: %v", err)
}
var payload map[string]any
if err := json.Unmarshal(raw, &payload); err != nil {
t.Fatalf("decode run detail response: %v", err)
}
if got := payload["continuation_scope"]; got != "agent:agent-1" {
t.Errorf("continuation_scope = %v, want %q", got, "agent:agent-1")
}
}
+1 -1
View File
@@ -328,7 +328,7 @@ function TaskStateIcon({
}
function TaskItemTitle({ title }: { title: string }) {
return <ScrollOnOverflow className="min-w-0 w-full">{title}</ScrollOnOverflow>;
return <ScrollOnOverflow className="min-w-0">{title}</ScrollOnOverflow>;
}
function TaskItemContent({
@@ -181,5 +181,26 @@ test.describe("GitLab MR badge on the sidebar", () => {
: "mr-then-pr";
});
expect(order).toBe("pr-then-mr");
const spacing = await row.evaluate((el, titleText) => {
const title = [...el.querySelectorAll("span")].find(
(candidate) =>
candidate.classList.contains("whitespace-nowrap") && candidate.textContent === titleText,
);
const pr = el.querySelector('[data-testid^="pr-task-icon-"]');
if (!title || !pr) return { found: false, gap: -1, titleTop: -1, prTop: -1 };
const titleBox = title.getBoundingClientRect();
const prBox = pr.getBoundingClientRect();
return {
found: true,
gap: prBox.left - titleBox.right,
titleTop: titleBox.top,
prTop: prBox.top,
};
}, "Sidebar PR and MR badge task");
expect(spacing.found).toBe(true);
expect(Math.abs(spacing.titleTop - spacing.prTop)).toBeLessThan(4);
expect(spacing.gap).toBeGreaterThanOrEqual(0);
expect(spacing.gap).toBeLessThan(32);
});
});
@@ -4,6 +4,7 @@ import path from "node:path";
import { test, expect } from "../../fixtures/test-base";
import { activeSessionId, seedSecondaryClarificationTask } from "../../helpers/clarification";
import { makeGitEnv } from "../../helpers/git-helper";
import { assertNoDocumentHorizontalOverflow } from "../../helpers/layout-assertions";
import { SessionPage } from "../../pages/session-page";
test.describe("Mobile sidebar task actions", () => {
@@ -51,6 +52,66 @@ test.describe("Mobile sidebar task actions", () => {
.toBe(true);
});
test("keeps a linked PR badge beside its title in the phone drawer", async ({
testPage,
apiClient,
seedData,
}) => {
const title = "Mobile sidebar PR badge spacing";
const task = await apiClient.createTask(seedData.workspaceId, title, {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
await apiClient.mockGitHubAssociateTaskPR({
task_id: task.id,
owner: "testorg",
repo: "testrepo",
pr_number: 902,
pr_url: "https://github.com/testorg/testrepo/pull/902",
pr_title: "Mobile sidebar spacing",
head_branch: "feature/mobile-sidebar-spacing",
base_branch: "main",
author_login: "e2e",
state: "open",
checks_state: "success",
mergeable_state: "clean",
});
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await session.mobileSessionMenu.tap();
const drawer = testPage.getByRole("dialog", { name: "Tasks" });
const row = drawer.getByTestId("sidebar-task-item").filter({ hasText: title });
await expect(row).toBeVisible();
const prIcon = row.getByTestId(`pr-task-icon-${task.id}`);
await expect(prIcon).toBeVisible({ timeout: 15_000 });
const spacing = await row.evaluate((el, titleText) => {
const titleElement = [...el.querySelectorAll("span")].find(
(candidate) =>
candidate.classList.contains("whitespace-nowrap") && candidate.textContent === titleText,
);
const pr = el.querySelector('[data-testid^="pr-task-icon-"]');
if (!titleElement || !pr) return { found: false, gap: -1, titleTop: -1, prTop: -1 };
const titleBox = titleElement.getBoundingClientRect();
const prBox = pr.getBoundingClientRect();
return {
found: true,
gap: prBox.left - titleBox.right,
titleTop: titleBox.top,
prTop: prBox.top,
};
}, title);
expect(spacing.found).toBe(true);
expect(Math.abs(spacing.titleTop - spacing.prTop)).toBeLessThan(4);
expect(spacing.gap).toBeGreaterThanOrEqual(0);
expect(spacing.gap).toBeLessThan(32);
await assertNoDocumentHorizontalOverflow(testPage, "mobile sidebar PR badge spacing");
});
test("switches to the selected task and its chat from the phone task drawer", async ({
testPage,
apiClient,
@@ -111,6 +111,8 @@ export type RunDetail = {
summary_injected?: string;
result_json?: string;
context_snapshot?: string;
// Persisted when the run is created. Identifies the continuation-summary chain.
continuation_scope?: string;
output_summary?: string;
// Routing snapshot — omitted for legacy concrete-profile runs.
routing?: RunRouting;