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:
Bohdan Triapitsyn
2026-02-09 03:13:34 +02:00
committed by GitHub
parent 3f29b2c6a2
commit 5b0a97d170
30 changed files with 3216 additions and 1443 deletions
+31
View File
@@ -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' };
+66
View File
@@ -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,
+98 -2
View File
@@ -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,
};
}
}