add Claude meta sidebar — activity + team roster
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 25s
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 25s
An always-pickable left-panel tab. Shows Claude activity read from ~/.claude/stats-cache.json (latest day's messages/sessions/tool-calls + lifetime totals, polled) and, when a tmux agent team is running, a roster of its members (colour · name · agent type · model) from the observer's join/left events — nothing re-tailed here. Scoped down from the original ticket: the account/team token budget isn't programmatically exposed under subscription auth (TUI-only; upstream #44328) and live per-member status needs the teammate status stream wired onto the bus — filed as T-158 and T-157 respectively. T-141. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import 'package:clide/builtin/claude/src/claude_meta_sidebar.dart';
|
||||
import 'package:clide/builtin/claude/src/claude_stats.dart';
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../helpers/kernel_fixture.dart';
|
||||
import '../../helpers/widget_harness.dart';
|
||||
|
||||
void main() {
|
||||
late KernelFixture f;
|
||||
setUp(() async => f = await KernelFixture.create());
|
||||
tearDown(() => f.dispose());
|
||||
|
||||
ClaudeMetaSidebar sidebar({ClaudeStats stats = const ClaudeStats()}) => ClaudeMetaSidebar(
|
||||
statsLoader: () async => stats,
|
||||
pollInterval: Duration.zero,
|
||||
);
|
||||
|
||||
testWidgets('shows the latest day + lifetime activity from stats', (tester) async {
|
||||
const stats = ClaudeStats(
|
||||
lastComputed: '2026-05-22',
|
||||
daily: [
|
||||
DailyActivity(date: '2026-05-01', messageCount: 100, sessionCount: 2, toolCallCount: 30),
|
||||
DailyActivity(date: '2026-05-22', messageCount: 250, sessionCount: 5, toolCallCount: 80),
|
||||
],
|
||||
);
|
||||
await tester.pumpWidget(harness(f, sidebar(stats: stats)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('2026-05-22'), findsOneWidget);
|
||||
expect(find.text('250 msgs · 5 sessions · 80 tools'), findsOneWidget);
|
||||
expect(find.text('Lifetime: 350 msgs over 2 days'), findsOneWidget);
|
||||
expect(find.text('No team active.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('adds and removes team members from the roster', (tester) async {
|
||||
await tester.pumpWidget(harness(f, sidebar()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
f.services.events.emit(const TeamMemberJoined(
|
||||
team: 't',
|
||||
agentId: 'a1',
|
||||
name: 'Scout',
|
||||
agentType: 'explorer',
|
||||
paneId: '%1',
|
||||
model: 'claude-opus-4-7',
|
||||
color: 'blue',
|
||||
));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(find.text('Scout'), findsOneWidget);
|
||||
expect(find.text('explorer · opus 4.7'), findsOneWidget);
|
||||
|
||||
f.services.events.emit(const TeamMemberLeft(team: 't', agentId: 'a1', paneId: '%1'));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(find.text('Scout'), findsNothing);
|
||||
expect(find.text('No team active.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('degrades to a no-activity message with empty stats', (tester) async {
|
||||
await tester.pumpWidget(harness(f, sidebar()));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('No activity recorded yet.'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:clide/builtin/claude/src/claude_stats.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('parseClaudeStats', () {
|
||||
test('parses daily activity, latest day, and lifetime totals', () {
|
||||
final json = jsonEncode({
|
||||
'version': 3,
|
||||
'lastComputedDate': '2026-05-22',
|
||||
'dailyActivity': [
|
||||
{'date': '2026-05-01', 'messageCount': 100, 'sessionCount': 2, 'toolCallCount': 30},
|
||||
{'date': '2026-05-22', 'messageCount': 250, 'sessionCount': 5, 'toolCallCount': 80},
|
||||
],
|
||||
});
|
||||
final s = parseClaudeStats(json);
|
||||
expect(s.lastComputed, '2026-05-22');
|
||||
expect(s.activeDays, 2);
|
||||
expect(s.latest!.date, '2026-05-22'); // most recent, not last in array
|
||||
expect(s.latest!.messageCount, 250);
|
||||
expect(s.lifetimeMessages, 350);
|
||||
expect(s.lifetimeSessions, 7);
|
||||
expect(s.lifetimeToolCalls, 110);
|
||||
});
|
||||
|
||||
test('picks the latest day regardless of array order', () {
|
||||
final json = jsonEncode({
|
||||
'dailyActivity': [
|
||||
{'date': '2026-05-22', 'messageCount': 1, 'sessionCount': 1, 'toolCallCount': 1},
|
||||
{'date': '2026-05-02', 'messageCount': 9, 'sessionCount': 9, 'toolCallCount': 9},
|
||||
],
|
||||
});
|
||||
expect(parseClaudeStats(json).latest!.date, '2026-05-22');
|
||||
});
|
||||
|
||||
test('missing counts default to zero', () {
|
||||
final json = jsonEncode({
|
||||
'dailyActivity': [
|
||||
{'date': '2026-05-22'},
|
||||
],
|
||||
});
|
||||
final s = parseClaudeStats(json);
|
||||
expect(s.latest!.messageCount, 0);
|
||||
expect(s.lifetimeToolCalls, 0);
|
||||
});
|
||||
|
||||
test('malformed or empty input yields empty stats', () {
|
||||
expect(parseClaudeStats('{ not json').daily, isEmpty);
|
||||
expect(parseClaudeStats('[]').daily, isEmpty);
|
||||
expect(parseClaudeStats('{}').latest, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user