Skip to content

Commit b57cbf9

Browse files
Copilotrebornix
andauthored
Show setup stage progress in session logs (#7330)
* Initial plan * Implement setup stage progress display in session logs * Only show setup logs when there are no main session logs Co-authored-by: rebornix <876920+rebornix@users.noreply.github.com> * fix copilot setup steps fetching. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rebornix <876920+rebornix@users.noreply.github.com> Co-authored-by: Peng Lyu <penn.lv@gmail.com>
1 parent 5f48d91 commit b57cbf9

12 files changed

Lines changed: 270 additions & 24 deletions

src/@types/vscode.proposed.chatParticipantAdditions.d.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ declare module 'vscode' {
6666

6767
export class ChatResponseConfirmationPart {
6868
title: string;
69-
message: string;
69+
message: string | MarkdownString;
7070
data: any;
7171
buttons?: string[];
72-
constructor(title: string, message: string, data: any, buttons?: string[]);
72+
constructor(title: string, message: string | MarkdownString, data: any, buttons?: string[]);
7373
}
7474

7575
export class ChatResponseCodeCitationPart {
@@ -205,7 +205,7 @@ declare module 'vscode' {
205205
* TODO@API should this be MarkdownString?
206206
* TODO@API should actually be a more generic function that takes an array of buttons
207207
*/
208-
confirmation(title: string, message: string, data: any, buttons?: string[]): void;
208+
confirmation(title: string, message: string | MarkdownString, data: any, buttons?: string[]): void;
209209

210210
/**
211211
* Push a warning to this stream. Short-hand for
@@ -265,6 +265,43 @@ declare module 'vscode' {
265265
export const onDidChangeChatRequestTools: Event<ChatRequest>;
266266
}
267267

268+
export class LanguageModelToolExtensionSource {
269+
/**
270+
* ID of the extension that published the tool.
271+
*/
272+
readonly id: string;
273+
274+
/**
275+
* Label of the extension that published the tool.
276+
*/
277+
readonly label: string;
278+
279+
private constructor(id: string, label: string);
280+
}
281+
282+
export class LanguageModelToolMCPSource {
283+
/**
284+
* Editor-configured label of the MCP server that published the tool.
285+
*/
286+
readonly label: string;
287+
288+
/**
289+
* Server-defined name of the MCP server.
290+
*/
291+
readonly name: string;
292+
293+
/**
294+
* Server-defined instructions for MCP tool use.
295+
*/
296+
readonly instructions?: string;
297+
298+
private constructor(label: string, name: string, instructions?: string);
299+
}
300+
301+
export interface LanguageModelToolInformation {
302+
source: LanguageModelToolExtensionSource | LanguageModelToolMCPSource | undefined;
303+
}
304+
268305
// TODO@API fit this into the stream
269306
export interface ChatUsedContext {
270307
documents: ChatDocumentContext[];

src/github/common.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ export namespace OctokitCommon {
7272
export type CommitFiles = CompareCommits['files']
7373
export type Notification = Endpoints['GET /notifications']['response']['data'][0];
7474
export type ListEventsForTimelineResponse = Endpoints['GET /repos/{owner}/{repo}/issues/{issue_number}/timeline']['response']['data'][0];
75-
export type ListWorkflowRunsForRepo = Endpoints['GET /repos/{owner}/{repo}/actions/runs']['response']['data']['workflow_runs'];
75+
export type ListWorkflowRunsForRepo = Endpoints['GET /repos/{owner}/{repo}/actions/runs']['response']['data'];
7676
export type WorkflowRun = Endpoints['GET /repos/{owner}/{repo}/actions/runs']['response']['data']['workflow_runs'][0];
77+
export type WorkflowJob = Endpoints['GET /repos/{owner}/{repo}/actions/jobs/{job_id}']['response']['data'];
78+
export type WorkflowJobs = Endpoints['GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs']['response']['data'];
7779
}
7880

7981
export type Schema = { [key: string]: any, definitions: any[]; };

src/github/copilotApi.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,11 @@ export interface SessionInfo {
267267
error: string | null;
268268
}
269269

270+
export interface SessionSetupStep {
271+
name: string;
272+
status: 'completed' | 'in_progress' | 'queued';
273+
}
274+
270275
export async function getCopilotApi(credentialStore: CredentialStore, telemetry: ITelemetry, authProvider?: AuthProvider): Promise<CopilotApi | undefined> {
271276
if (!authProvider) {
272277
if (credentialStore.isAuthenticated(AuthProvider.githubEnterprise) && hasEnterpriseUri()) {

src/github/copilotRemoteAgent.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { CODING_AGENT, CODING_AGENT_AUTO_COMMIT_AND_PUSH, CODING_AGENT_ENABLED }
1414
import { ITelemetry } from '../common/telemetry';
1515
import { toOpenPullRequestWebviewUri } from '../common/uri';
1616
import { OctokitCommon } from './common';
17-
import { ChatSessionWithPR, CopilotApi, getCopilotApi, RemoteAgentJobPayload, SessionInfo } from './copilotApi';
17+
import { ChatSessionWithPR, CopilotApi, getCopilotApi, RemoteAgentJobPayload, SessionInfo, SessionSetupStep } from './copilotApi';
1818
import { CopilotPRWatcher, CopilotStateModel } from './copilotPrWatcher';
1919
import { CredentialStore } from './credentials';
2020
import { FolderRepositoryManager } from './folderRepositoryManager';
@@ -29,6 +29,7 @@ type RemoteAgentResult = RemoteAgentSuccessResult | RemoteAgentErrorResult;
2929
export interface IAPISessionLogs {
3030
readonly info: SessionInfo;
3131
readonly logs: string;
32+
readonly setupSteps: SessionSetupStep[] | undefined;
3233
}
3334

3435
export interface ICopilotRemoteAgentCommandArgs {
@@ -604,13 +605,39 @@ export class CopilotRemoteAgentManager extends Disposable {
604605
return await capi.getLogsFromZipUrl(lastRun.logs_url);
605606
}
606607

608+
async getWorkflowStepsFromAction(pullRequest: PullRequestModel): Promise<SessionSetupStep[]> {
609+
const lastRun = await this.getLatestCodingAgentFromAction(pullRequest, 0, false);
610+
if (!lastRun) {
611+
return [];
612+
}
613+
614+
try {
615+
const jobs = await pullRequest.githubRepository.getWorkflowJobs(lastRun.id);
616+
const steps: SessionSetupStep[] = [];
617+
618+
for (const job of jobs) {
619+
if (job.steps) {
620+
for (const step of job.steps) {
621+
steps.push({ name: step.name, status: step.status });
622+
}
623+
}
624+
}
625+
626+
return steps;
627+
} catch (error) {
628+
Logger.error(`Failed to get workflow steps: ${error}`, CopilotRemoteAgentManager.ID);
629+
return [];
630+
}
631+
}
632+
607633
async getLatestCodingAgentFromAction(pullRequest: PullRequestModel, sessionIndex = 0, completedOnly = true): Promise<OctokitCommon.WorkflowRun | undefined> {
608634
const capi = await this.copilotApi;
609635
if (!capi) {
610636
return;
611637
}
612638
const runs = await pullRequest.githubRepository.getWorkflowRunsFromAction(pullRequest.createdAt);
613-
const padawanRuns = runs
639+
const workflowRuns = runs.flatMap(run => run.workflow_runs);
640+
const padawanRuns = workflowRuns
614641
.filter(run => run.path && run.path.startsWith('dynamic/copilot-swe-agent'))
615642
.filter(run => run.pull_requests?.some(pr => pr.id === pullRequest.id));
616643

@@ -622,20 +649,33 @@ export class CopilotRemoteAgentManager extends Disposable {
622649
return this.getLatestRun(padawanRuns);
623650
}
624651

625-
async getSessionLogFromPullRequest(pullRequestId: number, sessionIndex = 0, completedOnly = true): Promise<IAPISessionLogs | undefined> {
652+
async getSessionLogFromPullRequest(pullRequest: PullRequestModel, sessionIndex = 0, completedOnly = true): Promise<IAPISessionLogs | undefined> {
626653
const capi = await this.copilotApi;
627654
if (!capi) {
628655
return undefined;
629656
}
630657

631-
const sessions = await capi.getAllSessions(pullRequestId);
658+
const sessions = await capi.getAllSessions(pullRequest.id);
632659
const session = sessions.filter(s => !completedOnly || s.state === 'completed').at(sessionIndex);
633660
if (!session) {
634661
return undefined;
635662
}
636663

637664
const logs = await capi.getLogsFromSession(session.id);
638-
return { info: session, logs };
665+
666+
// If session is in progress, try to fetch workflow steps to show setup progress
667+
let setupSteps: SessionSetupStep[] | undefined;
668+
if (session.state === 'in_progress' || logs.trim().length === 0 || true) {
669+
try {
670+
// Get workflow steps instead of logs
671+
setupSteps = await this.getWorkflowStepsFromAction(pullRequest);
672+
} catch (error) {
673+
// If we can't fetch workflow steps, don't fail the entire request
674+
Logger.warn(`Failed to fetch workflow steps for session ${session.id}: ${error}`, CopilotRemoteAgentManager.ID);
675+
}
676+
}
677+
678+
return { info: session, logs, setupSteps };
639679
}
640680

641681
async getSessionUrlFromPullRequest(pullRequest: PullRequestModel): Promise<string | undefined> {

src/github/githubRepository.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -872,11 +872,11 @@ export class GitHubRepository extends Disposable {
872872
}
873873
}
874874

875-
public async getWorkflowRunsFromAction(fromDate: string): Promise<OctokitCommon.ListWorkflowRunsForRepo> {
875+
public async getWorkflowRunsFromAction(fromDate: string): Promise<OctokitCommon.ListWorkflowRunsForRepo[]> {
876876
const { octokit, remote } = await this.ensure();
877877
const createdDate = new Date(fromDate);
878878
const created = `>=${createdDate.getFullYear()}-${String(createdDate.getMonth() + 1).padStart(2, '0')}-${String(createdDate.getDate()).padStart(2, '0')}`;
879-
const allRuns = await restPaginate<typeof octokit.api.actions.listWorkflowRunsForRepo, OctokitCommon.ListWorkflowRunsForRepo[0]>(octokit.api.actions.listWorkflowRunsForRepo, {
879+
const allRuns = await restPaginate<typeof octokit.api.actions.listWorkflowRunsForRepo, OctokitCommon.ListWorkflowRunsForRepo>(octokit.api.actions.listWorkflowRunsForRepo, {
880880
owner: remote.owner,
881881
repo: remote.repositoryName,
882882
event: 'dynamic',
@@ -886,6 +886,16 @@ export class GitHubRepository extends Disposable {
886886
return allRuns;
887887
}
888888

889+
public async getWorkflowJobs(workflowRunId: number): Promise<OctokitCommon.WorkflowJob[]> {
890+
const { octokit, remote } = await this.ensure();
891+
const jobs = await octokit.call(octokit.api.actions.listJobsForWorkflowRun, {
892+
owner: remote.owner,
893+
repo: remote.repositoryName,
894+
run_id: workflowRunId
895+
});
896+
return jobs.data.jobs;
897+
}
898+
889899
async fork(): Promise<string | undefined> {
890900
try {
891901
Logger.debug(`Fork repository`, this.id);

src/lm/tools/activePullRequestTool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class ActivePullRequestTool implements vscode.LanguageModelTool<FetchIssu
9090
): Promise<string | string[]> {
9191
let copilotSteps: string | string[] = [];
9292
try {
93-
const logs = await this.copilotRemoteAgentManager.getSessionLogFromPullRequest(pullRequest.id);
93+
const logs = await this.copilotRemoteAgentManager.getSessionLogFromPullRequest(pullRequest);
9494
if (!logs) {
9595
throw new Error('Could not get session logs');
9696
}

src/view/sessionLogView.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,13 @@ class SessionLogView extends Disposable {
357357
this.copilotApi.getSessionInfo(this._source.sessionId),
358358
this.copilotApi.getLogsFromSession(this._source.sessionId)
359359
]);
360-
return { logs, info };
360+
return { logs, info, setupSteps: undefined };
361361
} else {
362-
return this.copilotAgentManager.getSessionLogFromPullRequest(this._source.pullRequest.id, -1 - this._source.link.sessionIndex, false);
362+
const pullRequest = await this.getPullRequestModel(this._source.pullRequest);
363+
if (!pullRequest) {
364+
return undefined;
365+
}
366+
return this.copilotAgentManager.getSessionLogFromPullRequest(pullRequest, -1 - this._source.link.sessionIndex, false);
363367
}
364368
};
365369
if (this._source.type === 'session') {
@@ -409,7 +413,8 @@ class SessionLogView extends Disposable {
409413
type: 'loaded',
410414
pullInfo: this._source.type === 'pull' ? this._source.pullRequest : undefined,
411415
info: apiResponse.info,
412-
logs: apiResponse.logs
416+
logs: apiResponse.logs,
417+
setupSteps: apiResponse.setupSteps
413418
} as messages.LoadedMessage);
414419

415420
if (apiResponse.info.state === 'in_progress') {
@@ -430,7 +435,8 @@ class SessionLogView extends Disposable {
430435
type: 'update',
431436
pullInfo: this._source.type === 'pull' ? this._source.pullRequest : undefined,
432437
info: apiResult.info,
433-
logs: apiResult.logs
438+
logs: apiResult.logs,
439+
setupSteps: apiResult.setupSteps
434440
} as messages.UpdateMessage);
435441

436442
if (apiResult.info.state !== 'in_progress') {

webviews/sessionLogView/app.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import * as React from 'react';
99
import { createHighlighter } from 'shiki';
1010
import { vscode } from '../common/message';
1111
import type * as messages from './messages';
12-
import { parseSessionLogs, SessionInfo, SessionResponseLogChunk } from './sessionsApi';
12+
import { parseSessionLogs, SessionInfo, SessionResponseLogChunk, SessionSetupStepResponse } from './sessionsApi';
1313
import { SessionView } from './sessionView';
1414

1515
const themeName = 'vscode-theme';
1616

1717
type SessionViewState =
1818
| { state: 'loading' }
19-
| { state: 'ready'; readonly info: SessionInfo; readonly logs: readonly SessionResponseLogChunk[]; readonly pullInfo: messages.PullInfo | undefined }
19+
| { state: 'ready'; readonly info: SessionInfo; readonly logs: readonly SessionResponseLogChunk[]; readonly pullInfo: messages.PullInfo | undefined; readonly setupSteps?: readonly SessionSetupStepResponse[] }
2020
| { state: 'error'; readonly url: string | undefined }
2121
;
2222

@@ -49,7 +49,8 @@ export function App() {
4949
state: 'ready',
5050
info: message.info,
5151
logs: parseSessionLogs(message.logs),
52-
pullInfo: message.pullInfo
52+
pullInfo: message.pullInfo,
53+
setupSteps: message.setupSteps
5354
});
5455
break;
5556
}
@@ -89,7 +90,7 @@ export function App() {
8990
</div>
9091
);
9192
} else {
92-
return <SessionView info={state.info} logs={state.logs} pullInfo={state.pullInfo} />;
93+
return <SessionView info={state.info} logs={state.logs} pullInfo={state.pullInfo} setupSteps={state.setupSteps} />;
9394
}
9495
}
9596

webviews/sessionLogView/index.css

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,70 @@ button.codeview-expand:hover {
185185
to {
186186
transform: rotate(360deg);
187187
}
188+
}
189+
190+
.setup-stage-log {
191+
margin: 1em 0;
192+
border: 1px solid var(--vscode-widget-border);
193+
border-radius: 4px;
194+
overflow: hidden;
195+
}
196+
197+
.setup-stage-title {
198+
display: flex;
199+
align-items: center;
200+
padding: 8px 12px;
201+
background: var(--vscode-sideBarSectionHeader-background);
202+
border-bottom: 1px solid var(--vscode-sideBarSectionHeader-border);
203+
color: var(--vscode-sideBarSectionHeader-foreground);
204+
font-size: 13px;
205+
font-weight: 600;
206+
margin: 0;
207+
gap: 8px;
208+
}
209+
210+
.setup-log-content {
211+
padding: 8px 12px;
212+
background: var(--vscode-notebook-cellEditorBackground);
213+
max-height: 200px;
214+
overflow-y: auto;
215+
}
216+
217+
.setup-log-line {
218+
display: flex;
219+
align-items: center;
220+
font-family: var(--vscode-editor-font-family);
221+
font-size: var(--vscode-editor-font-size);
222+
line-height: 1.4;
223+
color: var(--vscode-foreground);
224+
margin: 2px 0;
225+
white-space: pre-wrap;
226+
word-wrap: break-word;
227+
gap: 8px;
228+
}
229+
230+
.setup-step-icon {
231+
flex-shrink: 0;
232+
font-size: 14px;
233+
display: flex;
234+
align-items: center;
235+
justify-content: center;
236+
width: 16px;
237+
height: 16px;
238+
}
239+
240+
.setup-step-name {
241+
flex: 1;
242+
}
243+
244+
.setup-step-completed {
245+
opacity: 0.8;
246+
}
247+
248+
.setup-step-in-progress {
249+
font-weight: 500;
250+
}
251+
252+
.setup-step-queued {
253+
opacity: 0.6;
188254
}

webviews/sessionLogView/messages.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { SessionLinkInfo } from '../../src/common/timelineEvent';
7-
import { SessionInfo } from './sessionsApi';
7+
import { SessionInfo, SessionSetupStepResponse } from './sessionsApi';
88

99
export type PullInfo = SessionLinkInfo & {
1010
title: string;
@@ -20,13 +20,15 @@ export interface LoadedMessage {
2020
pullInfo: PullInfo | undefined;
2121
info: SessionInfo;
2222
logs: string;
23+
setupSteps: SessionSetupStepResponse[] | undefined;
2324
}
2425

2526
export interface UpdateMessage {
2627
type: 'update';
2728
pullInfo: PullInfo | undefined;
2829
info: SessionInfo;
2930
logs: string;
31+
setupSteps: SessionSetupStepResponse[] | undefined;
3032
}
3133

3234
export interface ResetMessage {

0 commit comments

Comments
 (0)