split the Claude meta sidebar into Activity / Team / Config sub-tabs
test / unit + widget + golden + a11y (push) Failing after 29s
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 34s
test / unit + widget + golden + a11y (push) Failing after 29s
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 34s
The sidebar had outgrown one scroll (stats + roster + config don't fit). A sub-tab strip now switches between three surfaces: Activity (usage stats + the primary session's live runtime), Team (the member roster, auto-fronted when a team spawns and otherwise quiet), and Config (the Claude-environment settings table over ClaudeConfig). Activity and Config render their key→value rows through one shared table (same label column + row pitch + header style) so toggling tabs doesn't move anything. The expandable skills/agents/commands/permissions/MCP browser on the Config tab is the follow-up (T-183). Exposes StreamJsonSession.status so the runtime row can seed from the session's current state. T-182, D-77. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clide/builtin/claude/src/claude_config.dart';
|
||||
import 'package:clide/builtin/claude/src/claude_meta_sidebar.dart';
|
||||
import 'package:clide/builtin/claude/src/claude_stats.dart';
|
||||
import 'package:clide/builtin/claude/src/transcript_publisher.dart';
|
||||
@@ -13,31 +16,83 @@ void main() {
|
||||
setUp(() async => f = await KernelFixture.create());
|
||||
tearDown(() => f.dispose());
|
||||
|
||||
ClaudeMetaSidebar sidebar({ClaudeStats stats = const ClaudeStats()}) => ClaudeMetaSidebar(
|
||||
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),
|
||||
],
|
||||
);
|
||||
|
||||
ClaudeMetaSidebar sidebar({
|
||||
ClaudeStats stats = const ClaudeStats(),
|
||||
ClaudeConfig? config,
|
||||
SidebarTab initialTab = SidebarTab.activity,
|
||||
}) =>
|
||||
ClaudeMetaSidebar(
|
||||
statsLoader: () async => stats,
|
||||
pollInterval: Duration.zero,
|
||||
config: config,
|
||||
initialTab: initialTab,
|
||||
);
|
||||
|
||||
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),
|
||||
],
|
||||
);
|
||||
testWidgets('Activity tab shows today + lifetime stats on the table', (tester) async {
|
||||
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);
|
||||
expect(find.text('TODAY'), findsOneWidget);
|
||||
expect(find.text('LIFETIME'), findsOneWidget);
|
||||
expect(find.text('250'), findsOneWidget); // today messages
|
||||
expect(find.text('80'), findsOneWidget); // today tool calls
|
||||
expect(find.text('350'), findsOneWidget); // lifetime messages
|
||||
expect(find.text('messages'), findsNWidgets(2)); // today + lifetime rows
|
||||
});
|
||||
|
||||
testWidgets('adds and removes team members from the roster', (tester) async {
|
||||
await tester.pumpWidget(harness(f, sidebar()));
|
||||
testWidgets('defaults to Activity, not the roster', (tester) async {
|
||||
await tester.pumpWidget(harness(f, sidebar(stats: stats)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('No team active.'), findsNothing);
|
||||
expect(find.text('TODAY'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('tapping a sub-tab switches the body', (tester) async {
|
||||
await tester.pumpWidget(harness(f, sidebar(stats: stats)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Team'));
|
||||
await tester.pump();
|
||||
expect(find.text('No team active.'), findsOneWidget);
|
||||
expect(find.text('TODAY'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Activity'));
|
||||
await tester.pump();
|
||||
expect(find.text('TODAY'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Config tab renders the settings table over ClaudeConfig', (tester) async {
|
||||
final dir = Directory.systemTemp.createTempSync('cfg');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
final config = ClaudeConfig(globalDir: dir, cacheDir: dir);
|
||||
|
||||
await tester.pumpWidget(harness(f, sidebar(config: config, initialTab: SidebarTab.config)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('SETTINGS'), findsOneWidget);
|
||||
expect(find.text('output style'), findsOneWidget);
|
||||
expect(find.text('permission mode'), findsOneWidget);
|
||||
expect(find.text('~/.claude + .claude'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Config tab degrades when no environment is loaded', (tester) async {
|
||||
await tester.pumpWidget(harness(f, sidebar(initialTab: SidebarTab.config)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Claude environment not loaded.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a team spawn auto-fronts the Team tab', (tester) async {
|
||||
await tester.pumpWidget(harness(f, sidebar(stats: stats)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('TODAY'), findsOneWidget); // starts on Activity
|
||||
|
||||
f.services.events.emit(const TeamMemberJoined(
|
||||
team: 't',
|
||||
@@ -50,8 +105,28 @@ void main() {
|
||||
));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
// Fronted to Team without a tap.
|
||||
expect(find.text('Scout'), findsOneWidget);
|
||||
expect(find.text('explorer · opus 4.7'), findsOneWidget);
|
||||
expect(find.text('TODAY'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('removing the last member leaves the Team tab empty', (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',
|
||||
color: 'blue',
|
||||
));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(find.text('Scout'), findsOneWidget);
|
||||
|
||||
f.services.events.emit(const TeamMemberLeft(team: 't', agentId: 'a1', paneId: '%1'));
|
||||
await tester.pump();
|
||||
@@ -60,7 +135,7 @@ void main() {
|
||||
expect(find.text('No team active.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('folds live per-member status (mode + context) into the roster row', (tester) async {
|
||||
testWidgets('folds live per-member status into the roster row', (tester) async {
|
||||
await tester.pumpWidget(harness(f, sidebar()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -89,7 +164,7 @@ void main() {
|
||||
expect(find.text('explorer · opus 4.7 · accept-edits · 21k ctx'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('degrades to a no-activity message with empty stats', (tester) async {
|
||||
testWidgets('Activity 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);
|
||||
|
||||
Reference in New Issue
Block a user