vibe-coding-master 0.6.1 → 0.6.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.
- package/dist/backend/gateway/gateway-service.js +289 -47
- package/dist/backend/server.js +1 -0
- package/dist/backend/services/claude-hook-service.js +52 -9
- package/dist/backend/services/translation-service.js +261 -9
- package/dist/shared/types/app-settings.js +1 -0
- package/dist-frontend/assets/{index-D5LBogZG.js → index-UT2aoyEt.js} +1 -1
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
|
@@ -13,6 +13,8 @@ const TRANSLATION_MODEL = "translator";
|
|
|
13
13
|
const OUTPUT_TRANSLATION_BATCH_DELAY_MS = 10000;
|
|
14
14
|
const TRANSCRIPT_REPLAY_GRACE_MS = 5000;
|
|
15
15
|
const TRANSLATION_TASK_FEED_RETENTION_LIMIT = 2000;
|
|
16
|
+
const GATEWAY_TRANSLATION_REUSE_GRACE_MS = 1500;
|
|
17
|
+
const GATEWAY_TRANSLATION_REUSE_POLL_MS = 50;
|
|
16
18
|
export function createTranslationService(deps) {
|
|
17
19
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
18
20
|
const id = deps.id ?? (() => `tr_${Date.now()}_${Math.random().toString(16).slice(2)}`);
|
|
@@ -241,11 +243,13 @@ export function createTranslationService(deps) {
|
|
|
241
243
|
let displayed = false;
|
|
242
244
|
if (event.kind === "text") {
|
|
243
245
|
const shouldTranslate = shouldTranslateTextTranscriptEvent(state, event, config);
|
|
246
|
+
const metadata = getTextTranscriptEntryMetadata(event);
|
|
244
247
|
displayed = shouldTranslate
|
|
245
248
|
? processClaudeOutputText(sessionId, event.text, config, event.id, {
|
|
246
|
-
flushImmediately: event.stopReason === "end_turn"
|
|
249
|
+
flushImmediately: event.stopReason === "end_turn",
|
|
250
|
+
metadata
|
|
247
251
|
})
|
|
248
|
-
: pushPreservedProseEntry(sessionId, event.id, event.text, config);
|
|
252
|
+
: pushPreservedProseEntry(sessionId, event.id, event.text, config, metadata);
|
|
249
253
|
if (displayed && shouldTranslate) {
|
|
250
254
|
state.lastAssistantText = event.text;
|
|
251
255
|
}
|
|
@@ -267,6 +271,9 @@ export function createTranslationService(deps) {
|
|
|
267
271
|
if (config.outputMode === "all") {
|
|
268
272
|
return true;
|
|
269
273
|
}
|
|
274
|
+
if (config.outputMode === "round-final") {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
270
277
|
if (event.stopReason !== "end_turn") {
|
|
271
278
|
return false;
|
|
272
279
|
}
|
|
@@ -279,9 +286,16 @@ export function createTranslationService(deps) {
|
|
|
279
286
|
return startClaudeOutputTranslation(sessionId, rawText, config, {
|
|
280
287
|
entryId,
|
|
281
288
|
replaceExisting: false,
|
|
282
|
-
flushImmediately: options.flushImmediately === true
|
|
289
|
+
flushImmediately: options.flushImmediately === true,
|
|
290
|
+
metadata: options.metadata
|
|
283
291
|
}) !== undefined;
|
|
284
292
|
}
|
|
293
|
+
function getTextTranscriptEntryMetadata(event) {
|
|
294
|
+
return {
|
|
295
|
+
...(event.stopReason !== undefined ? { transcriptStopReason: event.stopReason } : {}),
|
|
296
|
+
transcriptTimestamp: event.timestamp
|
|
297
|
+
};
|
|
298
|
+
}
|
|
285
299
|
function startClaudeOutputTranslation(sessionId, rawText, config, options) {
|
|
286
300
|
const session = deps.runtime.getSession(sessionId);
|
|
287
301
|
const roleSession = deps.sessionRegistry.get(sessionId);
|
|
@@ -302,7 +316,8 @@ export function createTranslationService(deps) {
|
|
|
302
316
|
config,
|
|
303
317
|
status: "queued",
|
|
304
318
|
contextUsed: false,
|
|
305
|
-
id: options.entryId
|
|
319
|
+
id: options.entryId,
|
|
320
|
+
...options.metadata
|
|
306
321
|
})
|
|
307
322
|
};
|
|
308
323
|
if (options.replaceExisting) {
|
|
@@ -437,10 +452,10 @@ export function createTranslationService(deps) {
|
|
|
437
452
|
function pushPreservedTranscriptEntry(sessionId, entryId, sourceText, config) {
|
|
438
453
|
return pushPreservedOutputEntry(sessionId, entryId, sourceText, "tool-output", config);
|
|
439
454
|
}
|
|
440
|
-
function pushPreservedProseEntry(sessionId, entryId, sourceText, config) {
|
|
441
|
-
return pushPreservedOutputEntry(sessionId, entryId, sourceText, "prose", config);
|
|
455
|
+
function pushPreservedProseEntry(sessionId, entryId, sourceText, config, metadata = {}) {
|
|
456
|
+
return pushPreservedOutputEntry(sessionId, entryId, sourceText, "prose", config, metadata);
|
|
442
457
|
}
|
|
443
|
-
function pushPreservedOutputEntry(sessionId, entryId, sourceText, sourceKind, config) {
|
|
458
|
+
function pushPreservedOutputEntry(sessionId, entryId, sourceText, sourceKind, config, metadata = {}) {
|
|
444
459
|
if (!sourceText.trim()) {
|
|
445
460
|
return false;
|
|
446
461
|
}
|
|
@@ -460,7 +475,8 @@ export function createTranslationService(deps) {
|
|
|
460
475
|
contextUsed: false,
|
|
461
476
|
id: entryId,
|
|
462
477
|
translatedText: sourceText,
|
|
463
|
-
completedAt: now()
|
|
478
|
+
completedAt: now(),
|
|
479
|
+
...metadata
|
|
464
480
|
});
|
|
465
481
|
pushEntry(sessionId, entry);
|
|
466
482
|
return true;
|
|
@@ -550,7 +566,11 @@ export function createTranslationService(deps) {
|
|
|
550
566
|
markFailureRetrying(sessionId, existingFailure);
|
|
551
567
|
const retrying = startClaudeOutputTranslation(sessionId, original.sourceText, config, {
|
|
552
568
|
entryId: original.id,
|
|
553
|
-
replaceExisting: true
|
|
569
|
+
replaceExisting: true,
|
|
570
|
+
metadata: {
|
|
571
|
+
transcriptStopReason: original.transcriptStopReason,
|
|
572
|
+
transcriptTimestamp: original.transcriptTimestamp
|
|
573
|
+
}
|
|
554
574
|
});
|
|
555
575
|
if (!retrying) {
|
|
556
576
|
throw new VcmError({
|
|
@@ -574,6 +594,8 @@ export function createTranslationService(deps) {
|
|
|
574
594
|
translatedText: input.translatedText ?? "",
|
|
575
595
|
status: input.status,
|
|
576
596
|
contextUsed: input.contextUsed,
|
|
597
|
+
transcriptStopReason: input.transcriptStopReason,
|
|
598
|
+
transcriptTimestamp: input.transcriptTimestamp,
|
|
577
599
|
boundaryKind: input.boundaryKind,
|
|
578
600
|
conversationTurn: input.conversationTurn,
|
|
579
601
|
occurredAt: input.occurredAt,
|
|
@@ -674,6 +696,7 @@ export function createTranslationService(deps) {
|
|
|
674
696
|
};
|
|
675
697
|
},
|
|
676
698
|
async pollTaskFeed(input) {
|
|
699
|
+
const config = await loadConfig();
|
|
677
700
|
const cursor = Number.isFinite(input.after) ? Math.max(1, Math.floor(input.after)) : 1;
|
|
678
701
|
const maxEvents = Math.min(Math.max(1, Math.floor(input.limit ?? 500)), 1000);
|
|
679
702
|
const roleSessions = await deps.sessionService.listRoleSessions(input.repoRoot, input.taskSlug);
|
|
@@ -698,6 +721,9 @@ export function createTranslationService(deps) {
|
|
|
698
721
|
status: state.status
|
|
699
722
|
});
|
|
700
723
|
}
|
|
724
|
+
if (config.outputMode === "round-final") {
|
|
725
|
+
await translateRoundFinalReplyIfReady(input, config);
|
|
726
|
+
}
|
|
701
727
|
const feed = getTaskFeed(input.taskRepoRoot, input.taskSlug);
|
|
702
728
|
const events = feed.events
|
|
703
729
|
.filter((event) => event.seq >= cursor)
|
|
@@ -1004,6 +1030,10 @@ export function createTranslationService(deps) {
|
|
|
1004
1030
|
},
|
|
1005
1031
|
async translateGatewayOutput(input) {
|
|
1006
1032
|
const config = await loadConfig();
|
|
1033
|
+
const reusable = await findReusableGatewayOutputTranslation(input, config);
|
|
1034
|
+
if (reusable) {
|
|
1035
|
+
return reusable.trim();
|
|
1036
|
+
}
|
|
1007
1037
|
const translation = await translateText({
|
|
1008
1038
|
repoRoot: input.repoRoot,
|
|
1009
1039
|
taskSlug: input.taskSlug,
|
|
@@ -1033,6 +1063,228 @@ export function createTranslationService(deps) {
|
|
|
1033
1063
|
};
|
|
1034
1064
|
}
|
|
1035
1065
|
};
|
|
1066
|
+
async function translateRoundFinalReplyIfReady(input, config) {
|
|
1067
|
+
if (!deps.roundService || !deps.projectService) {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
const projectConfig = await deps.projectService.loadConfig(input.repoRoot);
|
|
1071
|
+
const round = await deps.roundService.getSessionRoundState({
|
|
1072
|
+
repoRoot: input.repoRoot,
|
|
1073
|
+
stateRepoRoot: input.taskRepoRoot,
|
|
1074
|
+
stateRoot: projectConfig.stateRoot,
|
|
1075
|
+
taskSlug: input.taskSlug
|
|
1076
|
+
});
|
|
1077
|
+
if (!isNormalStoppedRound(round)) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const candidate = findRoundFinalReplyCandidate(input, round);
|
|
1081
|
+
if (!candidate || candidate.entry.status !== "preserved") {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
startClaudeOutputTranslation(candidate.sessionId, candidate.entry.sourceText, config, {
|
|
1085
|
+
entryId: candidate.entry.id,
|
|
1086
|
+
replaceExisting: true,
|
|
1087
|
+
flushImmediately: true,
|
|
1088
|
+
metadata: {
|
|
1089
|
+
transcriptStopReason: candidate.entry.transcriptStopReason,
|
|
1090
|
+
transcriptTimestamp: candidate.entry.transcriptTimestamp
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
function isNormalStoppedRound(round) {
|
|
1095
|
+
return round.status === "stopped"
|
|
1096
|
+
&& Boolean(round.roundId)
|
|
1097
|
+
&& !round.stopReason
|
|
1098
|
+
&& round.roleRecovery?.status !== "failed"
|
|
1099
|
+
&& round.flowPause?.reason !== "role-recovery-failed";
|
|
1100
|
+
}
|
|
1101
|
+
function findRoundFinalReplyCandidate(input, round) {
|
|
1102
|
+
const candidates = [];
|
|
1103
|
+
for (const [sessionId, state] of sessionStates) {
|
|
1104
|
+
if (!isRoundFinalCandidateState(state, input, round.activeRole)) {
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
for (const entry of state.entries) {
|
|
1108
|
+
if (isRoundFinalCandidateEntry(entry, input, round.stoppedAt)) {
|
|
1109
|
+
candidates.push({ sessionId, entry });
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return candidates
|
|
1114
|
+
.sort((left, right) => compareRoundFinalCandidates(left.entry, right.entry))
|
|
1115
|
+
.at(-1);
|
|
1116
|
+
}
|
|
1117
|
+
function isRoundFinalCandidateState(state, input, activeRole) {
|
|
1118
|
+
if (state.repoRoot !== input.taskRepoRoot || state.taskSlug !== input.taskSlug || !state.role) {
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
return !activeRole || state.role === activeRole;
|
|
1122
|
+
}
|
|
1123
|
+
function isRoundFinalCandidateEntry(entry, input, stoppedAt) {
|
|
1124
|
+
if (entry.taskSlug !== input.taskSlug ||
|
|
1125
|
+
entry.direction !== "cc-output-to-user" ||
|
|
1126
|
+
entry.sourceKind !== "prose" ||
|
|
1127
|
+
entry.transcriptStopReason !== "end_turn") {
|
|
1128
|
+
return false;
|
|
1129
|
+
}
|
|
1130
|
+
if (entry.status !== "preserved" && entry.status !== "queued" && entry.status !== "translating" && entry.status !== "translated") {
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
if (!stoppedAt || !entry.transcriptTimestamp) {
|
|
1134
|
+
return true;
|
|
1135
|
+
}
|
|
1136
|
+
return entry.transcriptTimestamp <= stoppedAt;
|
|
1137
|
+
}
|
|
1138
|
+
function compareRoundFinalCandidates(left, right) {
|
|
1139
|
+
const leftTime = left.transcriptTimestamp ?? left.createdAt;
|
|
1140
|
+
const rightTime = right.transcriptTimestamp ?? right.createdAt;
|
|
1141
|
+
if (leftTime === rightTime) {
|
|
1142
|
+
return left.id.localeCompare(right.id);
|
|
1143
|
+
}
|
|
1144
|
+
return leftTime.localeCompare(rightTime);
|
|
1145
|
+
}
|
|
1146
|
+
async function findReusableGatewayOutputTranslation(input, config) {
|
|
1147
|
+
const graceDeadline = Date.now() + (input.sourceEntryIds?.length ? GATEWAY_TRANSLATION_REUSE_GRACE_MS : 0);
|
|
1148
|
+
while (true) {
|
|
1149
|
+
const lookup = await lookupGatewayOutputTranslation(input);
|
|
1150
|
+
if (lookup.kind === "translated") {
|
|
1151
|
+
return lookup.text;
|
|
1152
|
+
}
|
|
1153
|
+
if (lookup.kind === "active") {
|
|
1154
|
+
return waitForReusableGatewayOutputTranslation(input, config);
|
|
1155
|
+
}
|
|
1156
|
+
if (!input.sourceEntryIds?.length || Date.now() >= graceDeadline) {
|
|
1157
|
+
return undefined;
|
|
1158
|
+
}
|
|
1159
|
+
await delay(GATEWAY_TRANSLATION_REUSE_POLL_MS);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
async function waitForReusableGatewayOutputTranslation(input, config) {
|
|
1163
|
+
const deadline = Date.now() + config.requestTimeoutMs;
|
|
1164
|
+
while (Date.now() <= deadline) {
|
|
1165
|
+
const lookup = await lookupGatewayOutputTranslation(input);
|
|
1166
|
+
if (lookup.kind === "translated") {
|
|
1167
|
+
return lookup.text;
|
|
1168
|
+
}
|
|
1169
|
+
if (lookup.kind !== "active") {
|
|
1170
|
+
return undefined;
|
|
1171
|
+
}
|
|
1172
|
+
await delay(GATEWAY_TRANSLATION_REUSE_POLL_MS);
|
|
1173
|
+
}
|
|
1174
|
+
throw new VcmError({
|
|
1175
|
+
code: "TRANSLATION_TIMEOUT",
|
|
1176
|
+
message: "Gateway output translation timed out while waiting for the existing PM reply translation.",
|
|
1177
|
+
statusCode: 504
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
async function lookupGatewayOutputTranslation(input) {
|
|
1181
|
+
const states = await getGatewayOutputCandidateStates(input);
|
|
1182
|
+
return selectGatewayOutputTranslation(states, input);
|
|
1183
|
+
}
|
|
1184
|
+
async function getGatewayOutputCandidateStates(input) {
|
|
1185
|
+
const states = [];
|
|
1186
|
+
const seen = new Set();
|
|
1187
|
+
const add = (state) => {
|
|
1188
|
+
if (seen.has(state) || !isGatewayOutputCandidateState(state, input)) {
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
seen.add(state);
|
|
1192
|
+
states.push(state);
|
|
1193
|
+
};
|
|
1194
|
+
const roleSession = await getGatewayRoleSession(input);
|
|
1195
|
+
if (roleSession) {
|
|
1196
|
+
const state = await prepareCache({
|
|
1197
|
+
repoRoot: roleSession.cwd,
|
|
1198
|
+
baseRepoRoot: input.repoRoot,
|
|
1199
|
+
taskSlug: input.taskSlug,
|
|
1200
|
+
role: input.role,
|
|
1201
|
+
sessionId: roleSession.id
|
|
1202
|
+
});
|
|
1203
|
+
if (roleSession.status === "running") {
|
|
1204
|
+
startTranscriptTail(roleSession);
|
|
1205
|
+
}
|
|
1206
|
+
add(state);
|
|
1207
|
+
}
|
|
1208
|
+
for (const state of sessionStates.values()) {
|
|
1209
|
+
add(state);
|
|
1210
|
+
}
|
|
1211
|
+
return states;
|
|
1212
|
+
}
|
|
1213
|
+
async function getGatewayRoleSession(input) {
|
|
1214
|
+
try {
|
|
1215
|
+
return await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.role);
|
|
1216
|
+
}
|
|
1217
|
+
catch {
|
|
1218
|
+
return undefined;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
function isGatewayOutputCandidateState(state, input) {
|
|
1222
|
+
if (state.taskSlug !== input.taskSlug || state.role !== input.role) {
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
return state.baseRepoRoot === input.repoRoot
|
|
1226
|
+
|| state.repoRoot === input.repoRoot
|
|
1227
|
+
|| Boolean(state.repoRoot?.startsWith(`${input.repoRoot}${path.sep}`));
|
|
1228
|
+
}
|
|
1229
|
+
function selectGatewayOutputTranslation(states, input) {
|
|
1230
|
+
const sourceEntryIds = normalizeGatewaySourceEntryIds(input.sourceEntryIds);
|
|
1231
|
+
if (sourceEntryIds.length > 0) {
|
|
1232
|
+
const entries = sourceEntryIds
|
|
1233
|
+
.map((entryId) => findGatewayOutputEntryById(states, input, entryId));
|
|
1234
|
+
const foundEntries = entries.filter((entry) => Boolean(entry));
|
|
1235
|
+
if (foundEntries.length === sourceEntryIds.length) {
|
|
1236
|
+
if (foundEntries.some(isActiveTranslationEntry)) {
|
|
1237
|
+
return { kind: "active" };
|
|
1238
|
+
}
|
|
1239
|
+
if (foundEntries.every(isReusableGatewayOutputEntry)) {
|
|
1240
|
+
return {
|
|
1241
|
+
kind: "translated",
|
|
1242
|
+
text: foundEntries.map((entry) => entry.translatedText).join("\n\n")
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
else if (foundEntries.some(isActiveTranslationEntry)) {
|
|
1247
|
+
return { kind: "active" };
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const normalizedSourceText = normalizeGatewaySourceText(input.text);
|
|
1251
|
+
const sourceMatches = states
|
|
1252
|
+
.flatMap((state) => state.entries)
|
|
1253
|
+
.filter((entry) => isGatewayOutputEntry(entry, input)
|
|
1254
|
+
&& normalizeGatewaySourceText(entry.sourceText) === normalizedSourceText);
|
|
1255
|
+
const translated = [...sourceMatches].reverse().find(isReusableGatewayOutputEntry);
|
|
1256
|
+
if (translated) {
|
|
1257
|
+
return { kind: "translated", text: translated.translatedText };
|
|
1258
|
+
}
|
|
1259
|
+
if (sourceMatches.some(isActiveTranslationEntry)) {
|
|
1260
|
+
return { kind: "active" };
|
|
1261
|
+
}
|
|
1262
|
+
return { kind: "missing" };
|
|
1263
|
+
}
|
|
1264
|
+
function findGatewayOutputEntryById(states, input, entryId) {
|
|
1265
|
+
for (const state of states) {
|
|
1266
|
+
const entry = state.entries.find((candidate) => candidate.id === entryId && isGatewayOutputEntry(candidate, input));
|
|
1267
|
+
if (entry) {
|
|
1268
|
+
return entry;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return undefined;
|
|
1272
|
+
}
|
|
1273
|
+
function isGatewayOutputEntry(entry, input) {
|
|
1274
|
+
return entry.taskSlug === input.taskSlug
|
|
1275
|
+
&& entry.role === input.role
|
|
1276
|
+
&& entry.direction === "cc-output-to-user"
|
|
1277
|
+
&& entry.sourceKind === "prose";
|
|
1278
|
+
}
|
|
1279
|
+
function isReusableGatewayOutputEntry(entry) {
|
|
1280
|
+
return entry.status === "translated" && Boolean(entry.translatedText.trim());
|
|
1281
|
+
}
|
|
1282
|
+
function normalizeGatewaySourceEntryIds(sourceEntryIds) {
|
|
1283
|
+
return Array.from(new Set((sourceEntryIds ?? []).map((entryId) => entryId.trim()).filter(Boolean)));
|
|
1284
|
+
}
|
|
1285
|
+
function normalizeGatewaySourceText(text) {
|
|
1286
|
+
return text.trim();
|
|
1287
|
+
}
|
|
1036
1288
|
async function writeToCurrentRole(repoRoot, taskSlug, role, text) {
|
|
1037
1289
|
const record = await deps.sessionService.getRoleSession(repoRoot, taskSlug, role);
|
|
1038
1290
|
if (!record || record.status !== "running") {
|
|
@@ -12,6 +12,7 @@ export const TRANSLATION_TARGET_LANGUAGE_OPTIONS = [
|
|
|
12
12
|
{ value: "es", label: "Spanish" }
|
|
13
13
|
];
|
|
14
14
|
export const TRANSLATION_OUTPUT_MODE_OPTIONS = [
|
|
15
|
+
{ value: "round-final", label: "Round final reply" },
|
|
15
16
|
{ value: "pm-final-only", label: "PM final reply" },
|
|
16
17
|
{ value: "final-only", label: "Each role final reply" },
|
|
17
18
|
{ value: "all", label: "All replies" }
|