diff --git a/vscode/package.json b/vscode/package.json index ea9b98a7d182..88a0f8d365d4 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -271,6 +271,14 @@ "group": "Cody", "icon": "$(feedback)" }, + { + "command": "cody.command.autoedit.open-debug-panel", + "title": "Debug Auto-Edit", + "category": "Cody", + "group": "Cody", + "icon": "$(debug)", + "enablement": "cody.activated" + }, { "command": "cody.command.explain-output", "title": "Ask Cody to Explain", @@ -582,6 +590,12 @@ "command": "cody.command.autoedit-manual-trigger", "title": "Autoedits Manual Trigger", "enablement": "cody.activated && config.cody.suggestions.mode == 'auto-edit (Experimental)'" + }, + { + "command": "cody.command.autoedit.open-debug-panel", + "category": "Cody", + "title": "Debug Auto-Edit", + "enablement": "cody.activated" } ], "keybindings": [ @@ -721,6 +735,10 @@ ], "menus": { "commandPalette": [ + { + "command": "cody.command.autoedit.open-debug-panel", + "when": "cody.activated" + }, { "command": "cody.command.edit-code", "when": "cody.activated && editorIsOpen" diff --git a/vscode/src/autoedits/analytics-logger/analytics-logger.test.ts b/vscode/src/autoedits/analytics-logger/analytics-logger.test.ts index b45441a12cc6..07fe1e632025 100644 --- a/vscode/src/autoedits/analytics-logger/analytics-logger.test.ts +++ b/vscode/src/autoedits/analytics-logger/analytics-logger.test.ts @@ -17,13 +17,13 @@ import type { AutoeditModelOptions } from '../adapters/base' import { getCodeToReplaceData } from '../prompt/prompt-utils' import { getDecorationInfo } from '../renderer/diff-utils' +import { AutoeditAnalyticsLogger } from './analytics-logger' import { - AutoeditAnalyticsLogger, type AutoeditRequestID, autoeditDiscardReason, autoeditSource, autoeditTriggerKind, -} from './analytics-logger' +} from './types' describe('AutoeditAnalyticsLogger', () => { let autoeditLogger: AutoeditAnalyticsLogger @@ -109,11 +109,17 @@ describe('AutoeditAnalyticsLogger', () => { autoeditLogger.markAsLoaded({ requestId, prompt: modelOptions.prompt, + modelResponse: { + prediction, + requestHeaders: {}, + requestUrl: modelOptions.url, + responseHeaders: {}, + responseBody: {}, + }, payload: { prediction, source: autoeditSource.network, isFuzzyMatch: false, - responseHeaders: {}, }, }) diff --git a/vscode/src/autoedits/analytics-logger/analytics-logger.ts b/vscode/src/autoedits/analytics-logger/analytics-logger.ts index 404150e54987..6367c14cfdac 100644 --- a/vscode/src/autoedits/analytics-logger/analytics-logger.ts +++ b/vscode/src/autoedits/analytics-logger/analytics-logger.ts @@ -14,343 +14,33 @@ import { import type { TelemetryEventParameters } from '@sourcegraph/telemetry' import { getOtherCompletionProvider } from '../../completions/analytics-logger' -import type { ContextSummary } from '../../completions/context/context-mixer' import { lines } from '../../completions/text-processing' -import { type CodeGenEventMetadata, charactersLogger } from '../../services/CharactersLogger' +import { charactersLogger } from '../../services/CharactersLogger' import { upstreamHealthProvider } from '../../services/UpstreamHealthProvider' import { captureException, shouldErrorBeReported } from '../../services/sentry/sentry' import { splitSafeMetadata } from '../../services/telemetry-v2' -import type { AutoeditsPrompt } from '../adapters/base' +import type { AutoeditsPrompt, ModelResponse } from '../adapters/base' import { autoeditsOutputChannelLogger } from '../output-channel-logger' import type { CodeToReplaceData } from '../prompt/prompt-utils' import type { DecorationInfo } from '../renderer/decorators/base' -import { type DecorationStats, getDecorationStats } from '../renderer/diff-utils' +import { getDecorationStats } from '../renderer/diff-utils' +import { autoeditDebugStore } from '../debug-panel/debug-store' import { autoeditIdRegistry } from './suggestion-id-registry' - -/** - * This file implements a state machine to manage the lifecycle of an autoedit request. - * Each phase of the request is represented by a distinct state interface, and metadata - * evolves as the request progresses. - * - * 1. Each autoedit request phase (e.g., `started`, `loaded`, `accepted`) is represented by a - * `state` interface that extends `AutoeditBaseState` and adds phase-specific fields. - * - * 2. Valid transitions between phases are enforced using the `validRequestTransitions` map, - * ensuring logical progression through the request lifecycle. - * - * 3. The `payload` field in each state encapsulates the exact list of fields that we plan to send - * to our analytics backend. - * - * 4. Other top-level `state` fields are saved only for bookkeeping and won't end up at our - * analytics backend. This ensures we don't send unintentional or redundant information to - * the analytics backend. - * - * 5. Metadata is progressively enriched as the request transitions between states. - * - * 6. Eventually, once we reach one of the terminal states and log its current `payload`. - */ - -/** - * Defines the possible phases of our autoedit request state machine. - */ -type Phase = - /** The autoedit request has started. */ - | 'started' - /** The context for the autoedit has been loaded. */ - | 'contextLoaded' - /** The autoedit suggestion has been loaded — we have a prediction string. */ - | 'loaded' - /** - * The suggestion is not discard during post processing and we have all the data to render the suggestion. - * This intermediate step is required for the agent API. We cannot graduate the request to the suggested - * state right away. We first need to save requests metadata to the analytics logger cache, so that - * agent can access it using the request ID only in `unstable_handleDidShowCompletionItem` calls. - */ - | 'postProcessed' - /** The autoedit suggestion has been suggested to the user. */ - | 'suggested' - /** The autoedit suggestion is marked as read is it's still visible to the user after a hardcoded timeout. */ - | 'read' - /** The user has accepted the suggestion. */ - | 'accepted' - /** The user has rejected the suggestion. */ - | 'rejected' - /** The autoedit request was discarded by our heuristics before being suggested to a user */ - | 'discarded' - -/** - * Defines which phases can transition to which other phases. - */ -const validRequestTransitions = { - started: ['contextLoaded', 'discarded'], - contextLoaded: ['loaded', 'discarded'], - loaded: ['postProcessed', 'discarded'], - postProcessed: ['suggested', 'discarded'], - suggested: ['read', 'accepted', 'rejected'], - read: ['accepted', 'rejected'], - accepted: [], - rejected: [], - discarded: [], -} as const satisfies Record - -export const autoeditTriggerKind = { - /** Suggestion was triggered automatically while editing. */ - automatic: 1, - - /** Suggestion was triggered manually by the user invoking the keyboard shortcut. */ - manual: 2, - - /** When the user uses the suggest widget to cycle through different suggestions. */ - suggestWidget: 3, - - /** Suggestion was triggered automatically by the selection change event. */ - cursor: 4, -} as const - -/** We use numeric keys to send these to the analytics backend */ -type AutoeditTriggerKindMetadata = (typeof autoeditTriggerKind)[keyof typeof autoeditTriggerKind] - -interface AutoeditStartedMetadata { - /** Document language ID (e.g., 'typescript'). */ - languageId: string - - /** Model used by Cody client to request the autosuggestion suggestion. */ - model: string - - /** Optional trace ID for cross-service correlation, if your environment provides it. */ - traceId?: string - - /** Describes how the autoedit request was triggered by the user. */ - triggerKind: AutoeditTriggerKindMetadata - - /** - * The code to rewrite by autoedit. - * 🚨 SECURITY: included only for DotCom users. - */ - codeToRewrite?: string - - /** True if other autoedit/completion providers might also be active (e.g., Copilot). */ - otherCompletionProviderEnabled: boolean - - /** The exact list of other providers that are active, if known. */ - otherCompletionProviders: string[] - - /** The round trip timings to reach the Sourcegraph and Cody Gateway instances. */ - upstreamLatency?: number - gatewayLatency?: number -} - -interface AutoeditContextLoadedMetadata extends AutoeditStartedMetadata { - /** - * Information about the context retrieval process that lead to this autoedit request. Refer - * to the documentation of {@link ContextSummary} - */ - contextSummary?: ContextSummary -} - -/** - * A stable ID that identifies a particular autoedit suggestion. If the same text - * and context recurs, we reuse this ID to avoid double-counting. - */ -export type AutoeditSuggestionID = string & { readonly _brand: 'AutoeditSuggestionID' } - -export const autoeditSource = { - /** Autoedit originated from a request to our backend for the suggestion. */ - network: 1, - /** Autoedit originated from a client cached suggestion. */ - cache: 2, -} as const - -/** We use numeric keys to send these to the analytics backend */ -type AutoeditSourceMetadata = (typeof autoeditSource)[keyof typeof autoeditSource] - -export const autoeditDiscardReason = { - clientAborted: 1, - emptyPrediction: 2, - predictionEqualsCodeToRewrite: 3, - recentEdits: 4, - suffixOverlap: 5, - emptyPredictionAfterInlineCompletionExtraction: 6, - noActiveEditor: 7, - conflictingDecorationWithEdits: 8, - notEnoughLinesEditor: 9, -} as const - -/** We use numeric keys to send these to the analytics backend */ -type AutoeditDiscardReasonMetadata = (typeof autoeditDiscardReason)[keyof typeof autoeditDiscardReason] - -interface AutoeditLoadedMetadata extends AutoeditContextLoadedMetadata { - /** - * An ID to uniquely identify a suggest autoedit. Note: It is possible for this ID to be part - * of two suggested events. This happens when the exact same autoedit text is shown again at - * the exact same location. We count this as the same autoedit and thus use the same ID. - */ - id: AutoeditSuggestionID - - /** - * Unmodified by the client prediction text snippet of the suggestion. - * Might be `undefined` if too long. - * 🚨 SECURITY: included only for DotCom users. - */ - prediction?: string - - /** The source of the suggestion, e.g. 'network', 'cache', etc. */ - source?: AutoeditSourceMetadata - - /** True if we fuzzy-matched this suggestion from a local or remote cache. */ - isFuzzyMatch?: boolean - - /** Optional set of relevant response headers (e.g. from Cody Gateway). */ - responseHeaders?: Record - - /** Time (ms) to generate or load the suggestion after it was started. */ - latency: number -} - -interface AutoeditPostProcessedMetadata extends AutoeditLoadedMetadata { - /** The number of added, modified, removed lines and characters from suggestion. */ - decorationStats?: DecorationStats - /** The number of lines and added chars attributed to an inline completion item. */ - inlineCompletionStats?: { - lineCount: number - charCount: number - } -} - -interface AutoEditFinalMetadata extends AutoeditPostProcessedMetadata { - /** Displayed to the user for this many milliseconds. */ - timeFromSuggestedAt: number - /** True if the suggestion was explicitly/intentionally accepted. */ - isAccepted: boolean - /** - * True if the suggestion was visible for a certain time - * Required to correctly calculate CAR and other metrics where we - * want to account only for suggestions visible for a certain time. - * - * `timeFromSuggestedAt` is not a reliable source of truth for - * this case because a user could have rejected a suggestion without - * triggering `accepted` or `discarded` immediately. This is related to - * limited VS Code APIs which do not provide a reliable way to know - * if a suggestion is really visible. - */ - isRead: boolean - /** The number of the auto-edit started since the last suggestion was shown. */ - suggestionsStartedSinceLastSuggestion: number -} - -interface AutoeditAcceptedEventPayload - extends AutoEditFinalMetadata, - Omit {} - -interface AutoeditRejectedEventPayload extends AutoEditFinalMetadata {} -interface AutoeditDiscardedEventPayload extends AutoeditContextLoadedMetadata { - discardReason: AutoeditDiscardReasonMetadata -} - -/** - * An ephemeral ID for a single “request” from creation to acceptance or rejection. - */ -export type AutoeditRequestID = string & { readonly _brand: 'AutoeditRequestID' } - -/** - * The base fields common to all request states. We track ephemeral times and - * the partial payload. Once we reach a certain phase, we log the payload as a telemetry event. - */ -interface AutoeditBaseState { - requestId: AutoeditRequestID - /** Current phase of the autoedit request */ - phase: Phase -} - -interface StartedState extends AutoeditBaseState { - phase: 'started' - /** Time (ms) when we started computing or requesting the suggestion. */ - startedAt: number - - /** Metadata required to show a suggestion based on `requestId` only. */ - codeToReplaceData: CodeToReplaceData - document: vscode.TextDocument - position: vscode.Position - docContext: DocumentContext - - /** Partial payload for this phase. Will be augmented with more info as we progress. */ - payload: AutoeditStartedMetadata -} - -export interface ContextLoadedState extends Omit { - phase: 'contextLoaded' - payload: AutoeditContextLoadedMetadata -} - -interface LoadedState extends Omit { - phase: 'loaded' - /** Timestamp when the suggestion completed generation/loading. */ - loadedAt: number - payload: AutoeditLoadedMetadata -} - -export interface PostProcessedState extends Omit { - phase: 'postProcessed' - - /** Metadata required to show a suggestion based on `requestId` only. */ - prediction: string - decorationInfo: DecorationInfo | null - inlineCompletionItems: vscode.InlineCompletionItem[] | null - - payload: AutoeditPostProcessedMetadata -} - -export interface SuggestedState extends Omit { - phase: 'suggested' - /** Timestamp when the suggestion was first shown to the user. */ - suggestedAt: number -} - -export interface ReadState extends Omit { - phase: 'read' - /** Timestamp when the suggestion was marked as visible to the user. */ - readAt: number -} - -export interface AcceptedState extends Omit { - phase: 'accepted' - /** Timestamp when the user accepted the suggestion. */ - acceptedAt: number - /** Timestamp when the suggestion was logged to our analytics backend. This is to avoid double-logging. */ - suggestionLoggedAt?: number - /** Optional because it might be accepted before the read timeout */ - readAt?: number - payload: AutoeditAcceptedEventPayload -} - -export interface RejectedState extends Omit { - phase: 'rejected' - /** Timestamp when the suggestion was logged to our analytics backend. This is to avoid double-logging. */ - suggestionLoggedAt?: number - /** Optional because it might be accepted before the read timeout */ - readAt?: number - payload: AutoeditRejectedEventPayload -} - -interface DiscardedState extends Omit { - phase: 'discarded' - /** Timestamp when the suggestion was logged to our analytics backend. This is to avoid double-logging. */ - suggestionLoggedAt?: number - payload: AutoeditDiscardedEventPayload -} - -interface PhaseStates { - started: StartedState - contextLoaded: ContextLoadedState - loaded: LoadedState - postProcessed: PostProcessedState - suggested: SuggestedState - read: ReadState - accepted: AcceptedState - rejected: RejectedState - discarded: DiscardedState -} +import { + type AcceptedState, + type AutoeditDiscardReasonMetadata, + type AutoeditRequestID, + type ContextLoadedState, + type DiscardedState, + type LoadedState, + type Phase, + type PhaseStates, + type RejectedState, + type StartedState, + type SuggestedState, + validRequestTransitions, +} from './types' /** * Using the validTransitions definition, we can derive which "from phases" lead to a given next phase, @@ -410,7 +100,7 @@ export class AutoeditAnalyticsLogger { position: vscode.Position docContext: DocumentContext payload: Required< - Pick + Pick > }): AutoeditRequestID { const { codeToRewrite, ...restPayload } = payload @@ -438,6 +128,7 @@ export class AutoeditAnalyticsLogger { this.activeRequests.set(requestId, request) this.autoeditsStartedSinceLastSuggestion++ + return requestId } @@ -446,10 +137,11 @@ export class AutoeditAnalyticsLogger { payload, }: { requestId: AutoeditRequestID - payload: Pick + payload: Pick }): void { this.tryTransitionTo(requestId, 'contextLoaded', request => ({ ...request, + contextLoadedAt: getTimeNowInMillis(), payload: { ...request.payload, contextSummary: payload.contextSummary, @@ -466,14 +158,14 @@ export class AutoeditAnalyticsLogger { requestId, prompt, payload, + modelResponse, }: { + modelResponse: ModelResponse requestId: AutoeditRequestID prompt: AutoeditsPrompt - payload: Required< - Pick - > + payload: Required> }): void { - const { prediction, source, isFuzzyMatch, responseHeaders } = payload + const { prediction, source, isFuzzyMatch } = payload const stableId = autoeditIdRegistry.getOrCreate(prompt, prediction) const loadedAt = getTimeNowInMillis() @@ -481,6 +173,7 @@ export class AutoeditAnalyticsLogger { return { ...request, loadedAt, + modelResponse, payload: { ...request.payload, id: stableId, @@ -488,7 +181,7 @@ export class AutoeditAnalyticsLogger { prediction: isDotComAuthed() && prediction.length < 300 ? prediction : undefined, source, isFuzzyMatch, - responseHeaders, + responseHeaders: modelResponse.responseHeaders, latency: Math.floor(loadedAt - request.startedAt), }, } @@ -515,6 +208,7 @@ export class AutoeditAnalyticsLogger { return { ...request, + postProcessedAt: getTimeNowInMillis(), prediction, decorationInfo, inlineCompletionItems, @@ -598,13 +292,16 @@ export class AutoeditAnalyticsLogger { } public markAsRejected(requestId: AutoeditRequestID): void { + const rejectedAt = getTimeNowInMillis() + const result = this.tryTransitionTo(requestId, 'rejected', request => ({ ...request, + rejectedAt, payload: { ...request.payload, isAccepted: false, isRead: 'readAt' in request, - timeFromSuggestedAt: getTimeNowInMillis() - request.suggestedAt, + timeFromSuggestedAt: rejectedAt - request.suggestedAt, suggestionsStartedSinceLastSuggestion: this.autoeditsStartedSinceLastSuggestion, }, })) @@ -628,9 +325,10 @@ export class AutoeditAnalyticsLogger { const result = this.tryTransitionTo(requestId, 'discarded', request => { return { ...request, + discardedAt: getTimeNowInMillis(), payload: { ...request.payload, - discardReason: discardReason, + discardReason, }, } }) @@ -661,7 +359,11 @@ export class AutoeditAnalyticsLogger { phase: nextPhase, } as PhaseStates[P] + // Integrate auto-edit analytics logger with the auto-edit debug panel. + autoeditDebugStore.addAutoeditRequestDebugState(updatedRequest) + this.activeRequests.set(requestId, updatedRequest) + return { updatedRequest, currentRequest } } diff --git a/vscode/src/autoedits/analytics-logger/index.ts b/vscode/src/autoedits/analytics-logger/index.ts index 6ae02d5165b1..410caf032de7 100644 --- a/vscode/src/autoedits/analytics-logger/index.ts +++ b/vscode/src/autoedits/analytics-logger/index.ts @@ -1 +1,2 @@ export * from './analytics-logger' +export * from './types' diff --git a/vscode/src/autoedits/analytics-logger/suggestion-id-registry.ts b/vscode/src/autoedits/analytics-logger/suggestion-id-registry.ts index 11cef9e27251..043718bae507 100644 --- a/vscode/src/autoedits/analytics-logger/suggestion-id-registry.ts +++ b/vscode/src/autoedits/analytics-logger/suggestion-id-registry.ts @@ -2,7 +2,7 @@ import { LRUCache } from 'lru-cache' import * as uuid from 'uuid' import type { AutoeditsPrompt } from '../adapters/base' -import type { AutoeditSuggestionID } from './analytics-logger' +import type { AutoeditSuggestionID } from './types' /** * A specialized string type for the stable “suggestion key” in caches. diff --git a/vscode/src/autoedits/analytics-logger/types.ts b/vscode/src/autoedits/analytics-logger/types.ts new file mode 100644 index 000000000000..7f6f86870362 --- /dev/null +++ b/vscode/src/autoedits/analytics-logger/types.ts @@ -0,0 +1,341 @@ +import type * as vscode from 'vscode' + +import type { DocumentContext } from '@sourcegraph/cody-shared' + +import type { ContextSummary } from '../../completions/context/context-mixer' +import type { CodeGenEventMetadata } from '../../services/CharactersLogger' +import type { ModelResponse } from '../adapters/base' +import type { CodeToReplaceData } from '../prompt/prompt-utils' +import type { DecorationInfo } from '../renderer/decorators/base' +import type { DecorationStats } from '../renderer/diff-utils' + +/** + * This file implements a state machine to manage the lifecycle of an autoedit request. + * Each phase of the request is represented by a distinct state interface, and metadata + * evolves as the request progresses. + * + * 1. Each autoedit request phase (e.g., `started`, `loaded`, `accepted`) is represented by a + * `state` interface that extends `AutoeditBaseState` and adds phase-specific fields. + * + * 2. Valid transitions between phases are enforced using the `validRequestTransitions` map, + * ensuring logical progression through the request lifecycle. + * + * 3. The `payload` field in each state encapsulates the exact list of fields that we plan to send + * to our analytics backend. + * + * 4. Other top-level `state` fields are saved only for bookkeeping and won't end up at our + * analytics backend. This ensures we don't send unintentional or redundant information to + * the analytics backend. + * + * 5. Metadata is progressively enriched as the request transitions between states. + * + * 6. Eventually, once we reach one of the terminal states and log its current `payload`. + */ + +/** + * Defines the possible phases of our autoedit request state machine. + */ +export type Phase = + /** The autoedit request has started. */ + | 'started' + /** The context for the autoedit has been loaded. */ + | 'contextLoaded' + /** The autoedit suggestion has been loaded — we have a prediction string. */ + | 'loaded' + /** + * The suggestion is not discard during post processing and we have all the data to render the suggestion. + * This intermediate step is required for the agent API. We cannot graduate the request to the suggested + * state right away. We first need to save requests metadata to the analytics logger cache, so that + * agent can access it using the request ID only in `unstable_handleDidShowCompletionItem` calls. + */ + | 'postProcessed' + /** The autoedit suggestion has been suggested to the user. */ + | 'suggested' + /** The autoedit suggestion is marked as read is it's still visible to the user after a hardcoded timeout. */ + | 'read' + /** The user has accepted the suggestion. */ + | 'accepted' + /** The user has rejected the suggestion. */ + | 'rejected' + /** The autoedit request was discarded by our heuristics before being suggested to a user */ + | 'discarded' + +/** + * Defines which phases can transition to which other phases. + */ +export const validRequestTransitions = { + started: ['contextLoaded', 'discarded'], + contextLoaded: ['loaded', 'discarded'], + loaded: ['postProcessed', 'discarded'], + postProcessed: ['suggested', 'discarded'], + suggested: ['read', 'accepted', 'rejected'], + read: ['accepted', 'rejected'], + accepted: [], + rejected: [], + discarded: [], +} as const satisfies Record + +export const autoeditTriggerKind = { + /** Suggestion was triggered automatically while editing. */ + automatic: 1, + + /** Suggestion was triggered manually by the user invoking the keyboard shortcut. */ + manual: 2, + + /** When the user uses the suggest widget to cycle through different suggestions. */ + suggestWidget: 3, + + /** Suggestion was triggered automatically by the selection change event. */ + cursor: 4, +} as const + +/** We use numeric keys to send these to the analytics backend */ +export type AutoeditTriggerKindMetadata = (typeof autoeditTriggerKind)[keyof typeof autoeditTriggerKind] + +export const autoeditSource = { + /** Autoedit originated from a request to our backend for the suggestion. */ + network: 1, + /** Autoedit originated from a client cached suggestion. */ + cache: 2, +} as const + +/** We use numeric keys to send these to the analytics backend */ +export type AutoeditSourceMetadata = (typeof autoeditSource)[keyof typeof autoeditSource] + +export const autoeditDiscardReason = { + clientAborted: 1, + emptyPrediction: 2, + predictionEqualsCodeToRewrite: 3, + recentEdits: 4, + suffixOverlap: 5, + emptyPredictionAfterInlineCompletionExtraction: 6, + noActiveEditor: 7, + conflictingDecorationWithEdits: 8, + notEnoughLinesEditor: 9, +} as const + +/** We use numeric keys to send these to the analytics backend */ +export type AutoeditDiscardReasonMetadata = + (typeof autoeditDiscardReason)[keyof typeof autoeditDiscardReason] + +/** + * A stable ID that identifies a particular autoedit suggestion. If the same text + * and context recurs, we reuse this ID to avoid double-counting. + */ +export type AutoeditSuggestionID = string & { readonly _brand: 'AutoeditSuggestionID' } + +/** + * An ephemeral ID for a single "request" from creation to acceptance or rejection. + */ +export type AutoeditRequestID = string & { readonly _brand: 'AutoeditRequestID' } + +/** + * The base fields common to all request states. We track ephemeral times and + * the partial payload. Once we reach a certain phase, we log the payload as a telemetry event. + */ +export interface AutoeditBaseState { + requestId: AutoeditRequestID + /** Current phase of the autoedit request */ + phase: Phase +} + +export interface StartedState extends AutoeditBaseState { + phase: 'started' + /** Time (ms) when we started computing or requesting the suggestion. */ + startedAt: number + + /** Metadata required to show a suggestion based on `requestId` only. */ + codeToReplaceData: CodeToReplaceData + document: vscode.TextDocument + position: vscode.Position + docContext: DocumentContext + + /** Partial payload for this phase. Will be augmented with more info as we progress. */ + payload: { + /** Document language ID (e.g., 'typescript'). */ + languageId: string + + /** Model used by Cody client to request the autosuggestion suggestion. */ + model: string + + /** Optional trace ID for cross-service correlation, if your environment provides it. */ + traceId?: string + + /** Describes how the autoedit request was triggered by the user. */ + triggerKind: AutoeditTriggerKindMetadata + + /** + * The code to rewrite by autoedit. + * 🚨 SECURITY: included only for DotCom users. + */ + codeToRewrite?: string + + /** True if other autoedit/completion providers might also be active (e.g., Copilot). */ + otherCompletionProviderEnabled: boolean + + /** The exact list of other providers that are active, if known. */ + otherCompletionProviders: string[] + + /** The round trip timings to reach the Sourcegraph and Cody Gateway instances. */ + upstreamLatency?: number + gatewayLatency?: number + } +} + +export interface ContextLoadedState extends Omit { + phase: 'contextLoaded' + /** Timestamp when the context for the autoedit was loaded. */ + contextLoadedAt: number + payload: StartedState['payload'] & { + /** + * Information about the context retrieval process that lead to this autoedit request. Refer + * to the documentation of {@link ContextSummary} + */ + contextSummary?: ContextSummary + } +} + +export interface LoadedState extends Omit { + phase: 'loaded' + /** Timestamp when the suggestion completed generation/loading. */ + loadedAt: number + /** Model response metadata for the debug panel */ + modelResponse: ModelResponse + payload: ContextLoadedState['payload'] & { + /** + * An ID to uniquely identify a suggest autoedit. Note: It is possible for this ID to be part + * of two suggested events. This happens when the exact same autoedit text is shown again at + * the exact same location. We count this as the same autoedit and thus use the same ID. + */ + id: AutoeditSuggestionID + + /** + * Unmodified by the client prediction text snippet of the suggestion. + * Might be `undefined` if too long. + * 🚨 SECURITY: included only for DotCom users. + */ + prediction?: string + + /** The source of the suggestion, e.g. 'network', 'cache', etc. */ + source?: AutoeditSourceMetadata + + /** True if we fuzzy-matched this suggestion from a local or remote cache. */ + isFuzzyMatch?: boolean + + /** Optional set of relevant response headers (e.g. from Cody Gateway). */ + responseHeaders?: Record + + /** Time (ms) to generate or load the suggestion after it was started. */ + latency: number + } +} + +export interface PostProcessedState extends Omit { + phase: 'postProcessed' + /** Timestamp when the post-processing of the suggestion was completed. */ + postProcessedAt: number + + /** Metadata required to show a suggestion based on `requestId` only. */ + prediction: string + /** + * The decoration info after the post-processing of the suggestion. + * Won't include insertions rendered as inline completions. + */ + decorationInfo: DecorationInfo | null + inlineCompletionItems: vscode.InlineCompletionItem[] | null + + payload: LoadedState['payload'] & { + /** The number of added, modified, removed lines and characters from suggestion. */ + decorationStats?: DecorationStats + /** The number of lines and added chars attributed to an inline completion item. */ + inlineCompletionStats?: { + lineCount: number + charCount: number + } + } +} + +export interface SuggestedState extends Omit { + phase: 'suggested' + /** Timestamp when the suggestion was first shown to the user. */ + suggestedAt: number + payload: PostProcessedState['payload'] +} + +export interface ReadState extends Omit { + phase: 'read' + /** Timestamp when the suggestion was marked as visible to the user. */ + readAt: number + payload: PostProcessedState['payload'] +} + +/** + * Common final payload properties shared between accepted and rejected states + */ +export type FinalPayload = PostProcessedState['payload'] & { + /** Displayed to the user for this many milliseconds. */ + timeFromSuggestedAt: number + /** True if the suggestion was explicitly/intentionally accepted. */ + isAccepted: boolean + /** + * True if the suggestion was visible for a certain time + * Required to correctly calculate CAR and other metrics where we + * want to account only for suggestions visible for a certain time. + * + * `timeFromSuggestedAt` is not a reliable source of truth for + * this case because a user could have rejected a suggestion without + * triggering `accepted` or `discarded` immediately. This is related to + * limited VS Code APIs which do not provide a reliable way to know + * if a suggestion is really visible. + */ + isRead: boolean + /** The number of the auto-edit started since the last suggestion was shown. */ + suggestionsStartedSinceLastSuggestion: number +} + +export interface AcceptedState extends Omit { + phase: 'accepted' + /** Timestamp when the user accepted the suggestion. */ + acceptedAt: number + /** Timestamp when the suggestion was logged to our analytics backend. This is to avoid double-logging. */ + suggestionLoggedAt?: number + /** Optional because it might be accepted before the read timeout */ + readAt?: number + payload: FinalPayload & Omit +} + +export interface RejectedState extends Omit { + phase: 'rejected' + /** Timestamp when the user rejected the suggestion. */ + rejectedAt: number + /** Timestamp when the suggestion was logged to our analytics backend. This is to avoid double-logging. */ + suggestionLoggedAt?: number + /** Optional because it might be accepted before the read timeout */ + readAt?: number + payload: FinalPayload +} + +export interface DiscardedState extends Omit { + phase: 'discarded' + /** Timestamp when the suggestion was discarded. */ + discardedAt: number + /** Timestamp when the suggestion was logged to our analytics backend. This is to avoid double-logging. */ + suggestionLoggedAt?: number + payload: StartedState['payload'] & { + discardReason: AutoeditDiscardReasonMetadata + } +} + +export interface PhaseStates { + started: StartedState + contextLoaded: ContextLoadedState + loaded: LoadedState + postProcessed: PostProcessedState + suggested: SuggestedState + read: ReadState + accepted: AcceptedState + rejected: RejectedState + discarded: DiscardedState +} + +export type AutoeditRequestState = PhaseStates[keyof PhaseStates] diff --git a/vscode/src/autoedits/autoedits-config.ts b/vscode/src/autoedits/autoedits-config.ts index 39c9e9561b1e..6946cc4574e9 100644 --- a/vscode/src/autoedits/autoedits-config.ts +++ b/vscode/src/autoedits/autoedits-config.ts @@ -18,7 +18,7 @@ interface BaseAutoeditsProviderConfig { isChatModel: boolean } -interface AutoeditsProviderConfig extends BaseAutoeditsProviderConfig { +export interface AutoeditsProviderConfig extends BaseAutoeditsProviderConfig { experimentalAutoeditsConfigOverride: AutoEditsModelConfig | undefined isMockResponseFromCurrentDocumentTemplateEnabled: boolean } diff --git a/vscode/src/autoedits/autoedits-provider.ts b/vscode/src/autoedits/autoedits-provider.ts index fba6d924ebcc..037d857dd23e 100644 --- a/vscode/src/autoedits/autoedits-provider.ts +++ b/vscode/src/autoedits/autoedits-provider.ts @@ -296,10 +296,10 @@ export class AutoeditsProvider implements vscode.InlineCompletionItemProvider, v autoeditAnalyticsLogger.markAsLoaded({ requestId, prompt, + modelResponse: predictionResult, payload: { source: autoeditSource.network, isFuzzyMatch: false, - responseHeaders: predictionResult.responseHeaders, prediction: initialPrediction, }, }) diff --git a/vscode/src/autoedits/create-autoedits-provider.ts b/vscode/src/autoedits/create-autoedits-provider.ts index b01d5f9762d3..d532631aa31c 100644 --- a/vscode/src/autoedits/create-autoedits-provider.ts +++ b/vscode/src/autoedits/create-autoedits-provider.ts @@ -20,6 +20,7 @@ import type { FixupController } from '../non-stop/FixupController' import type { CodyStatusBar } from '../services/StatusBar' import { AutoeditsProvider } from './autoedits-provider' +import { AutoeditDebugPanel } from './debug-panel/debug-panel' import { autoeditsOutputChannelLogger } from './output-channel-logger' import { initImageSuggestionService } from './renderer/image-gen' @@ -54,6 +55,7 @@ interface AutoeditsItemProviderArgs { autoeditImageRenderingEnabled: boolean fixupController: FixupController statusBar: CodyStatusBar + context: vscode.ExtensionContext } export function createAutoEditsProvider({ @@ -64,6 +66,7 @@ export function createAutoEditsProvider({ autoeditImageRenderingEnabled, fixupController, statusBar, + context, }: AutoeditsItemProviderArgs): Observable { if (!configuration.experimentalAutoEditEnabled) { return NEVER @@ -109,6 +112,9 @@ export function createAutoEditsProvider({ [{ scheme: 'file', language: '*' }, { notebookType: '*' }], provider ), + vscode.commands.registerCommand('cody.command.autoedit.open-debug-panel', () => { + AutoeditDebugPanel.showPanel(context) + }), provider, ] }), diff --git a/vscode/src/autoedits/debug-panel/debug-panel.ts b/vscode/src/autoedits/debug-panel/debug-panel.ts new file mode 100644 index 000000000000..6243b5da0a04 --- /dev/null +++ b/vscode/src/autoedits/debug-panel/debug-panel.ts @@ -0,0 +1,172 @@ +import * as vscode from 'vscode' + +import { manipulateWebviewHTML } from '../../chat/chat-view/ChatController' +import type { AutoeditDebugMessageFromExtension } from './debug-protocol' + +import { autoeditDebugStore } from './debug-store' + +/** + * A panel that displays debug information about auto-edit requests. + */ +export class AutoeditDebugPanel { + public static currentPanel: AutoeditDebugPanel | undefined + private static readonly viewType = 'codyAutoeditDebugPanel' + + private readonly panel: vscode.WebviewPanel + private readonly extensionContext: vscode.ExtensionContext + private disposables: vscode.Disposable[] = [] + private updatePending = false + private readonly throttleMs = 500 // Throttle updates to at most once per 500ms + + private constructor(panel: vscode.WebviewPanel, extensionContext: vscode.ExtensionContext) { + this.panel = panel + this.extensionContext = extensionContext + + // Set the webview's initial content + void this.updateContent() + + // Listen for when the panel is disposed + // This happens when the user closes the panel or when the panel is closed programmatically + this.panel.onDidDispose(() => this.dispose(), null, this.disposables) + + // Subscribe to store changes with throttling + this.disposables.push( + autoeditDebugStore.onDidChange(() => { + // If an update is already pending, don't schedule another one + if (!this.updatePending) { + this.updatePending = true + setTimeout(() => { + this.updatePending = false + void this.updateContent() + }, this.throttleMs) + } + }) + ) + + // Handle messages from the webview + this.panel.webview.onDidReceiveMessage( + message => { + if (message.type === 'ready') { + // Send the initial data when the webview is ready + void this.updateContent() + } + }, + null, + this.disposables + ) + } + + /** + * Type-safe wrapper for sending messages to the webview. + * Ensures that only valid messages defined in the protocol are sent. + */ + private postMessageToWebview(message: AutoeditDebugMessageFromExtension): void { + this.panel.webview.postMessage(message) + } + + /** + * Shows the debug panel in the editor. + * If the panel already exists, it will be revealed. + */ + public static showPanel(extensionContext: vscode.ExtensionContext): void { + // Try to reveal existing panel if available + if (AutoeditDebugPanel.currentPanel) { + try { + const viewColumn = AutoeditDebugPanel.currentPanel.panel.viewColumn + AutoeditDebugPanel.currentPanel.panel.reveal(viewColumn, false) + return + } catch (error) { + console.log('Error revealing panel:', error) + AutoeditDebugPanel.currentPanel = undefined + } + } + + // Determine view column for a new panel + let viewColumn = vscode.ViewColumn.Beside + + if (vscode.window.activeTextEditor?.viewColumn === vscode.ViewColumn.One) { + viewColumn = vscode.ViewColumn.Two + } else if (vscode.window.activeTextEditor?.viewColumn) { + viewColumn = vscode.window.activeTextEditor.viewColumn + } + + // Create a new panel + const panel = vscode.window.createWebviewPanel( + AutoeditDebugPanel.viewType, + 'Cody Auto-Edit Debug Panel', + viewColumn, + { + enableScripts: true, + localResourceRoots: [vscode.Uri.joinPath(extensionContext.extensionUri, 'dist')], + } + ) + + AutoeditDebugPanel.currentPanel = new AutoeditDebugPanel(panel, extensionContext) + } + + /** + * Updates the content of the panel with the latest auto-edit requests. + */ + private async updateContent(): Promise { + const entries = autoeditDebugStore.getAutoeditRequestDebugStates() + + // Send the updated entries to the webview using the type-safe protocol + this.postMessageToWebview({ + type: 'updateEntries', + entries, + }) + + // If no HTML content is set yet, set the initial HTML + if (!this.panel.webview.html) { + this.panel.webview.html = await this.getHtmlForWebview(this.panel.webview) + } + } + + /** + * Generates the HTML for the webview panel, including the React app. + */ + private async getHtmlForWebview(webview: vscode.Webview): Promise { + // Read the compiled HTML file using VS Code's file system API + try { + const htmlPath = vscode.Uri.joinPath( + this.extensionContext.extensionUri, + 'dist', + 'webviews', + 'autoedit-debug.html' + ) + const htmlBytes = await vscode.workspace.fs.readFile(htmlPath) + const htmlContent = new TextDecoder('utf-8').decode(htmlBytes) + + // Create URI for the webview resources + const webviewResourcesUri = webview.asWebviewUri( + vscode.Uri.joinPath(this.extensionContext.extensionUri, 'dist', 'webviews') + ) + + // Use the shared manipulateWebviewHTML function + return manipulateWebviewHTML(htmlContent, { + cspSource: webview.cspSource, + resources: webviewResourcesUri, + }) + } catch (error) { + console.error('Error getting HTML for webview:', error) + return '' + } + } + + /** + * Dispose of the panel when it's closed. + */ + public dispose(): void { + AutoeditDebugPanel.currentPanel = undefined + + // Clean up our resources + this.panel.dispose() + + while (this.disposables.length) { + const disposable = this.disposables.pop() + if (disposable) { + disposable.dispose() + } + } + } +} diff --git a/vscode/src/autoedits/debug-panel/debug-protocol.ts b/vscode/src/autoedits/debug-panel/debug-protocol.ts new file mode 100644 index 000000000000..69abe4b02b3b --- /dev/null +++ b/vscode/src/autoedits/debug-panel/debug-protocol.ts @@ -0,0 +1,13 @@ +import type { AutoeditRequestDebugState } from './debug-store' + +export type AutoeditDebugMessageFromExtension = { + type: 'updateEntries' + entries: ReadonlyArray +} + +export type AutoeditDebugMessageFromWebview = { type: 'ready' } + +export interface VSCodeAutoeditDebugWrapper { + postMessage: (message: AutoeditDebugMessageFromWebview) => void + onMessage: (callback: (message: AutoeditDebugMessageFromExtension) => void) => () => void +} diff --git a/vscode/src/autoedits/debug-panel/debug-store.ts b/vscode/src/autoedits/debug-panel/debug-store.ts new file mode 100644 index 000000000000..c8d47a8f5e82 --- /dev/null +++ b/vscode/src/autoedits/debug-panel/debug-store.ts @@ -0,0 +1,121 @@ +import * as vscode from 'vscode' + +import type { AutoeditRequestState } from '../analytics-logger/types' +import { type AutoeditsProviderConfig, autoeditsProviderConfig } from '../autoedits-config' +import type { DecorationInfo } from '../renderer/decorators/base' +import { getDecorationInfo } from '../renderer/diff-utils' + +/** + * Enhanced debug entry for auto-edit requests that extends the analytics logger state + * with additional debug-specific properties. + */ +export interface AutoeditRequestDebugState { + /** The underlying analytics logger state object */ + state: AutoeditRequestState + /** Timestamp when the status was last updated */ + updatedAt: number + /** The autoedits provider config used for this request */ + autoeditsProviderConfig: AutoeditsProviderConfig + /** + * The side-by-side diff decoration info for the auto-edit request + * Different from the `state.updatedDecorationInfo` by the regex used to split + * the code in chunks for diffing it. + */ + sideBySideDiffDecorationInfo?: DecorationInfo +} + +const CHARACTER_REGEX = /./g + +/** + * A simple in-memory store for debugging auto-edit requests. + * Stores the most recent requests in a ring buffer. + */ +export class AutoeditDebugStore implements vscode.Disposable { + /** Auto-edit requests, stored in reverse chronological order (newest first) */ + private autoeditRequests: AutoeditRequestDebugState[] = [] + /** Maximum number of auto-edit requests to store */ + private maxEntries = 50 + /** Event emitter for notifying when data changes */ + private readonly onDidChangeEmitter = new vscode.EventEmitter() + /** Event that fires when the auto-edit requests data changes */ + public readonly onDidChange = this.onDidChangeEmitter.event + + /** + * Add a new auto-edit request debug state to the store. + * If the store is full, the oldest entry will be removed. + */ + public addAutoeditRequestDebugState(state: AutoeditRequestState): void { + const requestId = state.requestId + const existingIndex = this.autoeditRequests.findIndex( + entry => entry.state.requestId === requestId + ) + + if (existingIndex !== -1) { + this.updateExistingEntry(existingIndex, state) + } else { + this.addNewEntry(state) + } + } + + private updateExistingEntry(index: number, state: AutoeditRequestState): void { + const entry = this.autoeditRequests[index] + + this.autoeditRequests[index] = this.createDebugState(state, { + ...entry, + state, + sideBySideDiffDecorationInfo: this.calculateSideBySideDiff(state), + }) + + this.notifyChange() + } + + private addNewEntry(state: AutoeditRequestState): void { + const debugState = this.createDebugState(state) + this.autoeditRequests.unshift(debugState) + + this.enforceMaxEntries() + this.notifyChange() + } + + private createDebugState( + state: AutoeditRequestState, + baseState?: Partial + ): AutoeditRequestDebugState { + return { + state, + updatedAt: Date.now(), + autoeditsProviderConfig: { ...autoeditsProviderConfig }, + sideBySideDiffDecorationInfo: this.calculateSideBySideDiff(state), + ...baseState, + } + } + + private calculateSideBySideDiff(state: AutoeditRequestState): DecorationInfo | undefined { + // TODO: remove @ts-ignore once all auto-edit debug panel changes are merged. + return 'prediction' in state + ? // @ts-ignore + getDecorationInfo(state.codeToReplaceData.codeToRewrite, state.prediction, CHARACTER_REGEX) + : undefined + } + + private enforceMaxEntries(): void { + if (this.autoeditRequests.length > this.maxEntries) { + this.autoeditRequests = this.autoeditRequests.slice(0, this.maxEntries) + } + } + + private notifyChange(): void { + this.onDidChangeEmitter.fire() + } + + public getAutoeditRequestDebugStates(): ReadonlyArray { + return this.autoeditRequests + } + + public dispose(): void { + this.onDidChangeEmitter.dispose() + } +} + +// Singleton instance +export const autoeditDebugStore = new AutoeditDebugStore() diff --git a/vscode/src/autoedits/debug-panel/index.ts b/vscode/src/autoedits/debug-panel/index.ts new file mode 100644 index 000000000000..0becc4eab53a --- /dev/null +++ b/vscode/src/autoedits/debug-panel/index.ts @@ -0,0 +1,2 @@ +export { autoeditDebugStore, type AutoeditRequestDebugState } from './debug-store' +export { AutoeditDebugPanel } from './debug-panel' diff --git a/vscode/src/main.ts b/vscode/src/main.ts index a8e52d2c5541..3d7c2c181b36 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -53,6 +53,7 @@ import { tokenCallbackHandler, } from './auth/auth' import { createAutoEditsProvider } from './autoedits/create-autoedits-provider' +import { autoeditDebugStore } from './autoedits/debug-panel/debug-store' import { autoeditsOutputChannelLogger } from './autoedits/output-channel-logger' import { registerAutoEditTestRenderCommand } from './autoedits/renderer/mock-renderer' import type { MessageProviderOptions } from './chat/MessageProvider' @@ -292,7 +293,7 @@ const register = async ( registerAutocomplete(platform, statusBar, disposables) const tutorialSetup = tryRegisterTutorial(context, disposables) - await registerCodyCommands(statusBar, chatClient, fixupController, disposables) + await registerCodyCommands({ statusBar, chatClient, fixupController, disposables, context }) registerAuthCommands(disposables) registerChatCommands(disposables) disposables.push(...registerSidebarCommands()) @@ -413,12 +414,19 @@ async function registerOtherCommands(disposables: vscode.Disposable[]) { ) } -async function registerCodyCommands( - statusBar: CodyStatusBar, - chatClient: ChatClient, - fixupController: FixupController, +async function registerCodyCommands({ + statusBar, + chatClient, + fixupController, + disposables, + context, +}: { + statusBar: CodyStatusBar + chatClient: ChatClient + fixupController: FixupController disposables: vscode.Disposable[] -): Promise { + context: vscode.ExtensionContext +}): Promise { // Execute Cody Commands and Cody Custom Commands const executeCommand = ( commandKey: DefaultCodyCommands | string, @@ -462,7 +470,7 @@ async function registerCodyCommands( ) // Initialize autoedit provider if experimental feature is enabled - registerAutoEdits(chatClient, fixupController, statusBar, disposables) + registerAutoEdits({ chatClient, fixupController, statusBar, disposables, context }) // Initialize autoedit tester disposables.push( @@ -715,13 +723,21 @@ async function tryRegisterTutorial( } } -function registerAutoEdits( - chatClient: ChatClient, - fixupController: FixupController, - statusBar: CodyStatusBar, +function registerAutoEdits({ + chatClient, + fixupController, + statusBar, + disposables, + context, +}: { + chatClient: ChatClient + fixupController: FixupController + statusBar: CodyStatusBar disposables: vscode.Disposable[] -): void { + context: vscode.ExtensionContext +}): void { disposables.push( + autoeditDebugStore, subscriptionDisposable( combineLatest( resolvedConfig, @@ -754,6 +770,7 @@ function registerAutoEdits( autoeditImageRenderingEnabled, fixupController, statusBar, + context, }) } ), diff --git a/vscode/webviews/autoedit-debug.html b/vscode/webviews/autoedit-debug.html new file mode 100644 index 000000000000..e3c3d80b7633 --- /dev/null +++ b/vscode/webviews/autoedit-debug.html @@ -0,0 +1,17 @@ + + + + + + + + + + + Cody Auto-Edit Debug + + +
+ + + diff --git a/vscode/webviews/autoedit-debug.tsx b/vscode/webviews/autoedit-debug.tsx new file mode 100644 index 000000000000..911896f398c2 --- /dev/null +++ b/vscode/webviews/autoedit-debug.tsx @@ -0,0 +1,94 @@ +import { useEffect, useState } from 'react' +import { createRoot } from 'react-dom/client' + +import type { + AutoeditDebugMessageFromExtension, + VSCodeAutoeditDebugWrapper, +} from '../src/autoedits/debug-panel/debug-protocol' +import type { AutoeditRequestDebugState } from '../src/autoedits/debug-panel/debug-store' + +import { getVSCodeAPI } from './utils/VSCodeApi' + +/** + * Transforms array-formatted VS Code Range objects back into proper Range objects with start and end properties. + * + * @param obj The object to transform, which may contain Range objects that were converted to arrays. + * @returns A new object with any Range arrays converted back to objects with start and end properties. + */ +function transformRanges(obj: any): any { + if (obj === null || obj === undefined) { + return obj + } + + if (Array.isArray(obj)) { + // Check if it's a serialized Range in the format [{"line":1,"character":0},{"line":1,"character":4}] + if ( + obj.length === 2 && + typeof obj[0] === 'object' && + obj[0] !== null && + 'line' in obj[0] && + 'character' in obj[0] && + typeof obj[1] === 'object' && + obj[1] !== null && + 'line' in obj[1] && + 'character' in obj[1] + ) { + return { + start: { line: obj[0].line, character: obj[0].character }, + end: { line: obj[1].line, character: obj[1].character }, + } + } + // Otherwise, process each element of the array + return obj.map(item => transformRanges(item)) + } + + if (typeof obj === 'object') { + const result: Record = {} + for (const key in obj) { + result[key] = transformRanges(obj[key]) + } + return result + } + + return obj +} + +const vscode = getVSCodeAPI() as unknown as VSCodeAutoeditDebugWrapper + +function App() { + const [entries, setEntries] = useState([]) + + useEffect(() => { + // Listen for messages from VS Code + const handleMessage = (event: MessageEvent) => { + const message = event.data as AutoeditDebugMessageFromExtension + if (message.type === 'updateEntries') { + // Transform any Range arrays back to objects with start and end properties + const processedEntries = message.entries.map(entry => transformRanges(entry)) + + // Sort entries by updatedAt in descending order (newest first) + const sortedEntries = [...processedEntries].sort((a, b) => b.updatedAt - a.updatedAt) + setEntries(sortedEntries) + } + } + + window.addEventListener('message', handleMessage) + + // Request initial data + vscode.postMessage({ type: 'ready' }) + + return () => { + window.removeEventListener('message', handleMessage) + } + }, []) + + return ( +
+

Auto-Edit Debug Panel

+

We have {entries.length} entries

+
+ ) +} + +const root = createRoot(document.getElementById('root')!) +root.render() diff --git a/vscode/webviews/tailwind.config.mjs b/vscode/webviews/tailwind.config.mjs index bb0e4ee9d4ca..81224244df63 100644 --- a/vscode/webviews/tailwind.config.mjs +++ b/vscode/webviews/tailwind.config.mjs @@ -4,7 +4,12 @@ import plugin from 'tailwindcss/plugin' export default { content: { relative: true, - files: ['**/*.{ts,tsx}', '../../lib/**/**/*.{ts,tsx}'], + files: [ + '**/*.{ts,tsx}', + '../../lib/**/**/*.{ts,tsx}', + 'autoedit-debug/**/*.{ts,tsx}', + 'autoedit-debug/**/*.css', + ], }, prefix: 'tw-', theme: { diff --git a/vscode/webviews/vite.config.mts b/vscode/webviews/vite.config.mts index fa35a8187f18..f18785340300 100644 --- a/vscode/webviews/vite.config.mts +++ b/vscode/webviews/vite.config.mts @@ -26,6 +26,7 @@ export default defineProjectWithDefaults(__dirname, { }, input: { index: resolve(__dirname, 'index.html'), + autoeditDebug: resolve(__dirname, 'autoedit-debug.html'), }, output: { entryFileNames: '[name].js',