diff --git a/src/commands/git/branch.ts b/src/commands/git/branch.ts index c4174220480d7..a6e008bbb1cd1 100644 --- a/src/commands/git/branch.ts +++ b/src/commands/git/branch.ts @@ -1,12 +1,14 @@ import { QuickInputButtons } from 'vscode'; import type { Container } from '../../container'; -import { getNameWithoutRemote } from '../../git/models/branch.utils'; +import { addAssociatedIssueToBranch, getNameWithoutRemote } from '../../git/models/branch.utils'; +import type { IssueShape } from '../../git/models/issue'; import type { GitBranchReference, GitReference } from '../../git/models/reference'; import { getReferenceLabel, isBranchReference, isRevisionReference } from '../../git/models/reference.utils'; import { Repository } from '../../git/models/repository'; import type { GitWorktree } from '../../git/models/worktree'; import { getWorktreesByBranch } from '../../git/models/worktree.utils'; import { showGenericErrorMessage } from '../../messages'; +import { getIssueOwner } from '../../plus/integrations/providers/utils'; import type { QuickPickItemOfT } from '../../quickpicks/items/common'; import { createQuickPickSeparator } from '../../quickpicks/items/common'; import type { FlagsQuickPickItem } from '../../quickpicks/items/flags'; @@ -61,6 +63,7 @@ interface CreateState { suggestNameOnly?: boolean; suggestRepoOnly?: boolean; confirmOptions?: CreateFlags[]; + associateWithIssue?: IssueShape; } function isCreateState(state: Partial | undefined): state is Partial { @@ -415,6 +418,7 @@ export class BranchGitCommand extends QuickCommand { } endSteps(state); + if (state.flags.includes('--switch')) { await state.repo.switch(state.reference.ref, { createBranch: state.name }); } else { @@ -426,6 +430,16 @@ export class BranchGitCommand extends QuickCommand { return showGenericErrorMessage('Unable to create branch'); } } + + if (state.associateWithIssue != null) { + const issue = state.associateWithIssue; + const branch = await state.repo.git.getBranch(state.name); + // TODO: These descriptors are hacked in. Use an integration function to get the right resource for the issue. + const owner = getIssueOwner(issue); + if (branch != null && owner != null) { + await addAssociatedIssueToBranch(this.container, branch, { ...issue, type: 'issue' }, owner); + } + } } } diff --git a/src/constants.ts b/src/constants.ts index 3d9c08a763784..c4e36ee76d9d7 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -50,6 +50,7 @@ export const enum CharCode { export type GitConfigKeys = | `branch.${string}.${'gk' | 'vscode'}-merge-base` | `branch.${string}.gk-target-base` + | `branch.${string}.gk-associated-issues` | `branch.${string}.github-pr-owner-number`; export const enum GlyphChars { diff --git a/src/git/models/branch.ts b/src/git/models/branch.ts index 4a6f55fa11162..1dfe2abbc1e51 100644 --- a/src/git/models/branch.ts +++ b/src/git/models/branch.ts @@ -10,7 +10,8 @@ import { getBranchTrackingWithoutRemote, getRemoteNameFromBranchName, getRemoteNameSlashIndex, - isDetachedHead } from './branch.utils'; + isDetachedHead, +} from './branch.utils'; import type { PullRequest, PullRequestState } from './pullRequest'; import type { GitBranchReference } from './reference'; import type { GitRemote } from './remote'; diff --git a/src/git/models/branch.utils.ts b/src/git/models/branch.utils.ts index f17a72ce029b2..b3b2f751992c3 100644 --- a/src/git/models/branch.utils.ts +++ b/src/git/models/branch.utils.ts @@ -1,10 +1,19 @@ import type { CancellationToken } from 'vscode'; import type { GitConfigKeys } from '../../constants'; import type { Container } from '../../container'; +import type { IssueResourceDescriptor, RepositoryDescriptor } from '../../plus/integrations/integration'; +import type { GitConfigEntityIdentifier } from '../../plus/integrations/providers/models'; +import { + decodeEntityIdentifiersFromGitConfig, + encodeIssueOrPullRequestForGitConfig, + getIssueFromGitConfigEntityIdentifier, +} from '../../plus/integrations/providers/utils'; +import { Logger } from '../../system/logger'; import { PageableResult } from '../../system/paging'; import type { MaybePausedResult } from '../../system/promise'; import { getSettledValue, pauseOnCancelOrTimeout } from '../../system/promise'; import type { GitBranch } from './branch'; +import type { Issue } from './issue'; import type { PullRequest } from './pullRequest'; import type { GitBranchReference, GitReference } from './reference'; import type { Repository } from './repository'; @@ -208,3 +217,102 @@ export function getNameWithoutRemote(ref: GitReference) { } return ref.name; } + +export async function getAssociatedIssuesForBranch( + container: Container, + branch: GitBranch, + options?: { + cancellation?: CancellationToken; + timeout?: number; + }, +): Promise> { + const { encoded } = await getConfigKeyAndEncodedAssociatedIssuesForBranch(container, branch); + if (options?.cancellation?.isCancellationRequested) return { value: undefined, paused: false }; + + let associatedIssues: GitConfigEntityIdentifier[] | undefined; + if (encoded != null) { + try { + associatedIssues = decodeEntityIdentifiersFromGitConfig(encoded); + } catch (ex) { + Logger.error(ex, 'getAssociatedIssuesForBranch'); + return { value: undefined, paused: false }; + } + + if (associatedIssues != null) { + return pauseOnCancelOrTimeout( + (async () => { + return ( + await Promise.allSettled( + (associatedIssues ?? []).map(i => getIssueFromGitConfigEntityIdentifier(container, i)), + ) + ) + .map(r => getSettledValue(r)) + .filter((i): i is Issue => i != null); + })(), + options?.cancellation, + options?.timeout, + ); + } + } + + return { value: undefined, paused: false }; +} + +export async function addAssociatedIssueToBranch( + container: Container, + branch: GitBranchReference, + issue: Issue, + owner: RepositoryDescriptor | IssueResourceDescriptor, + options?: { + cancellation?: CancellationToken; + }, +) { + const { key, encoded } = await getConfigKeyAndEncodedAssociatedIssuesForBranch(container, branch); + if (options?.cancellation?.isCancellationRequested) return; + try { + const associatedIssues: GitConfigEntityIdentifier[] = encoded + ? (JSON.parse(encoded) as GitConfigEntityIdentifier[]) + : []; + if (associatedIssues.some(i => i.entityId === issue.nodeId)) { + return; + } + associatedIssues.push(encodeIssueOrPullRequestForGitConfig(issue, owner)); + await container.git.setConfig(branch.repoPath, key, JSON.stringify(associatedIssues)); + } catch (ex) { + Logger.error(ex, 'addAssociatedIssueToBranch'); + } +} + +export async function removeAssociatedIssueFromBranch( + container: Container, + branch: GitBranchReference, + id: string, + options?: { + cancellation?: CancellationToken; + }, +) { + const { key, encoded } = await getConfigKeyAndEncodedAssociatedIssuesForBranch(container, branch); + if (options?.cancellation?.isCancellationRequested) return; + try { + let associatedIssues: GitConfigEntityIdentifier[] = encoded + ? (JSON.parse(encoded) as GitConfigEntityIdentifier[]) + : []; + associatedIssues = associatedIssues.filter(i => i.entityId !== id); + if (associatedIssues.length === 0) { + await container.git.setConfig(branch.repoPath, key, undefined); + } else { + await container.git.setConfig(branch.repoPath, key, JSON.stringify(associatedIssues)); + } + } catch (ex) { + Logger.error(ex, 'removeAssociatedIssueFromBranch'); + } +} + +async function getConfigKeyAndEncodedAssociatedIssuesForBranch( + container: Container, + branch: GitBranchReference, +): Promise<{ key: GitConfigKeys; encoded: string | undefined }> { + const key = `branch.${branch.name}.gk-associated-issues` satisfies GitConfigKeys; + const encoded = await container.git.getConfig(branch.repoPath, key); + return { key: key, encoded: encoded }; +} diff --git a/src/plus/integrations/providers/models.ts b/src/plus/integrations/providers/models.ts index 3f913c66b0bb7..180d0a5288504 100644 --- a/src/plus/integrations/providers/models.ts +++ b/src/plus/integrations/providers/models.ts @@ -1,6 +1,7 @@ import type { Account, ActionablePullRequest, + AnyEntityIdentifierInput, AzureDevOps, AzureOrganization, AzureProject, @@ -891,3 +892,11 @@ export type EnrichablePullRequest = ProviderPullRequest & { }; export const getActionablePullRequests = GitProviderUtils.getActionablePullRequests; + +export type GitConfigEntityIdentifier = AnyEntityIdentifierInput & { + metadata: { + id: string; + owner: { key: string; name: string; id: string | undefined; owner: string | undefined }; + createdDate: string; + }; +}; diff --git a/src/plus/integrations/providers/utils.ts b/src/plus/integrations/providers/utils.ts index c9ef32f22cf9d..164bf4ba23c3b 100644 --- a/src/plus/integrations/providers/utils.ts +++ b/src/plus/integrations/providers/utils.ts @@ -1,10 +1,16 @@ import type { AnyEntityIdentifierInput, EntityIdentifier } from '@gitkraken/provider-apis'; import { EntityIdentifierProviderType, EntityType, EntityVersion } from '@gitkraken/provider-apis'; import type { IntegrationId } from '../../../constants.integrations'; -import { HostingIntegrationId, SelfHostedIntegrationId } from '../../../constants.integrations'; -import type { IssueOrPullRequest } from '../../../git/models/issue'; +import { HostingIntegrationId, IssueIntegrationId, SelfHostedIntegrationId } from '../../../constants.integrations'; +import type { Container } from '../../../container'; +import type { Issue, IssueOrPullRequest, IssueShape } from '../../../git/models/issue'; +import type { PullRequest } from '../../../git/models/pullRequest'; +import { Logger } from '../../../system/logger'; import { equalsIgnoreCase } from '../../../system/string'; import type { LaunchpadItem } from '../../launchpad/launchpadProvider'; +import type { IssueResourceDescriptor, RepositoryDescriptor } from '../integration'; +import { isIssueResourceDescriptor, isRepositoryDescriptor } from '../integration'; +import type { GitConfigEntityIdentifier } from './models'; function isGitHubDotCom(domain: string): boolean { return equalsIgnoreCase(domain, 'github.com'); @@ -18,6 +24,10 @@ function isLaunchpadItem(item: IssueOrPullRequest | LaunchpadItem): item is Laun return (item as LaunchpadItem).uuid !== undefined; } +function isIssue(item: IssueOrPullRequest | LaunchpadItem): item is Issue { + return item.type === 'issue'; +} + export function getEntityIdentifierInput(entity: IssueOrPullRequest | LaunchpadItem): AnyEntityIdentifierInput { let entityType = EntityType.Issue; if (entity.type === 'pullrequest') { @@ -34,13 +44,23 @@ export function getEntityIdentifierInput(entity: IssueOrPullRequest | LaunchpadI provider = EntityIdentifierProviderType.GitlabSelfHosted; domain = entity.provider.domain; } + let projectId = null; + let resourceId = null; + if (provider === EntityIdentifierProviderType.Jira) { + if (!isIssue(entity) || entity.project == null) { + throw new Error('Jira issues must have a project'); + } + + projectId = entity.project.id; + resourceId = entity.project.resourceId; + } return { accountOrOrgId: null, // needed for Trello issues, once supported organizationName: null, // needed for Azure issues and PRs, once supported - projectId: null, // needed for Jira issues, Trello issues, and Azure issues and PRs, once supported + projectId: projectId, // needed for Jira issues, Trello issues, and Azure issues and PRs, once supported repoId: null, // needed for Azure and BitBucket PRs, once supported - resourceId: null, // needed for Jira issues, once supported + resourceId: resourceId, // needed for Jira issues provider: provider, entityType: entityType, version: EntityVersion.One, @@ -49,12 +69,20 @@ export function getEntityIdentifierInput(entity: IssueOrPullRequest | LaunchpadI }; } -export function getProviderIdFromEntityIdentifier(entityIdentifier: EntityIdentifier): IntegrationId | undefined { +export function getProviderIdFromEntityIdentifier( + entityIdentifier: EntityIdentifier | AnyEntityIdentifierInput, +): IntegrationId | undefined { switch (entityIdentifier.provider) { case EntityIdentifierProviderType.Github: return HostingIntegrationId.GitHub; case EntityIdentifierProviderType.GithubEnterprise: return SelfHostedIntegrationId.GitHubEnterprise; + case EntityIdentifierProviderType.Gitlab: + return HostingIntegrationId.GitLab; + case EntityIdentifierProviderType.GitlabSelfHosted: + return SelfHostedIntegrationId.GitLabSelfHosted; + case EntityIdentifierProviderType.Jira: + return IssueIntegrationId.Jira; default: return undefined; } @@ -66,7 +94,138 @@ function fromStringToEntityIdentifierProviderType(str: string): EntityIdentifier return EntityIdentifierProviderType.Github; case 'gitlab': return EntityIdentifierProviderType.Gitlab; + case 'jira': + return EntityIdentifierProviderType.Jira; default: throw new Error(`Unknown provider type '${str}'`); } } + +export function encodeIssueOrPullRequestForGitConfig( + entity: Issue | PullRequest, + owner: RepositoryDescriptor | IssueResourceDescriptor, +): GitConfigEntityIdentifier { + const encodedOwner: GitConfigEntityIdentifier['metadata']['owner'] = { + key: owner.key, + name: owner.name, + id: undefined, + owner: undefined, + }; + if (isRepositoryDescriptor(owner)) { + encodedOwner.owner = owner.owner; + } else if (isIssueResourceDescriptor(owner)) { + encodedOwner.id = owner.id; + } else { + throw new Error('Invalid owner'); + } + + return { + ...getEntityIdentifierInput(entity), + metadata: { + id: entity.id, + owner: encodedOwner, + createdDate: new Date().toISOString(), + }, + }; +} + +export function isGitConfigEntityIdentifier(entity: unknown): entity is GitConfigEntityIdentifier { + return ( + entity != null && + typeof entity === 'object' && + 'provider' in entity && + entity.provider != null && + 'entityType' in entity && + entity.entityType != null && + 'version' in entity && + entity.version != null && + 'entityId' in entity && + entity.entityId != null && + 'metadata' in entity && + entity.metadata != null && + typeof entity.metadata === 'object' && + 'id' in entity.metadata && + entity.metadata.id != null && + 'owner' in entity.metadata && + entity.metadata.owner != null && + 'createdDate' in entity.metadata && + entity.metadata.createdDate != null + ); +} + +export function isGitConfigEntityIdentifiers(entities: unknown): entities is GitConfigEntityIdentifier[] { + return Array.isArray(entities) && entities.every(entity => isGitConfigEntityIdentifier(entity)); +} + +export function decodeEntityIdentifiersFromGitConfig(str: string): GitConfigEntityIdentifier[] { + const decoded = JSON.parse(str); + + if (!isGitConfigEntityIdentifiers(decoded)) { + debugger; + Logger.error('Invalid entity identifiers in git config'); + return []; + } + + for (const decodedEntity of decoded) { + if ( + decodedEntity.provider === EntityIdentifierProviderType.Jira && + (decodedEntity.resourceId == null || decodedEntity.projectId == null) + ) { + debugger; + Logger.error('Invalid Jira issue in git config'); + continue; + } + } + + return decoded; +} + +export async function getIssueFromGitConfigEntityIdentifier( + container: Container, + identifier: GitConfigEntityIdentifier, +): Promise { + if (identifier.entityType !== EntityType.Issue) { + return undefined; + } + + // TODO: Centralize where we represent all supported providers for issues + if ( + identifier.provider !== EntityIdentifierProviderType.Jira && + identifier.provider !== EntityIdentifierProviderType.Github && + identifier.provider !== EntityIdentifierProviderType.Gitlab + ) { + return undefined; + } + + const integrationId = getProviderIdFromEntityIdentifier(identifier); + if (integrationId == null) { + return undefined; + } + + const integration = await container.integrations.get(integrationId); + if (integration == null) { + return undefined; + } + + return integration.getIssue( + { + id: identifier.metadata.owner.id, + key: identifier.metadata.owner.key, + owner: identifier.metadata.owner.owner, + name: identifier.metadata.owner.name, + }, + identifier.metadata.id, + ); +} + +export function getIssueOwner(issue: IssueShape): RepositoryDescriptor | IssueResourceDescriptor | undefined { + return issue.repository + ? { + key: `${issue.repository.owner}/${issue.repository.repo}`, + owner: issue.repository.owner, + name: issue.repository.repo, + } + : issue.project + ? { key: issue.project.resourceId, id: issue.project.resourceId, name: issue.project.resourceId } + : undefined; +} diff --git a/src/plus/startWork/startWork.ts b/src/plus/startWork/startWork.ts index 9bec844d2cdfb..cab8d1ef5edff 100644 --- a/src/plus/startWork/startWork.ts +++ b/src/plus/startWork/startWork.ts @@ -225,6 +225,7 @@ export class StartWorkCommand extends QuickCommand { suggestNameOnly: true, suggestRepoOnly: true, confirmOptions: ['--switch', '--worktree'], + associateWithIssue: issue, }, }, this.pickedVia, @@ -386,7 +387,6 @@ export class StartWorkCommand extends QuickCommand { return { label: i.item.issue.title.length > 60 ? `${i.item.issue.title.substring(0, 60)}...` : i.item.issue.title, - // description: `${i.repoAndOwner}#${i.id}, by @${i.author}`, description: `\u00a0 ${ i.item.issue.repository ? `${i.item.issue.repository.owner}/${i.item.issue.repository.repo}#` : '' }${i.item.issue.id} \u00a0`, diff --git a/src/webviews/apps/plus/home/components/active-work.ts b/src/webviews/apps/plus/home/components/active-work.ts index 70b7108aa8cd4..1f378fdec1dbc 100644 --- a/src/webviews/apps/plus/home/components/active-work.ts +++ b/src/webviews/apps/plus/home/components/active-work.ts @@ -223,7 +223,7 @@ export class GlActiveBranchCard extends GlBranchCardBase { return html` ${this.renderBranchIndicator()}${this.renderBranchItem( this.renderBranchStateActions(), - )}${this.renderPrItem()}${this.renderAutolinksItem()} + )}${this.renderPrItem()}${this.renderIssuesItem()} `; } diff --git a/src/webviews/apps/plus/home/components/branch-card.ts b/src/webviews/apps/plus/home/components/branch-card.ts index 2fc512cfa19fb..0af9f34c65e34 100644 --- a/src/webviews/apps/plus/home/components/branch-card.ts +++ b/src/webviews/apps/plus/home/components/branch-card.ts @@ -224,19 +224,20 @@ export abstract class GlBranchCardBase extends GlElement { this.toggleExpanded(true); } - protected renderAutolinks() { - const { autolinks } = this.branch; - if (!autolinks) return nothing; + protected renderIssues() { + const { autolinks, issues } = this.branch; + const issuesSource = issues?.length ? issues : autolinks; + if (!issuesSource?.length) return nothing; return html` - ${autolinks.map(autolink => { + ${issuesSource.map(issue => { return html`

- + - ${autolink.title} - #${autolink.id} + ${issue.title} + #${issue.id}

`; })} @@ -383,12 +384,12 @@ export abstract class GlBranchCardBase extends GlElement { `; } - protected renderAutolinksItem() { - if (!this.branch.autolinks?.length) return nothing; + protected renderIssuesItem() { + if (!this.branch.issues?.length && !this.branch.autolinks?.length) return nothing; return html` -
${this.renderAutolinks()}
+
${this.renderIssues()}
`; } @@ -408,7 +409,7 @@ export class GlBranchCard extends GlBranchCardBase { return html`
- ${this.renderBranchItem(this.renderActions())}${this.renderPrItem()}${this.renderAutolinksItem()} + ${this.renderBranchItem(this.renderActions())}${this.renderPrItem()}${this.renderIssuesItem()}
`; diff --git a/src/webviews/home/homeWebview.ts b/src/webviews/home/homeWebview.ts index a6feae2a87707..62562168a0059 100644 --- a/src/webviews/home/homeWebview.ts +++ b/src/webviews/home/homeWebview.ts @@ -21,7 +21,8 @@ import * as RepoActions from '../../git/actions/repository'; import type { BranchContributorOverview } from '../../git/gitProvider'; import type { GitBranch } from '../../git/models/branch'; import type { BranchTargetInfo } from '../../git/models/branch.utils'; -import { getBranchTargetInfo } from '../../git/models/branch.utils'; +import { getAssociatedIssuesForBranch, getBranchTargetInfo } from '../../git/models/branch.utils'; +import type { Issue } from '../../git/models/issue'; import type { MergeConflict } from '../../git/models/mergeConflict'; import type { PullRequest } from '../../git/models/pullRequest'; import { getComparisonRefsForPullRequest } from '../../git/models/pullRequest'; @@ -1116,6 +1117,7 @@ async function getOverviewBranches( let repoStatusPromise: Promise | undefined; const prPromises = new Map>(); const autolinkPromises = new Map | undefined>>(); + const issuePromises = new Map>(); const statusPromises = new Map>(); const contributorPromises = new Map>(); const targetAndPotentialConflictPromises = new Map>(); @@ -1133,6 +1135,10 @@ async function getOverviewBranches( if (options?.isPro !== false) { prPromises.set(branch.id, getEnrichedPullRequest(branch, { avatarSize: 16 })); autolinkPromises.set(branch.id, branch.getEnrichedAutolinks()); + issuePromises.set( + branch.id, + getAssociatedIssuesForBranch(container, branch).then(issues => issues.value), + ); contributorPromises.set( branch.id, container.git.getBranchContributorOverview(branch.repoPath, branch.ref), @@ -1169,6 +1175,10 @@ async function getOverviewBranches( if (options?.isPro !== false) { prPromises.set(branch.id, getEnrichedPullRequest(branch)); autolinkPromises.set(branch.id, branch.getEnrichedAutolinks()); + issuePromises.set( + branch.id, + getAssociatedIssuesForBranch(container, branch).then(issues => issues.value), + ); contributorPromises.set( branch.id, container.git.getBranchContributorOverview(branch.repoPath, branch.ref), @@ -1212,6 +1222,10 @@ async function getOverviewBranches( if (options?.isPro !== false) { autolinkPromises.set(branch.id, branch.getEnrichedAutolinks()); + issuePromises.set( + branch.id, + getAssociatedIssuesForBranch(container, branch).then(issues => issues.value), + ); } const timestamp = branch.date?.getTime(); @@ -1256,6 +1270,7 @@ async function getOverviewBranches( overviewBranches, prPromises, autolinkPromises, + issuePromises, statusPromises, contributorPromises, targetAndPotentialConflictPromises, @@ -1270,12 +1285,13 @@ async function enrichOverviewBranches( overviewBranches: GetOverviewBranches, prPromises: Map>, autolinkPromises: Map | undefined>>, + issuePromises: Map>, statusPromises: Map>, contributorPromises: Map>, targetAndPotentialConflictPromises: Map>, container: Container, ) { - const [prResults, autolinkResults, statusResults, contributorResults, targetAndPotentialConflictResults] = + const [prResults, autolinkResults, issueResults, statusResults, contributorResults, targetAndPotentialConflictResults] = await Promise.allSettled([ pauseOnCancelOrTimeoutMap(prPromises, true), Promise.allSettled( @@ -1283,6 +1299,9 @@ async function enrichOverviewBranches( autolinks.then<[string, Map | undefined]>(a => [id, a]), ), ), + Promise.allSettled( + map(issuePromises, ([id, issues]) => issues.then<[string, Issue[] | undefined]>(issues => [id, issues])), + ), Promise.allSettled( map(statusPromises, ([id, status]) => status.then<[string, GitStatus | undefined]>(status => [id, status]), @@ -1368,6 +1387,22 @@ async function enrichOverviewBranches( ); } + const issues = new Map( + getSettledValue(issueResults) + ?.filter(r => r.status === 'fulfilled') + .map(({ value: [issueId, issues] }) => [ + issueId, + issues + ? (issues.map(issue => ({ + id: issue.id, + title: issue.title, + state: issue.state, + url: issue.url, + })) satisfies GetOverviewBranch['issues']) + : undefined, + ]), + ); + const statuses = new Map( getSettledValue(statusResults) ?.filter(r => r.status === 'fulfilled') @@ -1390,6 +1425,9 @@ async function enrichOverviewBranches( const autolinksForBranch = autolinks.get(branch.id); branch.autolinks = autolinksForBranch; + const issuesForBranch = issues.get(branch.id); + branch.issues = issuesForBranch; + const status = statuses.get(branch.id); if (status != null) { branch.workingTreeState = status.getDiffStatus(); diff --git a/src/webviews/home/protocol.ts b/src/webviews/home/protocol.ts index 91a80e031fb88..62c5e5bc145a1 100644 --- a/src/webviews/home/protocol.ts +++ b/src/webviews/home/protocol.ts @@ -153,6 +153,12 @@ export interface GetOverviewBranch { state: string; hasIssue: boolean; }[]; + issues?: { + id: string; + title: string; + url: string; + state: string; + }[]; worktree?: { name: string; uri: string;