fix: stream bash output and harden OpenCode connectivity (#2522)

* fix(ui): stream bash tool output while running

* perf(ui): render streaming bash output incrementally

* fix(ui): keep tool duration timer running

* fix(web): recover stalled OpenCode SSE streams

* fix(web): prevent OpenCode restart storms

* fix: address streaming recovery review
This commit is contained in:
Bohdan Triapitsyn
2026-07-29 17:26:11 +03:00
committed by GitHub
parent c84305bf7e
commit 0f830f8804
12 changed files with 372 additions and 41 deletions
@@ -2,6 +2,46 @@ import { describe, expect, test } from 'bun:test';
import { readTaskTagSessionIdFromOutput } from './taskSessionIdParser';
import { tryParseJsonOutput } from '../toolRenderers';
import { getStreamingThrottleText } from '../../hooks/useStreamingTextThrottle';
import { getStreamingOutputAppend, getToolOutput } from './toolOutput';
describe('getToolOutput', () => {
test('prefers authoritative state output', () => {
expect(getToolOutput('bash', 'final output', 'streamed output')).toBe('final output');
expect(getToolOutput('bash', '', 'streamed output')).toBe('');
});
test('falls back to streamed metadata output for bash', () => {
expect(getToolOutput('bash', undefined, 'streamed output')).toBe('streamed output');
expect(getToolOutput('bash', undefined, '')).toBe(undefined);
});
test('does not expose metadata output for other tools', () => {
expect(getToolOutput('read', undefined, 'metadata output')).toBe(undefined);
});
});
describe('getStreamingOutputAppend', () => {
test('returns only newly appended output', () => {
expect(getStreamingOutputAppend('first\n', 'first\nsecond\n')).toBe('second\n');
});
test('requires replacement when output is rewritten or shortened', () => {
expect(getStreamingOutputAppend('progress 10%', 'progress 20%')).toBe(undefined);
expect(getStreamingOutputAppend('long output', 'short')).toBe(undefined);
});
});
describe('streaming output transitions', () => {
test('allows bash snapshots to be rewritten or shortened while running', () => {
expect(getStreamingThrottleText('progress 10%', 'progress 20%', true, true)).toBe('progress 20%');
expect(getStreamingThrottleText('long output', 'short', true, true)).toBe('short');
});
test('preserves monotonic streaming text by default', () => {
expect(getStreamingThrottleText('long output', 'short', true, false)).toBe('long output');
});
});
describe('readTaskTagSessionIdFromOutput', () => {
test('parses task tags without state attributes', () => {