Skip to content

Commit ad1a19f

Browse files
committed
feat: experimenting with trace-in-chat
1 parent 937925f commit ad1a19f

File tree

4 files changed

+790
-456
lines changed

4 files changed

+790
-456
lines changed

npm-app/src/cli-handlers/__tests__/chat-postcontent-streaming.test.ts

Lines changed: 100 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { describe, test, expect, beforeEach, afterEach, spyOn, mock } from 'bun:test'
1+
import {
2+
describe,
3+
test,
4+
expect,
5+
beforeEach,
6+
afterEach,
7+
spyOn,
8+
mock,
9+
} from 'bun:test'
210

311
// Mock the logger
412
mock.module('../utils/logger', () => ({
@@ -49,7 +57,9 @@ spyOn(process.stdout, 'write').mockImplementation((data) => {
4957
})
5058

5159
// Mock process.stdin for keypress handling
52-
spyOn(process.stdin, 'removeAllListeners').mockImplementation(() => process.stdin)
60+
spyOn(process.stdin, 'removeAllListeners').mockImplementation(
61+
() => process.stdin,
62+
)
5363
spyOn(process.stdin, 'on').mockImplementation(() => process.stdin)
5464
spyOn(process.stdin, 'listeners').mockImplementation(() => [])
5565
spyOn(process.stdin, 'setRawMode').mockImplementation(() => process.stdin)
@@ -61,17 +71,22 @@ Object.defineProperty(process.stdout, 'columns', { value: 80, writable: true })
6171
Object.defineProperty(process.stdin, 'isTTY', { value: true, writable: true })
6272

6373
describe('PostContent Streaming Tests', () => {
64-
let streamingOrder: Array<{ type: 'content' | 'postContent'; nodeId: string; text: string; timestamp: number }> = []
65-
74+
let streamingOrder: Array<{
75+
type: 'content'
76+
nodeId: string
77+
text: string
78+
timestamp: number
79+
}> = []
80+
6681
beforeEach(() => {
6782
streamingOrder = []
6883
mockWrites.length = 0
6984
})
70-
85+
7186
afterEach(() => {
7287
mock.restore()
7388
})
74-
89+
7590
test('should stream parent postContent only after all children finish', async () => {
7691
// Create a mock response with nested children
7792
const mockResponse = {
@@ -93,23 +108,27 @@ describe('PostContent Streaming Tests', () => {
93108
],
94109
},
95110
{
96-
content: 'Child 2 content',
111+
content: 'Child 2 content',
97112
agent: 'reviewer',
98113
postContent: 'Child 2 summary',
99114
children: [],
100115
},
101116
],
102117
}
103-
118+
104119
// We need to create a test harness that simulates the streaming logic
105120
// Let's create a simplified version to test the core logic
106-
107-
const streamedItems: Array<{ nodeId: string; type: 'content' | 'postContent'; text: string }> = []
108-
121+
122+
const streamedItems: Array<{
123+
nodeId: string
124+
type: 'content'
125+
text: string
126+
}> = []
127+
109128
// Mock streamTextToNodeProperty to track streaming order
110129
async function mockStreamTextToNodeProperty(
111130
node: SubagentNode,
112-
property: 'content' | 'postContent',
131+
property: 'content',
113132
text: string,
114133
): Promise<void> {
115134
streamedItems.push({
@@ -118,7 +137,7 @@ describe('PostContent Streaming Tests', () => {
118137
text: text,
119138
})
120139
}
121-
140+
122141
// Simulate the streamSubagentTreeContent logic with our test data
123142
async function testStreamSubagentTreeContent(
124143
responseNode: any,
@@ -128,36 +147,41 @@ describe('PostContent Streaming Tests', () => {
128147
if (!responseNode.children || responseNode.children.length === 0) {
129148
return []
130149
}
131-
132-
const allNodesWithPostContent: { nodeId: string; postContent: string }[] = []
133-
150+
151+
const allNodesWithPostContent: { nodeId: string; postContent: string }[] =
152+
[]
153+
134154
// First pass: Process all children content and create nodes
135155
const childNodes: { nodeId: string; originalChild: any }[] = []
136-
137-
for (let childIndex = 0; childIndex < responseNode.children.length; childIndex++) {
156+
157+
for (
158+
let childIndex = 0;
159+
childIndex < responseNode.children.length;
160+
childIndex++
161+
) {
138162
const child = responseNode.children[childIndex]
139163
const childPath = [...currentPath, childIndex]
140164
const nodeId = `${messageId}/${childPath.join('/')}`
141-
165+
142166
const childNode: SubagentNode = {
143167
id: nodeId,
144168
type: child.agent || 'unknown',
145169
content: '',
146170
children: [],
147171
}
148-
172+
149173
// Stream this child's content
150174
await mockStreamTextToNodeProperty(childNode, 'content', child.content)
151-
175+
152176
// Store for later processing
153177
childNodes.push({ nodeId, originalChild: child })
154178
}
155-
179+
156180
// Second pass: Process all children recursively (grandchildren)
157181
for (let i = 0; i < childNodes.length; i++) {
158182
const { nodeId: childNodeId, originalChild: child } = childNodes[i]
159183
const childPath = [...currentPath, i]
160-
184+
161185
// Recursively process grandchildren
162186
const descendantPostContentNodes = await testStreamSubagentTreeContent(
163187
child,
@@ -166,7 +190,7 @@ describe('PostContent Streaming Tests', () => {
166190
)
167191
allNodesWithPostContent.push(...descendantPostContentNodes)
168192
}
169-
193+
170194
// Third pass: After ALL descendants are processed, collect postContent from this level
171195
for (const { nodeId: childNodeId, originalChild: child } of childNodes) {
172196
if (child.postContent) {
@@ -176,66 +200,90 @@ describe('PostContent Streaming Tests', () => {
176200
})
177201
}
178202
}
179-
203+
180204
return allNodesWithPostContent
181205
}
182-
206+
183207
// Test the streaming logic
184208
const messageId = 'test-message'
185-
209+
186210
// Stream main content first
187211
await mockStreamTextToNodeProperty(
188212
{ id: messageId, type: 'assistant', content: '', children: [] },
189213
'content',
190-
mockResponse.content
214+
mockResponse.content,
191215
)
192-
216+
193217
// Process subagent tree
194218
const allPostContentNodes = await testStreamSubagentTreeContent(
195219
mockResponse,
196220
messageId,
197221
[],
198222
)
199-
223+
200224
// Add parent postContent to collection (should be last)
201225
if (mockResponse.postContent) {
202226
allPostContentNodes.push({
203227
nodeId: messageId,
204228
postContent: mockResponse.postContent,
205229
})
206230
}
207-
208-
// Stream all postContent
231+
232+
// Stream all postContent (simulated as content for the test)
209233
for (const item of allPostContentNodes) {
210234
await mockStreamTextToNodeProperty(
211235
{ id: item.nodeId, type: 'assistant', content: '', children: [] },
212-
'postContent',
213-
item.postContent
236+
'content',
237+
item.postContent,
214238
)
215239
}
216-
240+
217241
// Verify streaming order
218242
expect(streamedItems).toHaveLength(8) // 4 content + 4 postContent (including parent)
219-
243+
220244
// Content should stream first
221-
expect(streamedItems[0]).toMatchObject({ type: 'content', text: 'Parent content' })
222-
expect(streamedItems[1]).toMatchObject({ type: 'content', text: 'Child 1 content' })
223-
expect(streamedItems[2]).toMatchObject({ type: 'content', text: 'Child 2 content' })
224-
expect(streamedItems[3]).toMatchObject({ type: 'content', text: 'Grandchild 1 content' })
225-
245+
expect(streamedItems[0]).toMatchObject({
246+
type: 'content',
247+
text: 'Parent content',
248+
})
249+
expect(streamedItems[1]).toMatchObject({
250+
type: 'content',
251+
text: 'Child 1 content',
252+
})
253+
expect(streamedItems[2]).toMatchObject({
254+
type: 'content',
255+
text: 'Child 2 content',
256+
})
257+
expect(streamedItems[3]).toMatchObject({
258+
type: 'content',
259+
text: 'Grandchild 1 content',
260+
})
261+
226262
// PostContent should stream after ALL content, from deepest to shallowest
227-
expect(streamedItems[4]).toMatchObject({ type: 'postContent', text: 'Grandchild 1 summary' })
228-
expect(streamedItems[5]).toMatchObject({ type: 'postContent', text: 'Child 1 summary' })
229-
expect(streamedItems[6]).toMatchObject({ type: 'postContent', text: 'Child 2 summary' })
230-
263+
expect(streamedItems[4]).toMatchObject({
264+
type: 'content',
265+
text: 'Grandchild 1 summary',
266+
})
267+
expect(streamedItems[5]).toMatchObject({
268+
type: 'content',
269+
text: 'Child 1 summary',
270+
})
271+
expect(streamedItems[6]).toMatchObject({
272+
type: 'content',
273+
text: 'Child 2 summary',
274+
})
275+
231276
// MOST IMPORTANT: Parent postContent should be LAST
232-
expect(streamedItems[7]).toMatchObject({ type: 'postContent', text: 'Parent summary - should be LAST' })
277+
expect(streamedItems[7]).toMatchObject({
278+
type: 'content',
279+
text: 'Parent summary - should be LAST',
280+
})
233281
})
234-
282+
235283
test('should handle single level with postContent correctly', async () => {
236284
const mockResponse = {
237285
content: 'Single parent',
238-
agent: 'assistant',
286+
agent: 'assistant',
239287
postContent: 'Parent done',
240288
children: [
241289
{
@@ -246,12 +294,12 @@ describe('PostContent Streaming Tests', () => {
246294
},
247295
],
248296
}
249-
250-
const streamedItems: Array<{ type: 'content' | 'postContent'; text: string }> = []
251-
297+
298+
const streamedItems: Array<{ type: 'content'; text: string }> = []
299+
252300
// Simple test - parent postContent should come after child postContent
253301
// This test will pass with current broken logic, but helps verify our fix
254-
302+
255303
expect(true).toBe(true) // Placeholder for now
256304
})
257305
})

npm-app/src/cli-handlers/__tests__/chat-tree-wrapping.test.ts

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,16 @@ describe('Chat Tree Line Wrapping', () => {
6161
// First line should be metadata
6262
expect(lines[0]).toContain('Assistant')
6363

64-
// Second line should show preview (last 2 lines with "...") since collapsed
65-
expect(lines[1]).toContain('...')
66-
expect(lines[1]).toContain('terminal because it exceeds the')
64+
// Find the trace toggle line (should come after content)
65+
const traceToggleLine = lines.find((line) =>
66+
line.includes('trace: 1 agent'),
67+
)
68+
expect(traceToggleLine).toBeDefined()
69+
expect(traceToggleLine).toContain('1 agent')
6770

68-
// Continuation lines should maintain proper indentation
69-
const continuationLines = lines.slice(2)
70-
for (const line of continuationLines) {
71+
// Content lines should maintain proper indentation
72+
const contentLines = lines.slice(1, lines.indexOf(traceToggleLine!))
73+
for (const line of contentLines) {
7174
if (line.trim()) {
7275
// Should have proper indentation for wrapped content
7376
expect(line).toMatch(/^\s{2,}/)
@@ -134,22 +137,16 @@ describe('Chat Tree Line Wrapping', () => {
134137
content:
135138
'Nested thinker with extremely long content that definitely needs to wrap and should maintain the correct vertical tree structure with proper ancestor line continuation',
136139
children: [],
137-
postContent:
138-
'Thinker post content that is also quite long and should wrap properly',
139140
},
140141
],
141-
postContent: 'File picker completed successfully',
142142
},
143143
{
144144
id: createNodeId('test-msg-3', [1]),
145145
type: 'reviewer',
146146
content: 'Second top-level child with long content that wraps',
147147
children: [],
148-
postContent: 'Review completed',
149148
},
150149
],
151-
postContent:
152-
'All tasks completed successfully with comprehensive results',
153150
}
154151

155152
const uiState: SubagentUIState = {
@@ -226,7 +223,8 @@ describe('Chat Tree Line Wrapping', () => {
226223
firstChildProgress: new Map(),
227224
}
228225

229-
const lines = renderSubagentTree(tree, uiState, mockMetrics, 'test-msg-4') // Find grandchild lines and verify proper indentation
226+
const lines = renderSubagentTree(tree, uiState, mockMetrics, 'test-msg-4')
227+
// Find grandchild lines and verify proper indentation
230228
const grandchildLines = lines.filter(
231229
(line) => line.includes('grandchild') || line.includes('Grandchild'),
232230
)
@@ -242,7 +240,7 @@ describe('Chat Tree Line Wrapping', () => {
242240
}
243241
})
244242

245-
test('should handle mixed scenarios with postContent wrapping', () => {
243+
test('should handle mixed scenarios with content wrapping', () => {
246244
const tree: SubagentNode = {
247245
id: createNodeId('test-msg-5', []),
248246
type: 'assistant',
@@ -251,14 +249,11 @@ describe('Chat Tree Line Wrapping', () => {
251249
{
252250
id: createNodeId('test-msg-5', [0]),
253251
type: 'worker',
254-
content: 'Worker content',
252+
content:
253+
'Worker content that is long enough to wrap across multiple lines',
255254
children: [],
256-
postContent:
257-
'This is a very long post content message that should wrap across multiple lines while maintaining the proper tree structure and indentation',
258255
},
259256
],
260-
postContent:
261-
'Root post content that is also quite long and should wrap properly at the end of the entire tree structure',
262257
}
263258

264259
const uiState: SubagentUIState = {
@@ -269,13 +264,8 @@ describe('Chat Tree Line Wrapping', () => {
269264

270265
const lines = renderSubagentTree(tree, uiState, mockMetrics, 'test-msg-5')
271266

272-
// Should handle both child postContent and root postContent
273-
const postContentLines = lines.filter(
274-
(line) =>
275-
line.includes('post content') || line.includes('tree structure'),
276-
)
277-
278-
expect(postContentLines.length).toBeGreaterThanOrEqual(0)
267+
// Should handle content wrapping properly
268+
expect(lines.length).toBeGreaterThanOrEqual(0)
279269
})
280270
})
281271

0 commit comments

Comments
 (0)