sncommit 1.0.0 → 1.0.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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Better Commit
1
+ # Better-Commit
2
2
 
3
3
  AI-powered git commit message generator with a beautiful TUI (Terminal User Interface).
4
4
 
package/dist/index.js CHANGED
@@ -58323,7 +58323,15 @@ class GroqService {
58323
58323
  }
58324
58324
  return this.parseSuggestions(content);
58325
58325
  } catch (e2) {
58326
- console.error("Error generating suggestions:", e2);
58326
+ const error = e2;
58327
+ if (error?.status === 401) {
58328
+ console.warn(`
58329
+ \x1B[33m⚠️ Groq API Key is invalid.\x1B[0m`);
58330
+ console.warn(` Using static backup suggestions. Run \x1B[36mbetter-commit config\x1B[0m to set your key.
58331
+ `);
58332
+ } else {
58333
+ console.error("Error generation suggestions:", error?.message || String(e2));
58334
+ }
58327
58335
  const fallbackSuggestions = this.getFallbackSuggestions(stagedFiles);
58328
58336
  return fallbackSuggestions.map((s2) => ({ ...s2, isFallback: true }));
58329
58337
  }
@@ -58363,7 +58371,15 @@ class GroqService {
58363
58371
  }
58364
58372
  return this.parseSuggestions(content);
58365
58373
  } catch (e2) {
58366
- console.error("Error generating suggestions:", e2);
58374
+ const error = e2;
58375
+ if (error?.status === 401) {
58376
+ console.warn(`
58377
+ \x1B[33m⚠️ Groq API Key is invalid.\x1B[0m`);
58378
+ console.warn(` Using static backup suggestions. Run \x1B[36mbetter-commit config\x1B[0m to set your key.
58379
+ `);
58380
+ } else {
58381
+ console.error("Error generation suggestions:", error?.message || String(e2));
58382
+ }
58367
58383
  const fallbackSuggestions = this.getFallbackSuggestions(stagedFiles);
58368
58384
  return fallbackSuggestions.map((s2) => ({ ...s2, isFallback: true }));
58369
58385
  }
@@ -58510,11 +58526,13 @@ Output ONLY valid JSON.`;
58510
58526
  if (jsonMatch) {
58511
58527
  const parsed = JSON.parse(jsonMatch[0]);
58512
58528
  if (parsed.suggestions && Array.isArray(parsed.suggestions)) {
58513
- return parsed.suggestions.map((s2) => ({
58514
- message: s2.message || "",
58515
- type: s2.type || this.extractType(s2.message || ""),
58516
- description: s2.description || s2.message || ""
58517
- })).slice(0, 4);
58529
+ if (parsed.suggestions && Array.isArray(parsed.suggestions)) {
58530
+ return parsed.suggestions.map((s2) => ({
58531
+ message: s2.message || "",
58532
+ type: s2.type || this.extractType(s2.message || ""),
58533
+ description: s2.description || s2.message || ""
58534
+ })).slice(0, 4);
58535
+ }
58518
58536
  }
58519
58537
  }
58520
58538
  throw new Error("Invalid JSON structure");
@@ -58689,7 +58707,7 @@ var CommitSuggestions = ({
58689
58707
  }, 80);
58690
58708
  return () => clearInterval(timer);
58691
58709
  }
58692
- }, [isLoading]);
58710
+ }, [isLoading, spinnerFrames.length]);
58693
58711
  use_input_default((input, key) => {
58694
58712
  if (key.upArrow) {
58695
58713
  const newIndex = selectedIndex > 0 ? selectedIndex - 1 : totalOptions - 1;
@@ -58933,12 +58951,16 @@ var BetterCommitApp = ({
58933
58951
  if (successMessage) {
58934
58952
  write("\x1B[2J\x1B[0f");
58935
58953
  const timer = setTimeout(() => {
58936
- onExit(successMessage);
58937
58954
  exit();
58938
58955
  }, 1500);
58939
58956
  return () => clearTimeout(timer);
58940
58957
  }
58941
- }, [successMessage, onExit, exit, write]);
58958
+ }, [successMessage, exit, write]);
58959
+ import_react24.useEffect(() => {
58960
+ if (state.error) {
58961
+ exit();
58962
+ }
58963
+ }, [state.error, exit]);
58942
58964
  use_input_default((input, key) => {
58943
58965
  if (key.escape || key.ctrl && input === "c") {
58944
58966
  onExit("Operation cancelled");
@@ -58952,7 +58974,7 @@ var BetterCommitApp = ({
58952
58974
  try {
58953
58975
  const stagedFiles = await gitService.getStagedFiles();
58954
58976
  await gitService.getDiff();
58955
- await generateSuggestions(stagedFiles);
58977
+ await fetchSuggestions(stagedFiles);
58956
58978
  setState((prev) => ({
58957
58979
  ...prev,
58958
58980
  stagedFiles
@@ -58966,11 +58988,11 @@ var BetterCommitApp = ({
58966
58988
  };
58967
58989
  initializeApp();
58968
58990
  }, []);
58969
- const generateSuggestions = async (stagedFiles) => {
58991
+ const fetchSuggestions = async (stagedFiles, customPrompt) => {
58970
58992
  if (!config.groqApiKey || config.groqApiKey.trim() === "") {
58971
58993
  setState((prev) => ({
58972
58994
  ...prev,
58973
- error: 'Groq API key not configured. Run "better-commit --config" to set it up.',
58995
+ error: 'Groq API key not configured. Run "better-commit config" to set it up.',
58974
58996
  isLoading: false
58975
58997
  }));
58976
58998
  return;
@@ -58980,7 +59002,12 @@ var BetterCommitApp = ({
58980
59002
  const diff2 = await gitService.getDiff();
58981
59003
  const diffStats = await gitService.getDiffStats();
58982
59004
  const recentCommits = await gitService.getRecentCommits(config.maxHistoryCommits || 50);
58983
- const suggestions = await groqService.generateCommitSuggestions(stagedFiles, diff2, recentCommits, diffStats);
59005
+ let suggestions;
59006
+ if (customPrompt) {
59007
+ suggestions = await groqService.generateCommitSuggestionsFromCustomInput(stagedFiles, diff2, customPrompt, recentCommits, diffStats);
59008
+ } else {
59009
+ suggestions = await groqService.generateCommitSuggestions(stagedFiles, diff2, recentCommits, diffStats);
59010
+ }
58984
59011
  const hasFallback = suggestions.some((s2) => s2.isFallback);
58985
59012
  setState((prev) => ({
58986
59013
  ...prev,
@@ -59016,7 +59043,7 @@ var BetterCommitApp = ({
59016
59043
  };
59017
59044
  const handleTryAgain = () => {
59018
59045
  setState((prev) => ({ ...prev, isLoading: true, suggestions: [] }));
59019
- generateSuggestions(state.stagedFiles);
59046
+ fetchSuggestions(state.stagedFiles);
59020
59047
  };
59021
59048
  const handleCustomInput = () => {
59022
59049
  setIsCustomInputMode(true);
@@ -59034,82 +59061,26 @@ var BetterCommitApp = ({
59034
59061
  }
59035
59062
  setIsCustomInputMode(false);
59036
59063
  setState((prev) => ({ ...prev, isLoading: true, suggestions: [] }));
59037
- await generateSuggestionsFromCustomInput(customInput.trim());
59038
- };
59039
- const generateSuggestionsFromCustomInput = async (userInput) => {
59040
- if (!config.groqApiKey || config.groqApiKey.trim() === "") {
59041
- setState((prev) => ({
59042
- ...prev,
59043
- error: 'Groq API key not configured. Run "better-commit --config" to set it up.',
59044
- isLoading: false
59045
- }));
59046
- return;
59047
- }
59048
- try {
59049
- const groqService = new GroqService(config.groqApiKey, config);
59050
- const diff2 = await gitService.getDiff();
59051
- const diffStats = await gitService.getDiffStats();
59052
- const recentCommits = await gitService.getRecentCommits(config.maxHistoryCommits || 50);
59053
- const suggestions = await groqService.generateCommitSuggestionsFromCustomInput(state.stagedFiles, diff2, userInput, recentCommits, diffStats);
59054
- const hasFallback = suggestions.some((s2) => s2.isFallback);
59055
- setState((prev) => ({
59056
- ...prev,
59057
- suggestions,
59058
- isLoading: false,
59059
- error: undefined
59060
- }));
59061
- setIsUsingFallback(hasFallback);
59062
- } catch (error) {
59063
- setState((prev) => ({
59064
- ...prev,
59065
- error: `Failed to generate suggestions: ${error instanceof Error ? error.message : String(error)}`,
59066
- isLoading: false
59067
- }));
59068
- }
59064
+ await fetchSuggestions(state.stagedFiles, customInput.trim());
59069
59065
  };
59070
59066
  if (state.error) {
59071
59067
  return /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
59072
- flexDirection: "column",
59068
+ borderStyle: "single",
59069
+ borderColor: "#ef4444",
59073
59070
  padding: 2,
59071
+ marginBottom: 1,
59074
59072
  children: [
59073
+ /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
59074
+ color: "#ef4444",
59075
+ bold: true,
59076
+ children: "Error"
59077
+ }, undefined, false, undefined, this),
59075
59078
  /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
59076
- borderStyle: "single",
59077
- borderColor: "#ef4444",
59078
- padding: 2,
59079
- marginBottom: 1,
59080
- children: [
59081
- /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
59082
- color: "#ef4444",
59083
- bold: true,
59084
- children: "Error"
59085
- }, undefined, false, undefined, this),
59086
- /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
59087
- marginTop: 1,
59088
- children: /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
59089
- color: "#e5e7eb",
59090
- children: state.error
59091
- }, undefined, false, undefined, this)
59092
- }, undefined, false, undefined, this)
59093
- ]
59094
- }, undefined, true, undefined, this),
59095
- /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
59079
+ marginTop: 1,
59096
59080
  children: /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
59097
- color: "#6b7280",
59098
- children: [
59099
- "Press ",
59100
- /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
59101
- color: "#ef4444",
59102
- children: "Esc"
59103
- }, undefined, false, undefined, this),
59104
- " or",
59105
- " ",
59106
- /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
59107
- color: "#ef4444",
59108
- children: "Ctrl+C"
59109
- }, undefined, false, undefined, this),
59110
- " to exit"
59111
- ]
59112
- }, undefined, true, undefined, this)
59081
+ color: "#e5e7eb",
59082
+ children: state.error
59083
+ }, undefined, false, undefined, this)
59113
59084
  }, undefined, false, undefined, this)
59114
59085
  ]
59115
59086
  }, undefined, true, undefined, this);
@@ -59171,7 +59142,7 @@ var BetterCommitApp = ({
59171
59142
  /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
59172
59143
  bold: true,
59173
59144
  color: "#8b5cf6",
59174
- children: "Better Commit"
59145
+ children: "Better-Commit"
59175
59146
  }, undefined, false, undefined, this),
59176
59147
  /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
59177
59148
  marginTop: 1,
@@ -59193,7 +59164,7 @@ var BetterCommitApp = ({
59193
59164
  /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
59194
59165
  bold: true,
59195
59166
  color: "#8b5cf6",
59196
- children: "Better Commit"
59167
+ children: "Better-Commit"
59197
59168
  }, undefined, false, undefined, this),
59198
59169
  /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
59199
59170
  color: "#6b7280",
@@ -59477,7 +59448,18 @@ var ConfigApp = ({ onExit }) => {
59477
59448
  const [config, setConfig] = import_react26.useState(configManager.getConfig());
59478
59449
  const [activeDialog, setActiveDialog] = import_react26.useState(null);
59479
59450
  const [menuIndex, setMenuIndex] = import_react26.useState(0);
59480
- const menuItems = [
59451
+ const modelOptions = import_react26.useMemo(() => [
59452
+ {
59453
+ display: "llama-3.1-8b-instant (fastest)",
59454
+ value: "llama-3.1-8b-instant"
59455
+ },
59456
+ {
59457
+ display: "llama-3.3-70b-versatile (most capable)",
59458
+ value: "llama-3.3-70b-versatile"
59459
+ },
59460
+ { display: "openai/gpt-oss-20b (balanced)", value: "openai/gpt-oss-20b" }
59461
+ ], []);
59462
+ const menuItems = import_react26.useMemo(() => [
59481
59463
  {
59482
59464
  key: "groqApiKey",
59483
59465
  label: "Groq API Key",
@@ -59487,11 +59469,7 @@ var ConfigApp = ({ onExit }) => {
59487
59469
  key: "model",
59488
59470
  label: "AI Model",
59489
59471
  type: "select",
59490
- options: [
59491
- "llama-3.1-8b-instant",
59492
- "llama-3.3-70b-versatile",
59493
- "openai/gpt-oss-20b"
59494
- ]
59472
+ options: modelOptions.map((m2) => m2.display)
59495
59473
  },
59496
59474
  {
59497
59475
  key: "commitStyle",
@@ -59504,8 +59482,8 @@ var ConfigApp = ({ onExit }) => {
59504
59482
  label: "Custom Prompt",
59505
59483
  type: "textarea"
59506
59484
  }
59507
- ];
59508
- const saveAndExit = () => {
59485
+ ], [modelOptions]);
59486
+ const saveAndExit = import_react26.useCallback(() => {
59509
59487
  const finalConfig = {
59510
59488
  ...config,
59511
59489
  maxHistoryCommits: 40,
@@ -59514,11 +59492,11 @@ var ConfigApp = ({ onExit }) => {
59514
59492
  configManager.updateConfig(finalConfig);
59515
59493
  onExit("Configuration saved");
59516
59494
  exit();
59517
- };
59518
- const cancelAndExit = () => {
59495
+ }, [config, onExit, exit]);
59496
+ const cancelAndExit = import_react26.useCallback(() => {
59519
59497
  onExit("Configuration cancelled");
59520
59498
  exit();
59521
- };
59499
+ }, [onExit, exit]);
59522
59500
  const handleInput = import_react26.useCallback((input, key) => {
59523
59501
  if (activeDialog)
59524
59502
  return;
@@ -59543,11 +59521,18 @@ var ConfigApp = ({ onExit }) => {
59543
59521
  } else if (key.escape || key.ctrl && input === "c") {
59544
59522
  cancelAndExit();
59545
59523
  }
59546
- }, [activeDialog, menuIndex, menuItems]);
59524
+ }, [activeDialog, menuIndex, menuItems, saveAndExit, cancelAndExit]);
59547
59525
  use_input_default(handleInput, { isActive: !activeDialog });
59548
59526
  const handleDialogSubmit = (value) => {
59549
59527
  if (activeDialog) {
59550
- setConfig((prev) => ({ ...prev, [activeDialog.key]: value }));
59528
+ let finalValue = value;
59529
+ if (activeDialog.key === "model") {
59530
+ const modelOption = modelOptions.find((m2) => m2.display === value);
59531
+ if (modelOption) {
59532
+ finalValue = modelOption.value;
59533
+ }
59534
+ }
59535
+ setConfig((prev) => ({ ...prev, [activeDialog.key]: finalValue }));
59551
59536
  setActiveDialog(null);
59552
59537
  }
59553
59538
  };
@@ -59573,6 +59558,15 @@ var ConfigApp = ({ onExit }) => {
59573
59558
  backgroundColor: bgColor,
59574
59559
  children: "••••••••"
59575
59560
  }, undefined, false, undefined, this);
59561
+ if (key === "model") {
59562
+ const modelOption = modelOptions.find((m2) => m2.value === val);
59563
+ const displayValue = modelOption ? modelOption.display : val;
59564
+ return /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Text, {
59565
+ color: textColor,
59566
+ backgroundColor: bgColor,
59567
+ children: displayValue
59568
+ }, undefined, false, undefined, this);
59569
+ }
59576
59570
  if (key === "customPrompt")
59577
59571
  return /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Text, {
59578
59572
  color: textColor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sncommit",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "AI-powered git commit message generator with beautiful TUI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -53,4 +53,4 @@
53
53
  "node": ">=18.0.0",
54
54
  "bun": ">=1.0.0"
55
55
  }
56
- }
56
+ }
@@ -40,12 +40,18 @@ export const BetterCommitApp: React.FC<AppProps> = ({
40
40
  if (successMessage) {
41
41
  write("\x1B[2J\x1B[0f");
42
42
  const timer = setTimeout(() => {
43
- onExit(successMessage);
44
43
  exit();
45
44
  }, 1500);
46
45
  return () => clearTimeout(timer);
47
46
  }
48
- }, [successMessage, onExit, exit, write]);
47
+ }, [successMessage, exit, write]);
48
+
49
+ // Auto-exit when error occurs
50
+ useEffect(() => {
51
+ if (state.error) {
52
+ exit();
53
+ }
54
+ }, [state.error, exit]);
49
55
 
50
56
  // Handle global exit keys (disabled when custom input is active)
51
57
  useInput(
@@ -69,7 +75,7 @@ export const BetterCommitApp: React.FC<AppProps> = ({
69
75
 
70
76
  await gitService.getDiff();
71
77
 
72
- await generateSuggestions(stagedFiles);
78
+ await fetchSuggestions(stagedFiles);
73
79
 
74
80
  setState((prev) => ({
75
81
  ...prev,
@@ -87,12 +93,15 @@ export const BetterCommitApp: React.FC<AppProps> = ({
87
93
  // eslint-disable-next-line react-hooks/exhaustive-deps
88
94
  }, []);
89
95
 
90
- const generateSuggestions = async (stagedFiles: GitFile[]) => {
96
+ const fetchSuggestions = async (
97
+ stagedFiles: GitFile[],
98
+ customPrompt?: string,
99
+ ) => {
91
100
  if (!config.groqApiKey || config.groqApiKey.trim() === "") {
92
101
  setState((prev) => ({
93
102
  ...prev,
94
103
  error:
95
- 'Groq API key not configured. Run "better-commit --config" to set it up.',
104
+ 'Groq API key not configured. Run "better-commit config" to set it up.',
96
105
  isLoading: false,
97
106
  }));
98
107
  return;
@@ -106,12 +115,24 @@ export const BetterCommitApp: React.FC<AppProps> = ({
106
115
  config.maxHistoryCommits || 50,
107
116
  );
108
117
 
109
- const suggestions = await groqService.generateCommitSuggestions(
110
- stagedFiles,
111
- diff,
112
- recentCommits,
113
- diffStats,
114
- );
118
+ let suggestions;
119
+ if (customPrompt) {
120
+ suggestions =
121
+ await groqService.generateCommitSuggestionsFromCustomInput(
122
+ stagedFiles,
123
+ diff,
124
+ customPrompt,
125
+ recentCommits,
126
+ diffStats,
127
+ );
128
+ } else {
129
+ suggestions = await groqService.generateCommitSuggestions(
130
+ stagedFiles,
131
+ diff,
132
+ recentCommits,
133
+ diffStats,
134
+ );
135
+ }
115
136
 
116
137
  const hasFallback = suggestions.some((s) => s.isFallback);
117
138
  setState((prev) => ({
@@ -151,7 +172,7 @@ export const BetterCommitApp: React.FC<AppProps> = ({
151
172
 
152
173
  const handleTryAgain = () => {
153
174
  setState((prev) => ({ ...prev, isLoading: true, suggestions: [] }));
154
- generateSuggestions(state.stagedFiles);
175
+ fetchSuggestions(state.stagedFiles);
155
176
  };
156
177
 
157
178
  const handleCustomInput = () => {
@@ -173,76 +194,23 @@ export const BetterCommitApp: React.FC<AppProps> = ({
173
194
 
174
195
  setIsCustomInputMode(false);
175
196
  setState((prev) => ({ ...prev, isLoading: true, suggestions: [] }));
176
- await generateSuggestionsFromCustomInput(customInput.trim());
177
- };
178
-
179
- const generateSuggestionsFromCustomInput = async (userInput: string) => {
180
- if (!config.groqApiKey || config.groqApiKey.trim() === "") {
181
- setState((prev) => ({
182
- ...prev,
183
- error:
184
- 'Groq API key not configured. Run "better-commit --config" to set it up.',
185
- isLoading: false,
186
- }));
187
- return;
188
- }
189
-
190
- try {
191
- const groqService = new GroqService(config.groqApiKey, config);
192
- const diff = await gitService.getDiff();
193
- const diffStats = await gitService.getDiffStats();
194
- const recentCommits = await gitService.getRecentCommits(
195
- config.maxHistoryCommits || 50,
196
- );
197
-
198
- const suggestions =
199
- await groqService.generateCommitSuggestionsFromCustomInput(
200
- state.stagedFiles,
201
- diff,
202
- userInput,
203
- recentCommits,
204
- diffStats,
205
- );
206
-
207
- const hasFallback = suggestions.some((s) => s.isFallback);
208
- setState((prev) => ({
209
- ...prev,
210
- suggestions,
211
- isLoading: false,
212
- error: undefined,
213
- }));
214
- setIsUsingFallback(hasFallback);
215
- } catch (error) {
216
- setState((prev) => ({
217
- ...prev,
218
- error: `Failed to generate suggestions: ${error instanceof Error ? error.message : String(error)}`,
219
- isLoading: false,
220
- }));
221
- }
197
+ await fetchSuggestions(state.stagedFiles, customInput.trim());
222
198
  };
223
199
 
224
200
  // Error State
225
201
  if (state.error) {
226
202
  return (
227
- <Box flexDirection="column" padding={2}>
228
- <Box
229
- borderStyle="single"
230
- borderColor="#ef4444"
231
- padding={2}
232
- marginBottom={1}
233
- >
234
- <Text color="#ef4444" bold>
235
- Error
236
- </Text>
237
- <Box marginTop={1}>
238
- <Text color="#e5e7eb">{state.error}</Text>
239
- </Box>
240
- </Box>
241
- <Box>
242
- <Text color="#6b7280">
243
- Press <Text color="#ef4444">Esc</Text> or{" "}
244
- <Text color="#ef4444">Ctrl+C</Text> to exit
245
- </Text>
203
+ <Box
204
+ borderStyle="single"
205
+ borderColor="#ef4444"
206
+ padding={2}
207
+ marginBottom={1}
208
+ >
209
+ <Text color="#ef4444" bold>
210
+ Error
211
+ </Text>
212
+ <Box marginTop={1}>
213
+ <Text color="#e5e7eb">{state.error}</Text>
246
214
  </Box>
247
215
  </Box>
248
216
  );
@@ -290,7 +258,7 @@ export const BetterCommitApp: React.FC<AppProps> = ({
290
258
  return (
291
259
  <Box flexDirection="column" padding={2} justifyContent="center">
292
260
  <Text bold color="#8b5cf6">
293
- Better Commit
261
+ Better-Commit
294
262
  </Text>
295
263
  <Box marginTop={1}>
296
264
  <Text color="#6b7280">Loading staged files...</Text>
@@ -304,7 +272,7 @@ export const BetterCommitApp: React.FC<AppProps> = ({
304
272
  {/* Header */}
305
273
  <Box marginBottom={1}>
306
274
  <Text bold color="#8b5cf6">
307
- Better Commit
275
+ Better-Commit
308
276
  </Text>
309
277
  <Text color="#6b7280"> • AI-Powered Commit Suggestions</Text>
310
278
  </Box>
@@ -39,7 +39,7 @@ export const CommitSuggestions: React.FC<CommitSuggestionsProps> = ({
39
39
  }, 80);
40
40
  return () => clearInterval(timer);
41
41
  }
42
- }, [isLoading]);
42
+ }, [isLoading, spinnerFrames.length]);
43
43
 
44
44
  useInput((input, key) => {
45
45
  if (key.upArrow) {
@@ -1,4 +1,4 @@
1
- import React, { useState, useCallback } from "react";
1
+ import React, { useState, useCallback, useMemo } from "react";
2
2
  import { Box, Text, useInput, useApp, Key } from "ink";
3
3
  import { configManager } from "../utils/config";
4
4
  import { Config } from "../types";
@@ -21,36 +21,50 @@ export const ConfigApp: React.FC<ConfigAppProps> = ({ onExit }) => {
21
21
 
22
22
  const [menuIndex, setMenuIndex] = useState(0);
23
23
 
24
- const menuItems = [
25
- {
26
- key: "groqApiKey" as keyof Config,
27
- label: "Groq API Key",
28
- type: "password" as const,
29
- },
30
- {
31
- key: "model" as keyof Config,
32
- label: "AI Model",
33
- type: "select" as const,
34
- options: [
35
- "llama-3.1-8b-instant",
36
- "llama-3.3-70b-versatile",
37
- "openai/gpt-oss-20b",
38
- ],
39
- },
40
- {
41
- key: "commitStyle" as keyof Config,
42
- label: "Commit Style",
43
- type: "select" as const,
44
- options: ["conventional", "simple", "detailed"],
45
- },
46
- {
47
- key: "customPrompt" as keyof Config,
48
- label: "Custom Prompt",
49
- type: "textarea" as const,
50
- },
51
- ];
24
+ const modelOptions = useMemo(
25
+ () => [
26
+ {
27
+ display: "llama-3.1-8b-instant (fastest)",
28
+ value: "llama-3.1-8b-instant",
29
+ },
30
+ {
31
+ display: "llama-3.3-70b-versatile (most capable)",
32
+ value: "llama-3.3-70b-versatile",
33
+ },
34
+ { display: "openai/gpt-oss-20b (balanced)", value: "openai/gpt-oss-20b" },
35
+ ],
36
+ [],
37
+ );
52
38
 
53
- const saveAndExit = () => {
39
+ const menuItems = useMemo(
40
+ () => [
41
+ {
42
+ key: "groqApiKey" as keyof Config,
43
+ label: "Groq API Key",
44
+ type: "password" as const,
45
+ },
46
+ {
47
+ key: "model" as keyof Config,
48
+ label: "AI Model",
49
+ type: "select" as const,
50
+ options: modelOptions.map((m) => m.display),
51
+ },
52
+ {
53
+ key: "commitStyle" as keyof Config,
54
+ label: "Commit Style",
55
+ type: "select" as const,
56
+ options: ["conventional", "simple", "detailed"],
57
+ },
58
+ {
59
+ key: "customPrompt" as keyof Config,
60
+ label: "Custom Prompt",
61
+ type: "textarea" as const,
62
+ },
63
+ ],
64
+ [modelOptions],
65
+ );
66
+
67
+ const saveAndExit = useCallback(() => {
54
68
  const finalConfig = {
55
69
  ...config,
56
70
  // model was hardcoded here previously, now we let user select it
@@ -61,12 +75,12 @@ export const ConfigApp: React.FC<ConfigAppProps> = ({ onExit }) => {
61
75
  configManager.updateConfig(finalConfig);
62
76
  onExit("Configuration saved");
63
77
  exit();
64
- };
78
+ }, [config, onExit, exit]);
65
79
 
66
- const cancelAndExit = () => {
80
+ const cancelAndExit = useCallback(() => {
67
81
  onExit("Configuration cancelled");
68
82
  exit();
69
- };
83
+ }, [onExit, exit]);
70
84
 
71
85
  const handleInput = useCallback(
72
86
  (input: string, key: Key) => {
@@ -93,16 +107,25 @@ export const ConfigApp: React.FC<ConfigAppProps> = ({ onExit }) => {
93
107
  } else if (key.escape || (key.ctrl && input === "c")) {
94
108
  cancelAndExit();
95
109
  }
96
- // eslint-disable-next-line react-hooks/exhaustive-deps
97
110
  },
98
- [activeDialog, menuIndex, menuItems],
111
+ [activeDialog, menuIndex, menuItems, saveAndExit, cancelAndExit],
99
112
  );
100
113
 
101
114
  useInput(handleInput, { isActive: !activeDialog });
102
115
 
103
116
  const handleDialogSubmit = (value: string) => {
104
117
  if (activeDialog) {
105
- setConfig((prev) => ({ ...prev, [activeDialog.key]: value }));
118
+ let finalValue = value;
119
+
120
+ // Convert display label back to actual model ID
121
+ if (activeDialog.key === "model") {
122
+ const modelOption = modelOptions.find((m) => m.display === value);
123
+ if (modelOption) {
124
+ finalValue = modelOption.value;
125
+ }
126
+ }
127
+
128
+ setConfig((prev) => ({ ...prev, [activeDialog.key]: finalValue }));
106
129
  setActiveDialog(null);
107
130
  }
108
131
  };
@@ -129,6 +152,16 @@ export const ConfigApp: React.FC<ConfigAppProps> = ({ onExit }) => {
129
152
  ••••••••
130
153
  </Text>
131
154
  );
155
+ if (key === "model") {
156
+ // Display the friendly label instead of the raw model ID
157
+ const modelOption = modelOptions.find((m) => m.value === val);
158
+ const displayValue = modelOption ? modelOption.display : val;
159
+ return (
160
+ <Text color={textColor} backgroundColor={bgColor}>
161
+ {displayValue}
162
+ </Text>
163
+ );
164
+ }
132
165
  if (key === "customPrompt")
133
166
  return (
134
167
  <Text color={textColor} backgroundColor={bgColor}>
@@ -53,9 +53,21 @@ export class GroqService {
53
53
  }
54
54
 
55
55
  return this.parseSuggestions(content);
56
- } catch (e) {
57
- console.error("Error generating suggestions:", e);
58
- // Fallback to mock suggestions when API fails (silently handle)
56
+ } catch (e: unknown) {
57
+ const error = e as { status?: number; message?: string };
58
+ if (error?.status === 401) {
59
+ console.warn("\n\x1b[33m⚠️ Groq API Key is invalid.\x1b[0m");
60
+ console.warn(
61
+ " Using static backup suggestions. Run \x1b[36mbetter-commit config\x1b[0m to set your key.\n",
62
+ );
63
+ } else {
64
+ console.error(
65
+ "Error generation suggestions:",
66
+ error?.message || String(e),
67
+ );
68
+ }
69
+
70
+ // Fallback to mock suggestions when API fails
59
71
  const fallbackSuggestions = this.getFallbackSuggestions(stagedFiles);
60
72
  return fallbackSuggestions.map((s) => ({ ...s, isFallback: true }));
61
73
  }
@@ -110,8 +122,19 @@ export class GroqService {
110
122
  }
111
123
 
112
124
  return this.parseSuggestions(content);
113
- } catch (e) {
114
- console.error("Error generating suggestions:", e);
125
+ } catch (e: unknown) {
126
+ const error = e as { status?: number; message?: string };
127
+ if (error?.status === 401) {
128
+ console.warn("\n\x1b[33m⚠️ Groq API Key is invalid.\x1b[0m");
129
+ console.warn(
130
+ " Using static backup suggestions. Run \x1b[36mbetter-commit config\x1b[0m to set your key.\n",
131
+ );
132
+ } else {
133
+ console.error(
134
+ "Error generation suggestions:",
135
+ error?.message || String(e),
136
+ );
137
+ }
115
138
  // Fallback to mock suggestions when API fails (silently handle)
116
139
  const fallbackSuggestions = this.getFallbackSuggestions(stagedFiles);
117
140
  return fallbackSuggestions.map((s) => ({ ...s, isFallback: true }));
@@ -178,18 +201,20 @@ IMPORTANT: Carefully analyze the git diff below to understand WHAT changes were
178
201
  Files staged for commit:
179
202
  ${filesText}${statsText}
180
203
 
181
- ${diff
182
- ? `Complete git diff (analyze this carefully to understand the actual changes):\n${diff.slice(
183
- 0,
184
- 2000,
185
- )}\n${diff.length > 2000 ? "...(truncated)" : ""}`
186
- : "No diff available"
187
- }
204
+ ${
205
+ diff
206
+ ? `Complete git diff (analyze this carefully to understand the actual changes):\n${diff.slice(
207
+ 0,
208
+ 2000,
209
+ )}\n${diff.length > 2000 ? "...(truncated)" : ""}`
210
+ : "No diff available"
211
+ }
188
212
 
189
- ${recentCommitsText
190
- ? `Recent commit history for reference:\n${recentCommitsText}`
191
- : ""
192
- }
213
+ ${
214
+ recentCommitsText
215
+ ? `Recent commit history for reference:\n${recentCommitsText}`
216
+ : ""
217
+ }
193
218
 
194
219
  Requirements:
195
220
  - Generate exactly 4 different commit messages
@@ -206,9 +231,9 @@ Output ONLY valid JSON.`;
206
231
  const fileNames =
207
232
  stagedFiles.length > 0
208
233
  ? stagedFiles
209
- .map((f) => f.path)
210
- .slice(0, 3)
211
- .join(", ") + (stagedFiles.length > 3 ? "..." : "")
234
+ .map((f) => f.path)
235
+ .slice(0, 3)
236
+ .join(", ") + (stagedFiles.length > 3 ? "..." : "")
212
237
  : "files";
213
238
 
214
239
  const suggestions = [
@@ -294,18 +319,20 @@ IMPORTANT: Carefully analyze the git diff below to understand WHAT changes were
294
319
  Files staged for commit:
295
320
  ${filesText}${statsText}
296
321
 
297
- ${diff
298
- ? `Complete git diff (analyze this carefully to understand the actual changes):\n${diff.slice(
299
- 0,
300
- 2000,
301
- )}\n${diff.length > 2000 ? "...(truncated)" : ""}`
302
- : "No diff available"
303
- }
322
+ ${
323
+ diff
324
+ ? `Complete git diff (analyze this carefully to understand the actual changes):\n${diff.slice(
325
+ 0,
326
+ 2000,
327
+ )}\n${diff.length > 2000 ? "...(truncated)" : ""}`
328
+ : "No diff available"
329
+ }
304
330
 
305
- ${recentCommitsText
306
- ? `Recent commit history for reference:\n${recentCommitsText}`
307
- : ""
308
- }
331
+ ${
332
+ recentCommitsText
333
+ ? `Recent commit history for reference:\n${recentCommitsText}`
334
+ : ""
335
+ }
309
336
 
310
337
  Requirements:
311
338
  - Generate exactly 4 different commit messages
@@ -325,16 +352,29 @@ Output ONLY valid JSON.`;
325
352
  if (jsonMatch) {
326
353
  const parsed = JSON.parse(jsonMatch[0]);
327
354
  if (parsed.suggestions && Array.isArray(parsed.suggestions)) {
328
- return parsed.suggestions.map((s: any) => ({
329
- message: s.message || "",
330
- type: s.type || this.extractType(s.message || ""),
331
- description: s.description || s.message || "",
332
- })).slice(0, 4);
355
+ if (parsed.suggestions && Array.isArray(parsed.suggestions)) {
356
+ return parsed.suggestions
357
+ .map(
358
+ (s: {
359
+ message?: string;
360
+ type?: string;
361
+ description?: string;
362
+ }) => ({
363
+ message: s.message || "",
364
+ type: s.type || this.extractType(s.message || ""),
365
+ description: s.description || s.message || "",
366
+ }),
367
+ )
368
+ .slice(0, 4);
369
+ }
333
370
  }
334
371
  }
335
372
  throw new Error("Invalid JSON structure");
336
373
  } catch (e) {
337
- console.warn("Failed to parse JSON suggestions, falling back to basic parsing:", e);
374
+ console.warn(
375
+ "Failed to parse JSON suggestions, falling back to basic parsing:",
376
+ e,
377
+ );
338
378
  // Fallback to basic line parsing if JSON fails
339
379
  const lines = content.split("\n").filter((line) => line.trim());
340
380
  const suggestions: CommitSuggestion[] = [];