newpr 1.0.15 → 1.0.16
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
package/src/stack/pr-title.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { LlmClient } from "../llm/client.ts";
|
|
|
2
2
|
import type { StackGroup } from "./types.ts";
|
|
3
3
|
|
|
4
4
|
const MAX_TITLE_LENGTH = 72;
|
|
5
|
+
const KOREAN_NOUN_END_RE = /(추가|개선|수정|정리|분리|통합|구현|적용|도입|구성|지원|처리|보강|최적화|리팩터링|안정화|마이그레이션|업데이트|생성|검증|연동|변경|제거|작성|설정|관리|보호|강화|정의|확장|대응|복구|표시|유지|등록|삭제|작업)$/;
|
|
6
|
+
const KOREAN_VERB_END_RE = /(합니다|하였다|했다|한다|되었다|됐다|되다|하다)$/;
|
|
5
7
|
const TYPE_PREFIX: Record<string, string> = {
|
|
6
8
|
feature: "feat",
|
|
7
9
|
feat: "feat",
|
|
@@ -21,15 +23,63 @@ function normalizeTypePrefix(type: string): string {
|
|
|
21
23
|
return TYPE_PREFIX[type] ?? "chore";
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
+
function isKoreanLanguage(languageHint: string | null): boolean {
|
|
27
|
+
if (!languageHint) return false;
|
|
28
|
+
return /korean|^ko$/i.test(languageHint);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ensureNounFormDescription(description: string, languageHint: string | null): string {
|
|
32
|
+
let desc = description.trim().replace(/\.+$/, "");
|
|
33
|
+
if (!desc) return isKoreanLanguage(languageHint) ? "변경 작업" : "update";
|
|
34
|
+
|
|
35
|
+
const hasKoreanContext = /[가-힣]/.test(desc) || isKoreanLanguage(languageHint);
|
|
36
|
+
if (!hasKoreanContext) return desc;
|
|
37
|
+
if (KOREAN_NOUN_END_RE.test(desc)) return desc;
|
|
38
|
+
|
|
39
|
+
const verbTrimmed = desc.replace(KOREAN_VERB_END_RE, "").trim();
|
|
40
|
+
if (verbTrimmed) {
|
|
41
|
+
if (KOREAN_NOUN_END_RE.test(verbTrimmed)) return verbTrimmed;
|
|
42
|
+
return `${verbTrimmed} 작업`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return "변경 작업";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function splitTitle(raw: string): { prefix: string; description: string } {
|
|
49
|
+
const idx = raw.indexOf(":");
|
|
50
|
+
if (idx < 0) return { prefix: "", description: raw.trim() };
|
|
51
|
+
return {
|
|
52
|
+
prefix: raw.slice(0, idx).trim(),
|
|
53
|
+
description: raw.slice(idx + 1).trim(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sanitizeTitle(raw: string, languageHint: string | null): string {
|
|
58
|
+
const parsed = splitTitle(raw);
|
|
59
|
+
const prefix = parsed.prefix;
|
|
60
|
+
let description = ensureNounFormDescription(parsed.description, languageHint);
|
|
61
|
+
let title = prefix ? `${prefix}: ${description}` : description;
|
|
62
|
+
title = title.trim().replace(/\.+$/, "");
|
|
63
|
+
|
|
26
64
|
if (title.length > MAX_TITLE_LENGTH) {
|
|
27
|
-
|
|
65
|
+
if (prefix) {
|
|
66
|
+
const prefixText = `${prefix}: `;
|
|
67
|
+
const maxDesc = MAX_TITLE_LENGTH - prefixText.length;
|
|
68
|
+
description = description.slice(0, maxDesc).replace(/\s\S*$/, "").trimEnd();
|
|
69
|
+
description = ensureNounFormDescription(description, languageHint);
|
|
70
|
+
if (description.length > maxDesc) {
|
|
71
|
+
description = description.slice(0, maxDesc).trimEnd();
|
|
72
|
+
}
|
|
73
|
+
title = `${prefixText}${description || ensureNounFormDescription("", languageHint)}`;
|
|
74
|
+
} else {
|
|
75
|
+
title = title.slice(0, MAX_TITLE_LENGTH).replace(/\s\S*$/, "").trimEnd();
|
|
76
|
+
}
|
|
28
77
|
}
|
|
78
|
+
|
|
29
79
|
return title;
|
|
30
80
|
}
|
|
31
81
|
|
|
32
|
-
function fallbackTitle(g: StackGroup): string {
|
|
82
|
+
function fallbackTitle(g: StackGroup, languageHint: string | null): string {
|
|
33
83
|
const desc = g.description || g.name;
|
|
34
84
|
const cleaned = desc.replace(/[^\p{L}\p{N}\s\-/.,()]/gu, " ").replace(/\s+/g, " ").trim();
|
|
35
85
|
const prefix = `${normalizeTypePrefix(g.type)}: `;
|
|
@@ -37,7 +87,7 @@ function fallbackTitle(g: StackGroup): string {
|
|
|
37
87
|
const truncated = cleaned.length > maxDesc
|
|
38
88
|
? cleaned.slice(0, maxDesc).replace(/\s\S*$/, "").trimEnd()
|
|
39
89
|
: cleaned;
|
|
40
|
-
return truncated ? `${prefix}${truncated}` : `${prefix}${g.name}
|
|
90
|
+
return sanitizeTitle(truncated ? `${prefix}${truncated}` : `${prefix}${g.name}`, languageHint);
|
|
41
91
|
}
|
|
42
92
|
|
|
43
93
|
export async function generatePrTitles(
|
|
@@ -70,7 +120,7 @@ export async function generatePrTitles(
|
|
|
70
120
|
Rules:
|
|
71
121
|
- Format: "type: description"
|
|
72
122
|
- type must be one of: feat | fix | refactor | chore | docs | test | perf | style | ci
|
|
73
|
-
- description:
|
|
123
|
+
- description: 4-12 words, noun phrase only, must end with a noun, no trailing period
|
|
74
124
|
- Target length: 40-72 characters total
|
|
75
125
|
- Be specific about WHAT changed, not vague
|
|
76
126
|
- Each title must be unique across the set
|
|
@@ -78,19 +128,20 @@ Rules:
|
|
|
78
128
|
${langRule}
|
|
79
129
|
|
|
80
130
|
Good examples:
|
|
81
|
-
- "feat:
|
|
82
|
-
- "fix:
|
|
83
|
-
- "refactor:
|
|
84
|
-
- "chore:
|
|
85
|
-
- "feat:
|
|
86
|
-
- "test:
|
|
87
|
-
- "refactor:
|
|
131
|
+
- "feat: JWT token refresh middleware integration"
|
|
132
|
+
- "fix: session expiry null user crash prevention"
|
|
133
|
+
- "refactor: shared validation helper extraction"
|
|
134
|
+
- "chore: eslint flat config migration"
|
|
135
|
+
- "feat: canvas node drag-and-drop reorder support"
|
|
136
|
+
- "test: payment webhook integration test coverage"
|
|
137
|
+
- "refactor: monolithic API router module split"
|
|
88
138
|
|
|
89
139
|
Bad examples:
|
|
90
140
|
- "feat: auth" (too vague)
|
|
91
141
|
- "fix: bug" (meaningless)
|
|
92
142
|
- "refactor: code" (says nothing)
|
|
93
143
|
- "feat: jwt" (just a keyword, not a title)
|
|
144
|
+
- "feat: add JWT refresh middleware" (imperative verb)
|
|
94
145
|
- "" (empty)
|
|
95
146
|
|
|
96
147
|
Return ONLY a JSON array: [{"group_id": "...", "title": "..."}]`;
|
|
@@ -118,18 +169,18 @@ Generate a descriptive PR title (40-72 chars) for each group. Return JSON array:
|
|
|
118
169
|
}
|
|
119
170
|
for (const item of parsed) {
|
|
120
171
|
if (item.group_id && item.title?.trim()) {
|
|
121
|
-
titles.set(item.group_id, sanitizeTitle(item.title));
|
|
172
|
+
titles.set(item.group_id, sanitizeTitle(item.title, lang));
|
|
122
173
|
}
|
|
123
174
|
}
|
|
124
175
|
} catch {
|
|
125
176
|
for (const g of groups) {
|
|
126
|
-
titles.set(g.id, fallbackTitle(g));
|
|
177
|
+
titles.set(g.id, fallbackTitle(g, lang));
|
|
127
178
|
}
|
|
128
179
|
}
|
|
129
180
|
|
|
130
181
|
for (const g of groups) {
|
|
131
182
|
if (!titles.has(g.id) || !titles.get(g.id)) {
|
|
132
|
-
titles.set(g.id, fallbackTitle(g));
|
|
183
|
+
titles.set(g.id, fallbackTitle(g, lang));
|
|
133
184
|
}
|
|
134
185
|
}
|
|
135
186
|
|
|
@@ -79,6 +79,7 @@ interface PublishResultData {
|
|
|
79
79
|
base_branch: string;
|
|
80
80
|
head_branch: string;
|
|
81
81
|
}>;
|
|
82
|
+
publishedAt?: number;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
interface ServerStackState {
|
|
@@ -92,6 +93,7 @@ interface ServerStackState {
|
|
|
92
93
|
plan: PlanData | null;
|
|
93
94
|
execResult: ExecResultData | null;
|
|
94
95
|
verifyResult: VerifyResultData | null;
|
|
96
|
+
publishResult: PublishResultData | null;
|
|
95
97
|
startedAt: number;
|
|
96
98
|
finishedAt: number | null;
|
|
97
99
|
}
|
|
@@ -130,6 +132,7 @@ function applyServerState(server: ServerStackState): Partial<StackState> {
|
|
|
130
132
|
plan: server.plan,
|
|
131
133
|
execResult: server.execResult,
|
|
132
134
|
verifyResult: server.verifyResult,
|
|
135
|
+
publishResult: server.publishResult,
|
|
133
136
|
};
|
|
134
137
|
}
|
|
135
138
|
|
|
@@ -259,11 +262,15 @@ export function useStack(sessionId: string | null | undefined, options?: UseStac
|
|
|
259
262
|
if (!res.ok) throw new Error(data.error ?? "Publishing failed");
|
|
260
263
|
|
|
261
264
|
const publishResult = data.publish_result as PublishResultData;
|
|
265
|
+
const serverState = (data as { state?: ServerStackState }).state;
|
|
266
|
+
const nextPublishResult = serverState?.publishResult ?? publishResult;
|
|
262
267
|
|
|
263
268
|
setState((s) => ({
|
|
264
269
|
...s,
|
|
270
|
+
...(serverState ? applyServerState(serverState) : {}),
|
|
265
271
|
phase: "done",
|
|
266
|
-
publishResult,
|
|
272
|
+
publishResult: nextPublishResult,
|
|
273
|
+
progressMessage: null,
|
|
267
274
|
}));
|
|
268
275
|
|
|
269
276
|
if (options?.onTrackAnalysis && publishResult?.prs?.length > 0) {
|
|
@@ -75,6 +75,14 @@ interface StackPanelProps {
|
|
|
75
75
|
|
|
76
76
|
export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
77
77
|
const stack = useStack(sessionId, { onTrackAnalysis });
|
|
78
|
+
const publishedCount = stack.publishResult?.prs.length ?? 0;
|
|
79
|
+
const pushedCount = stack.publishResult?.branches.filter((b) => b.pushed).length ?? 0;
|
|
80
|
+
const publishFailures = stack.publishResult
|
|
81
|
+
? stack.publishResult.branches.filter((branch) => {
|
|
82
|
+
if (!branch.pushed) return true;
|
|
83
|
+
return !stack.publishResult?.prs.some((pr) => pr.head_branch === branch.name);
|
|
84
|
+
})
|
|
85
|
+
: [];
|
|
78
86
|
|
|
79
87
|
if (stack.phase === "idle") {
|
|
80
88
|
return (
|
|
@@ -263,30 +271,57 @@ export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
|
263
271
|
</button>
|
|
264
272
|
)}
|
|
265
273
|
|
|
266
|
-
{stack.publishResult &&
|
|
267
|
-
<div className="space-y-
|
|
268
|
-
|
|
269
|
-
<
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
274
|
+
{stack.publishResult && (
|
|
275
|
+
<div className="space-y-2 rounded-lg border border-border/70 bg-foreground/[0.015] p-2.5">
|
|
276
|
+
<div className="flex items-center justify-between px-1">
|
|
277
|
+
<div className="flex items-center gap-2">
|
|
278
|
+
<GitPullRequestArrow className="h-3.5 w-3.5 text-green-600/70 dark:text-green-400/70" />
|
|
279
|
+
<span className="text-[11px] font-medium text-foreground/80">Draft Publish Results</span>
|
|
280
|
+
</div>
|
|
281
|
+
<span className="text-[10px] text-muted-foreground/35 tabular-nums">{publishedCount}/{pushedCount} created</span>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
{stack.publishResult.prs.length > 0 ? (
|
|
285
|
+
<div className="space-y-1.5">
|
|
286
|
+
{stack.publishResult.prs.map((pr) => (
|
|
287
|
+
<a
|
|
288
|
+
key={pr.number}
|
|
289
|
+
href={pr.url}
|
|
290
|
+
target="_blank"
|
|
291
|
+
rel="noopener noreferrer"
|
|
292
|
+
className="group flex items-center gap-3 rounded-lg px-2.5 py-2 hover:bg-accent/30 transition-colors"
|
|
293
|
+
>
|
|
294
|
+
<GitPullRequestArrow className="h-3.5 w-3.5 text-green-600/60 dark:text-green-400/60 shrink-0" />
|
|
295
|
+
<div className="flex-1 min-w-0">
|
|
296
|
+
<div className="flex items-center gap-2">
|
|
297
|
+
<span className="text-[11px] font-medium truncate">{pr.title}</span>
|
|
298
|
+
<span className="text-[10px] text-muted-foreground/25 tabular-nums shrink-0">#{pr.number}</span>
|
|
299
|
+
</div>
|
|
300
|
+
<div className="flex items-center gap-1 mt-0.5">
|
|
301
|
+
<span className="text-[10px] font-mono text-muted-foreground/25">{pr.base_branch}</span>
|
|
302
|
+
<ArrowRight className="h-2.5 w-2.5 text-muted-foreground/20" />
|
|
303
|
+
<span className="text-[10px] font-mono text-muted-foreground/25">{pr.head_branch}</span>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
</a>
|
|
307
|
+
))}
|
|
308
|
+
</div>
|
|
309
|
+
) : (
|
|
310
|
+
<p className="text-[10px] text-muted-foreground/35 px-2.5 py-2">No draft PR URLs were returned.</p>
|
|
311
|
+
)}
|
|
312
|
+
|
|
313
|
+
{publishFailures.length > 0 && (
|
|
314
|
+
<div className="rounded-md bg-yellow-500/[0.06] px-2.5 py-2 space-y-1">
|
|
315
|
+
<p className="text-[10px] text-yellow-700/80 dark:text-yellow-300/80">
|
|
316
|
+
Some branches were pushed but PR creation did not complete.
|
|
317
|
+
</p>
|
|
318
|
+
{publishFailures.map((branch) => (
|
|
319
|
+
<div key={branch.name} className="text-[10px] font-mono text-yellow-700/70 dark:text-yellow-300/70 truncate">
|
|
320
|
+
{branch.name}
|
|
286
321
|
</div>
|
|
287
|
-
|
|
288
|
-
</
|
|
289
|
-
)
|
|
322
|
+
))}
|
|
323
|
+
</div>
|
|
324
|
+
)}
|
|
290
325
|
</div>
|
|
291
326
|
)}
|
|
292
327
|
</div>
|
package/src/web/server/routes.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { chatWithTools, createLlmClient, type ChatTool, type ChatStreamEvent } f
|
|
|
16
16
|
import { detectAgents, runAgent } from "../../workspace/agent.ts";
|
|
17
17
|
import { randomBytes } from "node:crypto";
|
|
18
18
|
import { publishStack } from "../../stack/publish.ts";
|
|
19
|
-
import { startStack, getStackState, cancelStack, subscribeStack, restoreCompletedStacks } from "./stack-manager.ts";
|
|
19
|
+
import { startStack, getStackState, cancelStack, subscribeStack, restoreCompletedStacks, setStackPublishResult } from "./stack-manager.ts";
|
|
20
20
|
import { getTelemetryConsent, setTelemetryConsent, telemetry } from "../../telemetry/index.ts";
|
|
21
21
|
|
|
22
22
|
function json(data: unknown, status = 200): Response {
|
|
@@ -1921,8 +1921,9 @@ Before posting an inline comment, ALWAYS call \`get_file_diff\` first to find th
|
|
|
1921
1921
|
owner: state.context.owner,
|
|
1922
1922
|
repo: state.context.repo,
|
|
1923
1923
|
});
|
|
1924
|
+
await setStackPublishResult(body.sessionId, result);
|
|
1924
1925
|
|
|
1925
|
-
return json({ publish_result: result });
|
|
1926
|
+
return json({ publish_result: result, state: getStackState(body.sessionId) });
|
|
1926
1927
|
} catch (err) {
|
|
1927
1928
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1928
1929
|
return json({ error: msg }, 500);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { NewprConfig } from "../../types/config.ts";
|
|
2
2
|
import type { FileGroup } from "../../types/output.ts";
|
|
3
|
-
import type { StackWarning, FeasibilityResult, StackExecResult } from "../../stack/types.ts";
|
|
3
|
+
import type { StackWarning, FeasibilityResult, StackExecResult, StackPublishResult } from "../../stack/types.ts";
|
|
4
4
|
import type { StackPlan } from "../../stack/types.ts";
|
|
5
5
|
import { loadSession } from "../../history/store.ts";
|
|
6
6
|
import { saveStackSidecar, loadStackSidecar } from "../../history/store.ts";
|
|
@@ -68,6 +68,12 @@ export interface StackVerifyData {
|
|
|
68
68
|
structured_warnings: StackWarning[];
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export interface StackPublishData {
|
|
72
|
+
branches: StackPublishResult["branches"];
|
|
73
|
+
prs: StackPublishResult["prs"];
|
|
74
|
+
publishedAt: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
export interface StackStateSnapshot {
|
|
72
78
|
status: StackStatus;
|
|
73
79
|
phase: StackPhase | null;
|
|
@@ -79,6 +85,7 @@ export interface StackStateSnapshot {
|
|
|
79
85
|
plan: StackPlanData | null;
|
|
80
86
|
execResult: StackExecResult | null;
|
|
81
87
|
verifyResult: StackVerifyData | null;
|
|
88
|
+
publishResult: StackPublishData | null;
|
|
82
89
|
startedAt: number;
|
|
83
90
|
finishedAt: number | null;
|
|
84
91
|
}
|
|
@@ -95,6 +102,7 @@ interface StackSession {
|
|
|
95
102
|
plan: StackPlanData | null;
|
|
96
103
|
execResult: StackExecResult | null;
|
|
97
104
|
verifyResult: StackVerifyData | null;
|
|
105
|
+
publishResult: StackPublishData | null;
|
|
98
106
|
events: StackEvent[];
|
|
99
107
|
subscribers: Set<(event: StackEvent | { type: "done" | "error"; data?: string }) => void>;
|
|
100
108
|
startedAt: number;
|
|
@@ -131,6 +139,7 @@ function toSnapshot(session: StackSession): StackStateSnapshot {
|
|
|
131
139
|
plan: session.plan,
|
|
132
140
|
execResult: session.execResult,
|
|
133
141
|
verifyResult: session.verifyResult,
|
|
142
|
+
publishResult: session.publishResult,
|
|
134
143
|
startedAt: session.startedAt,
|
|
135
144
|
finishedAt: session.finishedAt,
|
|
136
145
|
};
|
|
@@ -173,6 +182,7 @@ export function startStack(
|
|
|
173
182
|
plan: null,
|
|
174
183
|
execResult: null,
|
|
175
184
|
verifyResult: null,
|
|
185
|
+
publishResult: null,
|
|
176
186
|
events: [],
|
|
177
187
|
subscribers: new Set(),
|
|
178
188
|
startedAt: Date.now(),
|
|
@@ -235,12 +245,13 @@ export async function restoreCompletedStacks(sessionIds: string[]): Promise<void
|
|
|
235
245
|
context: snapshot.context,
|
|
236
246
|
partition: snapshot.partition,
|
|
237
247
|
feasibility: snapshot.feasibility,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
248
|
+
plan: snapshot.plan,
|
|
249
|
+
execResult: snapshot.execResult,
|
|
250
|
+
verifyResult: snapshot.verifyResult,
|
|
251
|
+
publishResult: snapshot.publishResult ?? null,
|
|
252
|
+
events: [],
|
|
253
|
+
subscribers: new Set(),
|
|
254
|
+
startedAt: snapshot.startedAt,
|
|
244
255
|
finishedAt: snapshot.finishedAt ?? Date.now(),
|
|
245
256
|
abortController: new AbortController(),
|
|
246
257
|
};
|
|
@@ -248,6 +259,22 @@ export async function restoreCompletedStacks(sessionIds: string[]): Promise<void
|
|
|
248
259
|
}
|
|
249
260
|
}
|
|
250
261
|
|
|
262
|
+
export async function setStackPublishResult(
|
|
263
|
+
analysisSessionId: string,
|
|
264
|
+
result: StackPublishResult,
|
|
265
|
+
): Promise<void> {
|
|
266
|
+
const session = sessions.get(analysisSessionId);
|
|
267
|
+
if (!session) return;
|
|
268
|
+
|
|
269
|
+
session.publishResult = {
|
|
270
|
+
branches: result.branches,
|
|
271
|
+
prs: result.prs,
|
|
272
|
+
publishedAt: Date.now(),
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
await saveStackSidecar(session.analysisSessionId, toSnapshot(session)).catch(() => {});
|
|
276
|
+
}
|
|
277
|
+
|
|
251
278
|
// ---------------------------------------------------------------------------
|
|
252
279
|
// Pipeline
|
|
253
280
|
// ---------------------------------------------------------------------------
|