mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
fix(tui): render inline diffs inside assistant completion
Follow-up for #13729: segment-level system artifacts still looked detached in real flow.\n\nInstead of appending inline_diff as a standalone segment/system row, queue sanitized diffs during tool.complete and append them as a fenced diff block to the assistant completion text on message.complete. This keeps the diff in the same message flow as the assistant response.
This commit is contained in:
parent
bddf0cd61e
commit
31b3b09ea4
3 changed files with 30 additions and 40 deletions
|
|
@ -143,7 +143,7 @@ describe('createGatewayEventHandler', () => {
|
|||
expect(appended[0]?.thinkingTokens).toBe(estimateTokensRough(fromServer))
|
||||
})
|
||||
|
||||
it('routes inline_diff into the active segment stream, not historyItems', () => {
|
||||
it('attaches inline_diff to the assistant completion body', () => {
|
||||
const appended: Msg[] = []
|
||||
const onEvent = createGatewayEventHandler(buildCtx(appended))
|
||||
const diff = '\u001b[31m--- a/foo.ts\u001b[0m\n\u001b[32m+++ b/foo.ts\u001b[0m\n@@\n-old\n+new'
|
||||
|
|
@ -158,26 +158,21 @@ describe('createGatewayEventHandler', () => {
|
|||
type: 'tool.complete'
|
||||
} as any)
|
||||
|
||||
// While streaming, nothing has flowed to historyItems yet — diff must be
|
||||
// held in segmentMessages so the transcript renders it inline with the
|
||||
// current turn rather than above it.
|
||||
// Diff is buffered for message.complete and sanitized (ANSI stripped).
|
||||
expect(appended).toHaveLength(0)
|
||||
expect(turnController.segmentMessages).toContainEqual(
|
||||
expect.objectContaining({ kind: 'trail', role: 'system', text: '' })
|
||||
)
|
||||
expect(turnController.segmentMessages).toContainEqual({ role: 'system', text: cleaned })
|
||||
expect(turnController.pendingInlineDiffs).toEqual([cleaned])
|
||||
|
||||
onEvent({
|
||||
payload: { text: 'patch applied' },
|
||||
type: 'message.complete'
|
||||
} as any)
|
||||
|
||||
// After the turn closes, the diff lands in history in the order the
|
||||
// gateway emitted it — before the assistant's final text, not above it.
|
||||
expect(appended).toHaveLength(3)
|
||||
expect(appended[0]).toMatchObject({ kind: 'trail', role: 'system', text: '' })
|
||||
expect(appended[1]).toMatchObject({ role: 'system', text: cleaned })
|
||||
expect(appended[2]).toMatchObject({ role: 'assistant', text: 'patch applied' })
|
||||
// Diff is rendered in the same assistant message body as the completion.
|
||||
expect(appended).toHaveLength(1)
|
||||
expect(appended[0]).toMatchObject({ role: 'assistant' })
|
||||
expect(appended[0]?.text).toContain('patch applied')
|
||||
expect(appended[0]?.text).toContain('```diff')
|
||||
expect(appended[0]?.text).toContain(cleaned)
|
||||
})
|
||||
|
||||
it('shows setup panel for missing provider startup error', () => {
|
||||
|
|
|
|||
|
|
@ -272,12 +272,10 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||
return
|
||||
}
|
||||
|
||||
// Push into the active turn's segment stream so the diff renders
|
||||
// inline with the assistant's output. Routing through `sys()`
|
||||
// lands it in the completed-history section above the streaming
|
||||
// bubble — which is why blitz testers saw diffs "appear at the
|
||||
// top, out of sequence" with the rest of the turn.
|
||||
turnController.appendSegmentMessage({ role: 'system', text: diffText })
|
||||
// Keep inline diffs attached to the assistant completion body so
|
||||
// they render in the same message flow, not as a standalone system
|
||||
// artifact that can look out-of-place around tool rows.
|
||||
turnController.queueInlineDiff(diffText)
|
||||
}
|
||||
|
||||
return
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class TurnController {
|
|||
bufRef = ''
|
||||
interrupted = false
|
||||
lastStatusNote = ''
|
||||
pendingInlineDiffs: string[] = []
|
||||
persistedToolLabels = new Set<string>()
|
||||
protocolWarned = false
|
||||
reasoningText = ''
|
||||
|
|
@ -76,6 +77,7 @@ class TurnController {
|
|||
this.activeTools = []
|
||||
this.streamTimer = clear(this.streamTimer)
|
||||
this.bufRef = ''
|
||||
this.pendingInlineDiffs = []
|
||||
this.pendingSegmentTools = []
|
||||
this.segmentMessages = []
|
||||
|
||||
|
|
@ -182,26 +184,14 @@ class TurnController {
|
|||
}, REASONING_PULSE_MS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Append an inline artifact (e.g. tool-complete inline diff) to the active
|
||||
* turn's segment stream. Routing through `historyItems` via `sys()` lands
|
||||
* the artifact above the currently-streaming assistant bubble; adding it
|
||||
* here keeps the paint order aligned with the order the gateway emitted.
|
||||
*/
|
||||
appendSegmentMessage(msg: Msg) {
|
||||
this.flushStreamingSegment()
|
||||
queueInlineDiff(diffText: string) {
|
||||
const text = diffText.trim()
|
||||
|
||||
if (this.pendingSegmentTools.length) {
|
||||
this.segmentMessages = [
|
||||
...this.segmentMessages,
|
||||
{ kind: 'trail', role: 'system', text: '', tools: this.pendingSegmentTools }
|
||||
]
|
||||
this.pendingSegmentTools = []
|
||||
patchTurnState({ streamPendingTools: [] })
|
||||
if (!text) {
|
||||
return
|
||||
}
|
||||
|
||||
this.segmentMessages = [...this.segmentMessages, msg]
|
||||
patchTurnState({ streamSegments: this.segmentMessages })
|
||||
this.pendingInlineDiffs = [...this.pendingInlineDiffs, text]
|
||||
}
|
||||
|
||||
pushActivity(text: string, tone: ActivityItem['tone'] = 'info', replaceLabel?: string) {
|
||||
|
|
@ -238,6 +228,7 @@ class TurnController {
|
|||
this.idle()
|
||||
this.clearReasoning()
|
||||
this.clearStatusTimer()
|
||||
this.pendingInlineDiffs = []
|
||||
this.pendingSegmentTools = []
|
||||
this.segmentMessages = []
|
||||
this.turnTools = []
|
||||
|
|
@ -248,6 +239,10 @@ class TurnController {
|
|||
const rawText = (payload.rendered ?? payload.text ?? this.bufRef).trimStart()
|
||||
const split = splitReasoning(rawText)
|
||||
const finalText = split.text
|
||||
const inlineDiffBlock = this.pendingInlineDiffs.length
|
||||
? `\`\`\`diff\n${this.pendingInlineDiffs.join('\n\n')}\n\`\`\``
|
||||
: ''
|
||||
const mergedText = [finalText, inlineDiffBlock].filter(Boolean).join('\n\n')
|
||||
const existingReasoning = this.reasoningText.trim() || String(payload.reasoning ?? '').trim()
|
||||
const savedReasoning = [existingReasoning, existingReasoning ? '' : split.reasoning].filter(Boolean).join('\n\n')
|
||||
const savedReasoningTokens = savedReasoning ? estimateTokensRough(savedReasoning) : 0
|
||||
|
|
@ -255,10 +250,10 @@ class TurnController {
|
|||
const tools = this.pendingSegmentTools
|
||||
const finalMessages = [...this.segmentMessages]
|
||||
|
||||
if (finalText) {
|
||||
if (mergedText) {
|
||||
finalMessages.push({
|
||||
role: 'assistant',
|
||||
text: finalText,
|
||||
text: mergedText,
|
||||
thinking: savedReasoning || undefined,
|
||||
thinkingTokens: savedReasoning ? savedReasoningTokens : undefined,
|
||||
toolTokens: savedToolTokens || undefined,
|
||||
|
|
@ -275,7 +270,7 @@ class TurnController {
|
|||
this.bufRef = ''
|
||||
patchTurnState({ activity: [], outcome: '' })
|
||||
|
||||
return { finalMessages, finalText, wasInterrupted }
|
||||
return { finalMessages, finalText: mergedText, wasInterrupted }
|
||||
}
|
||||
|
||||
recordMessageDelta({ rendered, text }: { rendered?: string; text?: string }) {
|
||||
|
|
@ -381,6 +376,7 @@ class TurnController {
|
|||
this.bufRef = ''
|
||||
this.interrupted = false
|
||||
this.lastStatusNote = ''
|
||||
this.pendingInlineDiffs = []
|
||||
this.pendingSegmentTools = []
|
||||
this.protocolWarned = false
|
||||
this.segmentMessages = []
|
||||
|
|
@ -426,6 +422,7 @@ class TurnController {
|
|||
this.endReasoningPhase()
|
||||
this.clearReasoning()
|
||||
this.activeTools = []
|
||||
this.pendingInlineDiffs = []
|
||||
this.turnTools = []
|
||||
this.toolTokenAcc = 0
|
||||
this.persistedToolLabels.clear()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue