Skip to content

Commit 8dfe19b

Browse files
authored
Merge branch 'main' into alexr00/planned-rooster
2 parents 5e7beb4 + 69ec833 commit 8dfe19b

5 files changed

Lines changed: 168 additions & 4 deletions

File tree

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3586,6 +3586,10 @@
35863586
"command": "pr.checkoutOnVscodeDevFromDescription",
35873587
"group": "checkout@1",
35883588
"when": "webviewId == PullRequestOverview && github:checkoutMenu"
3589+
},
3590+
{
3591+
"command": "pr.openSessionLogFromDescription",
3592+
"when": "webviewId == PullRequestOverview && github:codingAgentMenu"
35893593
}
35903594
],
35913595
"chat/chatSessions": [

src/commands.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ import Logger from './common/logger';
1414
import { FILE_LIST_LAYOUT, HIDE_VIEWED_FILES, PR_SETTINGS_NAMESPACE } from './common/settingKeys';
1515
import { editQuery } from './common/settingsUtils';
1616
import { ITelemetry } from './common/telemetry';
17+
import { SessionLinkInfo } from './common/timelineEvent';
1718
import { asTempStorageURI, fromPRUri, fromReviewUri, Schemes, toPRUri } from './common/uri';
1819
import { formatError } from './common/utils';
1920
import { EXTENSION_ID } from './constants';
2021
import { ICopilotRemoteAgentCommandArgs } from './github/common';
2122
import { ChatSessionWithPR, CrossChatSessionWithPR } from './github/copilotApi';
22-
import { CopilotRemoteAgentManager } from './github/copilotRemoteAgent';
23+
import { CopilotRemoteAgentManager, SessionIdForPr } from './github/copilotRemoteAgent';
2324
import { FolderRepositoryManager } from './github/folderRepositoryManager';
2425
import { GitHubRepository } from './github/githubRepository';
2526
import { Issue } from './github/interface';
@@ -724,6 +725,14 @@ export function registerCommands(
724725
return vscode.env.openExternal(vscode.Uri.parse(vscodeDevPrLink(resolved.pr)));
725726
}));
726727

728+
context.subscriptions.push(vscode.commands.registerCommand('pr.openSessionLogFromDescription', async (context: SessionLinkInfo | undefined) => {
729+
if (!context) {
730+
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for checkout.'));
731+
}
732+
const resource = SessionIdForPr.getResource(context.pullNumber, context.sessionIndex);
733+
return vscode.commands.executeCommand('vscode.open', resource);
734+
}));
735+
727736
context.subscriptions.push(
728737
vscode.commands.registerCommand('pr.exit', async (pr: PRNode | RepositoryChangesNode | PullRequestModel | undefined) => {
729738
let pullRequestModel: PullRequestModel | undefined;

src/github/graphql.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,12 @@ export interface Account extends Actor {
122122

123123
export function isAccount(x: Actor | Team | Node | undefined | null): x is Account {
124124
const asAccount = x as Partial<Account>;
125-
return !!asAccount && !!asAccount?.name && (asAccount?.email !== undefined);
125+
return !!asAccount && (asAccount?.name !== undefined) && (asAccount?.email !== undefined);
126126
}
127127

128128
export function isTeam(x: Actor | Team | Node | undefined | null): x is Team {
129129
const asTeam = x as Partial<Team>;
130-
return !!asTeam && !!asTeam?.slug;
130+
return !!asTeam && (asTeam?.slug !== undefined);
131131
}
132132

133133
export interface Team {

src/test/github/graphql.test.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as assert from 'assert';
7+
import { isAccount, isTeam, Actor, Account, Team, Node } from '../../github/graphql';
8+
9+
describe('graphql type guards', () => {
10+
11+
describe('isAccount', () => {
12+
it('returns true for a valid Account', () => {
13+
const account: Account = {
14+
__typename: 'User',
15+
id: 'acct1',
16+
login: 'alice',
17+
avatarUrl: 'https://example.com/a.png',
18+
url: 'https://example.com/alice',
19+
name: 'Alice',
20+
email: 'alice@example.com'
21+
};
22+
assert.strictEqual(isAccount(account), true);
23+
});
24+
25+
it('returns false for Actor missing name/email', () => {
26+
const actor: Actor = {
27+
__typename: 'User',
28+
id: 'act1',
29+
login: 'bob',
30+
avatarUrl: 'https://example.com/b.png',
31+
url: 'https://example.com/bob'
32+
};
33+
assert.strictEqual(isAccount(actor), false);
34+
});
35+
36+
it('returns false for Team object', () => {
37+
const team: Team = {
38+
avatarUrl: 'https://example.com/t.png',
39+
name: 'Dev Team',
40+
url: 'https://example.com/team',
41+
repositories: { nodes: [] },
42+
slug: 'dev-team',
43+
id: 'team1'
44+
};
45+
assert.strictEqual(isAccount(team), false);
46+
});
47+
48+
it('returns false for Node object', () => {
49+
const node: Node = { id: 'node1' };
50+
assert.strictEqual(isAccount(node), false);
51+
});
52+
53+
it('returns false for null and undefined', () => {
54+
assert.strictEqual(isAccount(null), false);
55+
assert.strictEqual(isAccount(undefined), false);
56+
});
57+
58+
it('returns true when name and email are null', () => {
59+
const obj: any = {
60+
__typename: 'User', id: 'null1', login: 'nullUser', avatarUrl: '', url: '', name: null, email: null
61+
};
62+
assert.strictEqual(isAccount(obj), true);
63+
});
64+
65+
it('returns true when name is null but email present', () => {
66+
const obj: any = {
67+
__typename: 'User', id: 'null2', login: 'nullName', avatarUrl: '', url: '', name: null, email: 'e@example.com'
68+
};
69+
assert.strictEqual(isAccount(obj), true);
70+
});
71+
72+
it('returns false when email or name is undefined', () => {
73+
const obj: any = {
74+
__typename: 'User', id: 'null3', login: 'nullEmail', avatarUrl: '', url: '', name: undefined, email: undefined
75+
};
76+
assert.strictEqual(isAccount(obj), false);
77+
});
78+
});
79+
80+
describe('isTeam', () => {
81+
it('returns true for a valid Team', () => {
82+
const team: Team = {
83+
avatarUrl: 'https://example.com/t.png',
84+
name: 'Engineering',
85+
url: 'https://example.com/eng',
86+
repositories: { nodes: [] },
87+
slug: 'engineering',
88+
id: 'team2'
89+
};
90+
assert.strictEqual(isTeam(team), true);
91+
});
92+
93+
it('returns false for Account object', () => {
94+
const account: Account = {
95+
__typename: 'User',
96+
id: 'acct2',
97+
login: 'carol',
98+
avatarUrl: 'https://example.com/c.png',
99+
url: 'https://example.com/carol',
100+
name: 'Carol',
101+
email: 'carol@example.com'
102+
};
103+
assert.strictEqual(isTeam(account), false);
104+
});
105+
106+
it('returns false for Actor without slug', () => {
107+
const actor: Actor = {
108+
__typename: 'User',
109+
id: 'act2',
110+
login: 'dave',
111+
avatarUrl: 'https://example.com/d.png',
112+
url: 'https://example.com/dave'
113+
};
114+
assert.strictEqual(isTeam(actor), false);
115+
});
116+
117+
it('returns false for Node object', () => {
118+
const node: Node = { id: 'node2' };
119+
assert.strictEqual(isTeam(node), false);
120+
});
121+
122+
it('returns false for null and undefined', () => {
123+
assert.strictEqual(isTeam(null), false);
124+
assert.strictEqual(isTeam(undefined), false);
125+
});
126+
127+
it('returns false when slug is undefined', () => {
128+
const obj: any = {
129+
avatarUrl: '', name: 'Team', url: '', repositories: { nodes: [] }, slug: undefined, id: 'tslugnull'
130+
};
131+
assert.strictEqual(isTeam(obj), false);
132+
});
133+
it('returns true when slug is null', () => {
134+
const obj: any = {
135+
avatarUrl: '', name: 'Team', url: '', repositories: { nodes: [] }, slug: null, id: 'tslugnull'
136+
};
137+
assert.strictEqual(isTeam(obj), true);
138+
});
139+
});
140+
});
141+

webviews/components/comment.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
77
import { ContextDropdown } from './contextDropdown';
8-
import { editIcon, quoteIcon, trashIcon } from './icon';
8+
import { copyIcon, editIcon, quoteIcon, trashIcon } from './icon';
99
import { nbsp, Spaced } from './space';
1010
import { Timestamp } from './timestamp';
1111
import { AuthorLink, Avatar } from './user';
@@ -48,6 +48,7 @@ export function CommentView(commentProps: Props) {
4848
const currentDraft = pr?.pendingCommentDrafts && pr.pendingCommentDrafts[id];
4949
const [inEditMode, setEditMode] = useState(!!currentDraft);
5050
const [showActionBar, setShowActionBar] = useState(false);
51+
const commentUrl = (comment as Partial<IComment | ReviewEvent | CommentEvent>).htmlUrl || (comment as PullRequest).url;
5152

5253
if (inEditMode) {
5354
return React.cloneElement(headerInEditMode ? <CommentBox for={comment} /> : <></>, {}, [
@@ -96,6 +97,15 @@ export function CommentView(commentProps: Props) {
9697
>
9798
{quoteIcon}
9899
</button>
100+
{commentUrl ? (
101+
<button
102+
title="Copy Comment Link"
103+
className="icon-button"
104+
onClick={() => navigator.clipboard.writeText(commentUrl)}
105+
>
106+
{copyIcon}
107+
</button>
108+
) : null}
99109
{canEdit ? (
100110
<button title="Edit comment" className="icon-button" onClick={() => setEditMode(true)}>
101111
{editIcon}

0 commit comments

Comments
 (0)