Activity tab: session controls + live usage block (T-415)

The Activity tab gains the power-panel's session strip and a usage block:

- SESSION controls (clear / compact / fork / resume + refresh-usage)
  publish their slash command on builtin.claude/command — the same path
  as typing it, so /clear semantics (and any future confirm behavior)
  live in exactly one place.
- The usage block revisits T-158's "blocked on upstream": probed against
  claude 2.1.175, a forwarded /usage IS answered headless, free
  (num_turns 0), as parseable text. parseUsageText() extracts session /
  week / week-Sonnet percentages (timezone parentheticals stripped); the
  sidebar watches the primary session's synthetic output for
  usage-shaped responses and renders them as a USAGE section. Refresh is
  user-initiated (the control sends /usage) — no polling, no background
  calls (D-64).
- The runtime row gains the session's effort level (T-412's status
  field).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 14:04:55 +02:00
co-authored by Claude Fable 5
parent 1674d3021a
commit 0fd8a241db
9 changed files with 183 additions and 13 deletions
@@ -133,6 +133,22 @@ void main() {
expect(find.text('Claude environment not loaded.'), findsOneWidget);
});
testWidgets('Activity session controls publish their slash commands (T-415)', (tester) async {
final published = <Message>[];
final sub = f.services.messages.subscribe(publisher: 'builtin.claude', channel: 'command').listen(published.add);
addTearDown(sub.cancel);
await tester.pumpWidget(harness(f, sidebar(stats: stats)));
await tester.pumpAndSettle();
expect(find.text('SESSION'), findsOneWidget);
await tester.tap(find.bySemanticsLabel('clear session'));
await tester.tap(find.bySemanticsLabel('compact session'));
await tester.tap(find.bySemanticsLabel('refresh usage session'));
await tester.pump();
expect(published.map((m) => m.data['text']), ['/clear', '/compact', '/usage']);
});
testWidgets('a settings control publishes its slash command on pick (T-414)', (tester) async {
final dir = Directory.systemTemp.createTempSync('cfg');
addTearDown(() => dir.deleteSync(recursive: true));
@@ -106,4 +106,33 @@ void main() {
expect(seg.trailing, isNull);
});
});
group('parseUsageText (T-415)', () {
// The probed 2.1.175 /usage response shape.
const probed =
'You are currently using your subscription to power your Claude Code usage\n'
'\n'
'Current session: 15% used · resets Jun 12, 3:39pm (Europe/Amsterdam)\n'
'Current week (all models): 53% used · resets Jun 15, 6:59pm (Europe/Amsterdam)\n'
'Current week (Sonnet only): 0% used';
test('parses the probed response, stripping timezone parentheticals', () {
final u = parseUsageText(probed)!;
expect(u.session, '15% used · resets Jun 12, 3:39pm');
expect(u.week, '53% used · resets Jun 15, 6:59pm');
expect(u.weekSonnet, '0% used');
});
test('tolerates missing lines', () {
final u = parseUsageText('Current session: 9% used')!;
expect(u.session, '9% used');
expect(u.week, isNull);
expect(u.weekSonnet, isNull);
});
test('non-usage text parses to null', () {
expect(parseUsageText("/effort isn't available in this environment."), isNull);
expect(parseUsageText('plain prose'), isNull);
});
});
}