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
18 changes: 3 additions & 15 deletions src/webviews/apps/plus/composer/components/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
OpenOnboardingCommand,
ReloadComposerCommand,
} from '../../../../plus/composer/protocol';
import { createCombinedDiffForCommit, updateHunkAssignments } from '../../../../plus/composer/utils';
import { updateHunkAssignments } from '../../../../plus/composer/utils';
import type { RepoButtonGroupClickEvent } from '../../../shared/components/repo-button-group';
import { focusableBaseStyles } from '../../../shared/components/styles/lit/a11y.css';
import { boxSizingBase } from '../../../shared/components/styles/lit/base.css';
Expand Down Expand Up @@ -1271,9 +1271,7 @@ export class ComposerApp extends LitElement {
private finishAndCommit() {
this._ipc.sendCommand(FinishAndCommitCommand, {
commits: this.state.commits,
hunks: this.hunksWithAssignments,
baseCommit: this.state.baseCommit,
safetyState: this.state.safetyState,
});
}

Expand All @@ -1288,7 +1286,6 @@ export class ComposerApp extends LitElement {
private handleReloadComposer() {
this.resetHistory();
this._ipc.sendCommand(ReloadComposerCommand, {
repoPath: this.state.safetyState.repoPath,
mode: this.state.mode,
});
}
Expand Down Expand Up @@ -1480,11 +1477,8 @@ export class ComposerApp extends LitElement {
this.saveToHistory();

this._ipc.sendCommand(GenerateCommitsCommand, {
hunks: hunksToGenerate,
// In preview mode, send empty commits array to overwrite existing commits
// In interactive mode, send existing commits to preserve them
hunkIndices: hunksToGenerate.map(hunk => hunk.index),
commits: this.isPreviewMode ? [] : this.state.commits,
hunkMap: this.state.hunkMap,
baseCommit: this.state.baseCommit,
customInstructions: customInstructions || undefined,
});
Expand All @@ -1499,15 +1493,9 @@ export class ComposerApp extends LitElement {
return;
}

// Create combined diff for the commit
const { patch } = createCombinedDiffForCommit(commit, this.hunksWithAssignments);
if (!patch) {
return;
}

this._ipc.sendCommand(GenerateCommitMessageCommand, {
commitId: commitId,
diff: patch,
commitHunkIndices: commit.hunkIndices,
overwriteExistingMessage: commit.message.trim() !== '',
});
}
Expand Down
2 changes: 0 additions & 2 deletions src/webviews/apps/plus/composer/stateProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,7 @@ export class ComposerStateProvider implements StateProvider<State> {
...this._state,
hunks: msg.params.hunks,
commits: msg.params.commits,
hunkMap: msg.params.hunkMap,
baseCommit: msg.params.baseCommit,
safetyState: msg.params.safetyState,
loadingError: msg.params.loadingError,
hasChanges: msg.params.hasChanges,
safetyError: null, // Clear any existing safety errors
Expand Down
77 changes: 55 additions & 22 deletions src/webviews/plus/composer/composerWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import type {
ComposerContext,
ComposerGenerateCommitMessageEventData,
ComposerGenerateCommitsEventData,
ComposerHunk,
ComposerLoadedErrorData,
ComposerSafetyState,
ComposerTelemetryEvent,
FinishAndCommitParams,
GenerateCommitMessageParams,
Expand Down Expand Up @@ -78,6 +80,7 @@ import {
import type { ComposerWebviewShowingArgs } from './registration';
import {
convertToComposerDiffInfo,
createCombinedDiffForCommit,
createHunksFromDiffs,
createSafetyState,
getWorkingTreeDiffs,
Expand All @@ -98,6 +101,10 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
private _repositorySubscription?: Disposable;
private _currentRepository?: Repository;

// Hunk map and safety state
private _hunks: ComposerHunk[] = [];
private _safetyState: ComposerSafetyState;

// Telemetry context - tracks composer-specific data for getTelemetryContext
private _context: ComposerContext;

Expand All @@ -111,6 +118,15 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
this.container.ai.onDidChangeModel(this.onAIModelChanged, this),
);
this._context = { ...baseContext };
this._safetyState = {
repoPath: '',
headSha: '',
hashes: {
staged: null,
unstaged: null,
unified: null,
},
};
}

dispose(): void {
Expand Down Expand Up @@ -308,7 +324,8 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
// Allow composer to open with no changes - we'll handle this in the UI
const hasChanges = Boolean(staged?.contents || unstaged?.contents);

const { hunkMap, hunks } = createHunksFromDiffs(staged?.contents, unstaged?.contents);
const hunks = createHunksFromDiffs(staged?.contents, unstaged?.contents);
this._hunks = hunks;

const baseCommit = getSettledValue(commitResult);
if (baseCommit == null) {
Expand Down Expand Up @@ -359,6 +376,7 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co

// Create safety state snapshot for validation
const safetyState = await createSafetyState(repo, diffs, baseCommit.sha);
this._safetyState = safetyState;

const aiEnabled = this.getAiEnabled();
const aiModel = await this.container.ai.getModel(
Expand Down Expand Up @@ -395,15 +413,13 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
return {
...this.initialState,
hunks: hunks,
hunkMap: hunkMap,
baseCommit: {
sha: baseCommit.sha,
message: baseCommit.message ?? '',
repoName: repo.name,
branchName: currentBranch.name,
},
commits: commits,
safetyState: safetyState,
aiEnabled: aiEnabled,
ai: {
model: aiModel,
Expand Down Expand Up @@ -533,9 +549,7 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
await this.host.notify(DidReloadComposerNotification, {
hunks: composerData.hunks,
commits: composerData.commits,
hunkMap: composerData.hunkMap,
baseCommit: composerData.baseCommit,
safetyState: composerData.safetyState,
loadingError: composerData.loadingError,
hasChanges: composerData.hasChanges,
repositoryState: composerData.repositoryState,
Expand Down Expand Up @@ -788,14 +802,10 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
await this.host.notify(DidStartGeneratingNotification, undefined);

// Transform the data for the AI service
const hunks = params.hunks.map(hunk => ({
index: hunk.index,
fileName: hunk.fileName,
diffHeader: hunk.diffHeader || `diff --git a/${hunk.fileName} b/${hunk.fileName}`,
hunkHeader: hunk.hunkHeader,
content: hunk.content,
source: hunk.source,
}));
const hunks = [];
for (const index of params.hunkIndices) {
hunks.push({ ...this._hunks.find(m => m.index === index)!, assigned: true });
}

const existingCommits = params.commits.map(commit => ({
id: commit.id,
Expand All @@ -808,7 +818,7 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
const result = await this.container.ai.generateCommits(
hunks,
existingCommits,
params.hunkMap,
this._hunks.map(m => ({ index: m.index, hunkHeader: m.hunkHeader })),
{ source: 'composer', correlationId: this.host.instanceId },
{
cancellation: this._generateCommitsCancellation.token,
Expand Down Expand Up @@ -942,9 +952,28 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
// Notify webview that commit message generation is starting
await this.host.notify(DidStartGeneratingCommitMessageNotification, { commitId: params.commitId });

// Create combined diff for the commit
const { patch } = createCombinedDiffForCommit(
this._hunks.filter(h => params.commitHunkIndices.includes(h.index)),
);
if (!patch) {
this._context.operations.generateCommitMessage.errorCount++;
this._context.errors.operation.count++;
// Send error notification for failure (not cancellation)
this.sendTelemetryEvent('composer/action/generateCommitMessage/failed', {
...eventData,
'failure.reason': 'error',
'failure.error.message': 'Failed to create diff for commit',
});
await this.host.notify(DidErrorAIOperationNotification, {
operation: 'generate commit message',
error: 'Failed to create diff for commit',
});
}

// Call the AI service to generate commit message
const result = await this.container.ai.generateCommitMessage(
params.diff,
patch,
{ source: 'composer', correlationId: this.host.instanceId },
{
cancellation: this._generateCommitMessageCancellation.token,
Expand Down Expand Up @@ -1032,7 +1061,7 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
await this.host.notify(DidStartCommittingNotification, undefined);

// Get the specific repository from the safety state
const repo = this.container.git.getRepository(params.safetyState.repoPath);
const repo = this.container.git.getRepository(this._safetyState.repoPath);
if (!repo) {
// Clear loading state and show safety error
await this.host.notify(DidFinishCommittingNotification, undefined);
Expand All @@ -1049,13 +1078,18 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
return;
}

// Extract hunk sources for smart validation
const hunksBeingCommitted = params.hunks.filter(hunk =>
const commitHunkIndices = params.commits.flatMap(c => c.hunkIndices);
const hunks: ComposerHunk[] = [];
for (const hunk of commitHunkIndices) {
hunks.push({ ...this._hunks.find(m => m.index === hunk)!, assigned: true });
}

const hunksBeingCommitted = hunks.filter(hunk =>
params.commits.some(c => c.hunkIndices.includes(hunk.index)),
);

// Validate repository safety state before proceeding
const validation = await validateSafetyState(repo, params.safetyState, hunksBeingCommitted);
const validation = await validateSafetyState(repo, this._safetyState, hunksBeingCommitted);
if (!validation.isValid) {
// Clear loading state and show safety error
await this.host.notify(DidFinishCommittingNotification, undefined);
Expand All @@ -1073,8 +1107,7 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
return;
}

// Convert composer data to ComposerDiffInfo format
const diffInfo = convertToComposerDiffInfo(params.commits, params.hunks);
const diffInfo = convertToComposerDiffInfo(params.commits, hunks);
const svc = this.container.git.getRepositoryService(repo.path);
if (!svc) {
this._context.errors.operation.count++;
Expand Down Expand Up @@ -1120,7 +1153,7 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co

if (
!validateResultingDiff(
params.safetyState,
this._safetyState,
await sha256(resultingDiff),
this._context.diff.unstagedIncluded,
)
Expand Down
16 changes: 1 addition & 15 deletions src/webviews/plus/composer/mockData.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Mock data for the composer following the proper data model
// This represents the structure that would come from AI rebase results

import type { ComposerCommit, ComposerHunk, ComposerHunkMap } from './protocol';
import type { ComposerCommit, ComposerHunk } from './protocol';

// Mock hunks following the AI rebase result structure
export const mockHunks: ComposerHunk[] = [
Expand Down Expand Up @@ -350,20 +350,6 @@ export const mockCommits: ComposerCommit[] = [

// Callbacks are no longer used - replaced with IPC commands

// Mock hunk map (maps hunk indices to hunk headers for combined diff)
export const mockHunkMap: ComposerHunkMap[] = [
{ index: 1, hunkHeader: '@@ -0,0 +1,15 @@' },
{ index: 2, hunkHeader: '@@ -0,0 +1,12 @@' },
{ index: 3, hunkHeader: '@@ -0,0 +1,18 @@' },
{ index: 4, hunkHeader: '@@ -0,0 +1,20 @@' },
{ index: 5, hunkHeader: '@@ -0,0 +1,10 @@' },
{ index: 6, hunkHeader: '@@ -0,0 +1,25 @@' },
{ index: 7, hunkHeader: '@@ -0,0 +1,30 @@' },
{ index: 8, hunkHeader: '@@ -0,0 +1,8 @@' },
{ index: 9, hunkHeader: '@@ -0,0 +1,15 @@' },
{ index: 10, hunkHeader: '@@ -0,0 +1,12 @@' },
];

// Mock base commit
export const mockBaseCommit = {
sha: 'abc123def456789',
Expand Down
Loading