@elench/testkit 0.1.102 → 0.1.104

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.
@@ -5,7 +5,7 @@ import {
5
5
  buildToolEvent,
6
6
  createHostedSessionRunner,
7
7
  } from "./shared.mjs";
8
- import { providerAssistantDelta, providerAssistantFinal, providerToolEnd, providerToolStart } from "./events.mjs";
8
+ import { providerAssistantDelta, providerAssistantFinal, providerToolStart } from "./events.mjs";
9
9
 
10
10
  export function startClaudeHostedSession({
11
11
  command = "claude",
@@ -130,7 +130,6 @@ export function parseClaudePayload(payload) {
130
130
  } else if (typeof payload.result === "string") {
131
131
  const event = providerAssistantFinal(payload.result);
132
132
  if (event) events.push(event);
133
- events.push(providerToolEnd("claude", { status: "ok", durationMs: payload.duration_ms || null }));
134
133
  }
135
134
  return events;
136
135
  }
@@ -4,12 +4,18 @@ import path from "path";
4
4
  import { execa } from "execa";
5
5
  import {
6
6
  buildErrorEvent,
7
- buildStatusEvent,
8
7
  buildToolEvent,
9
8
  createHostedSessionRunner,
10
9
  readTextFileIfPresent,
11
10
  } from "./shared.mjs";
12
- import { providerAssistantDelta, providerAssistantFinal, providerToolEnd, providerToolStart, providerToolUpdate } from "./events.mjs";
11
+ import {
12
+ providerAssistantDelta,
13
+ providerAssistantFinal,
14
+ providerStatus,
15
+ providerToolEnd,
16
+ providerToolStart,
17
+ providerToolUpdate,
18
+ } from "./events.mjs";
13
19
 
14
20
  export function startCodexHostedSession({
15
21
  command = "codex",
@@ -99,19 +105,22 @@ export function parseCodexPayload(payload) {
99
105
  }
100
106
 
101
107
  if (type === "thread.started") {
102
- const event = buildStatusEvent(payload.thread_id ? `Codex thread ${payload.thread_id} started` : "Codex thread started");
108
+ const event = providerStatus(
109
+ payload.thread_id ? `Codex thread ${payload.thread_id} started` : "Codex thread started",
110
+ { transient: true }
111
+ );
103
112
  if (event) events.push(event);
104
113
  return events;
105
114
  }
106
115
 
107
116
  if (type === "turn.started") {
108
- const event = buildStatusEvent("Codex turn started");
117
+ const event = providerStatus("Codex turn started", { transient: true });
109
118
  if (event) events.push(event);
110
119
  return events;
111
120
  }
112
121
 
113
122
  if (type === "turn.completed") {
114
- const event = providerToolEnd("codex", { status: "ok", usage: payload.usage || null });
123
+ const event = providerStatus("Codex turn completed", { transient: true, usage: payload.usage || null });
115
124
  if (event) events.push(event);
116
125
  return events;
117
126
  }
@@ -146,31 +155,46 @@ export function parseCodexPayload(payload) {
146
155
  return events;
147
156
  }
148
157
 
149
- const statusEvent = buildStatusEvent(type ? `Codex event: ${type}` : JSON.stringify(payload));
158
+ const statusEvent = providerStatus(type ? `Codex event: ${type}` : JSON.stringify(payload));
150
159
  if (statusEvent) events.push(statusEvent);
151
160
  return events;
152
161
  }
153
162
 
154
163
  function codexItemStartedEvent(item) {
155
164
  if (!item || typeof item !== "object") return null;
156
- if (item.type === "command_execution" || item.type === "tool_call") {
157
- return providerToolStart(item.command || item.name || item.type, {
165
+ if (item.type === "command_execution") {
166
+ return providerToolStart("command", {
167
+ id: item.id || null,
168
+ input: item.command || item.input || null,
169
+ });
170
+ }
171
+ if (item.type === "tool_call") {
172
+ return providerToolStart(item.name || item.type, {
158
173
  id: item.id || null,
159
174
  input: item.arguments || item.input || null,
160
175
  });
161
176
  }
162
- if (item.type === "agent_message") return buildStatusEvent("Codex started response");
163
- if (item.type) return buildStatusEvent(`Codex started ${item.type}`);
177
+ if (item.type === "agent_message") return providerStatus("Codex started response", { transient: true });
178
+ if (item.type) return providerStatus(`Codex started ${item.type}`);
164
179
  return null;
165
180
  }
166
181
 
167
182
  function codexItemUpdatedEvent(item) {
168
183
  if (!item || typeof item !== "object") return null;
169
184
  if (item.type === "agent_message" && typeof item.text === "string") {
170
- return providerAssistantDelta(item.text);
185
+ return providerAssistantDelta(item.text, {
186
+ id: item.id || null,
187
+ });
188
+ }
189
+ if (item.type === "command_execution") {
190
+ return providerToolUpdate("command", {
191
+ id: item.id || null,
192
+ text: item.output || item.status || null,
193
+ data: item,
194
+ });
171
195
  }
172
- if (item.type === "command_execution" || item.type === "tool_call") {
173
- return providerToolUpdate(item.command || item.name || item.type, {
196
+ if (item.type === "tool_call") {
197
+ return providerToolUpdate(item.name || item.type, {
174
198
  id: item.id || null,
175
199
  text: item.output || item.status || null,
176
200
  data: item,
@@ -182,16 +206,26 @@ function codexItemUpdatedEvent(item) {
182
206
  function codexItemCompletedEvent(item) {
183
207
  if (!item || typeof item !== "object") return null;
184
208
  if (item.type === "agent_message" && typeof item.text === "string") {
185
- return providerAssistantFinal(item.text);
209
+ return providerAssistantFinal(item.text, {
210
+ id: item.id || null,
211
+ });
212
+ }
213
+ if (item.type === "command_execution") {
214
+ return providerToolEnd("command", {
215
+ id: item.id || null,
216
+ status: item.status === "failed" ? "error" : "ok",
217
+ output: item.output || null,
218
+ data: item,
219
+ });
186
220
  }
187
- if (item.type === "command_execution" || item.type === "tool_call") {
188
- return providerToolEnd(item.command || item.name || item.type, {
221
+ if (item.type === "tool_call") {
222
+ return providerToolEnd(item.name || item.type, {
189
223
  id: item.id || null,
190
224
  status: item.status === "failed" ? "error" : "ok",
191
225
  output: item.output || null,
192
226
  data: item,
193
227
  });
194
228
  }
195
- if (item.type) return buildStatusEvent(`Codex completed ${item.type}`);
229
+ if (item.type) return providerStatus(`Codex completed ${item.type}`);
196
230
  return null;
197
231
  }
@@ -40,6 +40,7 @@ export function createHostedSessionRunner({
40
40
  emit(providerEvent("session-start"));
41
41
 
42
42
  const stdoutReader = readline.createInterface({ input: child.stdout });
43
+ const stdoutClosed = waitForReaderClose(stdoutReader);
43
44
  stdoutReader.on("line", (line) => {
44
45
  onRawLine?.({ provider, stream: "stdout", line });
45
46
  const parsed = tryParseJson(line);
@@ -53,6 +54,7 @@ export function createHostedSessionRunner({
53
54
  });
54
55
 
55
56
  const stderrReader = readline.createInterface({ input: child.stderr });
57
+ const stderrClosed = waitForReaderClose(stderrReader);
56
58
  stderrReader.on("line", (line) => {
57
59
  onRawLine?.({ provider, stream: "stderr", line });
58
60
  if (shouldIgnoreStatus?.(line)) return;
@@ -61,6 +63,7 @@ export function createHostedSessionRunner({
61
63
 
62
64
  const completion = (async () => {
63
65
  const result = await child;
66
+ await Promise.all([stdoutClosed, stderrClosed]);
64
67
  const fileFinalText = readFinalText ? readFinalText(result) : null;
65
68
  const resolvedFinalText = fileFinalText || finalText || assistantText.trim() || null;
66
69
  if ((result.exitCode ?? 0) !== 0) {
@@ -97,6 +100,12 @@ export function createHostedSessionRunner({
97
100
  };
98
101
  }
99
102
 
103
+ function waitForReaderClose(reader) {
104
+ return new Promise((resolve) => {
105
+ reader.once("close", resolve);
106
+ });
107
+ }
108
+
100
109
  export function tryParseJson(line) {
101
110
  const normalized = String(line || "").trim();
102
111
  if (!normalized) return null;
@@ -53,7 +53,12 @@ export async function runAssistantConversationTurn({
53
53
  effort: runtimeSettings.effort || null,
54
54
  });
55
55
  onStatus?.(`Thinking with ${resolvedProvider}...`);
56
- onProviderEvent?.({ type: "status", provider: resolvedProvider, text: `Thinking with ${resolvedProvider}...` });
56
+ onProviderEvent?.({
57
+ type: "status",
58
+ provider: resolvedProvider,
59
+ text: `Thinking with ${resolvedProvider}...`,
60
+ transient: true,
61
+ });
57
62
 
58
63
  observer.start();
59
64
  try {
@@ -635,12 +635,17 @@ function handleAssistantToolEvent(state, event, appendMessage) {
635
635
  function createProviderTurnState() {
636
636
  return {
637
637
  assistantMessageId: null,
638
+ assistantMessageIdsByProviderItem: new Map(),
638
639
  lastActivityText: null,
639
640
  };
640
641
  }
641
642
 
642
643
  function handleProviderEvent(turn, event, { appendMessage, updateMessage, setStatus } = {}) {
643
644
  if (!event) return;
645
+ if (event.transient || event.display === false) {
646
+ if (event.type === "status" && event.text) setStatus?.(event.text);
647
+ return;
648
+ }
644
649
  if (event.type === "assistant-delta") {
645
650
  appendAssistantDelta(turn, event, { appendMessage, updateMessage });
646
651
  setStatus?.(`${event.provider || "provider"} responding`);
@@ -652,21 +657,11 @@ function handleProviderEvent(turn, event, { appendMessage, updateMessage, setSta
652
657
  return;
653
658
  }
654
659
  if (event.type === "session-start") {
655
- appendProviderActivity(turn, {
656
- role: "provider-activity",
657
- title: formatProviderName(event.provider),
658
- text: "Session started",
659
- data: event,
660
- }, { appendMessage, setStatus });
660
+ setStatus?.(`${event.provider || "provider"} session started`);
661
661
  return;
662
662
  }
663
663
  if (event.type === "session-end") {
664
- appendProviderActivity(turn, {
665
- role: "provider-activity",
666
- title: formatProviderName(event.provider),
667
- text: Number.isInteger(event.exitCode) ? `Session ended with exit code ${event.exitCode}` : "Session ended",
668
- data: event,
669
- }, { appendMessage, setStatus });
664
+ setStatus?.(`${event.provider || "provider"} session ended`);
670
665
  return;
671
666
  }
672
667
  if (event.type === "status") {
@@ -699,15 +694,19 @@ function handleProviderEvent(turn, event, { appendMessage, updateMessage, setSta
699
694
  }
700
695
 
701
696
  function appendAssistantDelta(turn, event, { appendMessage, updateMessage }) {
702
- if (!turn.assistantMessageId) {
703
- turn.assistantMessageId = appendMessage({
697
+ const messageKey = providerAssistantMessageKey(event);
698
+ let messageId = messageKey ? turn.assistantMessageIdsByProviderItem.get(messageKey) : turn.assistantMessageId;
699
+ if (!messageId) {
700
+ messageId = appendMessage({
704
701
  role: "assistant",
705
702
  status: "streaming",
706
703
  provider: event.provider || null,
707
704
  text: "",
708
705
  });
706
+ if (messageKey) turn.assistantMessageIdsByProviderItem.set(messageKey, messageId);
707
+ else turn.assistantMessageId = messageId;
709
708
  }
710
- updateMessage(turn.assistantMessageId, (message) => ({
709
+ updateMessage(messageId, (message) => ({
711
710
  text: `${message.text || ""}${event.text || ""}`,
712
711
  status: "streaming",
713
712
  }));
@@ -715,20 +714,29 @@ function appendAssistantDelta(turn, event, { appendMessage, updateMessage }) {
715
714
 
716
715
  function finalizeAssistantMessage(turn, event, { appendMessage, updateMessage }) {
717
716
  const finalText = event.text || "";
718
- if (!turn.assistantMessageId) {
719
- turn.assistantMessageId = appendMessage({
717
+ const messageKey = providerAssistantMessageKey(event);
718
+ let messageId = messageKey ? turn.assistantMessageIdsByProviderItem.get(messageKey) : turn.assistantMessageId;
719
+ if (!messageId) {
720
+ messageId = appendMessage({
720
721
  role: "assistant",
721
722
  provider: event.provider || null,
722
723
  text: finalText,
723
724
  });
725
+ if (messageKey) turn.assistantMessageIdsByProviderItem.set(messageKey, messageId);
726
+ else turn.assistantMessageId = messageId;
724
727
  return;
725
728
  }
726
- updateMessage(turn.assistantMessageId, (message) => ({
729
+ updateMessage(messageId, (message) => ({
727
730
  text: finalText || message.text || "",
728
731
  status: null,
729
732
  }));
730
733
  }
731
734
 
735
+ function providerAssistantMessageKey(event) {
736
+ if (!event?.id) return null;
737
+ return `${event.provider || "provider"}:${event.id}`;
738
+ }
739
+
732
740
  function appendProviderActivity(turn, message, { appendMessage, setStatus }) {
733
741
  const text = String(message.text || "").trim();
734
742
  if (!text) return;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/next-analysis",
3
- "version": "0.1.102",
3
+ "version": "0.1.104",
4
4
  "description": "SWC-backed Next.js source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-bridge",
3
- "version": "0.1.102",
3
+ "version": "0.1.104",
4
4
  "description": "Browser bridge helpers for testkit",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "typecheck": "tsc -p tsconfig.json --noEmit"
23
23
  },
24
24
  "dependencies": {
25
- "@elench/testkit-protocol": "0.1.102"
25
+ "@elench/testkit-protocol": "0.1.104"
26
26
  },
27
27
  "private": false
28
28
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-protocol",
3
- "version": "0.1.102",
3
+ "version": "0.1.104",
4
4
  "description": "Shared browser protocol for testkit bridge and extension consumers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/ts-analysis",
3
- "version": "0.1.102",
3
+ "version": "0.1.104",
4
4
  "description": "TypeScript compiler-backed source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.102",
3
+ "version": "0.1.104",
4
4
  "description": "Assistant-first CLI for running, inspecting, and debugging local testkit suites",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -90,10 +90,10 @@
90
90
  },
91
91
  "dependencies": {
92
92
  "@babel/code-frame": "^7.29.0",
93
- "@elench/next-analysis": "0.1.102",
94
- "@elench/testkit-bridge": "0.1.102",
95
- "@elench/testkit-protocol": "0.1.102",
96
- "@elench/ts-analysis": "0.1.102",
93
+ "@elench/next-analysis": "0.1.104",
94
+ "@elench/testkit-bridge": "0.1.104",
95
+ "@elench/testkit-protocol": "0.1.104",
96
+ "@elench/ts-analysis": "0.1.104",
97
97
  "@oclif/core": "^4.10.6",
98
98
  "esbuild": "^0.25.11",
99
99
  "execa": "^9.5.0",