From 76adbcb8201eff86f7436c0dc7a8d5613934c18e Mon Sep 17 00:00:00 2001
From: Sergei Shmakov
Date: Fri, 22 Aug 2025 13:20:31 +0200
Subject: [PATCH 01/10] Add linear provider and implement getting linear teams
(#4543, #4579)
---
docs/telemetry-events.md | 14 +--
package.json | 2 +-
pnpm-lock.yaml | 53 ++++++++++--
src/config.ts | 1 +
src/constants.integrations.ts | 1 +
.../integrationAuthenticationService.ts | 5 ++
.../integrations/authentication/linear.ts | 54 ++++++++++++
.../integrations/authentication/models.ts | 3 +
src/plus/integrations/integrationService.ts | 10 +++
src/plus/integrations/providers/linear.ts | 86 +++++++++++++++++++
src/plus/integrations/providers/models.ts | 11 ++-
.../integrations/providers/providersApi.ts | 4 +
src/plus/launchpad/enrichmentService.ts | 1 +
src/plus/launchpad/models/enrichedItem.ts | 2 +-
src/plus/startWork/startWork.ts | 1 +
15 files changed, 233 insertions(+), 15 deletions(-)
create mode 100644 src/plus/integrations/authentication/linear.ts
create mode 100644 src/plus/integrations/providers/linear.ts
diff --git a/docs/telemetry-events.md b/docs/telemetry-events.md
index f267fd4528316..5eaeb1a95f388 100644
--- a/docs/telemetry-events.md
+++ b/docs/telemetry-events.md
@@ -668,7 +668,7 @@ void
```typescript
{
'hostingProvider.key': string,
- 'hostingProvider.provider': 'github' | 'gitlab' | 'bitbucket' | 'azureDevOps' | 'bitbucket-server' | 'github-enterprise' | 'cloud-github-enterprise' | 'gitlab-self-hosted' | 'cloud-gitlab-self-hosted' | 'azure-devops-server' | 'jira' | 'trello'
+ 'hostingProvider.provider': 'github' | 'gitlab' | 'bitbucket' | 'azureDevOps' | 'bitbucket-server' | 'github-enterprise' | 'cloud-github-enterprise' | 'gitlab-self-hosted' | 'cloud-gitlab-self-hosted' | 'azure-devops-server' | 'jira' | 'linear' | 'trello'
}
```
@@ -679,7 +679,7 @@ void
```typescript
{
'hostingProvider.key': string,
- 'hostingProvider.provider': 'github' | 'gitlab' | 'bitbucket' | 'azureDevOps' | 'bitbucket-server' | 'github-enterprise' | 'cloud-github-enterprise' | 'gitlab-self-hosted' | 'cloud-gitlab-self-hosted' | 'azure-devops-server' | 'jira' | 'trello'
+ 'hostingProvider.provider': 'github' | 'gitlab' | 'bitbucket' | 'azureDevOps' | 'bitbucket-server' | 'github-enterprise' | 'cloud-github-enterprise' | 'gitlab-self-hosted' | 'cloud-gitlab-self-hosted' | 'azure-devops-server' | 'jira' | 'linear' | 'trello'
}
```
@@ -690,7 +690,7 @@ void
```typescript
{
'issueProvider.key': string,
- 'issueProvider.provider': 'github' | 'gitlab' | 'bitbucket' | 'azureDevOps' | 'bitbucket-server' | 'github-enterprise' | 'cloud-github-enterprise' | 'gitlab-self-hosted' | 'cloud-gitlab-self-hosted' | 'azure-devops-server' | 'jira' | 'trello'
+ 'issueProvider.provider': 'github' | 'gitlab' | 'bitbucket' | 'azureDevOps' | 'bitbucket-server' | 'github-enterprise' | 'cloud-github-enterprise' | 'gitlab-self-hosted' | 'cloud-gitlab-self-hosted' | 'azure-devops-server' | 'jira' | 'linear' | 'trello'
}
```
@@ -701,7 +701,7 @@ void
```typescript
{
'issueProvider.key': string,
- 'issueProvider.provider': 'github' | 'gitlab' | 'bitbucket' | 'azureDevOps' | 'bitbucket-server' | 'github-enterprise' | 'cloud-github-enterprise' | 'gitlab-self-hosted' | 'cloud-gitlab-self-hosted' | 'azure-devops-server' | 'jira' | 'trello'
+ 'issueProvider.provider': 'github' | 'gitlab' | 'bitbucket' | 'azureDevOps' | 'bitbucket-server' | 'github-enterprise' | 'cloud-github-enterprise' | 'gitlab-self-hosted' | 'cloud-gitlab-self-hosted' | 'azure-devops-server' | 'jira' | 'linear' | 'trello'
}
```
@@ -735,7 +735,7 @@ or when connection refresh is skipped due to being a non-cloud session
```typescript
{
- 'integration.id': 'github' | 'gitlab' | 'bitbucket' | 'azureDevOps' | 'bitbucket-server' | 'github-enterprise' | 'cloud-github-enterprise' | 'gitlab-self-hosted' | 'cloud-gitlab-self-hosted' | 'azure-devops-server' | 'jira' | 'trello'
+ 'integration.id': 'github' | 'gitlab' | 'bitbucket' | 'azureDevOps' | 'bitbucket-server' | 'github-enterprise' | 'cloud-github-enterprise' | 'gitlab-self-hosted' | 'cloud-gitlab-self-hosted' | 'azure-devops-server' | 'jira' | 'linear' | 'trello'
}
```
@@ -2892,7 +2892,7 @@ void
```typescript
{
'hostingProvider.key': string,
- 'hostingProvider.provider': 'github' | 'gitlab' | 'bitbucket' | 'azureDevOps' | 'bitbucket-server' | 'github-enterprise' | 'cloud-github-enterprise' | 'gitlab-self-hosted' | 'cloud-gitlab-self-hosted' | 'azure-devops-server' | 'jira' | 'trello',
+ 'hostingProvider.provider': 'github' | 'gitlab' | 'bitbucket' | 'azureDevOps' | 'bitbucket-server' | 'github-enterprise' | 'cloud-github-enterprise' | 'gitlab-self-hosted' | 'cloud-gitlab-self-hosted' | 'azure-devops-server' | 'jira' | 'linear' | 'trello',
// @deprecated: true
'remoteProviders.key': string
}
@@ -2905,7 +2905,7 @@ void
```typescript
{
'hostingProvider.key': string,
- 'hostingProvider.provider': 'github' | 'gitlab' | 'bitbucket' | 'azureDevOps' | 'bitbucket-server' | 'github-enterprise' | 'cloud-github-enterprise' | 'gitlab-self-hosted' | 'cloud-gitlab-self-hosted' | 'azure-devops-server' | 'jira' | 'trello',
+ 'hostingProvider.provider': 'github' | 'gitlab' | 'bitbucket' | 'azureDevOps' | 'bitbucket-server' | 'github-enterprise' | 'cloud-github-enterprise' | 'gitlab-self-hosted' | 'cloud-gitlab-self-hosted' | 'azure-devops-server' | 'jira' | 'linear' | 'trello',
// @deprecated: true
'remoteProviders.key': string
}
diff --git a/package.json b/package.json
index 738f06c263d77..b7ded63810c8a 100644
--- a/package.json
+++ b/package.json
@@ -25043,7 +25043,7 @@
},
"dependencies": {
"@gitkraken/gitkraken-components": "13.0.0-vnext.8",
- "@gitkraken/provider-apis": "0.29.6",
+ "@gitkraken/provider-apis": "0.29.7",
"@gitkraken/shared-web-components": "0.1.1-rc.15",
"@gk-nzaytsev/fast-string-truncated-width": "1.1.0",
"@lit-labs/signals": "0.1.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 36d40308b3e1a..7c7caf2eef1eb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -19,8 +19,8 @@ importers:
specifier: 13.0.0-vnext.8
version: 13.0.0-vnext.8(@types/react@19.0.12)(react@19.0.0)
'@gitkraken/provider-apis':
- specifier: 0.29.6
- version: 0.29.6(encoding@0.1.13)
+ specifier: 0.29.7
+ version: 0.29.7(encoding@0.1.13)
'@gitkraken/shared-web-components':
specifier: 0.1.1-rc.15
version: 0.1.1-rc.15
@@ -657,8 +657,8 @@ packages:
peerDependencies:
react: 19.0.0
- '@gitkraken/provider-apis@0.29.6':
- resolution: {integrity: sha512-aRgR7lL6MxnCFtbbNB5AJuQVRGBy6nJlZE+Yn0FZ/G8QrqgxhBkexVtG0ji/wiq2HmQ4DxNk6eo3xMfYqoTq6w==}
+ '@gitkraken/provider-apis@0.29.7':
+ resolution: {integrity: sha512-i1StvQ0L5UPnVNnnud6vN+E80SAIXp/iX1UxlCIXXibYiw088x+cGBsfvzopx6rtkacuDd0rJTN75CMbEKsGwA==}
engines: {node: '>= 14'}
'@gitkraken/shared-web-components@0.1.1-rc.15':
@@ -667,6 +667,11 @@ packages:
'@gk-nzaytsev/fast-string-truncated-width@1.1.0':
resolution: {integrity: sha512-NPKNmdjRFUNpMRzQU3m+AmKzbiQ3WGFXxacMyfmRgm1N+vRhuCzAD3t2dRD29aX1n6a+PNBK2a6hwPwFTfx1rw==}
+ '@graphql-typed-document-node/core@3.2.0':
+ resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==}
+ peerDependencies:
+ graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
+
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -857,6 +862,10 @@ packages:
resolution: {integrity: sha512-LBSu5K0qAaaQcXX/0WIB9PGDevyCxxpnc1uq13vV/CgObaVxuis5hKl3Eboq/8gcb6ebnkAStW9NB/Em2eYyFA==}
engines: {node: '>= 20'}
+ '@linear/sdk@58.1.0':
+ resolution: {integrity: sha512-sqzo1j+uZsxeJlMTV2mrBH3yukB/liev7IySmkZil0ka7ic6b4RE9Jk3x+ohw8YgYB52IRR3SPWzhWu96E6W9g==}
+ engines: {node: '>=12.x', yarn: 1.x}
+
'@lit-labs/signals@0.1.3':
resolution: {integrity: sha512-P0yWgH5blwVyEwBg+WFspLzeu1i0ypJP1QB0l1Omr9qZLIPsUu0p4Fy2jshOg7oQyha5n163K3GJGeUhQQ682Q==}
@@ -3422,6 +3431,10 @@ packages:
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+ graphql@15.10.1:
+ resolution: {integrity: sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==}
+ engines: {node: '>= 10.x'}
+
gunzip-maybe@1.4.2:
resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==}
hasBin: true
@@ -3881,6 +3894,9 @@ packages:
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
engines: {node: '>=0.10.0'}
+ isomorphic-unfetch@3.1.0:
+ resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==}
+
istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
@@ -5971,6 +5987,9 @@ packages:
undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+ unfetch@4.2.0:
+ resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==}
+
unicorn-magic@0.1.0:
resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
engines: {node: '>=18'}
@@ -6537,8 +6556,9 @@ snapshots:
transitivePeerDependencies:
- '@types/react'
- '@gitkraken/provider-apis@0.29.6(encoding@0.1.13)':
+ '@gitkraken/provider-apis@0.29.7(encoding@0.1.13)':
dependencies:
+ '@linear/sdk': 58.1.0(encoding@0.1.13)
js-base64: 3.7.5
node-fetch: 2.7.0(encoding@0.1.13)
transitivePeerDependencies:
@@ -6551,6 +6571,10 @@ snapshots:
'@gk-nzaytsev/fast-string-truncated-width@1.1.0': {}
+ '@graphql-typed-document-node/core@3.2.0(graphql@15.10.1)':
+ dependencies:
+ graphql: 15.10.1
+
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
@@ -6712,6 +6736,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@linear/sdk@58.1.0(encoding@0.1.13)':
+ dependencies:
+ '@graphql-typed-document-node/core': 3.2.0(graphql@15.10.1)
+ graphql: 15.10.1
+ isomorphic-unfetch: 3.1.0(encoding@0.1.13)
+ transitivePeerDependencies:
+ - encoding
+
'@lit-labs/signals@0.1.3':
dependencies:
lit: 3.3.1
@@ -9540,6 +9572,8 @@ snapshots:
graphemer@1.4.0: {}
+ graphql@15.10.1: {}
+
gunzip-maybe@1.4.2:
dependencies:
browserify-zlib: 0.1.4
@@ -10006,6 +10040,13 @@ snapshots:
isobject@3.0.1: {}
+ isomorphic-unfetch@3.1.0(encoding@0.1.13):
+ dependencies:
+ node-fetch: 2.7.0(encoding@0.1.13)
+ unfetch: 4.2.0
+ transitivePeerDependencies:
+ - encoding
+
istanbul-lib-coverage@3.2.2: {}
istanbul-lib-report@3.0.1:
@@ -12309,6 +12350,8 @@ snapshots:
undici-types@5.26.5: {}
+ unfetch@4.2.0: {}
+
unicorn-magic@0.1.0: {}
unicorn-magic@0.3.0: {}
diff --git a/src/config.ts b/src/config.ts
index f28d595d89703..c9f07ec6eb96e 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -4,6 +4,7 @@ import type { DateTimeFormat } from './system/date';
import type { LogLevel } from './system/logger.constants';
export interface Config {
+ readonly 'temporary-configured-linear-config': string | null;
readonly advanced: AdvancedConfig;
readonly ai: AIConfig;
readonly autolinks: AutolinkConfig[] | null;
diff --git a/src/constants.integrations.ts b/src/constants.integrations.ts
index a8f828fb78fa2..90337aa74c0dc 100644
--- a/src/constants.integrations.ts
+++ b/src/constants.integrations.ts
@@ -16,6 +16,7 @@ export enum GitSelfManagedHostIntegrationId {
export enum IssuesCloudHostIntegrationId {
Jira = 'jira',
+ Linear = 'linear',
Trello = 'trello',
}
diff --git a/src/plus/integrations/authentication/integrationAuthenticationService.ts b/src/plus/integrations/authentication/integrationAuthenticationService.ts
index 50fe93f97483b..97ed9dd55b436 100644
--- a/src/plus/integrations/authentication/integrationAuthenticationService.ts
+++ b/src/plus/integrations/authentication/integrationAuthenticationService.ts
@@ -148,6 +148,11 @@ export class IntegrationAuthenticationService implements Disposable {
await import(/* webpackChunkName: "integrations" */ './jira')
).JiraAuthenticationProvider(this.container, this, this.configuredIntegrationService);
break;
+ case IssuesCloudHostIntegrationId.Linear:
+ provider = new (
+ await import(/* webpackChunkName: "integrations" */ './linear')
+ ).LinearAuthenticationProvider();
+ break;
default:
provider = new BuiltInAuthenticationProvider(
this.container,
diff --git a/src/plus/integrations/authentication/linear.ts b/src/plus/integrations/authentication/linear.ts
new file mode 100644
index 0000000000000..628ba6109e260
--- /dev/null
+++ b/src/plus/integrations/authentication/linear.ts
@@ -0,0 +1,54 @@
+import type { Disposable, Event } from 'vscode';
+import type { Sources } from '../../../constants.telemetry';
+import { configuration } from '../../../system/-webview/configuration';
+import type {
+ IntegrationAuthenticationProvider,
+ IntegrationAuthenticationSessionDescriptor,
+} from './integrationAuthenticationProvider';
+import type { ProviderAuthenticationSession } from './models';
+
+export class LinearAuthenticationProvider implements IntegrationAuthenticationProvider {
+ // I want to read the token from the config "temporary-configured-linear-config":
+ private currentToken: string | undefined =
+ (configuration.get('temporary-configured-linear-config') as string) ?? undefined;
+
+ deleteSession(_descriptor: IntegrationAuthenticationSessionDescriptor): Promise {
+ //throw new Error('Method not implemented.');
+ this.currentToken = undefined;
+ return Promise.resolve();
+ }
+ deleteAllSessions(): Promise {
+ //throw new Error('Method not implemented.');
+ this.currentToken = undefined;
+ return Promise.resolve();
+ }
+ getSession(
+ _descriptor: IntegrationAuthenticationSessionDescriptor,
+ _options?:
+ | { createIfNeeded?: boolean; forceNewSession?: boolean; sync?: never; source?: Sources }
+ | { createIfNeeded?: never; forceNewSession?: never; sync: boolean; source?: Sources },
+ ): Promise {
+ return Promise.resolve(
+ this.currentToken
+ ? {
+ accessToken: this.currentToken,
+ id: 'linear',
+ account: {
+ id: 'linear',
+ label: 'Linear',
+ },
+ scopes: ['read'],
+ cloud: true,
+ expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
+ domain: 'linear.app',
+ }
+ : undefined,
+ );
+ }
+ get onDidChange(): Event {
+ return (_listener: (e: void) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable => {
+ return { dispose: () => {} };
+ };
+ }
+ dispose(): void {}
+}
diff --git a/src/plus/integrations/authentication/models.ts b/src/plus/integrations/authentication/models.ts
index d7f10b099d9de..10bbfe8ee68f8 100644
--- a/src/plus/integrations/authentication/models.ts
+++ b/src/plus/integrations/authentication/models.ts
@@ -44,6 +44,7 @@ export interface CloudIntegrationConnection {
export type CloudIntegrationType =
| 'jira'
+ | 'linear'
| 'trello'
| 'gitlab'
| 'github'
@@ -70,6 +71,7 @@ export function isSupportedCloudIntegrationId(id: string): id is SupportedCloudI
export const toIntegrationId: { [key in CloudIntegrationType]: IntegrationIds } = {
jira: IssuesCloudHostIntegrationId.Jira,
+ linear: IssuesCloudHostIntegrationId.Linear,
trello: IssuesCloudHostIntegrationId.Trello,
gitlab: GitCloudHostIntegrationId.GitLab,
github: GitCloudHostIntegrationId.GitHub,
@@ -83,6 +85,7 @@ export const toIntegrationId: { [key in CloudIntegrationType]: IntegrationIds }
export const toCloudIntegrationType: { [key in IntegrationIds]: CloudIntegrationType | undefined } = {
[IssuesCloudHostIntegrationId.Jira]: 'jira',
+ [IssuesCloudHostIntegrationId.Linear]: 'linear',
[IssuesCloudHostIntegrationId.Trello]: 'trello',
[GitCloudHostIntegrationId.GitLab]: 'gitlab',
[GitCloudHostIntegrationId.GitHub]: 'github',
diff --git a/src/plus/integrations/integrationService.ts b/src/plus/integrations/integrationService.ts
index af66d72128ede..e6f5c1c79d7ca 100644
--- a/src/plus/integrations/integrationService.ts
+++ b/src/plus/integrations/integrationService.ts
@@ -540,6 +540,16 @@ export class IntegrationService implements Disposable {
) as IssuesIntegration as IntegrationById;
break;
+ case IssuesCloudHostIntegrationId.Linear:
+ integration = new (
+ await import(/* webpackChunkName: "integrations" */ './providers/linear')
+ ).LinearIntegration(
+ this.container,
+ this.authenticationService,
+ this.getProvidersApi.bind(this),
+ this._onDidChangeIntegrationConnection,
+ ) as IssuesIntegration as IntegrationById;
+ break;
default:
throw new Error(`Integration with '${id}' is not supported`);
}
diff --git a/src/plus/integrations/providers/linear.ts b/src/plus/integrations/providers/linear.ts
new file mode 100644
index 0000000000000..52df443c2ad50
--- /dev/null
+++ b/src/plus/integrations/providers/linear.ts
@@ -0,0 +1,86 @@
+import type { CancellationToken } from 'vscode';
+import { IssuesCloudHostIntegrationId } from '../../../constants.integrations';
+import type { Account } from '../../../git/models/author';
+import type { Issue, IssueShape } from '../../../git/models/issue';
+import type { IssueOrPullRequest, IssueOrPullRequestType } from '../../../git/models/issueOrPullRequest';
+import type { IssueResourceDescriptor, ResourceDescriptor } from '../../../git/models/resourceDescriptor';
+import type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider';
+import type { ProviderAuthenticationSession } from '../authentication/models';
+import { IssuesIntegration } from '../models/issuesIntegration';
+import type { IssueFilter } from './models';
+import { providersMetadata, toIssueShape } from './models';
+
+const metadata = providersMetadata[IssuesCloudHostIntegrationId.Linear];
+const authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });
+const maxPagesPerRequest = 10;
+
+export interface LinearTeamDescriptor extends IssueResourceDescriptor {
+ url: string;
+}
+
+export interface LinearProjectDescriptor extends IssueResourceDescriptor {}
+
+export class LinearIntegration extends IssuesIntegration {
+ protected override getProviderResourcesForUser(
+ _session: ProviderAuthenticationSession,
+ ): Promise {
+ throw new Error('Method not implemented.');
+ }
+ protected override getProviderProjectsForResources(
+ _session: ProviderAuthenticationSession,
+ _resources: ResourceDescriptor[],
+ ): Promise {
+ throw new Error('Method not implemented.');
+ }
+ readonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;
+
+ protected override getProviderAccountForResource(
+ _session: ProviderAuthenticationSession,
+ _resource: ResourceDescriptor,
+ ): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ protected override getProviderIssuesForProject(
+ _session: ProviderAuthenticationSession,
+ _project: ResourceDescriptor,
+ _options?: { user?: string; filters?: IssueFilter[] },
+ ): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ override get id(): IssuesCloudHostIntegrationId.Linear {
+ return IssuesCloudHostIntegrationId.Linear;
+ }
+ protected override get key(): 'linear' {
+ return 'linear';
+ }
+ override get name(): string {
+ return metadata.name;
+ }
+ override get domain(): string {
+ return metadata.domain;
+ }
+ protected override async searchProviderMyIssues(
+ _session: ProviderAuthenticationSession,
+ _resources?: ResourceDescriptor[],
+ _cancellation?: CancellationToken,
+ ): Promise {
+ return Promise.resolve(undefined);
+ }
+ protected override getProviderIssueOrPullRequest(
+ _session: ProviderAuthenticationSession,
+ _resource: ResourceDescriptor,
+ _id: string,
+ _type: undefined | IssueOrPullRequestType,
+ ): Promise {
+ throw new Error('Method not implemented.');
+ }
+ protected override getProviderIssue(
+ _session: ProviderAuthenticationSession,
+ _resource: ResourceDescriptor,
+ _id: string,
+ ): Promise {
+ throw new Error('Method not implemented.');
+ }
+}
diff --git a/src/plus/integrations/providers/models.ts b/src/plus/integrations/providers/models.ts
index 973db318f6e78..7f7a8afe1e896 100644
--- a/src/plus/integrations/providers/models.ts
+++ b/src/plus/integrations/providers/models.ts
@@ -19,6 +19,7 @@ import type {
Jira,
JiraProject,
JiraResource,
+ Linear,
NumberedPageInput,
Issue as ProviderApiIssue,
PullRequestWithUniqueID,
@@ -355,7 +356,7 @@ export type GetIssuesForResourceForCurrentUserFn = (
) => Promise<{ data: ProviderIssue[] }>;
export interface ProviderInfo extends ProviderMetadata {
- provider: GitHub | GitLab | Bitbucket | BitbucketServer | Jira | Trello | AzureDevOps;
+ provider: GitHub | GitLab | Bitbucket | BitbucketServer | Jira | Linear | Trello | AzureDevOps;
getRepoFn?: GetRepoFn;
getRepoOfProjectFn?: GetRepoOfProjectFn;
getPullRequestsForReposFn?: GetPullRequestsForReposFn;
@@ -602,6 +603,14 @@ export const providersMetadata: ProvidersMetadata = {
],
supportedIssueFilters: [IssueFilter.Author, IssueFilter.Assignee, IssueFilter.Mention],
},
+ [IssuesCloudHostIntegrationId.Linear]: {
+ domain: 'linear.app',
+ id: IssuesCloudHostIntegrationId.Linear,
+ name: 'Linear',
+ type: 'issues',
+ iconKey: IssuesCloudHostIntegrationId.Linear,
+ scopes: [],
+ },
[IssuesCloudHostIntegrationId.Trello]: {
domain: 'trello.com',
id: IssuesCloudHostIntegrationId.Trello,
diff --git a/src/plus/integrations/providers/providersApi.ts b/src/plus/integrations/providers/providersApi.ts
index c8018510e4d78..6c2148da39f4f 100644
--- a/src/plus/integrations/providers/providersApi.ts
+++ b/src/plus/integrations/providers/providersApi.ts
@@ -322,6 +322,10 @@ export class ProvidersApi {
providerApis.jira,
),
},
+ [IssuesCloudHostIntegrationId.Linear]: {
+ ...providersMetadata[IssuesCloudHostIntegrationId.Linear],
+ provider: providerApis.linear,
+ },
[IssuesCloudHostIntegrationId.Trello]: {
...providersMetadata[IssuesCloudHostIntegrationId.Trello],
provider: providerApis.trello,
diff --git a/src/plus/launchpad/enrichmentService.ts b/src/plus/launchpad/enrichmentService.ts
index 56dc6ca44f1cf..677aa441df9eb 100644
--- a/src/plus/launchpad/enrichmentService.ts
+++ b/src/plus/launchpad/enrichmentService.ts
@@ -204,6 +204,7 @@ const supportedIntegrationIdsToEnrich: Record
Date: Tue, 26 Aug 2025 17:20:30 +0200
Subject: [PATCH 02/10] Adds support for fetching user issues from Linear
(#4543, #4579)
---
src/plus/integrations/providers/linear.ts | 42 +++++++++++++++++--
src/plus/integrations/providers/models.ts | 6 +++
.../integrations/providers/providersApi.ts | 22 ++++++++++
3 files changed, 66 insertions(+), 4 deletions(-)
diff --git a/src/plus/integrations/providers/linear.ts b/src/plus/integrations/providers/linear.ts
index 52df443c2ad50..2cbc61f9bb872 100644
--- a/src/plus/integrations/providers/linear.ts
+++ b/src/plus/integrations/providers/linear.ts
@@ -4,6 +4,7 @@ import type { Account } from '../../../git/models/author';
import type { Issue, IssueShape } from '../../../git/models/issue';
import type { IssueOrPullRequest, IssueOrPullRequestType } from '../../../git/models/issueOrPullRequest';
import type { IssueResourceDescriptor, ResourceDescriptor } from '../../../git/models/resourceDescriptor';
+import { Logger } from '../../../system/logger';
import type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider';
import type { ProviderAuthenticationSession } from '../authentication/models';
import { IssuesIntegration } from '../models/issuesIntegration';
@@ -62,11 +63,44 @@ export class LinearIntegration extends IssuesIntegration {
- return Promise.resolve(undefined);
+ if (resources != null) {
+ return undefined;
+ }
+ const api = await this.getProvidersApi();
+ let cursor = undefined;
+ let hasMore = false;
+ let requestCount = 0;
+ const issues = [];
+ try {
+ do {
+ if (cancellation?.isCancellationRequested) {
+ break;
+ }
+ const result = await api.getIssuesForCurrentUser(this.id, {
+ accessToken: session.accessToken,
+ cursor: cursor,
+ });
+ requestCount += 1;
+ hasMore = result.paging?.more ?? false;
+ cursor = result.paging?.cursor;
+ const formattedIssues = result.values
+ .map(issue => toIssueShape(issue, this))
+ .filter((result): result is IssueShape => result != null);
+ if (formattedIssues.length > 0) {
+ issues.push(...formattedIssues);
+ }
+ } while (requestCount < maxPagesPerRequest && hasMore);
+ } catch (ex) {
+ if (issues.length === 0) {
+ throw ex;
+ }
+ Logger.error(ex, 'searchProviderMyIssues');
+ }
+ return issues;
}
protected override getProviderIssueOrPullRequest(
_session: ProviderAuthenticationSession,
diff --git a/src/plus/integrations/providers/models.ts b/src/plus/integrations/providers/models.ts
index 7f7a8afe1e896..c492631d7f742 100644
--- a/src/plus/integrations/providers/models.ts
+++ b/src/plus/integrations/providers/models.ts
@@ -281,6 +281,11 @@ export type GetIssuesForReposFn = (
options?: EnterpriseOptions,
) => Promise<{ data: ProviderIssue[]; pageInfo?: PageInfo }>;
+export type GetIssuesForCurrentUserFn = (
+ input: PagingInput,
+ options?: EnterpriseOptions,
+) => Promise<{ data: ProviderIssue[]; pageInfo?: PageInfo }>;
+
export type GetIssuesForRepoFn = (
input: GetIssuesForRepoInput & PagingInput,
options?: EnterpriseOptions,
@@ -365,6 +370,7 @@ export interface ProviderInfo extends ProviderMetadata {
getPullRequestsForAzureProjectsFn?: GetPullRequestsForAzureProjectsFn;
getIssueFn?: GetIssueFn;
getIssuesForReposFn?: GetIssuesForReposFn;
+ getIssuesForCurrentUserFn?: GetIssuesForCurrentUserFn;
getIssuesForRepoFn?: GetIssuesForRepoFn;
getIssuesForAzureProjectFn?: GetIssuesForAzureProjectFn;
getCurrentUserFn?: GetCurrentUserFn;
diff --git a/src/plus/integrations/providers/providersApi.ts b/src/plus/integrations/providers/providersApi.ts
index 6c2148da39f4f..67229281d0582 100644
--- a/src/plus/integrations/providers/providersApi.ts
+++ b/src/plus/integrations/providers/providersApi.ts
@@ -44,6 +44,7 @@ import type {
IssueFilter,
MergePullRequestFn,
PageInfo,
+ PagingInput,
PagingMode,
ProviderAccount,
ProviderAzureProject,
@@ -325,6 +326,7 @@ export class ProvidersApi {
[IssuesCloudHostIntegrationId.Linear]: {
...providersMetadata[IssuesCloudHostIntegrationId.Linear],
provider: providerApis.linear,
+ getIssuesForCurrentUserFn: providerApis.linear.getIssuesForCurrentUser.bind(providerApis.linear),
},
[IssuesCloudHostIntegrationId.Trello]: {
...providersMetadata[IssuesCloudHostIntegrationId.Trello],
@@ -1035,6 +1037,26 @@ export class ProvidersApi {
);
}
+ async getIssuesForCurrentUser(
+ providerId: IntegrationIds,
+ options?: PagingInput & { accessToken?: string; isPAT?: boolean; baseUrl?: string },
+ ): Promise> {
+ const { provider, token } = await this.ensureProviderTokenAndFunction(
+ providerId,
+ 'getIssuesForCurrentUserFn',
+ options?.accessToken,
+ );
+ return this.getPagedResult(
+ provider,
+ options,
+ provider.getIssuesForCurrentUserFn,
+ token,
+ options?.cursor ?? undefined,
+ options?.isPAT,
+ options?.baseUrl,
+ );
+ }
+
async getIssuesForAzureProject(
providerId: GitCloudHostIntegrationId.AzureDevOps | GitSelfManagedHostIntegrationId.AzureDevOpsServer,
namespace: string,
From 312d1ecb776161d9c4c0d7f60e6949a96df54adf Mon Sep 17 00:00:00 2001
From: Sergei Shmakov
Date: Wed, 27 Aug 2025 17:53:35 +0200
Subject: [PATCH 03/10] Makes sure that user can associate an issue with a
branch and it's saved (#4543, #4579)
---
src/plus/integrations/providers/utils.ts | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/plus/integrations/providers/utils.ts b/src/plus/integrations/providers/utils.ts
index b89ce5de01acc..dc50eb161e265 100644
--- a/src/plus/integrations/providers/utils.ts
+++ b/src/plus/integrations/providers/utils.ts
@@ -120,6 +120,8 @@ export function getProviderIdFromEntityIdentifier(
: GitSelfManagedHostIntegrationId.GitLabSelfHosted;
case EntityIdentifierProviderType.Jira:
return IssuesCloudHostIntegrationId.Jira;
+ case EntityIdentifierProviderType.Linear:
+ return IssuesCloudHostIntegrationId.Linear;
case EntityIdentifierProviderType.Azure:
return GitCloudHostIntegrationId.AzureDevOps;
case EntityIdentifierProviderType.AzureDevOpsServer:
@@ -147,6 +149,8 @@ function fromStringToEntityIdentifierProviderType(str: string): EntityIdentifier
return EntityIdentifierProviderType.Gitlab;
case 'jira':
return EntityIdentifierProviderType.Jira;
+ case 'linear':
+ return EntityIdentifierProviderType.Linear;
case 'azure':
case 'azureDevOps':
case 'azure-devops':
@@ -252,6 +256,7 @@ export async function getIssueFromGitConfigEntityIdentifier(
// TODO: Centralize where we represent all supported providers for issues
if (
identifier.provider !== EntityIdentifierProviderType.Jira &&
+ identifier.provider !== EntityIdentifierProviderType.Linear &&
identifier.provider !== EntityIdentifierProviderType.Github &&
identifier.provider !== EntityIdentifierProviderType.Gitlab &&
identifier.provider !== EntityIdentifierProviderType.GithubEnterprise &&
@@ -288,7 +293,7 @@ export async function getIssueFromGitConfigEntityIdentifier(
export function getIssueOwner(
issue: IssueShape,
): RepositoryDescriptor | IssueResourceDescriptor | AzureProjectInputDescriptor | undefined {
- const isAzure = issue.provider.id === 'azure' || GitCloudHostIntegrationId.AzureDevOps || 'azure-devops';
+ const isAzure = ['azure', GitCloudHostIntegrationId.AzureDevOps, 'azure-devops'].includes(issue.provider.id);
return issue.repository
? {
key: `${issue.repository.owner}/${issue.repository.repo}`,
From ea7e613ebee69081f8727a5a941b755527866716 Mon Sep 17 00:00:00 2001
From: Sergei Shmakov
Date: Wed, 27 Aug 2025 19:32:52 +0200
Subject: [PATCH 04/10] Fixes showing associated branch Linear issues on Home
View (#4543, #4579)
---
src/git/models/issue.ts | 1 +
src/plus/integrations/providers/linear.ts | 37 ++++++++++++++++---
src/plus/integrations/providers/models.ts | 1 +
.../integrations/providers/providersApi.ts | 1 +
.../apps/plus/home/components/branch-card.ts | 2 +-
src/webviews/home/homeWebview.ts | 7 +++-
6 files changed, 41 insertions(+), 8 deletions(-)
diff --git a/src/git/models/issue.ts b/src/git/models/issue.ts
index c9a30db5e51e3..d13a5b61875a7 100644
--- a/src/git/models/issue.ts
+++ b/src/git/models/issue.ts
@@ -37,6 +37,7 @@ export class Issue implements IssueShape {
public readonly thumbsUpCount?: number,
public readonly body?: string,
public readonly project?: IssueProject,
+ public readonly number?: string,
) {}
}
diff --git a/src/plus/integrations/providers/linear.ts b/src/plus/integrations/providers/linear.ts
index 2cbc61f9bb872..eb97537e97bb4 100644
--- a/src/plus/integrations/providers/linear.ts
+++ b/src/plus/integrations/providers/linear.ts
@@ -4,12 +4,13 @@ import type { Account } from '../../../git/models/author';
import type { Issue, IssueShape } from '../../../git/models/issue';
import type { IssueOrPullRequest, IssueOrPullRequestType } from '../../../git/models/issueOrPullRequest';
import type { IssueResourceDescriptor, ResourceDescriptor } from '../../../git/models/resourceDescriptor';
+import { isIssueResourceDescriptor } from '../../../git/utils/resourceDescriptor.utils';
import { Logger } from '../../../system/logger';
import type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider';
import type { ProviderAuthenticationSession } from '../authentication/models';
import { IssuesIntegration } from '../models/issuesIntegration';
import type { IssueFilter } from './models';
-import { providersMetadata, toIssueShape } from './models';
+import { fromProviderIssue, providersMetadata, toIssueShape } from './models';
const metadata = providersMetadata[IssuesCloudHostIntegrationId.Linear];
const authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });
@@ -110,11 +111,35 @@ export class LinearIntegration extends IssuesIntegration {
throw new Error('Method not implemented.');
}
- protected override getProviderIssue(
- _session: ProviderAuthenticationSession,
- _resource: ResourceDescriptor,
- _id: string,
+ protected override async getProviderIssue(
+ session: ProviderAuthenticationSession,
+ resource: ResourceDescriptor,
+ id: string,
): Promise {
- throw new Error('Method not implemented.');
+ const api = await this.getProvidersApi();
+ try {
+ if (!isIssueResourceDescriptor(resource)) {
+ Logger.error(undefined, 'getProviderIssue: resource is not an IssueResourceDescriptor');
+ return undefined;
+ }
+
+ const result = await api.getIssue(
+ this.id,
+ {
+ resourceId: resource.id,
+ number: id,
+ },
+ {
+ accessToken: session.accessToken,
+ },
+ );
+
+ if (result == null) return undefined;
+
+ return fromProviderIssue(result, this);
+ } catch (ex) {
+ Logger.error(ex, 'getProviderIssue');
+ return undefined;
+ }
}
}
diff --git a/src/plus/integrations/providers/models.ts b/src/plus/integrations/providers/models.ts
index c492631d7f742..d1579615b1e1f 100644
--- a/src/plus/integrations/providers/models.ts
+++ b/src/plus/integrations/providers/models.ts
@@ -1055,6 +1055,7 @@ export function fromProviderIssue(
resourceName: issue.project.namespace,
}
: undefined,
+ issue.number,
);
}
diff --git a/src/plus/integrations/providers/providersApi.ts b/src/plus/integrations/providers/providersApi.ts
index 67229281d0582..00d1386fee2ac 100644
--- a/src/plus/integrations/providers/providersApi.ts
+++ b/src/plus/integrations/providers/providersApi.ts
@@ -326,6 +326,7 @@ export class ProvidersApi {
[IssuesCloudHostIntegrationId.Linear]: {
...providersMetadata[IssuesCloudHostIntegrationId.Linear],
provider: providerApis.linear,
+ getIssueFn: providerApis.linear.getIssue.bind(providerApis.linear) as GetIssueFn,
getIssuesForCurrentUserFn: providerApis.linear.getIssuesForCurrentUser.bind(providerApis.linear),
},
[IssuesCloudHostIntegrationId.Trello]: {
diff --git a/src/webviews/apps/plus/home/components/branch-card.ts b/src/webviews/apps/plus/home/components/branch-card.ts
index 19f6e06a8cff7..f9ecf2b874837 100644
--- a/src/webviews/apps/plus/home/components/branch-card.ts
+++ b/src/webviews/apps/plus/home/components/branch-card.ts
@@ -567,7 +567,7 @@ export abstract class GlBranchCardBase extends GlElement {
${issue.title}
- #${issue.id}
+ ${isNaN(parseInt(issue.id)) ? '' : '#'}${issue.id}
`;
})}
diff --git a/src/webviews/home/homeWebview.ts b/src/webviews/home/homeWebview.ts
index 377cd9cb78ef8..cf85fb4d16c02 100644
--- a/src/webviews/home/homeWebview.ts
+++ b/src/webviews/home/homeWebview.ts
@@ -1808,7 +1808,12 @@ function enrichOverviewBranchesCore(
issues =>
issues?.map(
i =>
- ({ id: i.id, title: i.title, state: i.state, url: i.url }) satisfies NonNullable[0],
+ ({
+ id: i.number || i.id,
+ title: i.title,
+ state: i.state,
+ url: i.url,
+ }) satisfies NonNullable[0],
) ?? [],
);
From a7492fae01779911edd184e419b5444f3015a126 Mon Sep 17 00:00:00 2001
From: Sergei Shmakov
Date: Thu, 28 Aug 2025 21:31:48 +0200
Subject: [PATCH 05/10] Adds Linear autolink matching. (#4543, #4579)
---
.../utils/-webview/autolinks.utils.ts | 6 +-
src/plus/integrations/providers/linear.ts | 109 +++++++++++++++++-
src/plus/integrations/providers/models.ts | 8 ++
.../integrations/providers/providersApi.ts | 42 +++++++
4 files changed, 161 insertions(+), 4 deletions(-)
diff --git a/src/autolinks/utils/-webview/autolinks.utils.ts b/src/autolinks/utils/-webview/autolinks.utils.ts
index ff75d9e4bf4e6..9653da5c55f21 100644
--- a/src/autolinks/utils/-webview/autolinks.utils.ts
+++ b/src/autolinks/utils/-webview/autolinks.utils.ts
@@ -32,7 +32,7 @@ export function serializeAutolink(value: Autolink): Autolink {
return serialized;
}
-export const supportedAutolinkIntegrations = [IssuesCloudHostIntegrationId.Jira];
+export const supportedAutolinkIntegrations = [IssuesCloudHostIntegrationId.Jira, IssuesCloudHostIntegrationId.Linear];
export function isDynamic(ref: AutolinkReference | DynamicAutolinkReference): ref is DynamicAutolinkReference {
return !('prefix' in ref) && !('url' in ref);
@@ -154,10 +154,10 @@ export function getBranchAutolinks(branchName: string, refsets: Readonly {
- if (a[0]?.id === IssuesCloudHostIntegrationId.Jira || a[0]?.id === IssuesCloudHostIntegrationId.Trello) {
+ if (a[0]?.id && Object.values(IssuesCloudHostIntegrationId).includes(a[0].id)) {
return -1;
}
- if (b[0]?.id === IssuesCloudHostIntegrationId.Jira || b[0]?.id === IssuesCloudHostIntegrationId.Trello) {
+ if (b[0]?.id && Object.values(IssuesCloudHostIntegrationId).includes(b[0].id)) {
return 1;
}
return 0;
diff --git a/src/plus/integrations/providers/linear.ts b/src/plus/integrations/providers/linear.ts
index eb97537e97bb4..dfe7d2c50d22b 100644
--- a/src/plus/integrations/providers/linear.ts
+++ b/src/plus/integrations/providers/linear.ts
@@ -1,4 +1,5 @@
-import type { CancellationToken } from 'vscode';
+import type { AuthenticationSession, CancellationToken } from 'vscode';
+import type { AutolinkReference, DynamicAutolinkReference } from '../../../autolinks/models/autolinks';
import { IssuesCloudHostIntegrationId } from '../../../constants.integrations';
import type { Account } from '../../../git/models/author';
import type { Issue, IssueShape } from '../../../git/models/issue';
@@ -17,12 +18,118 @@ const authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes })
const maxPagesPerRequest = 10;
export interface LinearTeamDescriptor extends IssueResourceDescriptor {
+ avatarUrl: string | undefined;
+}
+
+export interface LinearOrganizationDescriptor extends IssueResourceDescriptor {
url: string;
}
export interface LinearProjectDescriptor extends IssueResourceDescriptor {}
export class LinearIntegration extends IssuesIntegration {
+ private _autolinks: Map | undefined;
+ override async autolinks(): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {
+ const connected = this.maybeConnected ?? (await this.isConnected());
+ if (!connected || this._session == null) {
+ return [];
+ }
+ const cachedAutolinks = this._autolinks?.get(this._session.accessToken);
+ if (cachedAutolinks != null) return cachedAutolinks;
+
+ const organization = await this.getOrganization(this._session);
+ if (organization == null) return [];
+
+ const autolinks: (AutolinkReference | DynamicAutolinkReference)[] = [];
+
+ const teams = await this.getTeams(this._session);
+ for (const team of teams ?? []) {
+ const dashedPrefix = `${team.key}-`;
+ const underscoredPrefix = `${team.key}_`;
+
+ autolinks.push({
+ prefix: dashedPrefix,
+ url: `${organization.url}/issue/${dashedPrefix}`,
+ alphanumeric: false,
+ ignoreCase: false,
+ title: `Open Issue ${dashedPrefix} on ${organization.name}`,
+
+ type: 'issue',
+ description: `${organization.name} Issue ${dashedPrefix}`,
+ descriptor: { ...organization },
+ });
+ autolinks.push({
+ prefix: underscoredPrefix,
+ url: `${organization.url}/issue/${dashedPrefix}`,
+ alphanumeric: false,
+ ignoreCase: false,
+ referenceType: 'branch',
+ title: `Open Issue ${dashedPrefix} on ${organization.name}`,
+
+ type: 'issue',
+ description: `${organization.name} Issue ${dashedPrefix}`,
+ descriptor: { ...organization },
+ });
+ }
+
+ this._autolinks ??= new Map();
+ this._autolinks.set(this._session.accessToken, autolinks);
+
+ return autolinks;
+ }
+
+ private _organizations: Map | undefined;
+ private async getOrganization(
+ { accessToken }: AuthenticationSession,
+ force: boolean = false,
+ ): Promise {
+ this._organizations ??= new Map();
+
+ const cachedResources = this._organizations.get(accessToken);
+
+ if (cachedResources == null || force) {
+ const api = await this.getProvidersApi();
+ const organization = await api.getLinearOrganization({ accessToken: accessToken });
+ const descriptor: LinearOrganizationDescriptor | undefined = organization && {
+ id: organization.id,
+ key: organization.key,
+ name: organization.name,
+ url: organization.url,
+ };
+ if (descriptor) {
+ this._organizations.set(accessToken, descriptor);
+ }
+ }
+
+ return this._organizations.get(accessToken);
+ }
+
+ private _teams: Map | undefined;
+ private async getTeams(
+ { accessToken }: AuthenticationSession,
+ force: boolean = false,
+ ): Promise {
+ this._teams ??= new Map();
+
+ const cachedResources = this._teams.get(accessToken);
+
+ if (cachedResources == null || force) {
+ const api = await this.getProvidersApi();
+ const teams = await api.getLinearTeamsForCurrentUser({ accessToken: accessToken });
+ const descriptors: LinearTeamDescriptor[] | undefined = teams?.map(t => ({
+ id: t.id,
+ key: t.key,
+ name: t.name,
+ avatarUrl: t.iconUrl,
+ }));
+ if (descriptors) {
+ this._teams.set(accessToken, descriptors);
+ }
+ }
+
+ return this._teams.get(accessToken);
+ }
+
protected override getProviderResourcesForUser(
_session: ProviderAuthenticationSession,
): Promise {
diff --git a/src/plus/integrations/providers/models.ts b/src/plus/integrations/providers/models.ts
index d1579615b1e1f..2e04a9622e7f7 100644
--- a/src/plus/integrations/providers/models.ts
+++ b/src/plus/integrations/providers/models.ts
@@ -20,6 +20,8 @@ import type {
JiraProject,
JiraResource,
Linear,
+ LinearOrganization,
+ LinearTeam,
NumberedPageInput,
Issue as ProviderApiIssue,
PullRequestWithUniqueID,
@@ -75,6 +77,8 @@ export type ProviderIssue = ProviderApiIssue;
export type ProviderEnterpriseOptions = EnterpriseOptions;
export type ProviderJiraProject = JiraProject;
export type ProviderJiraResource = JiraResource;
+export type ProviderLinearTeam = LinearTeam;
+export type ProviderLinearOrganization = LinearOrganization;
export type ProviderAzureProject = AzureProject;
export type ProviderAzureResource = AzureOrganization;
export type ProviderBitbucketResource = BitbucketWorkspaceStub;
@@ -315,6 +319,8 @@ export type GetCurrentUserForResourceFn = (
) => Promise<{ data: ProviderAccount }>;
export type GetJiraResourcesForCurrentUserFn = (options?: EnterpriseOptions) => Promise<{ data: JiraResource[] }>;
+export type GetLinearOrganizationFn = (options?: EnterpriseOptions) => Promise<{ data: LinearOrganization }>;
+export type GetLinearTeamsForCurrentUserFn = (options?: EnterpriseOptions) => Promise<{ data: LinearTeam[] }>;
export type GetJiraProjectsForResourcesFn = (
input: { resourceIds: string[] },
options?: EnterpriseOptions,
@@ -377,6 +383,8 @@ export interface ProviderInfo extends ProviderMetadata {
getCurrentUserForInstanceFn?: GetCurrentUserForInstanceFn;
getCurrentUserForResourceFn?: GetCurrentUserForResourceFn;
getJiraResourcesForCurrentUserFn?: GetJiraResourcesForCurrentUserFn;
+ getLinearOrganizationFn?: GetLinearOrganizationFn;
+ getLinearTeamsForCurrentUserFn?: GetLinearTeamsForCurrentUserFn;
getAzureResourcesForUserFn?: GetAzureResourcesForUserFn;
getBitbucketResourcesForUserFn?: GetBitbucketResourcesForUserFn;
getBitbucketPullRequestsAuthoredByUserForWorkspaceFn?: GetBitbucketPullRequestsAuthoredByUserForWorkspaceFn;
diff --git a/src/plus/integrations/providers/providersApi.ts b/src/plus/integrations/providers/providersApi.ts
index 00d1386fee2ac..95be946c5d3a4 100644
--- a/src/plus/integrations/providers/providersApi.ts
+++ b/src/plus/integrations/providers/providersApi.ts
@@ -54,6 +54,8 @@ import type {
ProviderIssue,
ProviderJiraProject,
ProviderJiraResource,
+ ProviderLinearOrganization,
+ ProviderLinearTeam,
ProviderPullRequest,
ProviderRepoInput,
ProviderReposInput,
@@ -328,6 +330,8 @@ export class ProvidersApi {
provider: providerApis.linear,
getIssueFn: providerApis.linear.getIssue.bind(providerApis.linear) as GetIssueFn,
getIssuesForCurrentUserFn: providerApis.linear.getIssuesForCurrentUser.bind(providerApis.linear),
+ getLinearOrganizationFn: providerApis.linear.getLinearOrganization.bind(providerApis.linear),
+ getLinearTeamsForCurrentUserFn: providerApis.linear.getTeamsForCurrentUser.bind(providerApis.linear),
},
[IssuesCloudHostIntegrationId.Trello]: {
...providersMetadata[IssuesCloudHostIntegrationId.Trello],
@@ -657,6 +661,44 @@ export class ProvidersApi {
}
}
+ async getLinearOrganization(options?: { accessToken?: string }): Promise {
+ const { provider, token } = await this.ensureProviderTokenAndFunction(
+ IssuesCloudHostIntegrationId.Linear,
+ 'getLinearOrganizationFn',
+ options?.accessToken,
+ );
+
+ try {
+ const x = await provider.getLinearOrganizationFn?.({ token: token });
+ const y = x?.data;
+ return y;
+ } catch (e) {
+ return this.handleProviderError(
+ IssuesCloudHostIntegrationId.Linear,
+ token,
+ e,
+ );
+ }
+ }
+
+ async getLinearTeamsForCurrentUser(options?: { accessToken?: string }): Promise {
+ const { provider, token } = await this.ensureProviderTokenAndFunction(
+ IssuesCloudHostIntegrationId.Linear,
+ 'getLinearTeamsForCurrentUserFn',
+ options?.accessToken,
+ );
+
+ try {
+ return (await provider.getLinearTeamsForCurrentUserFn?.({ token: token }))?.data;
+ } catch (e) {
+ return this.handleProviderError(
+ IssuesCloudHostIntegrationId.Linear,
+ token,
+ e,
+ );
+ }
+ }
+
async getAzureResourcesForUser(
userId: string,
integrationId: GitCloudHostIntegrationId.AzureDevOps | GitSelfManagedHostIntegrationId.AzureDevOpsServer,
From 2f82fe2ebdeb118c268a4de9fcaa0230b9afe4c5 Mon Sep 17 00:00:00 2001
From: Sergei Shmakov
Date: Thu, 28 Aug 2025 22:40:48 +0200
Subject: [PATCH 06/10] Lets Jira and Linear links appear in commit tooltips
(#4543, #4579)
---
src/git/models/commit.ts | 1 -
src/hovers/hovers.ts | 1 -
src/views/nodes/commitNode.ts | 2 +-
3 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/git/models/commit.ts b/src/git/models/commit.ts
index cb443dfaa0bf5..3603991134d16 100644
--- a/src/git/models/commit.ts
+++ b/src/git/models/commit.ts
@@ -551,7 +551,6 @@ export class GitCommit implements GitRevisionReference {
if (this.isUncommitted) return undefined;
remote ??= await this.container.git.getRepositoryService(this.repoPath).remotes.getBestRemoteWithIntegration();
- if (remote?.provider == null) return undefined;
// TODO@eamodio should we cache these? Seems like we would use more memory than it's worth
// async function getCore(this: GitCommit): Promise