@testchimp/cli 0.1.55 → 0.1.57
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 +91 -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
|
}));
|
|
@@ -712,6 +753,35 @@ function isMissingOpencodeSessionError(message) {
|
|
|
712
753
|
m.includes("unknown session") ||
|
|
713
754
|
m.includes("invalid session"));
|
|
714
755
|
}
|
|
756
|
+
/** Strip one layer of wrapping quotes models sometimes add when echoing. */
|
|
757
|
+
function stripOuterQuotes(text) {
|
|
758
|
+
const a = String(text || "").trim();
|
|
759
|
+
if ((a.startsWith('"') && a.endsWith('"')) ||
|
|
760
|
+
(a.startsWith("'") && a.endsWith("'"))) {
|
|
761
|
+
return a.slice(1, -1).trim();
|
|
762
|
+
}
|
|
763
|
+
return a;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* True when assistant text is an echo of a prompt we just sent to OpenCode
|
|
767
|
+
* (raw user text and/or host-wrapped effectivePrompt) — exact or streaming prefix.
|
|
768
|
+
*/
|
|
769
|
+
function isAssistantEchoOfSentPrompt(assistant, ...sentPrompts) {
|
|
770
|
+
const a = stripOuterQuotes(assistant);
|
|
771
|
+
if (!a)
|
|
772
|
+
return false;
|
|
773
|
+
for (const raw of sentPrompts) {
|
|
774
|
+
const p = String(raw || "").trim();
|
|
775
|
+
if (!p)
|
|
776
|
+
continue;
|
|
777
|
+
if (a === p)
|
|
778
|
+
return true;
|
|
779
|
+
// Streaming echo of the (usually long) wrapped prompt: only when clearly a prefix.
|
|
780
|
+
if (p.length >= 64 && a.length >= 24 && p.startsWith(a))
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
715
785
|
function wrapPromptWithContext(conversationSummary, userPrompt, isNewOpencodeSession, workingBranch, pullRequestUrl) {
|
|
716
786
|
const parts = [];
|
|
717
787
|
if (workingBranch?.trim()) {
|
|
@@ -1018,7 +1088,9 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1018
1088
|
return;
|
|
1019
1089
|
const partId = ev.part?.id || ev.part?.messageID;
|
|
1020
1090
|
if (!partId) {
|
|
1021
|
-
|
|
1091
|
+
if (!isTextDuplicateOfReasoning(chunk, reasoningBodiesFromPartMap(textByPartId))) {
|
|
1092
|
+
callbacks.postEvent(ROLE_ASSISTANT, chunk, { throttle: true });
|
|
1093
|
+
}
|
|
1022
1094
|
return;
|
|
1023
1095
|
}
|
|
1024
1096
|
// Without attach, --format json may emit completed cumulative or deltas.
|
|
@@ -1030,6 +1102,9 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1030
1102
|
? chunk
|
|
1031
1103
|
: prev + chunk;
|
|
1032
1104
|
textByPartId.set(partId, next);
|
|
1105
|
+
if (isTextDuplicateOfReasoning(next, reasoningBodiesFromPartMap(textByPartId))) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1033
1108
|
callbacks.postEvent(ROLE_ASSISTANT, next, {
|
|
1034
1109
|
throttle: true,
|
|
1035
1110
|
messageId: opencodeMessageId("oc_text_", ev.part),
|
|
@@ -1404,6 +1479,16 @@ export async function runChimphands(opts) {
|
|
|
1404
1479
|
}
|
|
1405
1480
|
poster.fireAndForget(role, content, bodyOpts);
|
|
1406
1481
|
};
|
|
1482
|
+
/**
|
|
1483
|
+
* Drop ASSISTANT bubbles that echo the prompt we just sent — either the raw
|
|
1484
|
+
* user text or the host-wrapped effectivePrompt (exact string OpenCode got).
|
|
1485
|
+
*/
|
|
1486
|
+
const postEventForTurn = (userPrompt, wrappedPrompt, role, content, opts) => {
|
|
1487
|
+
if (role === ROLE_ASSISTANT && isAssistantEchoOfSentPrompt(content, userPrompt, wrappedPrompt)) {
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
postEvent(role, content, opts);
|
|
1491
|
+
};
|
|
1407
1492
|
const complete = (status, errorMessage) => {
|
|
1408
1493
|
const body = { sessionId, status };
|
|
1409
1494
|
if (errorMessage)
|
|
@@ -1502,6 +1587,7 @@ export async function runChimphands(opts) {
|
|
|
1502
1587
|
let useOpencodeSessionId = opencodeSessionId;
|
|
1503
1588
|
let isNewOpencodeSession = !useOpencodeSessionId;
|
|
1504
1589
|
let effectivePrompt = wrapPromptWithContext(conversationSummary, prompt, isNewOpencodeSession, workingBranch, pullRequestUrl);
|
|
1590
|
+
const turnPostEvent = (role, content, opts) => postEventForTurn(prompt, effectivePrompt, role, content, opts);
|
|
1505
1591
|
// Visible in chat (not filtered as routine). OpenCode may not emit text until a
|
|
1506
1592
|
// part completes — without this the UI looks empty while the turn is running.
|
|
1507
1593
|
postEvent(ROLE_STATUS, "Agent is working…", { status: STATUS_RUNNING });
|
|
@@ -1514,7 +1600,7 @@ export async function runChimphands(opts) {
|
|
|
1514
1600
|
}).catch(() => { });
|
|
1515
1601
|
},
|
|
1516
1602
|
onWorkingBranch: noteWorkingBranch,
|
|
1517
|
-
postEvent,
|
|
1603
|
+
postEvent: turnPostEvent,
|
|
1518
1604
|
}, attachUrl);
|
|
1519
1605
|
if (result.code !== 0 &&
|
|
1520
1606
|
useOpencodeSessionId &&
|
|
@@ -1532,7 +1618,7 @@ export async function runChimphands(opts) {
|
|
|
1532
1618
|
}).catch(() => { });
|
|
1533
1619
|
},
|
|
1534
1620
|
onWorkingBranch: noteWorkingBranch,
|
|
1535
|
-
postEvent,
|
|
1621
|
+
postEvent: turnPostEvent,
|
|
1536
1622
|
}, attachUrl);
|
|
1537
1623
|
}
|
|
1538
1624
|
await poster.flush();
|
|
@@ -1545,7 +1631,7 @@ export async function runChimphands(opts) {
|
|
|
1545
1631
|
if (attachUrl && opencodeSessionId) {
|
|
1546
1632
|
try {
|
|
1547
1633
|
await reconcileOpencodeSessionMessages(attachUrl, opencodeSessionId, (role, content, opts) => {
|
|
1548
|
-
|
|
1634
|
+
turnPostEvent(role, content, {
|
|
1549
1635
|
...opts,
|
|
1550
1636
|
opencodeSessionId,
|
|
1551
1637
|
durable: true,
|
package/package.json
CHANGED