feat(ui): add desktop git sidebar + terminal dock and improve in-app PR workflow (#362)
* feat: add unified dropdown with services content in header * feat: add right Git sidebar with resizable panel * feat: implement responsive panel auto-toggle and terminal rehydration - Auto-close the right sidebar when width is below a threshold and auto-open it when space permits - Auto-close the bottom terminal when height is below a threshold and auto-open it when enough space - Apply a dedicated rehydrated streaming configuration for terminal sessions to optimize reconnect behavior * feat: enhance PR view with status caching and annotations * feat(ui): enable chat dispatch in PullRequestSection * feat(TerminalView): adjust layout * feat: refine chat input layout and text selection menu * fix(ui): show empty state in GitView when no changes * feat(git): update PR actions styling and create PR button
This commit is contained in:
committed by
GitHub
parent
3f29b2c6a2
commit
5b0a97d170
@@ -32,6 +32,7 @@ import {
|
||||
getPullRequestStatus,
|
||||
markPullRequestReady,
|
||||
mergePullRequest,
|
||||
updatePullRequest,
|
||||
} from './githubPr';
|
||||
|
||||
import {
|
||||
@@ -1393,6 +1394,36 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
||||
}
|
||||
}
|
||||
|
||||
case 'api:github/pr:update': {
|
||||
const context = ctx?.context;
|
||||
if (!context) return { id, type, success: false, error: 'Missing VS Code context' };
|
||||
const stored = await readGitHubAuth(context);
|
||||
if (!stored?.accessToken) return { id, type, success: false, error: 'GitHub not connected' };
|
||||
const directory = readStringField(payload, 'directory');
|
||||
const number = readNumberField(payload, 'number') ?? 0;
|
||||
const title = readStringField(payload, 'title');
|
||||
const body = readStringField(payload, 'body');
|
||||
if (!directory || !number || !title) {
|
||||
return { id, type, success: false, error: 'directory, number, title are required' };
|
||||
}
|
||||
try {
|
||||
const pr = await updatePullRequest(stored.accessToken, directory, {
|
||||
directory,
|
||||
number,
|
||||
title,
|
||||
...(typeof body === 'string' ? { body } : {}),
|
||||
});
|
||||
return { id, type, success: true, data: pr };
|
||||
} catch (error: unknown) {
|
||||
const status = (error && typeof error === 'object' && 'status' in error) ? (error as { status?: number }).status : undefined;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (status === 401 || message === 'unauthorized') {
|
||||
await clearGitHubAuth(context);
|
||||
}
|
||||
return { id, type, success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
case 'api:github/pr:merge': {
|
||||
const context = ctx?.context;
|
||||
if (!context) return { id, type, success: false, error: 'Missing VS Code context' };
|
||||
|
||||
@@ -20,6 +20,7 @@ type GitHubChecksSummary = {
|
||||
type GitHubPullRequest = {
|
||||
number: number;
|
||||
title: string;
|
||||
body?: string;
|
||||
url: string;
|
||||
state: 'open' | 'closed' | 'merged';
|
||||
draft: boolean;
|
||||
@@ -48,6 +49,13 @@ type GitHubPullRequestCreateInput = {
|
||||
draft?: boolean;
|
||||
};
|
||||
|
||||
type GitHubPullRequestUpdateInput = {
|
||||
directory: string;
|
||||
number: number;
|
||||
title: string;
|
||||
body?: string;
|
||||
};
|
||||
|
||||
type GitHubPullRequestMergeInput = {
|
||||
directory: string;
|
||||
number: number;
|
||||
@@ -208,6 +216,7 @@ export const getPullRequestStatus = async (
|
||||
const pr: GitHubPullRequest = {
|
||||
number: typeof prJson.number === 'number' ? prJson.number : 0,
|
||||
title: readString(prJson.title) || '',
|
||||
body: readString(prJson.body) || '',
|
||||
url: readString(prJson.html_url) || '',
|
||||
state,
|
||||
draft: Boolean(prJson.draft),
|
||||
@@ -326,6 +335,7 @@ export const createPullRequest = async (
|
||||
return {
|
||||
number: typeof json.number === 'number' ? json.number : 0,
|
||||
title: readString(json.title) || '',
|
||||
body: readString(json.body) || '',
|
||||
url: readString(json.html_url) || '',
|
||||
state: readString(json.state) === 'closed' ? 'closed' : 'open',
|
||||
draft: Boolean(json.draft),
|
||||
@@ -337,6 +347,62 @@ export const createPullRequest = async (
|
||||
};
|
||||
};
|
||||
|
||||
export const updatePullRequest = async (
|
||||
accessToken: string,
|
||||
directory: string,
|
||||
payload: GitHubPullRequestUpdateInput,
|
||||
): Promise<GitHubPullRequest> => {
|
||||
const repo = await resolveRepoFromDirectory(directory);
|
||||
if (!repo) {
|
||||
throw new Error('Unable to resolve GitHub repo from git remote');
|
||||
}
|
||||
|
||||
const resp = await githubFetch(`${API_BASE}/repos/${repo.owner}/${repo.repo}/pulls/${payload.number}`, accessToken, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: payload.title,
|
||||
...(typeof payload.body === 'string' ? { body: payload.body } : {}),
|
||||
}),
|
||||
});
|
||||
|
||||
if (resp.status === 403) {
|
||||
throw new Error('Not authorized to edit this PR');
|
||||
}
|
||||
if (resp.status === 401) {
|
||||
const error = new Error('unauthorized');
|
||||
(error as unknown as { status?: number }).status = 401;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const json = await jsonOrNull<JsonRecord>(resp);
|
||||
if (!resp.ok || !json) {
|
||||
const message = readString(json?.message);
|
||||
const firstError = Array.isArray(json?.errors) && json.errors.length > 0
|
||||
? readString((json.errors[0] as JsonRecord)?.message || (json.errors[0] as JsonRecord)?.code)
|
||||
: '';
|
||||
const details = [message, firstError].filter(Boolean).join(' · ');
|
||||
throw new Error(details || 'Failed to update PR');
|
||||
}
|
||||
|
||||
const merged = Boolean(json.merged || json.merged_at);
|
||||
const state = merged ? 'merged' : (readString(json.state) === 'closed' ? 'closed' : 'open');
|
||||
|
||||
return {
|
||||
number: typeof json.number === 'number' ? json.number : payload.number,
|
||||
title: readString(json.title) || payload.title,
|
||||
body: readString(json.body) || '',
|
||||
url: readString(json.html_url) || '',
|
||||
state,
|
||||
draft: Boolean(json.draft),
|
||||
base: readString((json.base as JsonRecord | undefined)?.ref) || '',
|
||||
head: readString((json.head as JsonRecord | undefined)?.ref) || '',
|
||||
headSha: readString((json.head as JsonRecord | undefined)?.sha) || undefined,
|
||||
mergeable: typeof json.mergeable === 'boolean' ? json.mergeable : null,
|
||||
mergeableState: readString(json.mergeable_state) || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const mergePullRequest = async (
|
||||
accessToken: string,
|
||||
directory: string,
|
||||
|
||||
@@ -37,8 +37,24 @@ type GitHubCheckRun = {
|
||||
url?: string;
|
||||
name?: string;
|
||||
conclusion?: string | null;
|
||||
steps?: Array<{ name: string; status?: string; conclusion?: string | null; number?: number }>;
|
||||
steps?: Array<{
|
||||
name: string;
|
||||
status?: string;
|
||||
conclusion?: string | null;
|
||||
number?: number;
|
||||
startedAt?: string;
|
||||
completedAt?: string;
|
||||
}>;
|
||||
};
|
||||
annotations?: Array<{
|
||||
path?: string;
|
||||
startLine?: number;
|
||||
endLine?: number;
|
||||
level?: string;
|
||||
message: string;
|
||||
title?: string;
|
||||
rawDetails?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
type GitHubPullRequestHeadRepo = { owner: string; repo: string; url: string; cloneUrl?: string };
|
||||
@@ -456,7 +472,77 @@ export const getPullRequestContext = async (
|
||||
jobsByRunId.set(runId, jobs.filter((j) => j && typeof j === 'object') as JsonRecord[]);
|
||||
}
|
||||
|
||||
const annotationsByRunId = new Map<number, Array<{
|
||||
path?: string;
|
||||
startLine?: number;
|
||||
endLine?: number;
|
||||
level?: string;
|
||||
message: string;
|
||||
title?: string;
|
||||
rawDetails?: string;
|
||||
}>>();
|
||||
|
||||
for (const run of checkRuns) {
|
||||
const runId = typeof run.id === 'number' ? run.id : 0;
|
||||
const conclusion = (run.conclusion || '').toLowerCase();
|
||||
const shouldLoadAnnotations = Boolean(
|
||||
runId > 0
|
||||
&& conclusion
|
||||
&& !['success', 'neutral', 'skipped'].includes(conclusion),
|
||||
);
|
||||
if (!shouldLoadAnnotations) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const annotations: Array<{
|
||||
path?: string;
|
||||
startLine?: number;
|
||||
endLine?: number;
|
||||
level?: string;
|
||||
message: string;
|
||||
title?: string;
|
||||
rawDetails?: string;
|
||||
}> = [];
|
||||
|
||||
for (let page = 1; page <= 3; page += 1) {
|
||||
const annotationsResp = await githubFetch(
|
||||
`${API_BASE}/repos/${repo.owner}/${repo.repo}/check-runs/${runId}/annotations?per_page=50&page=${page}`,
|
||||
accessToken,
|
||||
);
|
||||
if (annotationsResp.status === 401) {
|
||||
return { connected: false };
|
||||
}
|
||||
const annotationsJson = await jsonOrNull<unknown[]>(annotationsResp);
|
||||
const chunk = Array.isArray(annotationsJson) ? annotationsJson : [];
|
||||
chunk.forEach((entry) => {
|
||||
const rec = entry && typeof entry === 'object' ? (entry as JsonRecord) : null;
|
||||
const message = readString(rec?.message);
|
||||
if (!message) return;
|
||||
annotations.push({
|
||||
path: readString(rec?.path) || undefined,
|
||||
startLine: typeof rec?.start_line === 'number' ? rec.start_line : undefined,
|
||||
endLine: typeof rec?.end_line === 'number' ? rec.end_line : undefined,
|
||||
level: readString(rec?.annotation_level) || undefined,
|
||||
message,
|
||||
title: readString(rec?.title) || undefined,
|
||||
rawDetails: readString(rec?.raw_details) || undefined,
|
||||
});
|
||||
});
|
||||
if (chunk.length < 50) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (annotations.length > 0) {
|
||||
annotationsByRunId.set(runId, annotations);
|
||||
}
|
||||
}
|
||||
|
||||
for (const run of checkRuns) {
|
||||
if (run.id && annotationsByRunId.has(run.id)) {
|
||||
run.annotations = annotationsByRunId.get(run.id);
|
||||
}
|
||||
|
||||
const ids = parseIds(run.detailsUrl);
|
||||
if (!ids.runId) continue;
|
||||
const jobs = jobsByRunId.get(ids.runId) ?? [];
|
||||
@@ -480,9 +566,18 @@ export const getPullRequestContext = async (
|
||||
? (rec?.conclusion as string | null)
|
||||
: undefined,
|
||||
number: typeof rec?.number === 'number' ? rec.number : undefined,
|
||||
startedAt: readString(rec?.started_at) || undefined,
|
||||
completedAt: readString(rec?.completed_at) || undefined,
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as Array<{ name: string; status?: string; conclusion?: string | null; number?: number }>;
|
||||
.filter(Boolean) as Array<{
|
||||
name: string;
|
||||
status?: string;
|
||||
conclusion?: string | null;
|
||||
number?: number;
|
||||
startedAt?: string;
|
||||
completedAt?: string;
|
||||
}>;
|
||||
|
||||
run.job = {
|
||||
runId: ids.runId,
|
||||
@@ -494,6 +589,7 @@ export const getPullRequestContext = async (
|
||||
: undefined,
|
||||
steps: steps.length > 0 ? steps : undefined,
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user