@testchimp/cli 0.1.55 → 0.1.56
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/chimphands/run.js +68 -5
- package/package.json +1 -1
package/dist/chimphands/run.js
CHANGED
|
@@ -23,6 +23,34 @@ const STREAM_POST_MIN_INTERVAL_MS = 150;
|
|
|
23
23
|
function isStreamFanoutRole(role) {
|
|
24
24
|
return role === ROLE_ASSISTANT || role === ROLE_TOOL || role === ROLE_REASONING;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* OpenCode sometimes emits the same thought as both a reasoning part and a text part.
|
|
28
|
+
* Skip ASSISTANT fanout when content matches (or is a streaming prefix of) reasoning.
|
|
29
|
+
*/
|
|
30
|
+
function isTextDuplicateOfReasoning(text, reasoningBodies) {
|
|
31
|
+
const a = String(text || "").trim();
|
|
32
|
+
if (!a)
|
|
33
|
+
return false;
|
|
34
|
+
for (const raw of reasoningBodies) {
|
|
35
|
+
const r = String(raw || "").trim();
|
|
36
|
+
if (!r)
|
|
37
|
+
continue;
|
|
38
|
+
if (a === r)
|
|
39
|
+
return true;
|
|
40
|
+
if (a.length >= 32 && r.length >= 32 && (r.startsWith(a) || a.startsWith(r))) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
function reasoningBodiesFromPartMap(textByPartId) {
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const [key, val] of textByPartId) {
|
|
49
|
+
if (key.startsWith("reasoning:"))
|
|
50
|
+
out.push(val);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
26
54
|
const CHIMPHANDS_AGENT_PROMPT = `You are ChimpHands, TestChimp's coding agent. You run on GitHub Actions, but this chat is an **interactive** conversation with the user in the TestChimp UI — same expectations as Cursor/Claude Code locally.
|
|
27
55
|
|
|
28
56
|
## Interactive session (mandatory — default)
|
|
@@ -386,9 +414,16 @@ async function reconcileOpencodeSessionMessages(attachUrl, opencodeSessionId, po
|
|
|
386
414
|
for (const msg of turnSlice) {
|
|
387
415
|
if ((msg.info?.role || "").toLowerCase() !== "assistant")
|
|
388
416
|
continue;
|
|
417
|
+
const reasoningBodies = (msg.parts || [])
|
|
418
|
+
.filter((p) => p.type === "reasoning" && p.text?.trim())
|
|
419
|
+
.map((p) => p.text.trim());
|
|
389
420
|
for (const part of msg.parts || []) {
|
|
390
421
|
if (part.type === "text" && part.text?.trim()) {
|
|
391
|
-
|
|
422
|
+
const text = part.text.trim();
|
|
423
|
+
if (isTextDuplicateOfReasoning(text, reasoningBodies)) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
postEvent(ROLE_ASSISTANT, text, {
|
|
392
427
|
messageId: opencodeMessageId("oc_text_", part),
|
|
393
428
|
});
|
|
394
429
|
posted += 1;
|
|
@@ -498,6 +533,9 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
498
533
|
if (field === "text") {
|
|
499
534
|
const next = (textByPartId.get(partId) || "") + props.delta;
|
|
500
535
|
textByPartId.set(partId, next);
|
|
536
|
+
if (isTextDuplicateOfReasoning(next, reasoningBodiesFromPartMap(textByPartId))) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
501
539
|
callbacks.postEvent(ROLE_ASSISTANT, next, liveOpts({
|
|
502
540
|
messageId: `oc_text_${partId}`,
|
|
503
541
|
}));
|
|
@@ -537,6 +575,9 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
537
575
|
textByPartId.set(partId, next);
|
|
538
576
|
if (!next)
|
|
539
577
|
return;
|
|
578
|
+
if (isTextDuplicateOfReasoning(next, reasoningBodiesFromPartMap(textByPartId))) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
540
581
|
callbacks.postEvent(ROLE_ASSISTANT, next, liveOpts({
|
|
541
582
|
messageId: opencodeMessageId("oc_text_", part),
|
|
542
583
|
}));
|
|
@@ -1018,7 +1059,9 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1018
1059
|
return;
|
|
1019
1060
|
const partId = ev.part?.id || ev.part?.messageID;
|
|
1020
1061
|
if (!partId) {
|
|
1021
|
-
|
|
1062
|
+
if (!isTextDuplicateOfReasoning(chunk, reasoningBodiesFromPartMap(textByPartId))) {
|
|
1063
|
+
callbacks.postEvent(ROLE_ASSISTANT, chunk, { throttle: true });
|
|
1064
|
+
}
|
|
1022
1065
|
return;
|
|
1023
1066
|
}
|
|
1024
1067
|
// Without attach, --format json may emit completed cumulative or deltas.
|
|
@@ -1030,6 +1073,9 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1030
1073
|
? chunk
|
|
1031
1074
|
: prev + chunk;
|
|
1032
1075
|
textByPartId.set(partId, next);
|
|
1076
|
+
if (isTextDuplicateOfReasoning(next, reasoningBodiesFromPartMap(textByPartId))) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1033
1079
|
callbacks.postEvent(ROLE_ASSISTANT, next, {
|
|
1034
1080
|
throttle: true,
|
|
1035
1081
|
messageId: opencodeMessageId("oc_text_", ev.part),
|
|
@@ -1404,6 +1450,22 @@ export async function runChimphands(opts) {
|
|
|
1404
1450
|
}
|
|
1405
1451
|
poster.fireAndForget(role, content, bodyOpts);
|
|
1406
1452
|
};
|
|
1453
|
+
/** Drop assistant bubbles that are just the current user prompt echoed (often quoted). */
|
|
1454
|
+
const postEventForTurn = (userPrompt, role, content, opts) => {
|
|
1455
|
+
if (role === ROLE_ASSISTANT) {
|
|
1456
|
+
const a = String(content || "").trim();
|
|
1457
|
+
const u = normalizeUserMessage(userPrompt);
|
|
1458
|
+
if (a && u) {
|
|
1459
|
+
const unquoted = (a.startsWith('"') && a.endsWith('"')) || (a.startsWith("'") && a.endsWith("'"))
|
|
1460
|
+
? a.slice(1, -1).trim()
|
|
1461
|
+
: a;
|
|
1462
|
+
if (a === u || unquoted === u) {
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
postEvent(role, content, opts);
|
|
1468
|
+
};
|
|
1407
1469
|
const complete = (status, errorMessage) => {
|
|
1408
1470
|
const body = { sessionId, status };
|
|
1409
1471
|
if (errorMessage)
|
|
@@ -1502,6 +1564,7 @@ export async function runChimphands(opts) {
|
|
|
1502
1564
|
let useOpencodeSessionId = opencodeSessionId;
|
|
1503
1565
|
let isNewOpencodeSession = !useOpencodeSessionId;
|
|
1504
1566
|
let effectivePrompt = wrapPromptWithContext(conversationSummary, prompt, isNewOpencodeSession, workingBranch, pullRequestUrl);
|
|
1567
|
+
const turnPostEvent = (role, content, opts) => postEventForTurn(prompt, role, content, opts);
|
|
1505
1568
|
// Visible in chat (not filtered as routine). OpenCode may not emit text until a
|
|
1506
1569
|
// part completes — without this the UI looks empty while the turn is running.
|
|
1507
1570
|
postEvent(ROLE_STATUS, "Agent is working…", { status: STATUS_RUNNING });
|
|
@@ -1514,7 +1577,7 @@ export async function runChimphands(opts) {
|
|
|
1514
1577
|
}).catch(() => { });
|
|
1515
1578
|
},
|
|
1516
1579
|
onWorkingBranch: noteWorkingBranch,
|
|
1517
|
-
postEvent,
|
|
1580
|
+
postEvent: turnPostEvent,
|
|
1518
1581
|
}, attachUrl);
|
|
1519
1582
|
if (result.code !== 0 &&
|
|
1520
1583
|
useOpencodeSessionId &&
|
|
@@ -1532,7 +1595,7 @@ export async function runChimphands(opts) {
|
|
|
1532
1595
|
}).catch(() => { });
|
|
1533
1596
|
},
|
|
1534
1597
|
onWorkingBranch: noteWorkingBranch,
|
|
1535
|
-
postEvent,
|
|
1598
|
+
postEvent: turnPostEvent,
|
|
1536
1599
|
}, attachUrl);
|
|
1537
1600
|
}
|
|
1538
1601
|
await poster.flush();
|
|
@@ -1545,7 +1608,7 @@ export async function runChimphands(opts) {
|
|
|
1545
1608
|
if (attachUrl && opencodeSessionId) {
|
|
1546
1609
|
try {
|
|
1547
1610
|
await reconcileOpencodeSessionMessages(attachUrl, opencodeSessionId, (role, content, opts) => {
|
|
1548
|
-
|
|
1611
|
+
turnPostEvent(role, content, {
|
|
1549
1612
|
...opts,
|
|
1550
1613
|
opencodeSessionId,
|
|
1551
1614
|
durable: true,
|
package/package.json
CHANGED