Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/commands/gitCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,17 @@ export class GitCommandsCommand extends Command {
break;
case Commands.ShowLaunchpad:
args = { command: 'focus', ...args };
// TODO: Improve this. Args do not come in with a command property,
// but the contextual typing relies on it to recognize the other args.
if (args.command === 'focus') {
args.source ??= 'commandPalette';
this.container.telemetry.sendEvent('launchpad/opened', {
source: args.source,
group: args.state?.initialGroup ?? null,
selectTopItem: args.state?.selectTopItem ?? false,
});
}

break;
}

Expand Down
9 changes: 8 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,13 @@ export type TelemetryEvents =
| 'usage/track'
| 'openReviewMode'
| 'codeSuggestionCreated'
| 'codeSuggestionViewed';
| 'codeSuggestionViewed'
| 'launchpad/opened'
| 'launchpad/configurationChanged'
| 'launchpad/groupToggled'
| 'launchpad/actionTaken'
| 'launchpad/indicatorHidden'
| 'launchpad/indicatorFirstDataReceived';

export type AIProviders = 'anthropic' | 'gemini' | 'openai';
export type AIModels<Provider extends AIProviders = AIProviders> = Provider extends 'openai'
Expand Down Expand Up @@ -879,6 +885,7 @@ export type GlobalStorage = {
'views:welcome:visible': boolean;
'confirm:draft:storage': boolean;
'home:sections:collapsed': string[];
'launchpad:indicator:dataReceived': boolean;
} & { [key in `confirm:ai:tos:${AIProviders}`]: boolean } & {
[key in `provider:authentication:skip:${string}`]: boolean;
} & { [key in `gk:${string}:checkin`]: Stored<StoredGKCheckInResponse> } & {
Expand Down
102 changes: 95 additions & 7 deletions src/plus/focus/focus.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { QuickInputButton } from 'vscode';
import { commands, Uri } from 'vscode';
import { getAvatarUri } from '../../avatars';
import type {
Expand Down Expand Up @@ -74,16 +75,22 @@ const groupMap = new Map<FocusGroup, [string, string | undefined]>([
['snoozed', ['Snoozed', 'bell-slash']],
]);

export interface FocusItemQuickPickItem extends QuickPickItemOfT<FocusItem> {}
export interface FocusItemQuickPickItem extends QuickPickItemOfT<FocusItem> {
group: FocusGroup;
}

interface Context {
items: FocusItem[];
title: string;
collapsed: Map<FocusGroup, boolean>;
}

interface GroupedFocusItem extends FocusItem {
group: FocusGroup;
}

interface State {
item?: FocusItem;
item?: GroupedFocusItem;
action?: FocusAction | FocusTargetAction;
initialGroup?: FocusGroup;
selectTopItem?: boolean;
Expand All @@ -92,6 +99,7 @@ interface State {
export interface FocusCommandArgs {
readonly command: 'focus';
confirm?: boolean;
source?: 'indicator' | 'home' | 'commandPalette' | 'welcome';
state?: Partial<State>;
}

Expand Down Expand Up @@ -166,7 +174,7 @@ export class FocusCommand extends QuickCommand<State> {
context.items = await this.container.focus.getCategorizedItems();

if (state.counter < 2 || state.item == null) {
const result = yield* this.pickFocusItemStep(state, context, {
const result = yield* this.pickFocusItemStep(state, context, this.container, {
picked: state.item?.id,
selectTopItem: state.selectTopItem,
});
Expand All @@ -179,12 +187,17 @@ export class FocusCommand extends QuickCommand<State> {

if (this.confirm(state.confirm)) {
await this.container.focus.ensureFocusItemCodeSuggestions(state.item);
this.sendItemActionTelemetry('select', state.item, state.item.group);
const result = yield* this.confirmStep(state, context);
if (result === StepResultBreak) continue;

state.action = result;
}

if (state.action) {
this.sendItemActionTelemetry(state.action, state.item, state.item.group);
}

if (typeof state.action === 'string') {
switch (state.action) {
case 'merge': {
Expand Down Expand Up @@ -235,8 +248,9 @@ export class FocusCommand extends QuickCommand<State> {
private *pickFocusItemStep(
state: StepState<State>,
context: Context,
container: Container,
{ picked, selectTopItem }: { picked?: string; selectTopItem?: boolean },
): StepResultGenerator<FocusItem> {
): StepResultGenerator<GroupedFocusItem> {
function getItems(categorizedItems: FocusItem[]) {
const items: (FocusItemQuickPickItem | DirectiveQuickPickItem)[] = [];

Expand All @@ -260,7 +274,13 @@ export class FocusCommand extends QuickCommand<State> {
})\u00a0\u00a0${groupMap.get(ui)![0]?.toUpperCase()}`, //'\u00a0',
//detail: groupMap.get(group)?.[0].toUpperCase(),
onDidSelect: () => {
context.collapsed.set(ui, !context.collapsed.get(ui));
const collapse = !context.collapsed.get(ui);
context.collapsed.set(ui, collapse);
container.telemetry.sendEvent('launchpad/groupToggled', {
group: ui,
expanded: !collapse,
itemsCount: groupItems.length,
});
},
}),
);
Expand Down Expand Up @@ -299,6 +319,7 @@ export class FocusCommand extends QuickCommand<State> {
iconPath: i.author?.avatarUrl != null ? Uri.parse(i.author.avatarUrl) : undefined,
item: i,
picked: i.id === picked || i.id === topItem?.id,
group: ui,
};
}),
);
Expand Down Expand Up @@ -351,7 +372,7 @@ export class FocusCommand extends QuickCommand<State> {
}
},

onDidClickItemButton: async (quickpick, button, { item }) => {
onDidClickItemButton: async (quickpick, button, { group, item }) => {
switch (button) {
case OpenOnGitHubQuickInputButton:
this.container.focus.open(item);
Expand All @@ -377,6 +398,7 @@ export class FocusCommand extends QuickCommand<State> {
break;
}

this.sendItemActionTelemetry(button, item, group);
quickpick.busy = true;

try {
Expand All @@ -392,7 +414,9 @@ export class FocusCommand extends QuickCommand<State> {
});

const selection: StepSelection<typeof step> = yield step;
return canPickStepContinue(step, state, selection) ? selection[0].item : StepResultBreak;
return canPickStepContinue(step, state, selection)
? { ...selection[0].item, group: selection[0].group }
: StepResultBreak;
}

private *confirmStep(
Expand Down Expand Up @@ -548,6 +572,8 @@ export class FocusCommand extends QuickCommand<State> {
}
break;
}

this.sendItemActionTelemetry(button, state.item, state.item.group);
},
},
);
Expand Down Expand Up @@ -780,6 +806,68 @@ export class FocusCommand extends QuickCommand<State> {
return 'Open';
}
}

private sendItemActionTelemetry(
buttonOrAction: QuickInputButton | FocusAction | FocusTargetAction | 'select',
item: FocusItem,
group: FocusGroup,
) {
let action:
| FocusAction
| 'pin'
| 'unpin'
| 'snooze'
| 'unsnooze'
| 'open-suggestion'
| 'open-suggestion-browser'
| 'select'
| undefined;
if (typeof buttonOrAction !== 'string' && 'action' in buttonOrAction) {
action = buttonOrAction.action;
} else {
switch (buttonOrAction) {
case MergeQuickInputButton:
action = 'merge';
break;
case OpenOnGitHubQuickInputButton:
action = 'soft-open';
break;
case PinQuickInputButton:
action = 'pin';
break;
case UnpinQuickInputButton:
action = 'unpin';
break;
case SnoozeQuickInputButton:
action = 'snooze';
break;
case UnsnoozeQuickInputButton:
action = 'unsnooze';
break;
case OpenCodeSuggestionBrowserQuickInputButton:
action = 'open-suggestion-browser';
break;
case 'open':
case 'merge':
case 'soft-open':
case 'switch':
case 'select':
action = buttonOrAction;
break;
}
}

if (action == null) return;

this.container.telemetry.sendEvent('launchpad/actionTaken', {
action: action,
itemType: item.type,
itemProvider: item.provider.id,
itemActionableCategory: item.actionableCategory,
itemGroup: group,
itemCodeSuggestionCount: item.codeSuggestionsCount,
});
}
}

function isFocusTargetActionQuickPickItem(item: any): item is QuickPickItemOfT<FocusTargetAction> {
Expand Down
16 changes: 15 additions & 1 deletion src/plus/focus/focusIndicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class FocusIndicator implements Disposable {
: {
title: 'Open Launchpad',
command: Commands.ShowLaunchpad,
arguments: [{ state: { selectTopItem: label === 'item' } }],
arguments: [{ source: 'indicator', state: { selectTopItem: label === 'item' } }],
};
}

Expand Down Expand Up @@ -221,6 +221,7 @@ export class FocusIndicator implements Disposable {
);
this._statusBarFocus.color = undefined;
} else if (state === 'data') {
void this.maybeSendFirstDataEvent();
this._lastDataUpdate = new Date();
const useColors = configuration.get('launchpad.indicator.useColors');
const groups = configuration.get('launchpad.indicator.groups') ?? ([] satisfies FocusGroup[]);
Expand Down Expand Up @@ -259,6 +260,7 @@ export class FocusIndicator implements Disposable {
: pluralize('pull request', items.length)
} can be merged.](command:gitlens.showLaunchpad?${encodeURIComponent(
JSON.stringify({
source: 'indicator',
state: { initialGroup: 'mergeable', selectTopItem: labelText === 'item' },
}),
)} "Open Ready to Merge in Launchpad")`,
Expand Down Expand Up @@ -313,6 +315,7 @@ export class FocusIndicator implements Disposable {
hasMultipleCategories ? 'are blocked' : actionMessage
}.](command:gitlens.showLaunchpad?${encodeURIComponent(
JSON.stringify({
source: 'indicator',
state: { initialGroup: 'blocked', selectTopItem: labelText === 'item' },
}),
)} "Open Blocked in Launchpad")`,
Expand Down Expand Up @@ -346,6 +349,7 @@ export class FocusIndicator implements Disposable {
items.length > 1 ? 'require' : 'requires'
} follow-up.](command:gitlens.showLaunchpad?${encodeURIComponent(
JSON.stringify({
source: 'indicator',
state: { initialGroup: 'follow-up', selectTopItem: labelText === 'item' },
}),
)} "Open Follow-Up in Launchpad")`,
Expand All @@ -364,6 +368,7 @@ export class FocusIndicator implements Disposable {
items.length > 1 ? 'need' : 'needs'
} your review.](command:gitlens.showLaunchpad?${encodeURIComponent(
JSON.stringify({
source: 'indicator',
state: {
initialGroup: 'needs-review',
selectTopItem: labelText === 'item',
Expand Down Expand Up @@ -427,4 +432,13 @@ export class FocusIndicator implements Disposable {
: ''
}`;
}

private async maybeSendFirstDataEvent() {
const firstTimeDataReceived = this.container.storage.get('launchpad:indicator:dataReceived') ?? false;
if (!firstTimeDataReceived) {
void this.container.storage.store('launchpad:indicator:dataReceived', true);
const userId = (await this.container.subscription.getSubscription())?.account?.id;
this.container.telemetry.sendEvent('launchpad/indicatorFirstDataReceived', { userId: userId });
}
}
}
17 changes: 17 additions & 0 deletions src/plus/focus/focusProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,23 @@ export class FocusProvider implements Disposable {

private onConfigurationChanged(e: ConfigurationChangeEvent) {
if (!configuration.changed(e, 'launchpad')) return;
const launchpadConfig = configuration.get('launchpad');
this.container.telemetry.sendEvent('launchpad/configurationChanged', {
staleThreshold: launchpadConfig.staleThreshold,
ignoredRepositories: launchpadConfig.ignoredRepositories,
indicatorEnabled: launchpadConfig.indicator.enabled,
indicatorOpenInEditor: launchpadConfig.indicator.openInEditor,
indicatorIcon: launchpadConfig.indicator.icon,
indicatorLabel: launchpadConfig.indicator.label,
indicatorUseColors: launchpadConfig.indicator.useColors,
indicatorGroups: launchpadConfig.indicator.groups,
indicatorPollingEnabled: launchpadConfig.indicator.polling.enabled,
indicatorPollingInterval: launchpadConfig.indicator.polling.interval,
});

if (configuration.changed(e, 'launchpad.indicator.enabled') && !launchpadConfig.indicator.enabled) {
this.container.telemetry.sendEvent('launchpad/indicatorHidden');
}

if (
configuration.changed(e, 'launchpad.ignoredRepositories') ||
Expand Down
2 changes: 1 addition & 1 deletion src/webviews/apps/home/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ <h2 class="nav-list__title t-eyebrow sticky">Popular views</h2>
<gl-feature-badge placement="left" class="nav-list__access"></gl-feature-badge>
</div>
<div class="nav-list__item">
<a class="nav-list__link" href="command:gitlens.showLaunchpad" aria-label="Open Launchpad"
<a class="nav-list__link" href="command:gitlens.showLaunchpad?%7B%22source%22%3A%22home%22%7D" aria-label="Open Launchpad"
><code-icon class="nav-list__icon" icon="rocket"></code-icon
><gl-tooltip class="nav-list__group" placement="bottom" content="Open Launchpad"
><span class="nav-list__label">Launchpad</span
Expand Down
5 changes: 4 additions & 1 deletion src/webviews/apps/welcome/welcome.html
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,10 @@ <h3 class="sticky">

<section>
<h3 class="sticky">
<a class="muted" href="command:gitlens.showLaunchpad" aria-label="Open Launchpad"
<a
class="muted"
href="command:gitlens.showLaunchpad?%7B%22source%22%3A%22welcome%22%7D"
aria-label="Open Launchpad"
><gl-tooltip placement="bottom" content="Open Launchpad"
><span>Launchpad</span></gl-tooltip
></a
Expand Down