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
+154 -14
View File
@@ -6215,6 +6215,7 @@ async function main(options = {}) {
pr: {
number: prData.number,
title: prData.title,
body: prData.body || '',
url: prData.html_url,
state: mergedState,
draft: Boolean(prData.draft),
@@ -6280,6 +6281,7 @@ async function main(options = {}) {
return res.json({
number: pr.number,
title: pr.title,
body: pr.body || '',
url: pr.html_url,
state: pr.state === 'closed' ? 'closed' : 'open',
draft: Boolean(pr.draft),
@@ -6295,6 +6297,82 @@ async function main(options = {}) {
}
});
app.post('/api/github/pr/update', async (req, res) => {
try {
const directory = typeof req.body?.directory === 'string' ? req.body.directory.trim() : '';
const number = typeof req.body?.number === 'number' ? req.body.number : null;
const title = typeof req.body?.title === 'string' ? req.body.title.trim() : '';
const body = typeof req.body?.body === 'string' ? req.body.body : undefined;
if (!directory || !number || !title) {
return res.status(400).json({ error: 'directory, number, title are required' });
}
const { getOctokitOrNull } = await getGitHubLibraries();
const octokit = getOctokitOrNull();
if (!octokit) {
return res.status(401).json({ error: 'GitHub not connected' });
}
const { resolveGitHubRepoFromDirectory } = await import('./lib/github-repo.js');
const { repo } = await resolveGitHubRepoFromDirectory(directory);
if (!repo) {
return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' });
}
let updated;
try {
updated = await octokit.rest.pulls.update({
owner: repo.owner,
repo: repo.repo,
pull_number: number,
title,
...(typeof body === 'string' ? { body } : {}),
});
} catch (error) {
if (error?.status === 401) {
return res.status(401).json({ error: 'GitHub not connected' });
}
if (error?.status === 403) {
return res.status(403).json({ error: 'Not authorized to edit this PR' });
}
if (error?.status === 404) {
return res.status(404).json({ error: 'PR not found in this repository' });
}
if (error?.status === 422) {
const apiMessage = error?.response?.data?.message;
const firstError = Array.isArray(error?.response?.data?.errors) && error.response.data.errors.length > 0
? (error.response.data.errors[0]?.message || error.response.data.errors[0]?.code)
: null;
const message = [apiMessage, firstError].filter(Boolean).join(' · ') || 'Invalid PR update payload';
return res.status(422).json({ error: message });
}
throw error;
}
const pr = updated?.data;
if (!pr) {
return res.status(500).json({ error: 'Failed to update PR' });
}
return res.json({
number: pr.number,
title: pr.title,
body: pr.body || '',
url: pr.html_url,
state: pr.merged_at ? 'merged' : (pr.state === 'closed' ? 'closed' : 'open'),
draft: Boolean(pr.draft),
base: pr.base?.ref,
head: pr.head?.ref,
headSha: pr.head?.sha,
mergeable: pr.mergeable,
mergeableState: pr.mergeable_state,
});
} catch (error) {
console.error('Failed to update GitHub PR:', error);
return res.status(500).json({ error: error.message || 'Failed to update GitHub PR' });
}
});
app.post('/api/github/pr/merge', async (req, res) => {
try {
const directory = typeof req.body?.directory === 'string' ? req.body.directory.trim() : '';
@@ -6739,6 +6817,7 @@ async function main(options = {}) {
const checkRuns = Array.isArray(runs?.data?.check_runs) ? runs.data.check_runs : [];
if (checkRuns.length > 0) {
const parsedJobs = new Map();
const parsedAnnotations = new Map();
if (includeCheckDetails) {
// Prefetch actions jobs per runId.
const runIds = new Set();
@@ -6774,6 +6853,47 @@ async function main(options = {}) {
parsedJobs.set(runId, []);
}
}
for (const run of checkRuns) {
const runConclusion = typeof run?.conclusion === 'string' ? run.conclusion.toLowerCase() : '';
const shouldLoadAnnotations = Boolean(
run?.id
&& runConclusion
&& !['success', 'neutral', 'skipped'].includes(runConclusion)
);
if (!shouldLoadAnnotations) {
continue;
}
const checkRunId = Number(run.id);
if (!Number.isFinite(checkRunId) || checkRunId <= 0) {
continue;
}
const annotations = [];
for (let page = 1; page <= 3; page += 1) {
try {
const annotationsResp = await octokit.rest.checks.listAnnotations({
owner: repo.owner,
repo: repo.repo,
check_run_id: checkRunId,
per_page: 50,
page,
});
const chunk = Array.isArray(annotationsResp?.data) ? annotationsResp.data : [];
annotations.push(...chunk);
if (chunk.length < 50) {
break;
}
} catch {
break;
}
}
if (annotations.length > 0) {
parsedAnnotations.set(checkRunId, annotations);
}
}
}
checkRunsOut = checkRuns.map((run) => {
@@ -6796,14 +6916,16 @@ async function main(options = {}) {
url: picked.html_url,
name: picked.name,
conclusion: picked.conclusion,
steps: Array.isArray(picked.steps)
? picked.steps.map((s) => ({
name: s.name,
status: s.status,
conclusion: s.conclusion,
number: s.number,
}))
: undefined,
steps: Array.isArray(picked.steps)
? picked.steps.map((s) => ({
name: s.name,
status: s.status,
conclusion: s.conclusion,
number: s.number,
startedAt: s.started_at || undefined,
completedAt: s.completed_at || undefined,
}))
: undefined,
};
} else {
job = { runId, ...(jobId ? { jobId } : {}), url: detailsUrl };
@@ -6831,6 +6953,19 @@ async function main(options = {}) {
}
: undefined,
...(job ? { job } : {}),
...(run.id && parsedAnnotations.has(run.id)
? {
annotations: parsedAnnotations.get(run.id).map((a) => ({
path: a.path || undefined,
startLine: typeof a.start_line === 'number' ? a.start_line : undefined,
endLine: typeof a.end_line === 'number' ? a.end_line : undefined,
level: a.annotation_level || undefined,
message: a.message || '',
title: a.title || undefined,
rawDetails: a.raw_details || undefined,
})).filter((a) => a.message),
}
: {}),
};
});
const counts = { success: 0, failure: 0, pending: 0 };
@@ -7440,12 +7575,17 @@ async function main(options = {}) {
const diffSummaries = diffs.map(({ path, diff }) => `FILE: ${path}\n${diff}`).join('\n\n');
let prompt = `You are drafting a GitHub Pull Request title + description. Respond in JSON of the shape {"title": string, "body": string} (ONLY JSON in response, no markdown fences) with these rules:
- title: concise, sentence case, <= 80 chars, no trailing punctuation, no commit-style prefixes (no "feat:", "fix:")
- body: GitHub-flavored markdown with these sections in this order: Summary, Testing, Notes
- Summary: 3-6 bullet points describing user-visible changes; avoid internal helper function names
- Testing: bullet list ("- Not tested" allowed)
- Notes: bullet list; include breaking/rollout notes only when relevant
let prompt = `You are drafting a GitHub Pull Request title + description for a squash-merge workflow.
Respond in JSON of the shape {"title": string, "body": string} (ONLY JSON in response, no markdown fences) with these rules:
- Title format: conventional, outcome-first, <= 90 chars, no trailing punctuation.
- Use: <type>(<scope>): <summary>. Types: feat, fix, refactor, perf, docs, test, chore.
- Pick the most important user-facing outcome first; include a second major outcome only when needed.
- Body: GitHub-flavored markdown with sections in this exact order: ## Summary, ## Why, ## Testing.
- Summary: 3-6 bullets, concrete product/workflow impact, no vague filler, no internal helper names.
- Why: 1-3 bullets explaining motivation/tradeoff (what problem this solves for users/devs).
- Testing: checkbox list using "- [ ]"; include realistic manual/automated checks inferred from the diff.
- If tests were not run, include "- [ ] Not run locally" as first testing item.
- Keep language crisp and specific; avoid generic boilerplate.
Context:
- base branch: ${base}