orcommit 1.0.4 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +226 -59
  2. package/dist/cli.d.ts +4 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +31 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/data/thinking-phrases.d.ts +11 -0
  7. package/dist/data/thinking-phrases.d.ts.map +1 -0
  8. package/dist/data/thinking-phrases.js +146 -0
  9. package/dist/data/thinking-phrases.js.map +1 -0
  10. package/dist/modules/api.d.ts.map +1 -1
  11. package/dist/modules/api.js +2 -9
  12. package/dist/modules/api.js.map +1 -1
  13. package/dist/modules/core.d.ts +7 -1
  14. package/dist/modules/core.d.ts.map +1 -1
  15. package/dist/modules/core.js +326 -120
  16. package/dist/modules/core.js.map +1 -1
  17. package/dist/modules/diff-filter.d.ts.map +1 -1
  18. package/dist/modules/diff-filter.js +83 -21
  19. package/dist/modules/diff-filter.js.map +1 -1
  20. package/dist/modules/git.d.ts +9 -0
  21. package/dist/modules/git.d.ts.map +1 -1
  22. package/dist/modules/git.js +40 -0
  23. package/dist/modules/git.js.map +1 -1
  24. package/dist/modules/promo.d.ts +9 -0
  25. package/dist/modules/promo.d.ts.map +1 -0
  26. package/dist/modules/promo.js +66 -0
  27. package/dist/modules/promo.js.map +1 -0
  28. package/dist/modules/spinner.d.ts +90 -0
  29. package/dist/modules/spinner.d.ts.map +1 -0
  30. package/dist/modules/spinner.js +205 -0
  31. package/dist/modules/spinner.js.map +1 -0
  32. package/dist/types/index.d.ts +3 -0
  33. package/dist/types/index.d.ts.map +1 -1
  34. package/dist/types/index.js.map +1 -1
  35. package/dist/utils/formatting.d.ts +58 -0
  36. package/dist/utils/formatting.d.ts.map +1 -0
  37. package/dist/utils/formatting.js +120 -0
  38. package/dist/utils/formatting.js.map +1 -0
  39. package/package.json +42 -5
  40. package/preview.png +0 -0
@@ -6,8 +6,11 @@ import { logger } from './logger.js';
6
6
  import { tokenManager } from './tokenizer.js';
7
7
  import { cacheManager } from './cache.js';
8
8
  import { diffFilter } from './diff-filter.js';
9
- import { confirm, isCancel } from '@clack/prompts';
9
+ import { maybeShowPromo } from './promo.js';
10
+ import { confirm, isCancel, text } from '@clack/prompts';
10
11
  import chalk from 'chalk';
12
+ import { wrapInstructions, wrapRules, wrapContext, wrapUserFeedback, wrapDiffContent, wrapInBlock, cleanText, parseAIResponse } from '../utils/formatting.js';
13
+ import { createAIThinkingSpinner, createProcessingSpinner } from './spinner.js';
11
14
  export class CoreOrchestrator {
12
15
  config;
13
16
  /**
@@ -94,65 +97,86 @@ export class CoreOrchestrator {
94
97
  if (filterSummary.filesRemoved > 0) {
95
98
  contextualLogger.debug(`Filtered out ${filterSummary.filesRemoved} irrelevant files`);
96
99
  }
97
- // Phase 3: Generate commit message
98
- console.log(chalk.blue('\nšŸ¤– Generating commit message...'));
100
+ // Phase 3: Generate commit message
99
101
  const provider = options.provider || this.config.preferences.defaultProvider;
100
- contextualLogger.debug(`Using ${provider} provider`);
101
102
  // Initialize API client
102
103
  apiManager.initializeProvider(provider, this.config);
103
- // Generate commit message
104
- const commitMessage = await this.generateCommitMessage(diff, options, provider);
105
- if (options.dryRun) {
106
- console.log(chalk.blue('\nšŸ“ Generated commit message (dry run):'));
107
- console.log(chalk.gray('——————————————————'));
108
- console.log(commitMessage);
109
- console.log(chalk.gray('——————————————————\n'));
110
- return;
104
+ // Generate commit message with regeneration loop
105
+ let commitMessage;
106
+ let codeAssessment = null;
107
+ let userFeedback;
108
+ let regenerationAttempt = 0;
109
+ const maxRegenerations = 5; // Prevent infinite loops
110
+ // eslint-disable-next-line no-constant-condition
111
+ while (true) {
112
+ // Generate commit message (with optional user feedback)
113
+ const result = await this.generateCommitMessage(diff, options, provider, userFeedback);
114
+ commitMessage = result.commitMessage;
115
+ codeAssessment = result.assessment;
116
+ if (options.dryRun) {
117
+ console.log(chalk.blue('\nšŸ“ Generated commit message (dry run):'));
118
+ console.log(chalk.gray('——————————————————'));
119
+ console.log(commitMessage);
120
+ console.log(chalk.gray('——————————————————\n'));
121
+ if (userFeedback) {
122
+ console.log(chalk.yellow(`Regenerated with feedback: ${userFeedback}\n`));
123
+ }
124
+ return;
125
+ }
126
+ // Confirm or regenerate
127
+ const confirmation = await this.confirmCommit(commitMessage, codeAssessment, options);
128
+ if (confirmation.action === 'confirm') {
129
+ break; // Exit loop and proceed to commit
130
+ }
131
+ else if (confirmation.action === 'cancel') {
132
+ console.log(chalk.yellow('\nāœ– Commit cancelled by user'));
133
+ return;
134
+ }
135
+ else if (confirmation.action === 'regenerate') {
136
+ regenerationAttempt++;
137
+ if (regenerationAttempt >= maxRegenerations) {
138
+ console.log(chalk.yellow(`\n⚠ Maximum regeneration attempts (${maxRegenerations}) reached`));
139
+ const forceCommit = await confirm({
140
+ message: 'Use the last generated message anyway?',
141
+ });
142
+ if (isCancel(forceCommit) || !forceCommit) {
143
+ console.log(chalk.yellow('\nāœ– Commit cancelled'));
144
+ return;
145
+ }
146
+ break;
147
+ }
148
+ userFeedback = confirmation.feedback;
149
+ // Regenerate with user feedback
150
+ continue;
151
+ }
111
152
  }
112
- // Confirm or auto-commit
113
- const shouldCommit = await this.confirmCommit(commitMessage, options);
114
- if (shouldCommit) {
115
- console.log(chalk.blue('\nšŸ’¾ Creating commit...'));
116
- const commitProgress = contextualLogger.startProgress('Committing changes');
153
+ // Create the commit
154
+ if (commitMessage) {
155
+ const commitSpinner = createProcessingSpinner('Creating commit');
156
+ commitSpinner.start();
117
157
  await gitManager.createCommit(commitMessage);
118
- commitProgress.succeed('Commit created');
119
- console.log(chalk.green('āœ“ Commit: ') + chalk.white(commitMessage));
158
+ commitSpinner.succeed('Commit created');
159
+ console.log(chalk.gray('\nšŸ’¬ Message: ') + chalk.white(commitMessage));
160
+ // Maybe show promotional message (1% chance)
161
+ maybeShowPromo();
120
162
  // Phase 4: Handle push
121
- if (options.autoPush) {
122
- console.log(chalk.blue('\nšŸš€ Auto-pushing to remote...'));
123
- await this.performPush(contextualLogger);
124
- }
125
- else if (options.push) {
126
- console.log(chalk.blue('\nšŸš€ Pushing to remote...'));
163
+ if (options.autoPush || options.push) {
127
164
  await this.performPush(contextualLogger);
128
165
  }
129
166
  else if (!options.yes && await gitManager.hasUnpushedCommits()) {
130
167
  // Ask user if they want to push (only if not in auto mode)
131
168
  try {
132
- console.log(''); // Add some space
133
169
  const shouldPush = await confirm({
134
- message: 'Do you want to push to remote?'
170
+ message: 'Push to remote?'
135
171
  });
136
- if (isCancel(shouldPush)) {
137
- console.log(chalk.yellow('ℹ Push cancelled'));
138
- }
139
- else if (shouldPush) {
140
- console.log(chalk.blue('\nšŸš€ Pushing to remote...'));
172
+ if (!isCancel(shouldPush) && shouldPush) {
141
173
  await this.performPush(contextualLogger);
142
174
  }
143
- else {
144
- console.log(chalk.gray('šŸ’” Tip: Use --push to automatically push changes in the future'));
145
- }
146
175
  }
147
176
  catch (error) {
148
- // If interactive prompts fail (e.g., in CI), skip push
149
- console.log(chalk.gray('šŸ’” Tip: Use --push to automatically push changes'));
177
+ // If interactive prompts fail (e.g., in CI), skip push silently
150
178
  }
151
179
  }
152
- else if (await gitManager.hasUnpushedCommits()) {
153
- // Just inform about unpushed commits
154
- console.log(chalk.gray('šŸ’” Tip: Use --push to automatically push changes'));
155
- }
156
180
  }
157
181
  else {
158
182
  contextualLogger.info('Commit cancelled by user');
@@ -169,22 +193,31 @@ export class CoreOrchestrator {
169
193
  }
170
194
  }
171
195
  /**
172
- * Generate commit message from diff
196
+ * Generate commit message from diff with optional user feedback
173
197
  */
174
- async generateCommitMessage(diff, options, provider) {
175
- const progress = logger.startProgress('Generating commit message...');
198
+ async generateCommitMessage(diff, options, provider, userFeedback) {
199
+ const spinner = createAIThinkingSpinner(userFeedback ? 'Regenerating commit' : 'Generating commit');
200
+ spinner.start();
176
201
  try {
177
- // Create system prompt
178
- const systemPrompt = this.createSystemPrompt(options);
202
+ // Create system prompt (with optional user feedback for regeneration)
203
+ const systemPrompt = this.createSystemPrompt(options, userFeedback);
204
+ if (userFeedback) {
205
+ logger.debug('Regenerating with user feedback', { feedbackLength: userFeedback.length });
206
+ }
179
207
  // Prepare diff content for processing
180
- const diffContent = this.prepareDiffContent(diff);
208
+ const rawDiffContent = this.prepareDiffContent(diff);
209
+ const diffContent = wrapDiffContent(rawDiffContent); // Wrap in DIFF_CONTENT block
181
210
  const model = this.getModel(provider);
182
- // Check cache first (unless disabled)
183
- if (!options.noCache) {
184
- const cachedMessage = await cacheManager.get(diffContent, model, provider, this.config.preferences.temperature);
211
+ // Check cache first (unless disabled or regenerating with feedback)
212
+ if (!options.noCache && !userFeedback) {
213
+ const cachedMessage = await cacheManager.get(rawDiffContent, // Use raw content for cache key
214
+ model, provider, this.config.preferences.temperature);
185
215
  if (cachedMessage) {
186
- progress.succeed('Commit message retrieved from cache');
187
- return cachedMessage;
216
+ spinner.succeed('Retrieved from cache');
217
+ return {
218
+ commitMessage: cachedMessage,
219
+ assessment: null // Cached messages don't have assessment
220
+ };
188
221
  }
189
222
  }
190
223
  // Get optimal chunk size for the model
@@ -209,22 +242,33 @@ export class CoreOrchestrator {
209
242
  if (!result.success || !result.data) {
210
243
  throw new ApiError(result.error?.message || 'Failed to generate commit message');
211
244
  }
212
- // Cache the result
213
- if (!options.noCache) {
214
- await cacheManager.set(diffContent, model, provider, this.config.preferences.temperature, result.data);
245
+ const rawMessage = result.data;
246
+ // Parse JSON response (with fallback to plain text)
247
+ logger.debug('Raw AI response:', { length: rawMessage.length, preview: rawMessage.substring(0, 200) });
248
+ const parsed = parseAIResponse(rawMessage);
249
+ logger.debug('Parsed response:', { hasAssessment: !!parsed.assessment, messageLength: parsed.commitMessage.length });
250
+ spinner.update('Polishing the message');
251
+ // Stage 2: Finalize and clean the commit message part (with user context)
252
+ const finalMessage = await this.finalizeCommitMessage(parsed.commitMessage, provider, options, userFeedback);
253
+ // Cache the finalized result (skip if regenerating with feedback)
254
+ if (!options.noCache && !userFeedback) {
255
+ await cacheManager.set(rawDiffContent, // Use raw content for cache key
256
+ model, provider, this.config.preferences.temperature, finalMessage);
215
257
  }
216
- progress.succeed('Commit message generated');
217
- return result.data;
258
+ spinner.succeed('Commit message generated');
259
+ return {
260
+ commitMessage: finalMessage,
261
+ assessment: parsed.assessment
262
+ };
218
263
  }
219
264
  else {
220
265
  // Multiple chunks processing with token-based splitting
221
- progress.update('Processing large diff in chunks...');
266
+ spinner.update('Processing large diff in chunks');
222
267
  const chunks = tokenManager.splitIntoTokenChunks(diffContent, {
223
268
  model,
224
269
  maxTokens: optimalChunkSize,
225
270
  reservedTokens: systemTokens,
226
271
  });
227
- logger.debug(`Split into ${chunks.length} token-based chunks`);
228
272
  const baseRequest = {
229
273
  provider,
230
274
  model,
@@ -237,77 +281,196 @@ export class CoreOrchestrator {
237
281
  throw new ApiError(result.error?.message || 'Failed to process chunks');
238
282
  }
239
283
  // Combine chunk results into a single commit message
240
- const finalMessage = this.combineChunkResults(result.data, options);
241
- progress.succeed('Commit message generated from chunks');
242
- return finalMessage;
284
+ const rawMessage = this.combineChunkResults(result.data, options);
285
+ // Parse JSON response (with fallback to plain text)
286
+ logger.debug('Raw AI response (chunks):', { length: rawMessage.length, preview: rawMessage.substring(0, 200) });
287
+ const parsed = parseAIResponse(rawMessage);
288
+ logger.debug('Parsed response (chunks):', { hasAssessment: !!parsed.assessment, messageLength: parsed.commitMessage.length });
289
+ spinner.update('Polishing the message');
290
+ // Stage 2: Finalize and clean the commit message part (with user context)
291
+ const finalMessage = await this.finalizeCommitMessage(parsed.commitMessage, provider, options, userFeedback);
292
+ spinner.succeed('Commit message generated');
293
+ return {
294
+ commitMessage: finalMessage,
295
+ assessment: parsed.assessment
296
+ };
243
297
  }
244
298
  }
245
299
  catch (error) {
246
- progress.fail('Failed to generate commit message');
300
+ spinner.fail('Failed to generate commit message');
247
301
  throw error;
248
302
  }
249
303
  }
250
304
  /**
251
305
  * Create system prompt based on options and preferences
252
306
  */
253
- createSystemPrompt(options) {
307
+ createSystemPrompt(options, userFeedback) {
308
+ // Use custom prompt from CLI option first, then from config, then default
309
+ if (options.prompt) {
310
+ return options.prompt;
311
+ }
312
+ if (this.config.preferences.customPrompt) {
313
+ return this.config.preferences.customPrompt;
314
+ }
315
+ // Build default prompt with structured blocks
254
316
  const format = this.config.preferences.commitFormat;
255
317
  const language = this.config.preferences.language;
256
- let prompt = `You are an expert Git commit message generator. Generate a concise, meaningful commit message based on the provided git diff.
318
+ const sections = [];
319
+ // Main instructions
320
+ const mainInstructions = `You are a senior software engineer and Git commit message expert with deep understanding of software architecture and code quality.
321
+
322
+ YOUR MISSION: Analyze the git diff carefully and generate a professional, comprehensive commit message that accurately captures ALL significant changes.
257
323
 
258
- Requirements:
324
+ ANALYSIS REQUIREMENTS:
325
+ 1. THINK DEEPLY about what the code changes actually do
326
+ 2. Identify the PRIMARY purpose of the changes (feature, fix, refactor, etc.)
327
+ 3. Notice ALL important modifications - don't miss secondary changes
328
+ 4. Understand the INTENT behind the changes, not just the syntax
329
+ 5. Consider the IMPACT on the codebase (breaking changes, new features, bug fixes)
330
+ 6. Recognize patterns: new files, deletions, refactoring, configuration changes`;
331
+ sections.push(wrapInstructions(mainInstructions));
332
+ // Quality standards and rules
333
+ let rules = `- Be SPECIFIC about what changed (mention key functions, components, files when relevant)
334
+ - Be ACCURATE - every word should reflect the actual changes
335
+ - Be COMPLETE - include all important changes, don't omit significant details
336
+ - Be CONCISE but INFORMATIVE - no fluff, but don't skip important info
337
+ - Use technical terminology appropriately
259
338
  - Write in ${language === 'en' ? 'English' : language}
260
- - Use ${format === 'conventional' ? 'Conventional Commits format' : 'simple descriptive format'}
261
- - Focus on the most significant changes
262
- - Be specific and actionable
339
+ - Follow ${format === 'conventional' ? 'Conventional Commits format strictly' : 'simple descriptive format'}
263
340
 
264
- `;
265
- // Formatting constraints
341
+ THINK STEP BY STEP:
342
+ 1. What is the main change? (new feature, bug fix, refactor, etc.)
343
+ 2. What files/components are affected?
344
+ 3. Are there any breaking changes?
345
+ 4. Are there secondary important changes?
346
+ 5. What's the overall impact?`;
347
+ // Add formatting constraints to rules
266
348
  if (options.oneLine) {
267
- prompt += `- Generate a single-line commit message only\n`;
349
+ rules += `\n- Generate a single-line commit message only`;
268
350
  }
269
351
  else {
270
- prompt += `- Keep subject line under 72 characters\n- Add body if needed for complex changes\n`;
352
+ rules += `\n- Keep subject line under 72 characters\n- Add body if needed for complex changes`;
271
353
  }
272
354
  if (options.descriptionLength) {
273
- prompt += `- Limit description to ${options.descriptionLength} characters\n`;
355
+ rules += `\n- Limit description to ${options.descriptionLength} characters`;
274
356
  }
275
357
  if (options.emoji) {
276
- prompt += `- Include appropriate emoji at the start of the commit message\n`;
358
+ rules += `\n- Include appropriate emoji at the start of the commit message`;
277
359
  }
278
360
  if (format === 'conventional') {
279
- prompt += `\nConventional Commits format:
280
- <type>[optional scope]: <description>
281
-
282
- Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert
283
- `;
361
+ rules += `\n\nConventional Commits format:\n<type>[optional scope]: <description>\n\nTypes: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert`;
284
362
  if (options.emoji) {
285
- prompt += `Emoji mapping:
286
- - feat: ✨
287
- - fix: šŸ›
288
- - docs: šŸ“
289
- - style: šŸ’„
290
- - refactor: ā™»ļø
291
- - test: āœ…
292
- - chore: šŸ”§
293
- - perf: ⚔
294
- - ci: šŸ‘·
295
- - build: šŸ“¦
296
- - revert: āŖ
297
- `;
363
+ rules += `\n\nEmoji mapping:\n- feat: ✨\n- fix: šŸ›\n- docs: šŸ“\n- style: šŸ’„\n- refactor: ā™»ļø\n- test: āœ…\n- chore: šŸ”§\n- perf: ⚔\n- ci: šŸ‘·\n- build: šŸ“¦\n- revert: āŖ`;
298
364
  }
299
365
  }
300
366
  if (options.type) {
301
- prompt += `\nRequired type: ${options.type}`;
367
+ rules += `\n\nRequired type: ${options.type}`;
302
368
  }
303
369
  if (options.scope) {
304
- prompt += `\nRequired scope: ${options.scope}`;
370
+ rules += `\nRequired scope: ${options.scope}`;
305
371
  }
306
372
  if (options.breaking) {
307
- prompt += `\nThis is a BREAKING CHANGE - include "BREAKING CHANGE:" in the commit message.`;
373
+ rules += `\n\nāš ļø CRITICAL: This is a BREAKING CHANGE - MUST include "BREAKING CHANGE:" in the commit message footer with explanation.`;
374
+ }
375
+ sections.push(wrapRules(rules));
376
+ // Add context if provided
377
+ if (options.context) {
378
+ sections.push(wrapContext(options.context));
379
+ }
380
+ // Add user feedback if provided (from regeneration request)
381
+ if (userFeedback) {
382
+ sections.push(wrapUserFeedback(userFeedback));
383
+ }
384
+ // JSON schema for response
385
+ const jsonSchema = `{
386
+ "codeAssessment": "Brief (1-2 sentences) sarcastic, darkly humorous assessment of the code changes. Be witty and technically insightful. Channel maximum developer cynicism. ${userFeedback ? 'IMPORTANT: Follow user feedback requirements for language and style!' : `Write in ${language}.`}",
387
+ "commitMessage": "Professional ${format === 'conventional' ? 'conventional commits format' : 'descriptive'} commit message. ${userFeedback ? 'CRITICAL: Follow ALL user feedback instructions!' : `Write in ${language}.`}"
388
+ }`;
389
+ sections.push(wrapInBlock('RESPONSE_SCHEMA', jsonSchema, false));
390
+ // Final generation instructions
391
+ const finalInstructions = `GENERATE YOUR RESPONSE AS A VALID JSON OBJECT:
392
+
393
+ ${userFeedback ? 'āš ļø CRITICAL: The [IMPORTANT_USER_FEEDBACK] block above contains explicit user requirements. Follow ALL instructions from the user feedback - they override ALL other rules (including language, format, style, etc.).\n' : ''}
394
+ 1. CODE ASSESSMENT - Provide a brief (1-2 sentences), brutally honest, darkly humorous take on the code changes
395
+ Examples: "Someone discovered copy-paste today", "WIP commits everywhere, as expected", "Finally fixing that TODO from 2019"
396
+
397
+ 2. COMMIT MESSAGE - Generate a professional commit message:
398
+ - ${format === 'conventional' ? 'Use conventional commits format: type(scope): description' : 'Use clear descriptive format'}
399
+ - Subject line under 72 characters
400
+ - Add detailed body if changes are complex
401
+ - Include BREAKING CHANGE footer if applicable
402
+
403
+ CRITICAL: Return ONLY a valid JSON object matching the RESPONSE_SCHEMA above.
404
+ āš ļø DO NOT wrap in markdown code blocks (no \`\`\`json)
405
+ āš ļø DO NOT add explanations before or after
406
+ āš ļø START your response with { and END with }
407
+ āš ļø This is REQUIRED - the response MUST be parseable JSON
408
+
409
+ CORRECT Example:
410
+ {"codeAssessment": "Ah yes, another 'quick fix' that touches 47 files", "commitMessage": "refactor: restructure authentication flow\\n\\nMigrate from session-based to JWT authentication"}
411
+
412
+ WRONG Examples:
413
+ - \`\`\`json {"codeAssessment": "..."} \`\`\` āŒ
414
+ - Here is the JSON: {...} āŒ
415
+ - Just plain text without JSON āŒ`;
416
+ sections.push(wrapInstructions(finalInstructions));
417
+ return sections.join('\n\n');
418
+ }
419
+ /**
420
+ * Finalize and clean up the commit message (Stage 2)
421
+ * Takes the raw AI-generated message and ensures it's perfectly formatted
422
+ */
423
+ async finalizeCommitMessage(rawMessage, provider, options, userFeedback) {
424
+ const format = this.config.preferences.commitFormat;
425
+ // Create finalization prompt with structured blocks
426
+ const instructions = `You are a commit message quality control expert.
427
+
428
+ YOUR TASK: Clean and perfect the commit message below. Remove ANY explanatory text, prefixes, or formatting artifacts.
429
+ ${userFeedback ? '\nāš ļø CRITICAL: User provided feedback. You MUST preserve the language and style they requested!' : ''}`;
430
+ const rules = `1. Output ONLY the final commit message - nothing else
431
+ 2. Remove prefixes like "commit message:", "here is", "this is", etc.
432
+ 3. Remove surrounding quotes, backticks, or markdown
433
+ 4. Remove any explanations or comments
434
+ 5. Keep the message structure intact (subject + body + footer if present)
435
+ 6. Start directly with the commit type or message
436
+ 7. Preserve ${format === 'conventional' ? 'conventional commits format (type(scope): description)' : 'simple format'}
437
+ 8. Preserve line breaks for multi-line messages
438
+ 9. Ensure subject line is under 72 characters
439
+ 10. NO additional text, NO commentary, NO explanations
440
+ ${userFeedback ? `11. CRITICAL: Preserve the EXACT language used in the message below (user requested specific changes)` : ''}`;
441
+ const finalizationPrompt = `${wrapInstructions(instructions)}
442
+
443
+ ${wrapRules(rules)}
444
+
445
+ [RAW_MESSAGE_TO_CLEAN]
446
+ ${cleanText(rawMessage)}
447
+ [/RAW_MESSAGE_TO_CLEAN]
448
+
449
+ OUTPUT ONLY THE CLEANED COMMIT MESSAGE:`;
450
+ try {
451
+ const model = this.getModel(provider);
452
+ const result = await apiManager.generateCommitMessage({
453
+ provider,
454
+ model,
455
+ maxTokens: this.config.preferences.maxTokens,
456
+ temperature: 0.3, // Lower temperature for more consistent cleaning
457
+ messages: [
458
+ { role: 'system', content: finalizationPrompt },
459
+ { role: 'user', content: 'Clean this message now.' }
460
+ ],
461
+ }, provider);
462
+ if (!result.success || !result.data) {
463
+ // If finalization fails, return original message
464
+ logger.warn('Finalization failed, using original message');
465
+ return rawMessage;
466
+ }
467
+ return result.data.trim();
468
+ }
469
+ catch (error) {
470
+ // If finalization fails, return original message
471
+ logger.warn('Finalization error, using original message');
472
+ return rawMessage;
308
473
  }
309
- prompt += `\nGenerate only the commit message, no additional text or explanation.`;
310
- return prompt;
311
474
  }
312
475
  /**
313
476
  * Prepare diff content for API consumption
@@ -394,26 +557,71 @@ Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert
394
557
  }
395
558
  /**
396
559
  * Confirm commit with user (unless auto-confirm is enabled)
560
+ * Returns: 'confirm' | 'regenerate' | 'cancel'
397
561
  */
398
- async confirmCommit(message, options) {
562
+ async confirmCommit(message, assessment, options) {
399
563
  if (options.yes || this.config.preferences.autoConfirm) {
400
- return true;
564
+ return { action: 'confirm' };
401
565
  }
402
- // For now, we'll implement a simple console confirmation
403
- // In a full implementation, you might want to use a library like inquirer
404
- console.log(`\nProposed commit message:\n${message}\n`);
405
- // Simple readline implementation for confirmation
406
- const readline = await import('readline');
407
- const rl = readline.createInterface({
408
- input: process.stdin,
409
- output: process.stdout,
410
- });
411
- return new Promise((resolve) => {
412
- rl.question('Create this commit? (y/N): ', (answer) => {
413
- rl.close();
414
- resolve(answer.toLowerCase().startsWith('y'));
566
+ // Show sarcastic code assessment if available
567
+ if (assessment) {
568
+ console.log(chalk.dim('\nšŸ’­ AI thinks: ') + chalk.yellow.italic(assessment));
569
+ }
570
+ console.log(chalk.cyan('\nšŸ“ Generated commit message:'));
571
+ console.log(chalk.gray('——————————————————'));
572
+ console.log(chalk.white(message));
573
+ console.log(chalk.gray('——————————————————\n'));
574
+ try {
575
+ const action = await confirm({
576
+ message: 'Accept this commit message?',
577
+ initialValue: true,
415
578
  });
416
- });
579
+ if (isCancel(action)) {
580
+ return { action: 'cancel' };
581
+ }
582
+ if (action) {
583
+ return { action: 'confirm' };
584
+ }
585
+ // User rejected - ask if they want to regenerate with feedback
586
+ console.log('');
587
+ const shouldRegenerate = await confirm({
588
+ message: 'Would you like to regenerate with additional instructions?',
589
+ initialValue: true,
590
+ });
591
+ if (isCancel(shouldRegenerate) || !shouldRegenerate) {
592
+ return { action: 'cancel' };
593
+ }
594
+ // Get user feedback for regeneration
595
+ const feedback = await text({
596
+ message: 'What should be changed or improved?',
597
+ placeholder: 'e.g., "Be more specific about the bug fix" or "Mention the new API endpoint"',
598
+ validate: (value) => {
599
+ if (!value || value.trim().length < 3) {
600
+ return 'Please provide at least 3 characters of feedback';
601
+ }
602
+ return undefined; // Valid input
603
+ },
604
+ });
605
+ if (isCancel(feedback)) {
606
+ return { action: 'cancel' };
607
+ }
608
+ return { action: 'regenerate', feedback: feedback };
609
+ }
610
+ catch (error) {
611
+ // Fallback to simple confirmation if prompts fail
612
+ console.log(chalk.yellow('⚠ Interactive prompts unavailable, using simple mode'));
613
+ const readline = await import('readline');
614
+ const rl = readline.createInterface({
615
+ input: process.stdin,
616
+ output: process.stdout,
617
+ });
618
+ return new Promise((resolve) => {
619
+ rl.question('Accept this commit? (y/N): ', (answer) => {
620
+ rl.close();
621
+ resolve({ action: answer.toLowerCase().startsWith('y') ? 'confirm' : 'cancel' });
622
+ });
623
+ });
624
+ }
417
625
  }
418
626
  /**
419
627
  * Validate environment before processing
@@ -449,19 +657,17 @@ Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert
449
657
  const pushMessage = hasUpstream
450
658
  ? `Pushing to ${currentBranch}`
451
659
  : `Setting upstream and pushing to ${currentBranch}`;
452
- const pushProgress = contextualLogger.startProgress(pushMessage);
660
+ const pushSpinner = createProcessingSpinner(pushMessage);
661
+ pushSpinner.start();
453
662
  try {
454
663
  await gitManager.pushToRemote(true); // Set upstream if needed
455
664
  const successMessage = hasUpstream
456
665
  ? `Pushed to ${currentBranch}`
457
666
  : `Upstream set and pushed to ${currentBranch}`;
458
- pushProgress.succeed(successMessage);
459
- console.log(chalk.green('āœ“ Changes pushed successfully'));
667
+ pushSpinner.succeed(successMessage);
460
668
  }
461
669
  catch (error) {
462
- pushProgress.fail(`Push failed`);
463
- console.log(chalk.red(`āœ— Error: ${error.message}`));
464
- console.log(chalk.gray('šŸ’” You can try pushing manually: git push'));
670
+ pushSpinner.fail(`Push failed: ${error.message}`);
465
671
  }
466
672
  }
467
673
  /**