render typed tool cards and live session status from stream-json

The conversation pane now exploits the structured stream instead of
dumping tool input as JSON. ConversationController indexes tool_use by id
so a tool_result pairs back to its call and renders the Edit/Write diff or
is_error failure in place; per-tool bodies (Bash command+output, Read/Grep
file/query) reuse the shared renderers factored out of the permission
card. SessionStatus gains cost + contextWindow + rate-limit, read straight
off the init/result/rate_limit_event events, so the in-pane status line
reflects live state without the config probe.

Partial-message streaming is wired behind --include-partial-messages but
its event shape is unverified against the live binary and degrades to a
no-op if it differs — see T-184.

T-168.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-30 13:44:19 +02:00
co-authored by Claude
parent f6b88f503f
commit c5e58733a2
14 changed files with 722 additions and 117 deletions
@@ -580,9 +580,29 @@ void main() {
expect(m.contextTokens, 10);
});
test('equality compares all fields', () {
test('merge overlays cost, contextWindow, and rateLimitInfo (T-168)', () {
const a = SessionStatus(model: 'm1', cost: 0.01);
const b = SessionStatus(contextWindow: 200000, rateLimitInfo: 'rate limited');
final m = a.merge(b);
expect(m.model, 'm1');
expect(m.cost, 0.01);
expect(m.contextWindow, 200000);
expect(m.rateLimitInfo, 'rate limited');
});
test('equality compares all fields including cost/contextWindow/rateLimitInfo (T-168)', () {
expect(const SessionStatus(model: 'x'), const SessionStatus(model: 'x'));
expect(const SessionStatus(model: 'x'), isNot(const SessionStatus(model: 'y')));
expect(const SessionStatus(cost: 0.1), const SessionStatus(cost: 0.1));
expect(const SessionStatus(cost: 0.1), isNot(const SessionStatus(cost: 0.2)));
expect(const SessionStatus(contextWindow: 1000000), const SessionStatus(contextWindow: 1000000));
expect(const SessionStatus(rateLimitInfo: 'x'), isNot(const SessionStatus()));
});
test('isEmpty returns false when any new field is set (T-168)', () {
expect(const SessionStatus(cost: 0.0).isEmpty, isFalse);
expect(const SessionStatus(contextWindow: 0).isEmpty, isFalse);
expect(const SessionStatus(rateLimitInfo: 'rate limited').isEmpty, isFalse);
});
});