@mandujs/core 0.18.13 โ 0.18.15
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/package.json
CHANGED
|
@@ -43,6 +43,19 @@ const baseStyles = `
|
|
|
43
43
|
line-height: 1.5;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
button {
|
|
47
|
+
background: none;
|
|
48
|
+
border: none;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
font-family: inherit;
|
|
51
|
+
color: inherit;
|
|
52
|
+
font-size: inherit;
|
|
53
|
+
-webkit-appearance: none;
|
|
54
|
+
appearance: none;
|
|
55
|
+
padding: 0;
|
|
56
|
+
margin: 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
.mk-badge-container {
|
|
47
60
|
position: fixed;
|
|
48
61
|
z-index: ${zIndex.devtools};
|
|
@@ -247,7 +247,10 @@ export function ManduBadge({
|
|
|
247
247
|
onBlur={() => setIsHovered(false)}
|
|
248
248
|
aria-label={`Mandu Kitchen: ${character.message}${count > 0 ? `, ${count} issues` : ''}`}
|
|
249
249
|
>
|
|
250
|
-
<span
|
|
250
|
+
<span
|
|
251
|
+
aria-hidden="true"
|
|
252
|
+
style={{ fontFamily: "'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif" }}
|
|
253
|
+
>๐ฅ</span>
|
|
251
254
|
{count > 0 && <span style={countStyle}>{count}</span>}
|
|
252
255
|
</button>
|
|
253
256
|
);
|
|
@@ -112,7 +112,7 @@ const styles = {
|
|
|
112
112
|
tabActive: {
|
|
113
113
|
color: colors.brand.accent,
|
|
114
114
|
borderBottomColor: colors.brand.accent,
|
|
115
|
-
backgroundColor:
|
|
115
|
+
backgroundColor: 'rgba(232, 150, 122, 0.12)',
|
|
116
116
|
},
|
|
117
117
|
tabIcon: {
|
|
118
118
|
fontSize: typography.fontSize.md,
|
|
@@ -166,6 +166,8 @@ export function PanelContainer({
|
|
|
166
166
|
}: PanelContainerProps): React.ReactElement {
|
|
167
167
|
const [isResizing, setIsResizing] = useState(false);
|
|
168
168
|
const [height, setHeight] = useState(400);
|
|
169
|
+
const [hoveredTab, setHoveredTab] = useState<TabId | null>(null);
|
|
170
|
+
const [isCloseHovered, setIsCloseHovered] = useState(false);
|
|
169
171
|
|
|
170
172
|
const getTabBadgeCount = useCallback((tabId: TabId): number => {
|
|
171
173
|
switch (tabId) {
|
|
@@ -206,8 +208,13 @@ export function PanelContainer({
|
|
|
206
208
|
<span>Mandu Kitchen</span>
|
|
207
209
|
</div>
|
|
208
210
|
<button
|
|
209
|
-
style={
|
|
211
|
+
style={{
|
|
212
|
+
...styles.closeButton,
|
|
213
|
+
...(isCloseHovered ? { color: colors.text.primary, backgroundColor: 'rgba(255, 255, 255, 0.08)' } : {}),
|
|
214
|
+
}}
|
|
210
215
|
onClick={onClose}
|
|
216
|
+
onMouseEnter={() => setIsCloseHovered(true)}
|
|
217
|
+
onMouseLeave={() => setIsCloseHovered(false)}
|
|
211
218
|
aria-label="ํจ๋ ๋ซ๊ธฐ"
|
|
212
219
|
>
|
|
213
220
|
ร
|
|
@@ -229,8 +236,14 @@ export function PanelContainer({
|
|
|
229
236
|
style={{
|
|
230
237
|
...styles.tab,
|
|
231
238
|
...(isActive ? styles.tabActive : {}),
|
|
239
|
+
...(!isActive && hoveredTab === tab.id ? {
|
|
240
|
+
color: colors.text.primary,
|
|
241
|
+
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
|
242
|
+
} : {}),
|
|
232
243
|
}}
|
|
233
244
|
onClick={() => onTabChange(tab.id)}
|
|
245
|
+
onMouseEnter={() => setHoveredTab(tab.id)}
|
|
246
|
+
onMouseLeave={() => setHoveredTab(null)}
|
|
234
247
|
>
|
|
235
248
|
<span style={styles.tabIcon}>{tab.icon}</span>
|
|
236
249
|
<span>{tab.label}</span>
|
package/src/guard/negotiation.ts
CHANGED
|
@@ -37,6 +37,13 @@ export interface NegotiationRequest {
|
|
|
37
37
|
/** ๊ตฌํํ๋ ค๋ ๊ธฐ๋ฅ์ ์๋ */
|
|
38
38
|
intent: string;
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* ์๋ฌธ feature name slug (์์ด์ ํธ๊ฐ ์ ๊ณต).
|
|
42
|
+
* ์ ๊ณต๋๋ฉด extractFeatureName()์ ๊ฑด๋๋ฐ๊ณ ์ด ๊ฐ์ ๊ทธ๋๋ก ์ฌ์ฉ.
|
|
43
|
+
* ์: "chat", "user-auth", "payment", "file-upload"
|
|
44
|
+
*/
|
|
45
|
+
featureName?: string;
|
|
46
|
+
|
|
40
47
|
/** ์๊ตฌ์ฌํญ ๋ชฉ๋ก */
|
|
41
48
|
requirements?: string[];
|
|
42
49
|
|
|
@@ -1010,24 +1017,26 @@ function toCamelCase(str: string): string {
|
|
|
1010
1017
|
.replace(/^(.)/, (_, c) => c.toLowerCase());
|
|
1011
1018
|
}
|
|
1012
1019
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
"ํ": "team", "ํ๋ก์ ํธ": "project", "ํ์คํฌ": "task", "์ผ์ ": "schedule",
|
|
1025
|
-
"์ปดํฌ๋ํธ": "component", "๋ ์ด์์": "layout", "๋ค๋น๊ฒ์ด์
": "navigation",
|
|
1026
|
-
"์ค์๊ฐ": "realtime", "์คํธ๋ฆฌ๋ฐ": "streaming", "์น์์ผ": "websocket",
|
|
1027
|
-
};
|
|
1020
|
+
/** ์์ด์ ํธ ์ ๊ณต slug ์ ๋ฆฌ: lowercase kebab-case, ๋น ๋ฌธ์์ด์ด๋ฉด falsy */
|
|
1021
|
+
function sanitizeSlug(slug: string | undefined): string {
|
|
1022
|
+
if (!slug) return "";
|
|
1023
|
+
return slug
|
|
1024
|
+
.trim()
|
|
1025
|
+
.toLowerCase()
|
|
1026
|
+
.replace(/\s+/g, "-")
|
|
1027
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
1028
|
+
.replace(/-{2,}/g, "-")
|
|
1029
|
+
.replace(/^-+|-+$/g, "");
|
|
1030
|
+
}
|
|
1028
1031
|
|
|
1032
|
+
/**
|
|
1033
|
+
* intent์์ feature name ์ถ์ถ (featureName ๋ฏธ์ ๊ณต ์ fallback)
|
|
1034
|
+
*
|
|
1035
|
+
* MCP ์์ด์ ํธ๋ ํญ์ featureName์ ์๋ฌธ์ผ๋ก ์ ๊ณตํ๋ฏ๋ก,
|
|
1036
|
+
* ์ด ํจ์๋ CLI/ํ๋ก๊ทธ๋๋ฐ ์ง์ ํธ์ถ ์ fallback์ผ๋ก๋ง ์ฌ์ฉ.
|
|
1037
|
+
*/
|
|
1029
1038
|
function extractFeatureName(intent: string): string {
|
|
1030
|
-
// 1. ์๋ฌธ ํจํด
|
|
1039
|
+
// 1. ์๋ฌธ ํจํด ์ถ์ถ
|
|
1031
1040
|
const englishPatterns = [
|
|
1032
1041
|
/(?:add|implement|create|build)\s+(.+)/i,
|
|
1033
1042
|
/(.+?)\s+(?:feature|system|module|service)/i,
|
|
@@ -1046,46 +1055,13 @@ function extractFeatureName(intent: string): string {
|
|
|
1046
1055
|
}
|
|
1047
1056
|
}
|
|
1048
1057
|
|
|
1049
|
-
// 2.
|
|
1050
|
-
const koreanPatterns = [
|
|
1051
|
-
/(?:์ถ๊ฐ|๊ตฌํ|๋ง๋ค|๊ฐ๋ฐ|์์ฑ|์์ฑ)(?:์ด|ํด|ํ)[์ค์]?\s*[:\-]?\s*(.+)/,
|
|
1052
|
-
/(.+?)\s*(?:๊ธฐ๋ฅ|์์คํ
|๋ชจ๋|์๋น์ค|ํ์ด์ง|์ปดํฌ๋ํธ)\s*(?:์ถ๊ฐ|๊ตฌํ|๋ง๋ค|๊ฐ๋ฐ)?/,
|
|
1053
|
-
/(.+?)\s*(?:์|๋ฅผ|์|์)\s/,
|
|
1054
|
-
];
|
|
1055
|
-
|
|
1056
|
-
for (const pattern of koreanPatterns) {
|
|
1057
|
-
const match = intent.match(pattern);
|
|
1058
|
-
if (match) {
|
|
1059
|
-
const captured = match[1].trim();
|
|
1060
|
-
// ์บก์ฒ๋ ํ
์คํธ์์ ๋๋ฉ์ธ ํค์๋ ๋งค์นญ
|
|
1061
|
-
const slug = koreanToSlug(captured);
|
|
1062
|
-
if (slug) return slug;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// 3. intent ์ ์ฒด์์ ๋๋ฉ์ธ ํค์๋ ์ง์ ํ์
|
|
1067
|
-
const slug = koreanToSlug(intent);
|
|
1068
|
-
if (slug) return slug;
|
|
1069
|
-
|
|
1070
|
-
// 4. intent์์ ์๋ฌธ ๋จ์ด ์ถ์ถ ์๋
|
|
1058
|
+
// 2. intent ๋ด ์๋ฌธ ๋จ์ด ์ถ์ถ
|
|
1071
1059
|
const englishWord = intent.match(/[a-z][a-z0-9-]{2,}/i)?.[0]?.toLowerCase();
|
|
1072
1060
|
if (englishWord) return englishWord;
|
|
1073
1061
|
|
|
1074
1062
|
return "feature";
|
|
1075
1063
|
}
|
|
1076
1064
|
|
|
1077
|
-
function koreanToSlug(text: string): string {
|
|
1078
|
-
const matches: string[] = [];
|
|
1079
|
-
for (const [korean, english] of Object.entries(KOREAN_DOMAIN_MAP)) {
|
|
1080
|
-
if (text.includes(korean)) {
|
|
1081
|
-
matches.push(english);
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
if (matches.length === 0) return "";
|
|
1085
|
-
// ์ฌ๋ฌ ํค์๋ ๋งค์นญ ์ ํ์ดํฐ์ผ๋ก ์ฐ๊ฒฐ (์ต๋ 2๊ฐ)
|
|
1086
|
-
return matches.slice(0, 2).join("-");
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
1065
|
/**
|
|
1090
1066
|
* ํ๋ฆฌ์
์ ๋ฐ๋ผ ๊ตฌ์กฐ๋ฅผ ์กฐ์
|
|
1091
1067
|
* FSD, Clean, Hexagonal ๋ฑ ํ๋ฆฌ์
๋ณ ๋ ์ด์ด ๋งคํ ์ ์ฉ
|
|
@@ -1181,8 +1157,8 @@ export async function negotiate(
|
|
|
1181
1157
|
// 1. ์นดํ
๊ณ ๋ฆฌ ๊ฐ์ง
|
|
1182
1158
|
const category = request.category || detectCategory(intent);
|
|
1183
1159
|
|
|
1184
|
-
// 2. ๊ธฐ๋ฅ
|
|
1185
|
-
const featureName = extractFeatureName(intent);
|
|
1160
|
+
// 2. ๊ธฐ๋ฅ ์ด๋ฆ: ์์ด์ ํธ ์ ๊ณต ๊ฐ ์ฐ์ , ์์ผ๋ฉด ์๋ ์ถ์ถ
|
|
1161
|
+
const featureName = sanitizeSlug(request.featureName) || extractFeatureName(intent);
|
|
1186
1162
|
|
|
1187
1163
|
// 3. ๊ด๋ จ ๊ฒฐ์ ๊ฒ์
|
|
1188
1164
|
const categoryTags = CATEGORY_KEYWORDS[category] || [];
|