Skip to content
Draft
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
80 changes: 44 additions & 36 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ pnpm run build:quick # Fast build (no linting)
pnpm run build:turbo # Turbo build (no typechecking or linting)
pnpm run build:extension # Build only the extension (no webviews)
pnpm run build:webviews # Build only webviews
pnpm run build:tests # Build unit tests (not part of the main build)
pnpm run bundle # Production bundle
pnpm run bundle:e2e # E2E tests (turbo) production bundle (with DEBUG for account simulation)
pnpm run bundle:turbo # Turbo production bundle (no typechecking or linting)
```

Expand All @@ -73,40 +75,34 @@ pnpm run watch:quick # Fast watch mode (no linting)
pnpm run watch:turbo # Turbo watch mode (no typechecking or linting)
pnpm run watch:extension # Watch extension only
pnpm run watch:webviews # Watch webviews only
pnpm run watch:tests # Watch test files
pnpm run watch:tests # Watch unit test files
```

### Testing & Quality
### Testing

```bash
pnpm run test # Run VS Code extension tests
pnpm run test # Run unit tests (VS Code extension tests)
pnpm run test:e2e # Run Playwright E2E tests
pnpm run build:tests # Build test files with esbuild
```

### Quality

```bash
pnpm run lint # Run ESLint with TypeScript rules
pnpm run lint:fix # Auto-fix linting issues
pnpm run pretty # Format code with Prettier
pnpm run pretty:check # Check formatting
```

### Specialized Commands
### Specialized Commands (typically not needed during normal development as they are part of build/watch)

```bash
pnpm run generate:contributions # Generate package.json contributions from contributions.json
pnpm run extract:contributions # Extract contributions from package.json to contributions.json
pnpm run generate:commandTypes # Generate command types from contributions
pnpm run build:icons # Build icon font from SVG sources
pnpm run web # Run extension in web environment for testing
pnpm run package # Create VSIX package
```

### Debugging

- Use **"Watch & Run"** launch configuration (F5) for desktop debugging
- Use **"Watch & Run (web)"** for webworker/browser debugging
- Press `Ctrl+Shift+P` → "Tasks: Run Task" → "watch" to start build task
- Tests are co-located with source files in `__tests__/` directories
- Webview changes can be refreshed without restarting extension

## Git & Repository Requirements/Guidelines

### Committing
Expand All @@ -133,7 +129,7 @@ pnpm run package # Create VSIX package

Uses [Keep a Changelog](http://keepachangelog.com/) format under `[Unreleased]`.

### Section Mapping
#### Section Mapping

| Change Type | Section |
| ----------- | ---------- |
Expand All @@ -144,7 +140,7 @@ Uses [Keep a Changelog](http://keepachangelog.com/) format under `[Unreleased]`.
| Deprecation | Deprecated |
| Removal | Removed |

### Entry Format
#### Entry Format

```markdown
- [Verb] [description] ([#issue](url))
Expand All @@ -163,7 +159,7 @@ Uses [Keep a Changelog](http://keepachangelog.com/) format under `[Unreleased]`.
- Fixes an issue where the _Home_ view would not update when switching repositories ([#4717](https://github.com/gitkraken/vscode-gitlens/issues/4717))
```

### Detection
#### Detection

Check `[Unreleased]` section for:

Expand Down Expand Up @@ -266,6 +262,36 @@ tests/ # E2E and Unit tests
walkthroughs/ # Welcome and tips walkthroughs
```

### Testing Structure

**Unit Tests**

- Tests co-located with source files in `__tests__/` directories
- Pattern: `src/path/to/__tests__/file.test.ts`
- VS Code extension tests use `@vscode/test-cli`
- Unit tests are built separately: `pnpm run build:tests`

```bash
pnpm run test # Run unit tests (VS Code extension tests)
pnpm run watch:tests # Watch mode for tests
```

**End-to-End (E2E) Tests**

- E2E tests use Playwright in `tests/e2e/`
- Fixture setup and utilities in `tests/e2e/fixtures/`
- Page objects in `tests/e2e/pageObjects/`
- Test specs in `tests/e2e/specs/`
- E2E tests are built as part of the main build, but can be built directly: `pnpm run bundle:e2e`

```bash
pnpm run test:e2e # Run E2E tests
pnpm run bundle:e2e # Build E2E tests (production with DEBUG for account simulation)
pnpm run watch # Watch mode (includes E2E tests)
```

**Important**: If you have access to VS Code's `runTests` and `testFailures` tools, use them to run and debug E2E tests

### Core Architectural Patterns

**1. Service Locator (Container)**
Expand Down Expand Up @@ -422,24 +448,6 @@ Files in or under directories named "plus" fall under `LICENSE.plus` (non-OSS):

Pro features integrate with GitKraken accounts and require authentication via SubscriptionService.

### Testing Infrastructure

**Test Structure**

- Tests co-located with source files in `__tests__/` directories
- Pattern: `src/path/to/__tests__/file.test.ts`
- VS Code extension tests use `@vscode/test-cli`
- E2E tests use Playwright in `tests/e2e/`
- Build tests separately: `pnpm run build:tests`

**Running Tests**

```bash
pnpm run test # Run extension tests
pnpm run test:e2e # Run E2E tests
pnpm run watch:tests # Watch mode for tests
```

## Coding Standards & Style Rules

### TypeScript Configuration
Expand Down
6 changes: 4 additions & 2 deletions src/commands/resets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,14 @@ export class ResetCommand extends GlCommandBase {

case 'banners':
await this.container.storage.delete('home:sections:collapsed');
await this.container.storage.delete('home:walkthrough:dismissed');
await this.container.storage.delete('mcp:banner:dismissed');
await this.container.onboarding.resetAll();

// Deprecated keys
await this.container.storage.delete('home:banners:dismissed');
await this.container.storage.delete('home:sections:dismissed');
await this.container.storage.delete('home:walkthrough:dismissed');
await this.container.storage.delete('mcp:banner:dismissed');
await this.container.storage.delete('views:scm:grouped:welcome:dismissed');
break;

case 'integrations':
Expand Down
2 changes: 1 addition & 1 deletion src/commands/walkthroughs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import type { WalkthroughSteps } from '../constants.js';
import { urls } from '../constants.js';
import type { Source, Sources, TelemetryEvents } from '../constants.telemetry.js';
import type { Container } from '../container.js';
import { isWalkthroughSupported } from '../onboarding/walkthroughStateProvider.js';
import type { SubscriptionUpgradeCommandArgs } from '../plus/gk/models/subscription.js';
import type { LaunchpadCommandArgs } from '../plus/launchpad/launchpad.js';
import { command, executeCommand, executeCoreCommand } from '../system/-webview/command.js';
import { openUrl } from '../system/-webview/vscode/uris.js';
import { openWalkthrough as openWalkthroughCore } from '../system/-webview/vscode.js';
import { isWalkthroughSupported } from '../telemetry/walkthroughStateProvider.js';
import type { ComposerWebviewShowingArgs } from '../webviews/plus/composer/registration.js';
import type { WebviewPanelShowCommandArgs } from '../webviews/webviewsController.js';
import { GlCommandBase } from './commandBase.js';
Expand Down
2 changes: 1 addition & 1 deletion src/constants.commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ type InternalGlCommands =
| 'gitlens.diffWithPrevious:views'
| 'gitlens.diffWithWorking:command'
| 'gitlens.diffWithWorking:views'
| 'gitlens.onboarding.dismiss'
| 'gitlens.openCloudPatch'
| 'gitlens.openOnRemote'
| 'gitlens.openWalkthrough'
Expand All @@ -140,7 +141,6 @@ type InternalGlCommands =
| 'gitlens.showComposerPage'
| 'gitlens.showInCommitGraphView'
| 'gitlens.showQuickCommitDetails'
| 'gitlens.storage.store'
| 'gitlens.toggleFileBlame:codelens'
| 'gitlens.toggleFileBlame:mode'
| 'gitlens.toggleFileBlame:statusbar'
Expand Down
37 changes: 37 additions & 0 deletions src/constants.onboarding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { OnboardingItemDefinition } from './onboarding/models/onboarding.js';

/** Central registry of all dismissible/onboarding keys */
export const onboardingDefinitions = {
// Home View
'home:integrationBanner': { schema: '17.8.0', scope: 'global' },
'home:walkthrough': {
schema: '17.8.0',
scope: 'global',
state: undefined as unknown as { completedSteps: string[] },
},

// MCP Banner (shown in home and graph)
'mcp:banner': { schema: '17.8.0', scope: 'global' },

// Rebase Editor
'rebaseEditor:closeWarning': { schema: '17.8.0', scope: 'global' },

// Composer
'composer:onboarding': {
schema: '17.8.0',
scope: 'global',
state: undefined as unknown as { stepReached: number },
},

// Views
'views:scmGrouped:welcome': { schema: '17.8.0', scope: 'global' },
} as const satisfies Record<string, OnboardingItemDefinition<unknown>>;

export type OnboardingKeys = keyof typeof onboardingDefinitions;

/** Extract state type for a specific item key */
export type OnboardingItemState<K extends OnboardingKeys> = (typeof onboardingDefinitions)[K] extends {
state: infer State;
}
? State
: undefined;
14 changes: 11 additions & 3 deletions src/constants.storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { GroupableTreeViewTypes, TreeViewTypes } from './constants.views.js
import type { Environment } from './container.js';
import type { FeaturePreviews } from './features.js';
import type { GitRevisionRangeNotation } from './git/models/revision.js';
import type { OnboardingStorage } from './onboarding/models/onboarding.js';
import type { OrganizationSettings } from './plus/gk/models/organization.js';
import type { PaidSubscriptionPlanIds, Subscription } from './plus/gk/models/subscription.js';
import type { IntegrationConnectedKey } from './plus/integrations/models/integration.js';
Expand Down Expand Up @@ -43,6 +44,10 @@ export type DeprecatedGlobalStorage = {
/** @deprecated */
'home:banners:dismissed': string[];
/** @deprecated */
'home:walkthrough:dismissed': boolean;
/** @deprecated */
'mcp:banner:dismissed': boolean;
/** @deprecated */
pendingWelcomeOnFocus: boolean;
/** @deprecated */
'plus:discountNotificationShown': boolean;
Expand All @@ -55,6 +60,8 @@ export type DeprecatedGlobalStorage = {
/** @deprecated */
'views:commitDetails:dismissed': 'sidebar'[];
/** @deprecated */
'views:scm:grouped:welcome:dismissed': boolean;
/** @deprecated */
'views:welcome:visible': boolean;
} & {
/** @deprecated */
Expand Down Expand Up @@ -88,16 +95,15 @@ interface GlobalStorageCore {
'gk:cli:corePath': string;
'gk:cli:path': string;
'home:sections:collapsed': string[];
'home:walkthrough:dismissed': boolean;
'mcp:banner:dismissed': boolean;
'launchpad:groups:collapsed': StoredLaunchpadGroup[];
'launchpad:indicator:hasLoaded': boolean;
'launchpad:indicator:hasInteracted': string;
'launchpadView:groups:expanded': StoredLaunchpadGroup[];
'graph:searchMode': StoredGraphSearchMode;
'graph:useNaturalLanguageSearch': boolean;
'views:scm:grouped:welcome:dismissed': boolean;
'integrations:configured': StoredIntegrationConfigurations;
/** Unified onboarding/dismissible UI state */
'onboarding:state': OnboardingStorage;
}

type GlobalStorageDynamic = Record<`plus:preview:${FeaturePreviews}:usages`, StoredFeaturePreviewUsagePeriod[]> &
Expand Down Expand Up @@ -175,6 +181,8 @@ interface WorkspaceStorageCore {
gitPath: string;
'graph:columns': Record<string, StoredGraphColumn>;
'graph:filtersByRepo': Record<string, StoredGraphFilters>;
/** Unified onboarding/dismissible UI state (workspace-scoped items) */
'onboarding:state': OnboardingStorage;
'remote:default': string;
'starred:branches': StoredStarred;
'starred:repositories': StoredStarred;
Expand Down
11 changes: 9 additions & 2 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import { GitFileSystemProvider } from './git/fsProvider.js';
import { GitProviderService } from './git/gitProviderService.js';
import type { RepositoryLocationProvider } from './git/location/repositorylocationProvider.js';
import { LineHoverController } from './hovers/lineHoverController.js';
import { OnboardingService } from './onboarding/onboardingService.js';
import { UsageTracker } from './onboarding/usageTracker.js';
import { WalkthroughStateProvider } from './onboarding/walkthroughStateProvider.js';
import { AIProviderService } from './plus/ai/aiProviderService.js';
import { DraftService } from './plus/drafts/draftsService.js';
import { AccountAuthenticationProvider } from './plus/gk/authenticationProvider.js';
Expand Down Expand Up @@ -61,8 +64,6 @@ import { memoize } from './system/decorators/memoize.js';
import { Logger } from './system/logger.js';
import { AIFeedbackProvider } from './telemetry/aiFeedbackProvider.js';
import { TelemetryService } from './telemetry/telemetry.js';
import { UsageTracker } from './telemetry/usageTracker.js';
import { WalkthroughStateProvider } from './telemetry/walkthroughStateProvider.js';
import { GitTerminalLinkProvider } from './terminal/linkProvider.js';
import { GitDocumentTracker } from './trackers/documentTracker.js';
import { LineTracker } from './trackers/lineTracker.js';
Expand Down Expand Up @@ -204,6 +205,7 @@ export class Container {
this._disposables = [
configuration,
(this._storage = storage),
(this._onboarding = new OnboardingService(storage, version)),
(this._telemetry = new TelemetryService(this)),
(this._usage = new UsageTracker(this, storage)),
configuration.onDidChangeAny(this.onAnyConfigurationChanged, this),
Expand Down Expand Up @@ -731,6 +733,11 @@ export class Container {
return this._storage;
}

private readonly _onboarding: OnboardingService;
get onboarding(): OnboardingService {
return this._onboarding;
}

private _subscription: SubscriptionService;
get subscription(): SubscriptionService {
return this._subscription;
Expand Down
2 changes: 1 addition & 1 deletion src/env/node/gk/cli/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export class GkCliIntegrationProvider implements Disposable {
@gate()
@log({ exit: true })
private async setupMCP(source?: Sources, force = false): Promise<void> {
await this.container.storage.store('mcp:banner:dismissed', true);
await this.container.onboarding.dismiss('mcp:banner');

try {
const result = await window.withProgress(
Expand Down
29 changes: 29 additions & 0 deletions src/onboarding/models/onboarding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export interface OnboardingItemDefinition<T = undefined> {
readonly scope: 'global' | 'workspace';
/** GitLens version when this schema was introduced. Bump when data structure changes */
readonly schema?: `${number}.${number}.${number}`;
/** GitLens version when/if this item should be re-shown if the user dismissed it before this version */
readonly reshowAfter?: `${number}.${number}.${number}`;
/** Type marker for the data shape - value is never used at runtime */
readonly state?: T;
}

export interface OnboardingItem<T> {
/** GitLens version of the stored data schema (for migrations) */
schema?: `${number}.${number}.${number}`;

/** ISO timestamp when dismissed */
dismissedAt?: string;
/** GitLens version when dismissed (e.g., "17.1.0") */
dismissedVersion?: `${number}.${number}.${number}`;

/** Item-specific metadata */
state?: T;
}

export interface OnboardingStorage {
/** Whether legacy storage keys have been migrated to this state */
migrated?: boolean;
/** Map of item ID to its state (envelope + data) */
items: Record<string, OnboardingItem<unknown>>;
}
25 changes: 25 additions & 0 deletions src/onboarding/onboardingMigrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { OnboardingItemState, OnboardingKeys } from '../constants.onboarding.js';

/**
* Migrations are maps of GitLens version → migration function
* Each migration transforms data from the previous schema to the version's schema
* Migrations run in semver order for all versions between stored and current schema
*/
export type OnboardingMigration<T> = Record<`${number}.${number}.${number}`, (state: unknown) => T>;

export const onboardingMigrations: {
[K in OnboardingKeys]?: OnboardingMigration<OnboardingItemState<K>>;
} = {
'composer:onboarding': {
'17.8.0': (state: unknown) => {
const s = state as { stepReached?: number } | undefined;
return { stepReached: s?.stepReached ?? 0 };
},
},
'home:walkthrough': {
'17.8.0': (state: unknown) => {
const s = state as { completedSteps?: string[] } | undefined;
return { completedSteps: s?.completedSteps ?? [] };
},
},
};
Loading
Loading