guito 1.0.11 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ All notable changes to Guito are documented in this file. The project follows [S
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.0.12] - 2026-10-15
8
+
9
+ ### Changed
10
+
11
+ - Azure DevOps is now detected from the repository's remotes instead of configured. Guito derives the project from `origin` (or the first other remote pointing to Azure DevOps) and creates pull requests against that remote; the settings dialog shows the detected project. The `guito.azureDevOpsUrl` VS Code setting and the Server URL field in the settings dialog are removed.
12
+
13
+ ### Added
14
+
15
+ - Pull request **Comments** tab now shows code context on inline threads: a focused snippet around the commented line with a **Show code context** loader and an expandable full-file diff, so reviewers see what a comment refers to without leaving the tab.
16
+ - Azure-generated comment activity now reads as plain language: vote entries like "changed status to 10" render as "approved" (with suggestions / waiting for author / rejected mapped the same way) and named status changes use friendly labels like Resolved, with the raw text kept as a tooltip. System entries in an inline thread's diff view render as a quiet one-line note instead of a comment with avatar and markdown, again with the raw text as a tooltip.
17
+ - Filter pull request comments by thread status (with counts), location (General vs Inline), and entry kind (comments vs activity), with a **Clear** reset and a distinct "No comments match these filters" empty state.
18
+ - Mark automated review findings as fixed from the PR dialog's **Review** tab with a per-finding **I fixed it** button. Fixed points move to **Already handled** with a green **Fixed** label and are never raised again by future reviews, like dismissed ones. A **Dim handled** toggle dims the handled section via opacity so pending points stand out; it is on by default and remembered per browser.
19
+ - Decline to review a pull request from the PR dialog's **Review** menu (**Decline to review** / **Resume review**). Reviewers who declined appear in their own **Declined** section of the dialog's reviewer list, and the side panel marks pull requests you declined with a **Declined** badge.
20
+
21
+ ### Fixed
22
+
23
+ - Right-clicking a branch badge in the commit history no longer offers the wrong actions for local branches whose names contain a slash (e.g. `feature/topic`). Badges are now resolved against the loaded branch list instead of guessing from the name, so **Checkout Branch...** opens the local switch dialog and **Delete Branch...** deletes the local branch instead of failing against a misread remote.
24
+ - Detached `HEAD` and symbolic `origin/HEAD` badges no longer offer **Rename**, **Delete** or **Pull** actions that Git would refuse, and **Unselect in Branches Dropdown** now removes only that branch from the history filter (instead of clearing the whole selection) and stays disabled when the branch is not selected.
25
+
7
26
  ## [1.0.11] - 2026-10-12
8
27
 
9
28
  ### Added
package/README.md CHANGED
@@ -111,9 +111,10 @@ The settings dialog also manages preferences that have no VS Code equivalent: th
111
111
 
112
112
  ### Azure DevOps
113
113
 
114
+ Guito detects Azure DevOps from the repository's remotes: when `origin` (or, failing that, any other remote) has a URL like `https://server/DefaultCollection/Project/_git/Repo`, pull request creation, the pull request panel, and the automated reviewer are enabled for that project. ssh remotes are reached over https on the same host. Requests authenticate with Windows integrated auth.
115
+
114
116
  | Setting | Description |
115
117
  | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
116
- | `guito.azureDevOpsUrl` | Base URL of your on-prem Azure DevOps Server, for example `https://server/DefaultCollection`. Enables pull request creation and the automated reviewer; requests authenticate with Windows integrated auth. |
117
118
  | `guito.prBranchNameTemplate` | Branch name template used when Guito creates a new branch for a pull request. Defaults to `pr/${randomstring}`. Variables: `${username}`, `${randomstring}`, `${branch}`, `${targetbranch}`, `${title}`, `${repository}`, `${date}` (UTC `YYYY-MM-DD`), `${time}` (UTC `HHmmss`), and `${timestamp}`. |
118
119
  | `guito.allowMerge` | Offer the "No fast-forward (merge commit)" and "Semi-linear merge" pull request completion strategies. On by default. |
119
120
 
package/bin/ai-review.js CHANGED
@@ -346,8 +346,9 @@ export function buildReviewPrompt(input) {
346
346
  const incremental = Boolean(sinceCommit);
347
347
  const perFile = Math.max(2000, Math.floor(input.maxDiffChars / Math.max(diffs.length, 1)));
348
348
  const body = diffs.map((diff) => renderFileDiff(diff, perFile)).join('\n');
349
- const raised = previous.filter((finding) => finding.status !== 'dismissed');
349
+ const raised = previous.filter((finding) => finding.status === 'pending' || finding.status === 'posted');
350
350
  const dismissed = previous.filter((finding) => finding.status === 'dismissed');
351
+ const fixed = previous.filter((finding) => finding.status === 'fixed');
351
352
  const sections = [
352
353
  'You are reviewing an Azure DevOps pull request as a careful senior engineer on this team.',
353
354
  '',
@@ -377,6 +378,13 @@ export function buildReviewPrompt(input) {
377
378
  .join('\n')
378
379
  : ' None.',
379
380
  '',
381
+ 'POINTS THE USER FIXED (never raise these again):',
382
+ fixed.length
383
+ ? fixed
384
+ .map((finding) => ` - ${finding.file || 'pull request'} — ${finding.title}`)
385
+ .join('\n')
386
+ : ' None.',
387
+ '',
380
388
  'EXISTING COMMENT THREADS ON THE PULL REQUEST:',
381
389
  renderThreads(threads),
382
390
  '',
@@ -755,6 +763,22 @@ export function createAiReviewer(deps, initialConfig = DEFAULT_AI_REVIEW_CONFIG,
755
763
  return next;
756
764
  });
757
765
  },
766
+ async fix(pullRequestId, findingIds) {
767
+ const wanted = new Set(findingIds);
768
+ return mutateStore((store) => {
769
+ const current = store.pullRequests[String(pullRequestId)];
770
+ if (!current)
771
+ throw new Error(`Pull request ${pullRequestId} has not been reviewed yet.`);
772
+ const next = {
773
+ ...current,
774
+ findings: current.findings.map((finding) => finding.status === 'pending' && wanted.has(finding.id)
775
+ ? { ...finding, status: 'fixed', resolvedAt: now().toISOString() }
776
+ : finding),
777
+ };
778
+ store.pullRequests[String(pullRequestId)] = next;
779
+ return next;
780
+ });
781
+ },
758
782
  async forget(pullRequestId) {
759
783
  await mutateStore((store) => {
760
784
  delete store.pullRequests[String(pullRequestId)];
@@ -61,6 +61,20 @@ const AZURE_THREAD_STATUSES = {
61
61
  pending: 7,
62
62
  };
63
63
  const azureThreadStatusName = (value) => Object.entries(AZURE_THREAD_STATUSES).find(([, numeric]) => numeric === value)?.[0] ?? 'unknown';
64
+ /** Azure DevOps comment types, by their numeric REST value. */
65
+ const AZURE_COMMENT_TYPES = {
66
+ unknown: 0,
67
+ text: 1,
68
+ codeChange: 2,
69
+ system: 3,
70
+ };
71
+ const azureCommentTypeName = (value) => {
72
+ if (typeof value === 'string' && value) {
73
+ return value;
74
+ }
75
+ const numeric = Number(value ?? 0);
76
+ return (Object.entries(AZURE_COMMENT_TYPES).find(([, entry]) => entry === numeric)?.[0] ?? 'unknown');
77
+ };
64
78
  /** Pull request reviewer votes, by their numeric REST value. */
65
79
  const AZURE_VOTES = {
66
80
  reset: 0,
@@ -111,47 +125,40 @@ export function parseAzureRemoteUrl(remoteUrl) {
111
125
  return { projectPath, repo };
112
126
  }
113
127
  /**
114
- * Builds the encoded "{origin}[/{collection}]/{project}" prefix for Azure
115
- * DevOps REST calls. When the base URL already contains the collection
116
- * prefix of the remote path it is not duplicated.
128
+ * Detects the encoded "{origin}/{collection-or-org}/{project}" prefix for
129
+ * Azure DevOps REST calls from a git remote URL. https remotes keep their
130
+ * scheme, host and port (credentials are dropped); ssh remotes map to https
131
+ * on the same host, since ssh carries no web address.
117
132
  */
118
- export function buildAzureProjectPrefix(baseUrl, remoteUrl) {
119
- const base = (baseUrl ?? '').trim().replace(/\/+$/, '');
120
- if (!base) {
121
- return null;
122
- }
123
- const info = parseAzureRemoteUrl(remoteUrl);
133
+ export function buildAzureProjectPrefix(remoteUrl) {
134
+ const value = (remoteUrl ?? '').trim();
135
+ const info = parseAzureRemoteUrl(value);
124
136
  if (!info) {
125
137
  return null;
126
138
  }
127
- const encodePath = (value) => value
139
+ let origin;
140
+ const httpMatch = value.match(/^(https?):\/\/(?:[^@/]*@)?([^/]+)\//i);
141
+ if (httpMatch) {
142
+ origin = `${httpMatch[1].toLowerCase()}://${httpMatch[2]}`;
143
+ }
144
+ else {
145
+ // ssh://[user@]host[:port]/path or scp-style user@host:path.
146
+ const host = value.match(/^ssh:\/\/(?:[^@/]*@)?([^/:]+)/i)?.[1] ?? value.match(/^[^@/]+@([^:]+):/)?.[1];
147
+ if (!host) {
148
+ return null;
149
+ }
150
+ origin = `https://${host}`;
151
+ }
152
+ const path = info.projectPath
128
153
  .split('/')
129
154
  .filter(Boolean)
130
155
  .map((segment) => encodeURIComponent(segment))
131
156
  .join('/');
132
- try {
133
- const parsed = new URL(base);
134
- const decode = (path) => path
135
- .split('/')
136
- .filter(Boolean)
137
- .map((segment) => decodeURIComponent(segment));
138
- const baseParts = decode(parsed.pathname);
139
- const remoteParts = info.projectPath.split('/').filter(Boolean);
140
- let overlap = Math.min(baseParts.length, remoteParts.length);
141
- while (overlap &&
142
- !baseParts
143
- .slice(-overlap)
144
- .every((part, index) => part.toLowerCase() === remoteParts[index].toLowerCase()))
145
- overlap--;
146
- return `${parsed.origin}/${[...baseParts, ...remoteParts.slice(overlap)].map(encodeURIComponent).join('/')}`;
147
- }
148
- catch {
149
- return `${base}/${encodePath(info.projectPath)}`;
150
- }
157
+ return `${origin}/${path}`;
151
158
  }
152
- /** Builds the pullrequests REST URL from the server base URL and origin remote. */
153
- export function buildAzurePullRequestUrl(baseUrl, remoteUrl) {
154
- const prefix = buildAzureProjectPrefix(baseUrl, remoteUrl);
159
+ /** Builds the pullrequests REST URL from an Azure DevOps git remote URL. */
160
+ export function buildAzurePullRequestUrl(remoteUrl) {
161
+ const prefix = buildAzureProjectPrefix(remoteUrl);
155
162
  if (!prefix) {
156
163
  return null;
157
164
  }
@@ -251,7 +258,7 @@ const azureErrorMessage = (result) => {
251
258
  return result.body;
252
259
  }
253
260
  };
254
- export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 8080, apiToken, azureDevOpsUrl, prBranchNameTemplate, autoReload, diffViewer, showGraph, showTags, showRemoteBranches, grayRefBadges, issueRegex, issueUrl, allowMerge, fileListView, refListView, sidePanelSectionsExpanded, searchMode, searchCaseSensitive, worktreeSort, prSort, aiReview, azureRequestImpl, aiReviewModelImpl, avatarFetchImpl, onLog, }) {
261
+ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 8080, apiToken, prBranchNameTemplate, autoReload, diffViewer, showGraph, showTags, showRemoteBranches, grayRefBadges, issueRegex, issueUrl, allowMerge, fileListView, refListView, sidePanelSectionsExpanded, searchMode, searchCaseSensitive, worktreeSort, prSort, aiReview, azureRequestImpl, aiReviewModelImpl, avatarFetchImpl, onLog, }) {
255
262
  // Initialize server
256
263
  const app = fastify({
257
264
  logger: false,
@@ -496,10 +503,9 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
496
503
  return branch;
497
504
  };
498
505
  const repoRoot = async () => (await git.revparse(['--show-toplevel'])).trim();
499
- // ==================== Azure DevOps settings ====================
506
+ // ==================== Settings ====================
500
507
  // Settings live in the repository's git directory so they are per-repo and
501
- // never committed. The VS Code extension passes its own setting via
502
- // azureDevOpsUrl, which wins over the file.
508
+ // never committed.
503
509
  let settingsPath = '';
504
510
  let reviewStorePath = '';
505
511
  // Per worktree: each one has its own uncommitted changes.
@@ -527,33 +533,39 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
527
533
  await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
528
534
  };
529
535
  const azureRequest = azureRequestImpl ?? defaultAzureRequest;
530
- const effectiveAzureUrl = async () => {
531
- const configuredAzureUrl = azureDevOpsUrl?.trim() ?? '';
532
- if (configuredAzureUrl) {
533
- return { url: configuredAzureUrl, source: 'vscode' };
534
- }
535
- const settings = await readSettings();
536
- const url = typeof settings.azureDevOpsUrl === 'string' ? settings.azureDevOpsUrl.trim() : '';
537
- return { url, source: url ? 'file' : '' };
538
- };
539
- /** Resolves the Azure DevOps project prefix or throws a user-facing error. */
540
- const azureProjectContext = async () => {
541
- const effective = await effectiveAzureUrl();
542
- if (!effective.url) {
543
- throw new Error('Azure DevOps URL is not configured. Set it in Settings (gear icon) or the guito.azureDevOpsUrl VS Code setting.');
544
- }
545
- let remoteUrl = '';
536
+ /**
537
+ * Detects the Azure DevOps remote: "origin" when it points to Azure DevOps,
538
+ * else the first remote that does. Null when no remote is an Azure DevOps
539
+ * git URL.
540
+ */
541
+ const detectAzureRemote = async () => {
542
+ let remotes = [];
546
543
  try {
547
- remoteUrl = (await git.remote(['get-url', 'origin'])).trim();
544
+ remotes = await git.getRemotes(true);
548
545
  }
549
546
  catch {
550
- throw new Error('No "origin" remote is configured for this repository.');
547
+ return null;
551
548
  }
552
- const prefix = buildAzureProjectPrefix(effective.url, remoteUrl);
553
- if (!prefix) {
554
- throw new Error(`The origin remote does not point to an Azure DevOps project: ${remoteUrl}`);
549
+ const ordered = [
550
+ ...remotes.filter((entry) => entry.name === 'origin'),
551
+ ...remotes.filter((entry) => entry.name !== 'origin'),
552
+ ];
553
+ for (const entry of ordered) {
554
+ const remoteUrl = entry.refs.fetch.trim();
555
+ const prefix = buildAzureProjectPrefix(remoteUrl);
556
+ if (prefix) {
557
+ return { remote: entry.name, remoteUrl, prefix };
558
+ }
555
559
  }
556
- return { prefix, remoteUrl };
560
+ return null;
561
+ };
562
+ /** Resolves the Azure DevOps remote and project prefix or throws a user-facing error. */
563
+ const azureProjectContext = async () => {
564
+ const detected = await detectAzureRemote();
565
+ if (!detected) {
566
+ throw new Error('No remote of this repository points to an Azure DevOps project (expected a URL like https://server/Collection/Project/_git/Repo).');
567
+ }
568
+ return detected;
557
569
  };
558
570
  /** Human identity of the authenticated Windows/Azure user; cached per run. */
559
571
  let cachedAzureMe = null;
@@ -602,6 +614,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
602
614
  email: String(entry?.uniqueName ?? ''),
603
615
  vote: Number(entry?.vote ?? 0),
604
616
  isRequired: entry?.isRequired === true,
617
+ hasDeclined: entry?.hasDeclined === true,
605
618
  }));
606
619
  const mine = me ? reviewers.find((reviewer) => reviewer.id === me.id) : undefined;
607
620
  return {
@@ -622,6 +635,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
622
635
  reviewers,
623
636
  myVote: mine ? Number(mine.vote ?? 0) : 0,
624
637
  requiresMe: mine?.isRequired === true,
638
+ myDeclined: mine?.hasDeclined === true,
625
639
  };
626
640
  };
627
641
  /** REST base of one pull request (".../pullrequests/{id}") plus repo context. */
@@ -676,7 +690,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
676
690
  };
677
691
  /** Merges the VS Code settings with the server-side settings file. */
678
692
  const effectiveSettings = async () => {
679
- const [file, azure] = await Promise.all([readSettings(), effectiveAzureUrl()]);
693
+ const [file, azure] = await Promise.all([readSettings(), detectAzureRemote()]);
680
694
  const fileAutoReload = typeof file.autoReload === 'boolean' ? file.autoReload : undefined;
681
695
  const fileShowGraph = typeof file.showGraph === 'boolean' ? file.showGraph : undefined;
682
696
  const fileShowTags = typeof file.showTags === 'boolean' ? file.showTags : undefined;
@@ -734,9 +748,8 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
734
748
  }
735
749
  }
736
750
  return {
737
- azureDevOpsUrl: azure.url,
751
+ azureDevOpsUrl: azure?.prefix ?? '',
738
752
  prBranchNameTemplate: prBranchNameTemplate?.trim() || filePrBranchNameTemplate || DEFAULT_PR_BRANCH_NAME_TEMPLATE,
739
- source: azure.source,
740
753
  autoReload: typeof autoReload === 'boolean' ? autoReload : (fileAutoReload ?? true),
741
754
  diffViewer: diffViewer === 'vscode' ? 'vscode' : 'guito',
742
755
  showGraph: typeof showGraph === 'boolean' ? showGraph : (fileShowGraph ?? true),
@@ -1818,15 +1831,8 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
1818
1831
  try {
1819
1832
  const body = req.body ?? {};
1820
1833
  const settings = await readSettings();
1821
- if ('azureDevOpsUrl' in body) {
1822
- const url = String(body.azureDevOpsUrl ?? '').trim();
1823
- if (url && !/^https?:\/\//i.test(url)) {
1824
- return resp.status(400).type('application/json').send({
1825
- error: 'The Azure DevOps URL must start with http:// or https://.',
1826
- });
1827
- }
1828
- settings.azureDevOpsUrl = url;
1829
- }
1834
+ // Superseded by detection from the remotes; drop values older versions stored.
1835
+ delete settings.azureDevOpsUrl;
1830
1836
  if ('prBranchNameTemplate' in body) {
1831
1837
  const template = String(body.prBranchNameTemplate ?? '').trim();
1832
1838
  if (template.length > 200) {
@@ -1958,7 +1964,12 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
1958
1964
  if (!targetBranch) {
1959
1965
  return resp.status(400).type('application/json').send({ error: 'targetBranch required' });
1960
1966
  }
1961
- const { prefix, remoteUrl } = await azureProjectContext();
1967
+ const { remote, prefix, remoteUrl } = await azureProjectContext();
1968
+ // Branch names may arrive as remote-tracking refs ("origin/main").
1969
+ const stripRemote = (name) => {
1970
+ const branch = name.replace(/^refs\/heads\//, '');
1971
+ return branch.startsWith(`${remote}/`) ? branch.slice(remote.length + 1) : branch;
1972
+ };
1962
1973
  const info = parseAzureRemoteUrl(remoteUrl);
1963
1974
  if (!info) {
1964
1975
  return resp.status(400).type('application/json').send({
@@ -1983,9 +1994,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
1983
1994
  username: computerUsername,
1984
1995
  randomstring: randomBranchSuffix(),
1985
1996
  branch: (await git.revparse(['--abbrev-ref', 'HEAD'])).trim(),
1986
- targetbranch: String(targetBranch)
1987
- .replace(/^refs\/heads\//, '')
1988
- .replace(/^origin\//, ''),
1997
+ targetbranch: stripRemote(String(targetBranch)),
1989
1998
  title: String(title ?? '').trim() || 'pull-request',
1990
1999
  repository: basename(await repoRoot()),
1991
2000
  date: now.toISOString().slice(0, 10),
@@ -2001,23 +2010,23 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2001
2010
  });
2002
2011
  }
2003
2012
  const existingBranch = await git
2004
- .raw(['ls-remote', '--heads', 'origin', `refs/heads/${name}`])
2013
+ .raw(['ls-remote', '--heads', remote, `refs/heads/${name}`])
2005
2014
  .catch(() => '');
2006
2015
  if (existingBranch.trim()) {
2007
2016
  return resp.status(400).send({
2008
- error: `The pull request branch already exists on origin: ${name}`,
2017
+ error: `The pull request branch already exists on ${remote}: ${name}`,
2009
2018
  });
2010
2019
  }
2011
- await git.push(['origin', `HEAD:refs/heads/${name}`]);
2020
+ await git.push([remote, `HEAD:refs/heads/${name}`]);
2012
2021
  source = name;
2013
2022
  }
2014
2023
  else {
2015
2024
  if (!source) {
2016
2025
  return resp.status(400).type('application/json').send({ error: 'sourceBranch required' });
2017
2026
  }
2018
- if (source.startsWith('origin/')) {
2027
+ if (source.startsWith(`${remote}/`)) {
2019
2028
  // Remote-tracking branch: the PR source is the branch name on the remote.
2020
- source = source.slice('origin/'.length);
2029
+ source = source.slice(remote.length + 1);
2021
2030
  }
2022
2031
  else {
2023
2032
  // Publish the branch first when the remote does not have it yet.
@@ -2027,23 +2036,21 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2027
2036
  // simple-git, so list the tracking refs instead.
2028
2037
  const trackingRefs = await git.raw([
2029
2038
  'for-each-ref',
2030
- 'refs/remotes/origin',
2039
+ `refs/remotes/${remote}`,
2031
2040
  '--format=%(refname)',
2032
2041
  ]);
2033
2042
  const published = trackingRefs
2034
2043
  .split('\n')
2035
- .some((line) => line.trim() === `refs/remotes/origin/${source}`);
2044
+ .some((line) => line.trim() === `refs/remotes/${remote}/${source}`);
2036
2045
  if (!published) {
2037
- await git.push(['origin', source]);
2046
+ await git.push([remote, source]);
2038
2047
  }
2039
2048
  }
2040
2049
  }
2041
2050
  const payload = {
2042
2051
  isDraft: req.body?.isDraft === true,
2043
2052
  sourceRefName: `refs/heads/${source}`,
2044
- targetRefName: `refs/heads/${String(targetBranch)
2045
- .replace(/^refs\/heads\//, '')
2046
- .replace(/^origin\//, '')}`,
2053
+ targetRefName: `refs/heads/${stripRemote(String(targetBranch))}`,
2047
2054
  };
2048
2055
  const trimmedTitle = String(title ?? '').trim();
2049
2056
  const trimmedDescription = String(description ?? '').trim();
@@ -2480,7 +2487,27 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2480
2487
  }
2481
2488
  const { base } = await azurePrApiBase(Number(req.params?.id));
2482
2489
  const me = await azureMe();
2483
- await azureJson('PUT', `${base}/reviewers/${encodeURIComponent(me.id)}?api-version=${AZURE_API_VERSION}`, JSON.stringify({ vote: AZURE_VOTES[vote] }));
2490
+ await azureJson('PUT', `${base}/reviewers/${encodeURIComponent(me.id)}?api-version=${AZURE_API_VERSION}`, JSON.stringify({ vote: AZURE_VOTES[vote], hasDeclined: false }));
2491
+ return resp.type('application/json').send({ ok: true });
2492
+ }
2493
+ catch (err) {
2494
+ return resp.status(400).type('application/json').send({ error: err.message });
2495
+ }
2496
+ });
2497
+ // Declines (or resumes) the current user's review on the pull request.
2498
+ // Azure keeps the reviewer on the pull request with hasDeclined set; the
2499
+ // record is read first so the required flag survives the PUT.
2500
+ app.post('/api/azure-devops/pullrequests/:id/decline', async (req, resp) => {
2501
+ try {
2502
+ const declined = req.body?.declined !== false;
2503
+ const { base } = await azurePrApiBase(Number(req.params?.id));
2504
+ const me = await azureMe();
2505
+ const reviewerUrl = `${base}/reviewers/${encodeURIComponent(me.id)}?api-version=${AZURE_API_VERSION}`;
2506
+ const current = await azureJson('GET', reviewerUrl);
2507
+ await azureJson('PUT', reviewerUrl, JSON.stringify({
2508
+ isRequired: current?.isRequired === true,
2509
+ hasDeclined: declined,
2510
+ }));
2484
2511
  return resp.type('application/json').send({ ok: true });
2485
2512
  }
2486
2513
  catch (err) {
@@ -2508,6 +2535,9 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2508
2535
  if (req.body?.vote !== undefined && Number.isFinite(Number(req.body.vote))) {
2509
2536
  payload.vote = Number(req.body.vote);
2510
2537
  }
2538
+ if (req.body?.hasDeclined !== undefined) {
2539
+ payload.hasDeclined = req.body.hasDeclined === true;
2540
+ }
2511
2541
  await azureJson('PUT', reviewerUrl, JSON.stringify(payload));
2512
2542
  return resp.type('application/json').send({ ok: true });
2513
2543
  }
@@ -2627,6 +2657,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2627
2657
  content: String(comment?.content ?? ''),
2628
2658
  createdAt: String(comment?.publishedDate ?? ''),
2629
2659
  isDeleted: false,
2660
+ commentType: azureCommentTypeName(comment?.commentType),
2630
2661
  })),
2631
2662
  };
2632
2663
  });
@@ -3410,6 +3441,17 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
3410
3441
  return resp.status(400).type('application/json').send({ error: err.message });
3411
3442
  }
3412
3443
  });
3444
+ // Marks findings as fixed; a fixed point is never raised again for this PR.
3445
+ app.post('/api/azure-devops/pullrequests/:id/ai-review/fix', async (req, resp) => {
3446
+ try {
3447
+ const findingIds = (Array.isArray(req.body?.findingIds) ? req.body.findingIds : []).map((id) => String(id));
3448
+ const state = await aiReviewer.fix(Number(req.params?.id), findingIds);
3449
+ return resp.type('application/json').send({ state });
3450
+ }
3451
+ catch (err) {
3452
+ return resp.status(400).type('application/json').send({ error: err.message });
3453
+ }
3454
+ });
3413
3455
  // Drops everything remembered about a pull request, including dismissals.
3414
3456
  app.delete('/api/azure-devops/pullrequests/:id/ai-review', async (req, resp) => {
3415
3457
  try {
@@ -3745,7 +3787,6 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
3745
3787
  await app.close();
3746
3788
  },
3747
3789
  updateSettings: (settings) => {
3748
- azureDevOpsUrl = settings.azureDevOpsUrl;
3749
3790
  prBranchNameTemplate = settings.prBranchNameTemplate;
3750
3791
  autoReload = settings.autoReload;
3751
3792
  diffViewer = settings.diffViewer;
@@ -9,5 +9,5 @@
9
9
  <style>:root{--bg:#1e1e1e;--bg-alt:#232323;--bg-hover:#2a2d2e;--border:#3c3c3c;--text:#cccccc;--text-bright:#e8e8e8;--text-dim:#9d9d9d;--accent:#3794ff;--red:#f14c4c;--mono:ui-monospace, "Cascadia Code", "SF Mono", Menlo, Consolas, monospace}*{box-sizing:border-box}html,body{height:100%;margin:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:13px;color:var(--text);background:var(--bg);overflow:hidden}</style><link rel="stylesheet" href="styles-I7K2YZTW.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-I7K2YZTW.css"></noscript></head>
10
10
  <body>
11
11
  <app-root></app-root>
12
- <script src="polyfills-5CFQRCPP.js" type="module"></script><script src="main-3MOB345X.js" type="module"></script></body>
12
+ <script src="polyfills-5CFQRCPP.js" type="module"></script><script src="main-NA3UMMCQ.js" type="module"></script></body>
13
13
  </html>