hale-commenting-system 3.7.2 → 3.8.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hale-commenting-system",
3
- "version": "3.7.2",
3
+ "version": "3.8.0",
4
4
  "description": "A commenting system for PatternFly React applications that allows designers and developers to add comments directly on design pages, sync with GitHub Issues, and link Jira tickets.",
5
5
  "homepage": "https://www.npmjs.com/package/hale-commenting-system",
6
6
  "license": "MIT",
@@ -369,12 +369,34 @@ function generateFiles(config) {
369
369
 
370
370
  `;
371
371
 
372
- if (config.github && config.github.clientId) {
372
+ // Provider configuration (GitHub or GitLab)
373
+ if (config.provider && config.provider.type === 'gitlab') {
374
+ envContent += `# Provider Type
375
+ VITE_PROVIDER_TYPE=gitlab
376
+
377
+ # GitLab OAuth (client-side; safe to expose)
378
+ VITE_GITLAB_CLIENT_ID=${config.provider.clientId}
379
+ VITE_GITLAB_BASE_URL=${config.provider.baseUrl}
380
+
381
+ # Target project for Issues/Comments
382
+ VITE_GITLAB_PROJECT_PATH=${config.provider.projectPath}
383
+
384
+ `;
385
+ } else if (config.provider && config.provider.type === 'github') {
386
+ envContent += `# Provider Type
387
+ VITE_PROVIDER_TYPE=github
388
+
389
+ # GitHub OAuth (client-side; safe to expose)
390
+ VITE_GITHUB_CLIENT_ID=${config.provider.clientId}
391
+
392
+ # Target repo for Issues/Comments
393
+ VITE_GITHUB_OWNER=${config.provider.owner || config.owner}
394
+ VITE_GITHUB_REPO=${config.provider.repo || config.repo}
395
+
396
+ `;
397
+ } else if (config.github && config.github.clientId) {
398
+ // Backward compatibility: old GitHub-only config
373
399
  envContent += `# GitHub OAuth (client-side; safe to expose)
374
- # Get your Client ID from: https://github.com/settings/developers
375
- # 1. Click "New OAuth App"
376
- # 2. Fill in the form (Homepage: http://localhost:9000, Callback: http://localhost:9000/api/github-oauth-callback)
377
- # 3. Copy the Client ID
378
400
  VITE_GITHUB_CLIENT_ID=${config.github.clientId}
379
401
 
380
402
  # Target repo for Issues/Comments
@@ -385,14 +407,11 @@ VITE_GITHUB_REPO=${config.github.repo || config.repo}
385
407
  } else {
386
408
  envContent += `# GitHub OAuth (client-side; safe to expose)
387
409
  # Get your Client ID from: https://github.com/settings/developers
388
- # 1. Click "New OAuth App"
389
- # 2. Fill in the form (Homepage: http://localhost:9000, Callback: http://localhost:9000/api/github-oauth-callback)
390
- # 3. Copy the Client ID
391
410
  VITE_GITHUB_CLIENT_ID=
392
411
 
393
412
  # Target repo for Issues/Comments
394
- VITE_GITHUB_OWNER=${config.owner}
395
- VITE_GITHUB_REPO=${config.repo}
413
+ VITE_GITHUB_OWNER=${config.owner || ''}
414
+ VITE_GITHUB_REPO=${config.repo || ''}
396
415
 
397
416
  `;
398
417
  }
@@ -457,7 +476,19 @@ VITE_JIRA_BASE_URL=
457
476
 
458
477
  `;
459
478
 
460
- if (config.github && config.github.clientSecret) {
479
+ // Provider secrets (GitHub or GitLab)
480
+ if (config.provider && config.provider.type === 'gitlab' && config.provider.clientSecret) {
481
+ envServerContent += `# GitLab OAuth Application Secret (server-only)
482
+ GITLAB_CLIENT_SECRET=${config.provider.clientSecret}
483
+
484
+ `;
485
+ } else if (config.provider && config.provider.type === 'github' && config.provider.clientSecret) {
486
+ envServerContent += `# GitHub OAuth Client Secret (server-only)
487
+ GITHUB_CLIENT_SECRET=${config.provider.clientSecret}
488
+
489
+ `;
490
+ } else if (config.github && config.github.clientSecret) {
491
+ // Backward compatibility: old GitHub-only config
461
492
  envServerContent += `# GitHub OAuth Client Secret (server-only)
462
493
  # Get this from your GitHub OAuth App settings: https://github.com/settings/developers
463
494
  # Click on your OAuth App, then "Generate a new client secret"
@@ -1355,24 +1386,35 @@ async function main() {
1355
1386
  }
1356
1387
  }
1357
1388
 
1358
- // Step 2: GitHub OAuth Setup (Optional)
1359
- console.log('\nšŸ“¦ Step 2: GitHub Integration (Optional)\n');
1360
- console.log('GitHub integration allows comments to sync with GitHub Issues.');
1361
- console.log('You can set this up now or add it later.\n');
1362
-
1363
- const setupGitHub = await prompt([
1389
+ // Step 2: Issue Tracking Integration
1390
+ console.log('\nšŸ“¦ Step 2: Issue Tracking Integration\n');
1391
+ console.log('Comments can sync with GitHub or GitLab Issues.');
1392
+ console.log('This allows comments to persist and be managed like regular issues.\n');
1393
+ console.log('Options:');
1394
+ console.log(' • GitHub - Sync with GitHub Issues');
1395
+ console.log(' • GitLab - Sync with GitLab Issues (supports self-hosted)');
1396
+ console.log(' • Skip - Set up later (you can still use local comments)\n');
1397
+
1398
+ const platformChoice = await prompt([
1364
1399
  {
1365
- type: 'confirm',
1366
- name: 'setup',
1367
- message: 'Do you want to set up GitHub integration now?',
1368
- default: true
1400
+ type: 'list',
1401
+ name: 'platform',
1402
+ message: 'Select your issue tracking platform:',
1403
+ choices: [
1404
+ { name: 'GitHub', value: 'github' },
1405
+ { name: 'GitLab', value: 'gitlab' },
1406
+ { name: 'Skip (set up later)', value: 'skip' }
1407
+ ],
1408
+ default: 'github'
1369
1409
  }
1370
1410
  ]);
1371
1411
 
1372
- let githubConfig = null;
1373
- let githubValid = false;
1412
+ const selectedPlatform = platformChoice.platform; // 'github', 'gitlab', or 'skip'
1374
1413
 
1375
- if (setupGitHub.setup) {
1414
+ let providerConfig = null;
1415
+ let providerValid = false;
1416
+
1417
+ if (selectedPlatform === 'github') {
1376
1418
  console.log('\nTo sync comments with GitHub Issues, we need to authenticate with GitHub.');
1377
1419
  console.log('This requires creating a GitHub OAuth App.\n');
1378
1420
  console.log('Instructions:');
@@ -1610,14 +1652,112 @@ async function main() {
1610
1652
  }
1611
1653
  console.log('āœ… GitHub credentials validated!\n');
1612
1654
 
1613
- githubConfig = {
1655
+ providerConfig = {
1656
+ type: 'github',
1614
1657
  clientId: githubAnswers.clientId,
1615
1658
  clientSecret: githubAnswers.clientSecret,
1616
1659
  owner: targetOwner,
1617
1660
  repo: targetRepo
1618
1661
  };
1619
- } else {
1620
- console.log('\nā­ļø Skipping GitHub setup. You can add it later by editing .env and .env.server files.\n');
1662
+ providerValid = githubValid;
1663
+ } else if (selectedPlatform === 'gitlab') {
1664
+ // GitLab setup flow
1665
+ console.log('\nTo sync comments with GitLab Issues, we need to authenticate with GitLab.');
1666
+ console.log('This requires creating a GitLab OAuth Application.\n');
1667
+
1668
+ // Prompt for GitLab instance URL
1669
+ console.log('šŸ’” This supports both gitlab.com and self-hosted GitLab instances.');
1670
+ console.log(' Examples:');
1671
+ console.log(' • https://gitlab.com (public GitLab)');
1672
+ console.log(' • https://gitlab.cee.redhat.com (Red Hat internal)\n');
1673
+
1674
+ const gitlabInstanceAnswer = await prompt([
1675
+ {
1676
+ type: 'input',
1677
+ name: 'baseUrl',
1678
+ message: 'GitLab instance URL:',
1679
+ default: 'https://gitlab.com',
1680
+ validate: (input) => {
1681
+ if (!input.trim()) return 'Base URL is required';
1682
+ try {
1683
+ new URL(input);
1684
+ return true;
1685
+ } catch {
1686
+ return 'Invalid URL format (must start with https://)';
1687
+ }
1688
+ }
1689
+ }
1690
+ ]);
1691
+
1692
+ const baseUrl = gitlabInstanceAnswer.baseUrl.replace(/\/+$/, '');
1693
+ const isSelfHosted = !baseUrl.includes('gitlab.com');
1694
+
1695
+ console.log('\nInstructions:');
1696
+ console.log(`1. Visit: ${baseUrl}/-/user_settings/applications`);
1697
+ console.log('2. Click "Add new application"');
1698
+ console.log('3. Fill in the form:');
1699
+ console.log(' - Name: Your app name (e.g., "My Design Comments")');
1700
+ console.log(' - Redirect URI: http://localhost:9000/api/gitlab-oauth-callback');
1701
+ console.log(' - Confidential: āœ“ (checked)');
1702
+ console.log(' - Scopes: āœ“ api (full API access)');
1703
+ console.log('4. Click "Save application"');
1704
+ console.log('5. Copy the Application ID and Secret\n');
1705
+
1706
+ const gitlabAnswers = await prompt([
1707
+ {
1708
+ type: 'input',
1709
+ name: 'clientId',
1710
+ message: 'GitLab Application ID:',
1711
+ validate: (input) => {
1712
+ if (!input.trim()) return 'Application ID is required';
1713
+ return true;
1714
+ }
1715
+ },
1716
+ {
1717
+ type: 'password',
1718
+ name: 'clientSecret',
1719
+ message: 'GitLab Application Secret:',
1720
+ mask: '*',
1721
+ validate: (input) => {
1722
+ if (!input.trim()) return 'Application Secret is required';
1723
+ return true;
1724
+ }
1725
+ }
1726
+ ]);
1727
+
1728
+ // Prompt for project path
1729
+ console.log('\nWhere do you want to store comments as GitLab Issues?');
1730
+ console.log('This should be a project you have maintainer/owner access to.');
1731
+ console.log('Format: group/project or namespace/group/project\n');
1732
+
1733
+ const projectPathAnswer = await prompt([
1734
+ {
1735
+ type: 'input',
1736
+ name: 'projectPath',
1737
+ message: 'GitLab project path (e.g., mygroup/myproject):',
1738
+ validate: (input) => {
1739
+ if (!input.trim()) return 'Project path is required';
1740
+ if (!input.includes('/')) return 'Project path must include at least one slash (e.g., group/project)';
1741
+ return true;
1742
+ }
1743
+ }
1744
+ ]);
1745
+
1746
+ // Note: GitLab credential validation would require more complex setup
1747
+ console.log('\nāš ļø Note: GitLab credentials will not be validated automatically.');
1748
+ console.log('Please ensure you have maintainer/owner access to the project and the OAuth app is configured correctly.\n');
1749
+
1750
+ providerConfig = {
1751
+ type: 'gitlab',
1752
+ clientId: gitlabAnswers.clientId,
1753
+ clientSecret: gitlabAnswers.clientSecret,
1754
+ baseUrl: baseUrl,
1755
+ projectPath: projectPathAnswer.projectPath
1756
+ };
1757
+ providerValid = true; // Assume valid since we can't validate GitLab easily
1758
+ } else if (selectedPlatform === 'skip') {
1759
+ console.log('\nā­ļø Skipping issue tracking setup. Comments will work locally only.');
1760
+ console.log('You can add GitHub or GitLab integration later by editing .env and .env.server files.\n');
1621
1761
  }
1622
1762
 
1623
1763
  // Step 3: Jira Setup (Optional)
@@ -1702,10 +1842,11 @@ async function main() {
1702
1842
  // Step 4: Generate files
1703
1843
  console.log('šŸ“ Step 4: Generating configuration files...\n');
1704
1844
  generateFiles({
1705
- github: githubConfig,
1845
+ provider: providerConfig,
1846
+ github: providerConfig && providerConfig.type === 'github' ? providerConfig : null, // For backward compat
1706
1847
  jira: jiraConfig,
1707
- owner: githubConfig ? githubConfig.owner : owner,
1708
- repo: githubConfig ? githubConfig.repo : repo
1848
+ owner: providerConfig && providerConfig.type === 'github' ? providerConfig.owner : owner,
1849
+ repo: providerConfig && providerConfig.type === 'github' ? providerConfig.repo : repo
1709
1850
  });
1710
1851
 
1711
1852
  // Step 5: Integrate into project
@@ -1785,10 +1926,10 @@ async function main() {
1785
1926
  console.log(' 4. Restart your dev server\n');
1786
1927
  }
1787
1928
 
1788
- if (!githubConfig || !jiraConfig) {
1929
+ if (!providerConfig || !jiraConfig) {
1789
1930
  console.log('šŸ“ To add integrations later:');
1790
- if (!githubConfig) {
1791
- console.log(' • GitHub: Edit .env and .env.server files (see comments in files for instructions)');
1931
+ if (!providerConfig) {
1932
+ console.log(' • GitHub/GitLab: Edit .env and .env.server files (see comments in files for instructions)');
1792
1933
  }
1793
1934
  if (!jiraConfig) {
1794
1935
  console.log(' • Jira: Edit .env and .env.server files (see comments in files for instructions)');
@@ -17,8 +17,9 @@ import {
17
17
  TextArea,
18
18
  Title,
19
19
  } from '@patternfly/react-core';
20
- import { ExternalLinkAltIcon, GithubIcon, InfoCircleIcon, TrashIcon } from '@patternfly/react-icons';
20
+ import { ExternalLinkAltIcon, GithubIcon, GitlabIcon, InfoCircleIcon, TrashIcon } from '@patternfly/react-icons';
21
21
  import { useComments } from '../contexts/CommentContext';
22
+ import { useProviderAuth } from '../contexts/ProviderAuthContext';
22
23
  import { DetailsTab } from './DetailsTab';
23
24
  import { JiraTab } from './JiraTab';
24
25
  import { FloatingWidget } from './FloatingWidget';
@@ -41,6 +42,8 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
41
42
  removePin,
42
43
  retrySync,
43
44
  } = useComments();
45
+ const { providerType } = useProviderAuth();
46
+ const ProviderIcon = providerType === 'gitlab' ? GitlabIcon : GithubIcon;
44
47
  const location = useLocation();
45
48
  const detectedVersion = getVersionFromPathOrQuery(location.pathname, location.search);
46
49
  const [newCommentText, setNewCommentText] = React.useState('');
@@ -170,7 +173,7 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
170
173
  switch (status) {
171
174
  case 'synced':
172
175
  return (
173
- <Label color="green" icon={<GithubIcon />}>
176
+ <Label color="green" icon={<ProviderIcon />}>
174
177
  Synced
175
178
  </Label>
176
179
  );
@@ -293,13 +296,13 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
293
296
  rel="noopener noreferrer"
294
297
  style={{ display: 'inline-flex', alignItems: 'center', gap: '0.25rem' }}
295
298
  >
296
- <GithubIcon />
299
+ <ProviderIcon />
297
300
  Issue #{selectedThread.issueNumber}
298
301
  <ExternalLinkAltIcon style={{ fontSize: '0.75rem' }} />
299
302
  </a>
300
303
  ) : (
301
304
  <span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.25rem', color: 'var(--pf-t--global--text--color--subtle)' }}>
302
- <GithubIcon />
305
+ <ProviderIcon />
303
306
  Issue pending…
304
307
  </span>
305
308
  )}
@@ -1,9 +1,9 @@
1
1
  import * as React from 'react';
2
2
  import { createPortal } from 'react-dom';
3
- import { Button, Switch, Title } from '@patternfly/react-core';
4
- import { GripVerticalIcon, WindowMinimizeIcon, GithubIcon, ArrowsAltVIcon } from '@patternfly/react-icons';
3
+ import { Button, Dropdown, DropdownItem, DropdownList, MenuToggle, Switch, Title } from '@patternfly/react-core';
4
+ import { GripVerticalIcon, WindowMinimizeIcon, GithubIcon, GitlabIcon, ArrowsAltVIcon } from '@patternfly/react-icons';
5
5
  import { useComments } from '../contexts/CommentContext';
6
- import { useGitHubAuth } from '../contexts/GitHubAuthContext';
6
+ import { useProviderAuth } from '../contexts/ProviderAuthContext';
7
7
 
8
8
  interface FloatingWidgetProps {
9
9
  children: React.ReactNode;
@@ -23,7 +23,10 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
23
23
  const resizeHandleRef = React.useRef<HTMLDivElement>(null);
24
24
 
25
25
  const { commentsEnabled, setCommentsEnabled, showPinsEnabled, setShowPinsEnabled } = useComments();
26
- const { isAuthenticated, user, login, logout } = useGitHubAuth();
26
+ const { isAuthenticated, user, login, logout, providerType, providerDisplayName, availableProviders } = useProviderAuth();
27
+ const ProviderIcon = providerType === 'gitlab' ? GitlabIcon : GithubIcon;
28
+ const [isSignInOpen, setIsSignInOpen] = React.useState(false);
29
+ const showProviderMenu = availableProviders.length > 1;
27
30
 
28
31
  const handleMouseDown = (e: React.MouseEvent) => {
29
32
  if (!widgetRef.current) return;
@@ -211,16 +214,58 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
211
214
  {isAuthenticated ? (
212
215
  <>
213
216
  <span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.25rem', fontSize: 'var(--pf-t--global--font--size--sm)' }}>
214
- <GithubIcon />
217
+ <ProviderIcon />
215
218
  {user?.login ? `@${user.login}` : 'Signed in'}
216
219
  </span>
217
220
  <Button variant="link" isInline onClick={logout} style={{ fontSize: 'var(--pf-t--global--font--size--sm)' }}>
218
221
  Sign out
219
222
  </Button>
220
223
  </>
224
+ ) : showProviderMenu ? (
225
+ <Dropdown
226
+ isOpen={isSignInOpen}
227
+ onSelect={() => setIsSignInOpen(false)}
228
+ toggle={(toggleRef) => (
229
+ <MenuToggle
230
+ ref={toggleRef}
231
+ variant="plain"
232
+ isExpanded={isSignInOpen}
233
+ onClick={() => setIsSignInOpen((prev) => !prev)}
234
+ style={{ fontSize: 'var(--pf-t--global--font--size--sm)' }}
235
+ aria-label="Sign in menu"
236
+ >
237
+ Sign in
238
+ </MenuToggle>
239
+ )}
240
+ >
241
+ <DropdownList>
242
+ {availableProviders.includes('github') && (
243
+ <DropdownItem
244
+ onClick={() => {
245
+ setIsSignInOpen(false);
246
+ login('github');
247
+ }}
248
+ icon={<GithubIcon />}
249
+ >
250
+ Sign in with GitHub
251
+ </DropdownItem>
252
+ )}
253
+ {availableProviders.includes('gitlab') && (
254
+ <DropdownItem
255
+ onClick={() => {
256
+ setIsSignInOpen(false);
257
+ login('gitlab');
258
+ }}
259
+ icon={<GitlabIcon />}
260
+ >
261
+ Sign in with GitLab
262
+ </DropdownItem>
263
+ )}
264
+ </DropdownList>
265
+ </Dropdown>
221
266
  ) : (
222
- <Button variant="link" isInline icon={<GithubIcon />} onClick={login} style={{ fontSize: 'var(--pf-t--global--font--size--sm)' }}>
223
- Sign in with GitHub
267
+ <Button variant="link" isInline icon={<ProviderIcon />} onClick={() => login()} style={{ fontSize: 'var(--pf-t--global--font--size--sm)' }}>
268
+ Sign in with {providerDisplayName}
224
269
  </Button>
225
270
  )}
226
271
  </div>
@@ -1,6 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { Comment, Thread, ComponentMetadata } from '../types';
3
- import { getStoredUser, githubAdapter, isGitHubConfigured } from '../services/githubAdapter';
3
+ import { getProviderAdapter } from '../services/providerFactory';
4
+ import { getStoredUser } from '../services/githubAdapter';
4
5
 
5
6
  interface CommentContextType {
6
7
  threads: Thread[];
@@ -31,6 +32,8 @@ interface CommentContextType {
31
32
  const CommentContext = React.createContext<CommentContextType | undefined>(undefined);
32
33
 
33
34
  export const CommentProvider: React.FunctionComponent<{ children: React.ReactNode }> = ({ children }) => {
35
+ const adapter = getProviderAdapter();
36
+
34
37
  const stripHaleReplyMarkers = (body: string): string => {
35
38
  // Remove hidden markers we embed for threading reconstruction
36
39
  return body
@@ -164,10 +167,10 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
164
167
  }
165
168
  // If not set, default to enabled when GitHub is not configured (standalone mode)
166
169
  // This allows the commenting system to work without GitHub/Jira integration
167
- return !isGitHubConfigured();
170
+ return !adapter.isConfigured();
168
171
  } catch {
169
172
  // On error, default to enabled if GitHub is not configured
170
- return !isGitHubConfigured();
173
+ return !adapter.isConfigured();
171
174
  }
172
175
  });
173
176
  const [selectedThreadId, setSelectedThreadId] = React.useState<string | null>(null);
@@ -180,10 +183,10 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
180
183
  }
181
184
  // If not set, default to open when GitHub is not configured (standalone mode)
182
185
  // This makes the commenting system visible immediately
183
- return !isGitHubConfigured();
186
+ return !adapter.isConfigured();
184
187
  } catch {
185
188
  // On error, default to open if GitHub is not configured
186
- return !isGitHubConfigured();
189
+ return !adapter.isConfigured();
187
190
  }
188
191
  });
189
192
  const [floatingWidgetMode, setFloatingWidgetMode] = React.useState<boolean>(() => {
@@ -257,7 +260,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
257
260
 
258
261
  const addThread = (cssSelector: string, elementDescription: string, componentMetadata: ComponentMetadata | null, xPercent: number, yPercent: number, route: string, version?: string): string => {
259
262
  const threadId = `thread-${Date.now()}`;
260
- const isConfigured = isGitHubConfigured();
263
+ const isConfigured = adapter.isConfigured();
261
264
 
262
265
  console.log('šŸ“Œ addThread called:', {
263
266
  threadId,
@@ -295,7 +298,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
295
298
  if (isConfigured) {
296
299
  console.log(`šŸ”µ Creating GitHub issue for thread ${threadId}...`);
297
300
 
298
- githubAdapter
301
+ adapter
299
302
  .createIssue({
300
303
  title: `Feedback: ${route}`,
301
304
  body: `Thread created from pin at (${xPercent.toFixed(1)}%, ${yPercent.toFixed(1)}%).`,
@@ -318,7 +321,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
318
321
  const num = result.data.number;
319
322
  hiddenIssueNumbersRef.current.add(num);
320
323
  writeNumberSet(HIDDEN_ISSUES_KEY, hiddenIssueNumbersRef.current);
321
- githubAdapter.closeIssue(num).catch(() => undefined);
324
+ adapter.closeIssue(num).catch(() => undefined);
322
325
  removedThreadIdsRef.current.delete(threadId);
323
326
  }
324
327
 
@@ -394,7 +397,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
394
397
  };
395
398
 
396
399
  const syncFromGitHub = async (route: string, version?: string) => {
397
- if (!isGitHubConfigured()) return;
400
+ if (!adapter.isConfigured()) return;
398
401
 
399
402
  const key = `${route}::${version ?? ''}`;
400
403
  const existing = syncInFlightByKey.current.get(key);
@@ -417,7 +420,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
417
420
  const run = (async () => {
418
421
  setSyncInFlightCount((c) => c + 1);
419
422
  try {
420
- const issuesResult = await githubAdapter.fetchIssuesForRouteAndVersion(route, version);
423
+ const issuesResult = await adapter.fetchIssuesForRouteAndVersion(route, version);
421
424
  if (!issuesResult.success || !issuesResult.data) return;
422
425
 
423
426
  const hidden = hiddenIssueNumbersRef.current;
@@ -436,7 +439,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
436
439
 
437
440
  const metadata = parseMetadataFromIssueBody(issue?.body || '');
438
441
 
439
- const commentsResult = await githubAdapter.fetchIssueComments(issueNumber);
442
+ const commentsResult = await adapter.fetchIssueComments(issueNumber);
440
443
  const ghComments = commentsResult.success && commentsResult.data ? commentsResult.data : [];
441
444
 
442
445
  const mappedComments: Comment[] = (Array.isArray(ghComments) ? ghComments : []).map((c: any) => {
@@ -578,7 +581,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
578
581
  const thread = threadsRef.current.find((t) => t.id === threadId);
579
582
  const issueNumber = thread?.issueNumber;
580
583
 
581
- if (!isGitHubConfigured() || !thread) return;
584
+ if (!adapter.isConfigured() || !thread) return;
582
585
 
583
586
  // If the thread hasn't finished creating its issue yet, create it now, then backfill any local-only comments.
584
587
  const ensureIssueAndBackfill = async () => {
@@ -591,7 +594,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
591
594
  let ensuredIssueUrl = thread.issueUrl;
592
595
 
593
596
  if (!ensuredIssueNumber) {
594
- const created = await githubAdapter.createIssue({
597
+ const created = await adapter.createIssue({
595
598
  title: `Feedback: ${thread.route}`,
596
599
  body: `Thread created from pin${thread.elementDescription ? ` on ${thread.elementDescription}` : ''}.`,
597
600
  route: thread.route,
@@ -672,7 +675,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
672
675
  }
673
676
 
674
677
  const body = buildGitHubReplyBody(c.text, resolvedParentGitHubId ? { ...parentForBody, githubCommentId: resolvedParentGitHubId } : parentForBody);
675
- const res = await githubAdapter.createComment(ensuredIssueNumber!, body);
678
+ const res = await adapter.createComment(ensuredIssueNumber!, body);
676
679
  if (!res.success || !res.data?.id) {
677
680
  throw new Error(res.error || 'Failed to create GitHub comment');
678
681
  }
@@ -711,7 +714,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
711
714
  }
712
715
 
713
716
  const body = buildGitHubReplyBody(text, parent);
714
- githubAdapter
717
+ adapter
715
718
  .createComment(issueNumber, body)
716
719
  .then((result) => {
717
720
  if (!result.success) throw new Error(result.error || 'Failed to create GitHub comment');
@@ -758,8 +761,8 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
758
761
  }),
759
762
  );
760
763
 
761
- if (isGitHubConfigured() && issueNumber && githubCommentId) {
762
- githubAdapter.updateComment(githubCommentId, text).then((result) => {
764
+ if (adapter.isConfigured() && issueNumber && githubCommentId) {
765
+ adapter.updateComment(githubCommentId, text).then((result) => {
763
766
  if (result.success) {
764
767
  setThreads((prev) =>
765
768
  prev.map((t) => (t.id === threadId ? { ...t, syncStatus: 'synced', syncError: undefined } : t)),
@@ -787,7 +790,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
787
790
  issueNumber,
788
791
  githubCommentId,
789
792
  hasExistingComment: !!existingComment,
790
- isGitHubConfigured: isGitHubConfigured(),
793
+ isGitHubConfigured: adapter.isConfigured(),
791
794
  });
792
795
 
793
796
  // Remove from local state immediately (optimistic delete)
@@ -804,10 +807,10 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
804
807
  );
805
808
 
806
809
  // Attempt GitHub deletion if applicable
807
- if (isGitHubConfigured() && issueNumber && githubCommentId) {
810
+ if (adapter.isConfigured() && issueNumber && githubCommentId) {
808
811
  console.log(`šŸ”µ Attempting to delete GitHub comment #${githubCommentId} on issue #${issueNumber}`);
809
812
 
810
- githubAdapter.deleteComment(githubCommentId).then((result) => {
813
+ adapter.deleteComment(githubCommentId).then((result) => {
811
814
  if (result.success) {
812
815
  console.log(`āœ… Successfully deleted GitHub comment #${githubCommentId}`);
813
816
  setThreads((prev) =>
@@ -853,7 +856,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
853
856
  });
854
857
  } else {
855
858
  console.log(`ā„¹ļø GitHub deletion skipped:`, {
856
- reason: !isGitHubConfigured()
859
+ reason: !adapter.isConfigured()
857
860
  ? 'GitHub not configured'
858
861
  : !issueNumber
859
862
  ? 'No issue number'
@@ -878,10 +881,10 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
878
881
  // Keep the thread selected so the UI can switch to a "Reopen" state (GitHub-like)
879
882
 
880
883
  // Sync close to GitHub
881
- if (isGitHubConfigured() && issueNumber) {
884
+ if (adapter.isConfigured() && issueNumber) {
882
885
  console.log(`šŸ”µ Closing GitHub issue #${issueNumber}...`);
883
886
 
884
- githubAdapter.closeIssue(issueNumber).then((result) => {
887
+ adapter.closeIssue(issueNumber).then((result) => {
885
888
  if (result.success) {
886
889
  console.log(`āœ… Successfully closed GitHub issue #${issueNumber}`);
887
890
  setThreads((prev) =>
@@ -913,10 +916,10 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
913
916
  );
914
917
 
915
918
  // Sync reopen to GitHub
916
- if (isGitHubConfigured() && issueNumber) {
919
+ if (adapter.isConfigured() && issueNumber) {
917
920
  console.log(`šŸ”µ Reopening GitHub issue #${issueNumber}...`);
918
921
 
919
- githubAdapter.reopenIssue(issueNumber).then((result) => {
922
+ adapter.reopenIssue(issueNumber).then((result) => {
920
923
  if (result.success) {
921
924
  console.log(`āœ… Successfully reopened GitHub issue #${issueNumber}`);
922
925
  setThreads((prev) =>
@@ -944,7 +947,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
944
947
  setThreads((prev) => prev.filter((t) => t.id !== threadId));
945
948
  if (selectedThreadId === threadId) setSelectedThreadId(null);
946
949
 
947
- if (!isGitHubConfigured()) return;
950
+ if (!adapter.isConfigured()) return;
948
951
 
949
952
  if (!issueNumber) {
950
953
  // Issue may still be creating; mark so we can close it once we get the number.
@@ -959,7 +962,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
959
962
  pendingCloseIssueNumbersRef.current.add(issueNumber);
960
963
  writeNumberSet(PENDING_CLOSE_ISSUES_KEY, pendingCloseIssueNumbersRef.current);
961
964
 
962
- githubAdapter.closeIssue(issueNumber).then((result) => {
965
+ adapter.closeIssue(issueNumber).then((result) => {
963
966
  if (result.success) {
964
967
  pendingCloseIssueNumbersRef.current.delete(issueNumber);
965
968
  writeNumberSet(PENDING_CLOSE_ISSUES_KEY, pendingCloseIssueNumbersRef.current);
@@ -974,7 +977,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
974
977
  };
975
978
 
976
979
  const retrySync = async () => {
977
- if (!isGitHubConfigured()) return;
980
+ if (!adapter.isConfigured()) return;
978
981
  setSyncInFlightCount((c) => c + 1);
979
982
  try {
980
983
  const current = threadsRef.current;
@@ -985,7 +988,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
985
988
  if (t.syncStatus !== 'error' && t.syncStatus !== 'pending' && t.syncStatus !== 'syncing' && t.syncStatus !== 'local') continue;
986
989
 
987
990
  setThreads((prev) => prev.map((x) => (x.id === t.id ? { ...x, syncStatus: 'syncing', syncError: undefined } : x)));
988
- const created = await githubAdapter.createIssue({
991
+ const created = await adapter.createIssue({
989
992
  title: `Feedback: ${t.route}`,
990
993
  body: `Thread created from pin${t.elementDescription ? ` on ${t.elementDescription}` : ''}.`,
991
994
  route: t.route,
@@ -1021,7 +1024,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
1021
1024
  setThreads((prev) => prev.map((x) => (x.id === t.id ? { ...x, syncStatus: 'syncing', syncError: undefined } : x)));
1022
1025
 
1023
1026
  for (const c of localOnly) {
1024
- const res = await githubAdapter.createComment(t.issueNumber, c.text);
1027
+ const res = await adapter.createComment(t.issueNumber, c.text);
1025
1028
  if (res.success && res.data?.id) {
1026
1029
  const newId = res.data.id as number;
1027
1030
  setThreads((prev) =>