sncommit 1.0.1 → 1.0.3

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.
@@ -1,291 +0,0 @@
1
- import React, { useState, useCallback, useMemo } from "react";
2
- import { Box, Text, useInput, useApp, Key } from "ink";
3
- import { configManager } from "../utils/config";
4
- import { Config } from "../types";
5
- import { TuiDialog, DialogType } from "./TuiDialog";
6
-
7
- interface ConfigAppProps {
8
- onExit: (message?: string) => void;
9
- }
10
-
11
- export const ConfigApp: React.FC<ConfigAppProps> = ({ onExit }) => {
12
- const { exit } = useApp();
13
- const [config, setConfig] = useState<Config>(configManager.getConfig());
14
-
15
- const [activeDialog, setActiveDialog] = useState<{
16
- key: keyof Config;
17
- type: DialogType;
18
- title: string;
19
- options?: string[];
20
- } | null>(null);
21
-
22
- const [menuIndex, setMenuIndex] = useState(0);
23
-
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
- );
38
-
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(() => {
68
- const finalConfig = {
69
- ...config,
70
- // model was hardcoded here previously, now we let user select it
71
- // model: "llama-3.1-8b-instant",
72
- maxHistoryCommits: 40,
73
- language: "en",
74
- };
75
- configManager.updateConfig(finalConfig);
76
- onExit("Configuration saved");
77
- exit();
78
- }, [config, onExit, exit]);
79
-
80
- const cancelAndExit = useCallback(() => {
81
- onExit("Configuration cancelled");
82
- exit();
83
- }, [onExit, exit]);
84
-
85
- const handleInput = useCallback(
86
- (input: string, key: Key) => {
87
- if (activeDialog) return;
88
-
89
- if (key.upArrow) {
90
- setMenuIndex((prev) => (prev > 0 ? prev - 1 : menuItems.length + 1));
91
- } else if (key.downArrow) {
92
- setMenuIndex((prev) => (prev < menuItems.length + 1 ? prev + 1 : 0));
93
- } else if (key.return) {
94
- if (menuIndex < menuItems.length) {
95
- const item = menuItems[menuIndex];
96
- setActiveDialog({
97
- key: item.key,
98
- type: item.type,
99
- title: `Edit ${item.label}`,
100
- options: item.options,
101
- });
102
- } else if (menuIndex === menuItems.length) {
103
- saveAndExit();
104
- } else {
105
- cancelAndExit();
106
- }
107
- } else if (key.escape || (key.ctrl && input === "c")) {
108
- cancelAndExit();
109
- }
110
- },
111
- [activeDialog, menuIndex, menuItems, saveAndExit, cancelAndExit],
112
- );
113
-
114
- useInput(handleInput, { isActive: !activeDialog });
115
-
116
- const handleDialogSubmit = (value: string) => {
117
- if (activeDialog) {
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 }));
129
- setActiveDialog(null);
130
- }
131
- };
132
-
133
- const handleDialogCancel = () => {
134
- setActiveDialog(null);
135
- };
136
-
137
- const renderValue = (isSelected: boolean, key: keyof Config) => {
138
- const val = config[key];
139
- const textColor = isSelected ? "#111827" : "#e5e7eb";
140
- const bgColor = isSelected ? "#60a5fa" : undefined;
141
-
142
- if (!val)
143
- return (
144
- <Text color={textColor} backgroundColor={bgColor}>
145
- {" "}
146
- (not set)
147
- </Text>
148
- );
149
- if (key === "groqApiKey")
150
- return (
151
- <Text color={textColor} backgroundColor={bgColor}>
152
- ••••••••
153
- </Text>
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
- }
165
- if (key === "customPrompt")
166
- return (
167
- <Text color={textColor} backgroundColor={bgColor}>
168
- {(val as string).substring(0, 20) +
169
- ((val as string).length > 20 ? "..." : "")}
170
- </Text>
171
- );
172
- return (
173
- <Text color={textColor} backgroundColor={bgColor}>
174
- {val}
175
- </Text>
176
- );
177
- };
178
-
179
- return (
180
- <Box flexDirection="column" padding={1} flexGrow={1} width="100%">
181
- {/* Dialog or Main Content */}
182
- {activeDialog ? (
183
- <Box
184
- flexGrow={1}
185
- justifyContent="center"
186
- alignItems="center"
187
- paddingX={2}
188
- >
189
- <TuiDialog
190
- title={activeDialog.title}
191
- type={activeDialog.type}
192
- initialValue={String(config[activeDialog.key] || "")}
193
- options={activeDialog.options}
194
- onSubmit={handleDialogSubmit}
195
- onCancel={handleDialogCancel}
196
- />
197
- </Box>
198
- ) : (
199
- <Box
200
- flexDirection="column"
201
- flexGrow={1}
202
- borderStyle="round"
203
- borderColor="#374151"
204
- paddingX={1}
205
- marginBottom={1}
206
- >
207
- {/* Menu Items */}
208
- <Box flexDirection="column" marginBottom={2}>
209
- {menuItems.map((item, index) => (
210
- <Box
211
- key={item.key}
212
- flexDirection="row"
213
- justifyContent="space-between"
214
- paddingY={0.5}
215
- paddingX={1}
216
- >
217
- <Box flexGrow={0.6}>
218
- <Text
219
- bold={index === menuIndex}
220
- color={index === menuIndex ? "#111827" : "#e5e7eb"}
221
- backgroundColor={
222
- index === menuIndex ? "#60a5fa" : undefined
223
- }
224
- >
225
- {index === menuIndex ? "› " : " "}
226
- {item.label}
227
- </Text>
228
- </Box>
229
- <Box flexGrow={0.4}>
230
- <Text
231
- color={index === menuIndex ? "#111827" : "#e5e7eb"}
232
- backgroundColor={
233
- index === menuIndex ? "#60a5fa" : undefined
234
- }
235
- >
236
- {renderValue(index === menuIndex, item.key)}
237
- </Text>
238
- </Box>
239
- </Box>
240
- ))}
241
- </Box>
242
-
243
- {/* Actions */}
244
- <Box flexDirection="row" marginBottom={2}>
245
- <Box marginRight={2}>
246
- <Text
247
- bold={menuIndex === menuItems.length}
248
- color={menuIndex === menuItems.length ? "#111827" : "#e5e7eb"}
249
- backgroundColor={
250
- menuIndex === menuItems.length ? "#10b981" : undefined
251
- }
252
- >
253
- {menuIndex === menuItems.length ? "› " : " "}Save & Exit
254
- </Text>
255
- </Box>
256
- <Box>
257
- <Text
258
- bold={menuIndex === menuItems.length + 1}
259
- color={
260
- menuIndex === menuItems.length + 1 ? "#111827" : "#e5e7eb"
261
- }
262
- backgroundColor={
263
- menuIndex === menuItems.length + 1 ? "#ef4444" : undefined
264
- }
265
- >
266
- {menuIndex === menuItems.length + 1 ? "› " : " "}Cancel
267
- </Text>
268
- </Box>
269
- </Box>
270
- </Box>
271
- )}
272
-
273
- {/* Footer - only show when no dialog is open */}
274
- {!activeDialog && (
275
- <Box
276
- borderTop={true}
277
- borderStyle="single"
278
- borderColor="#374151"
279
- paddingTop={1}
280
- paddingLeft={1}
281
- paddingRight={1}
282
- >
283
- <Text color="#6b7280">
284
- Use <Text color="#60a5fa">↑↓</Text> to navigate,{" "}
285
- <Text color="#10b981">Enter</Text> to edit/select
286
- </Text>
287
- </Box>
288
- )}
289
- </Box>
290
- );
291
- };
@@ -1,82 +0,0 @@
1
- import React, { useState } from "react";
2
- import { Box, Text, useInput } from "ink";
3
-
4
- interface CustomInputPromptProps {
5
- value: string;
6
- onChange: (value: string) => void;
7
- onSubmit: () => void;
8
- onCancel: () => void;
9
- }
10
-
11
- export const CustomInputPrompt: React.FC<CustomInputPromptProps> = ({
12
- value,
13
- onChange,
14
- onSubmit,
15
- onCancel,
16
- }) => {
17
- const [cursorPosition, setCursorPosition] = useState(value.length);
18
-
19
- useInput((input, key) => {
20
- if (key.return) {
21
- onSubmit();
22
- } else if (key.escape) {
23
- onCancel();
24
- } else if (key.backspace || key.delete) {
25
- if (cursorPosition > 0) {
26
- const before = value.slice(0, cursorPosition - 1);
27
- const after = value.slice(cursorPosition);
28
- onChange(before + after);
29
- setCursorPosition(cursorPosition - 1);
30
- }
31
- } else if (key.leftArrow) {
32
- setCursorPosition((prev: number) => Math.max(0, prev - 1));
33
- } else if (key.rightArrow) {
34
- setCursorPosition((prev: number) => Math.min(value.length, prev + 1));
35
- } else if (input && !key.ctrl && !key.meta) {
36
- const before = value.slice(0, cursorPosition);
37
- const after = value.slice(cursorPosition);
38
- onChange(before + input + after);
39
- setCursorPosition(cursorPosition + input.length);
40
- }
41
- });
42
-
43
- const displayValue = value || "";
44
- const beforeCursor = displayValue.slice(0, cursorPosition);
45
- const cursorChar = displayValue[cursorPosition] || " ";
46
- const afterCursor = displayValue.slice(cursorPosition + 1);
47
-
48
- return (
49
- <Box
50
- flexDirection="column"
51
- borderStyle="round"
52
- borderColor="#8b5cf6"
53
- paddingX={1}
54
- >
55
- <Box>
56
- <Text bold color="#8b5cf6">
57
- Custom Message
58
- </Text>
59
- </Box>
60
-
61
- <Box marginLeft={1}>
62
- <Text>
63
- <Text color="#e5e7eb">{beforeCursor}</Text>
64
- <Text backgroundColor="#8b5cf6" color="#ffffff" bold>
65
- {cursorChar}
66
- </Text>
67
- <Text color="#e5e7eb">{afterCursor}</Text>
68
- {displayValue.length === 0 && (
69
- <Text color="#6b7280"> type your commit message...</Text>
70
- )}
71
- </Text>
72
- </Box>
73
-
74
- <Box marginTop={1}>
75
- <Text color="#6b7280">
76
- <Text color="#10b981">Enter</Text> submit{" "}
77
- <Text color="#ef4444">Esc</Text> cancel
78
- </Text>
79
- </Box>
80
- </Box>
81
- );
82
- };
@@ -1,52 +0,0 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
3
- import { GitFile } from "../types";
4
-
5
- interface StagedFilesProps {
6
- files: GitFile[];
7
- }
8
-
9
- export const StagedFiles: React.FC<StagedFilesProps> = ({ files }) => {
10
- if (files.length === 0) {
11
- return (
12
- <Box
13
- borderStyle="round"
14
- borderColor="#ef4444"
15
- paddingX={1}
16
- marginBottom={1}
17
- >
18
- <Text color="#ef4444">No staged files found</Text>
19
- </Box>
20
- );
21
- }
22
-
23
- return (
24
- <Box
25
- flexDirection="column"
26
- borderStyle="round"
27
- borderColor="#374151"
28
- paddingX={1}
29
- marginBottom={1}
30
- >
31
- <Box>
32
- <Text bold color="#8b5cf6">
33
- Staged Files
34
- </Text>
35
- <Text color="#6b7280"> ({files.length})</Text>
36
- </Box>
37
-
38
- {files.slice(0, 6).map((file, index) => (
39
- <Box key={index} marginLeft={1}>
40
- <Text color="#10b981">•</Text>
41
- <Text color="#e5e7eb"> {file.path}</Text>
42
- </Box>
43
- ))}
44
-
45
- {files.length > 6 && (
46
- <Box marginLeft={1}>
47
- <Text color="#6b7280">+{files.length - 6} more files</Text>
48
- </Box>
49
- )}
50
- </Box>
51
- );
52
- };
@@ -1,177 +0,0 @@
1
- import React, { useState } from "react";
2
- import { Box, Text, useInput } from "ink";
3
-
4
- export type DialogType = "input" | "password" | "textarea" | "select";
5
-
6
- export interface DialogProps {
7
- title: string;
8
- type: DialogType;
9
- initialValue?: string;
10
- options?: string[];
11
- onSubmit: (value: string) => void;
12
- onCancel: () => void;
13
- }
14
-
15
- export const TuiDialog: React.FC<DialogProps> = ({
16
- title,
17
- type,
18
- initialValue = "",
19
- options = [],
20
- onSubmit,
21
- onCancel,
22
- }) => {
23
- const [value, setValue] = useState(initialValue);
24
- const [selectedIndex, setSelectedIndex] = useState(() => {
25
- if (type === "select" && initialValue) {
26
- const index = options.indexOf(initialValue);
27
- return index !== -1 ? index : 0;
28
- }
29
- return 0;
30
- });
31
- const [cursorPosition, setCursorPosition] = useState(initialValue.length);
32
-
33
- useInput((input, key) => {
34
- // Handle escape to cancel
35
- if (key.escape) {
36
- onCancel();
37
- return;
38
- }
39
-
40
- if (type === "select") {
41
- if (key.upArrow) {
42
- setSelectedIndex((prev) => (prev > 0 ? prev - 1 : options.length - 1));
43
- } else if (key.downArrow) {
44
- setSelectedIndex((prev) => (prev < options.length - 1 ? prev + 1 : 0));
45
- } else if (key.return) {
46
- onSubmit(options[selectedIndex]);
47
- }
48
- } else {
49
- // Input / Password / Textarea
50
- if (key.tab && type === "textarea") {
51
- // Tab: Submit for textarea
52
- onSubmit(value.trim());
53
- } else if (key.return) {
54
- if (type === "textarea") {
55
- // Enter: Insert newline for textarea
56
- const before = value.slice(0, cursorPosition);
57
- const after = value.slice(cursorPosition);
58
- setValue(before + "\n" + after);
59
- setCursorPosition(cursorPosition + 1);
60
- } else {
61
- // Enter: Submit for input/password
62
- onSubmit(value.trim());
63
- }
64
- } else if (key.leftArrow) {
65
- setCursorPosition((prev) => Math.max(0, prev - 1));
66
- } else if (key.rightArrow) {
67
- setCursorPosition((prev) => Math.min(value.length, prev + 1));
68
- } else if (key.backspace || key.delete) {
69
- if (cursorPosition > 0) {
70
- const before = value.slice(0, cursorPosition - 1);
71
- const after = value.slice(cursorPosition);
72
- setValue(before + after);
73
- setCursorPosition(cursorPosition - 1);
74
- }
75
- } else if (input && !key.ctrl && !key.meta) {
76
- const before = value.slice(0, cursorPosition);
77
- const after = value.slice(cursorPosition);
78
- setValue(before + input + after);
79
- setCursorPosition(cursorPosition + input.length);
80
- }
81
- }
82
- });
83
-
84
- const renderInputField = () => {
85
- const displayValue = type === "password" ? "•".repeat(value.length) : value;
86
- const beforeCursor = displayValue.slice(0, cursorPosition);
87
- const cursorChar = displayValue[cursorPosition] || " ";
88
- const afterCursor = displayValue.slice(cursorPosition + 1);
89
-
90
- return (
91
- <Box borderStyle="round" borderColor="#6366f1" paddingX={1} flexGrow={1}>
92
- <Text>
93
- <Text color="#e5e7eb">{beforeCursor}</Text>
94
- <Text backgroundColor="#8b5cf6" color="#ffffff" bold>
95
- {cursorChar}
96
- </Text>
97
- <Text color="#e5e7eb">{afterCursor}</Text>
98
- </Text>
99
- </Box>
100
- );
101
- };
102
-
103
- return (
104
- <Box
105
- flexDirection="column"
106
- flexGrow={1}
107
- padding={2}
108
- borderStyle="round"
109
- borderColor="#6366f1"
110
- >
111
- {/* Header */}
112
- <Box marginBottom={1}>
113
- <Text bold color="#8b5cf6">
114
- {title}
115
- </Text>
116
- </Box>
117
-
118
- {/* Content */}
119
- {type === "select" ? (
120
- <Box flexDirection="column" marginBottom={1}>
121
- {options.map((option, index) => (
122
- <Box key={option} paddingX={1}>
123
- <Text
124
- bold={index === selectedIndex}
125
- inverse={index === selectedIndex}
126
- >
127
- {index === selectedIndex ? "› " : " "}
128
- {option}
129
- </Text>
130
- </Box>
131
- ))}
132
- </Box>
133
- ) : (
134
- <Box flexDirection="column" marginBottom={1}>
135
- {renderInputField()}
136
- {type === "textarea" && (
137
- <Box marginTop={1}>
138
- <Text color="#6b7280" dimColor>
139
- Enter for new line • Tab to submit
140
- </Text>
141
- </Box>
142
- )}
143
- </Box>
144
- )}
145
-
146
- {/* Footer */}
147
- <Box
148
- borderTop={true}
149
- borderStyle="single"
150
- borderColor="#374151"
151
- paddingTop={1}
152
- >
153
- <Text color="#6b7280">
154
- {type === "select" ? (
155
- <Text>
156
- <Text color="#60a5fa">↑↓</Text> Navigate •{" "}
157
- <Text color="#10b981">Enter</Text> Confirm •{" "}
158
- <Text color="#ef4444">Esc</Text> Cancel
159
- </Text>
160
- ) : type === "textarea" ? (
161
- <Text>
162
- <Text color="#60a5fa">←→</Text> Move •{" "}
163
- <Text color="#10b981">Tab</Text> Submit •{" "}
164
- <Text color="#ef4444">Esc</Text> Cancel
165
- </Text>
166
- ) : (
167
- <Text>
168
- <Text color="#60a5fa">←→</Text> Move •{" "}
169
- <Text color="#10b981">Enter</Text> Confirm •{" "}
170
- <Text color="#ef4444">Esc</Text> Cancel
171
- </Text>
172
- )}
173
- </Text>
174
- </Box>
175
- </Box>
176
- );
177
- };