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
@@ -1,28 +1,47 @@
/// The Activity tab: usage stats (stats-cache.json) + the primary
/// session's live runtime row. Split out of claude_meta_sidebar.dart
/// (T-395).
/// The Activity tab: session controls, usage, stats (stats-cache.json), and
/// the primary session's live runtime row. Split out of
/// claude_meta_sidebar.dart (T-395); session controls + the usage block are
/// the power-panel additions (T-415).
library;
import 'package:clide/builtin/claude/src/claude_config.dart';
import 'package:clide/builtin/claude/src/claude_stats.dart';
import 'package:clide/builtin/claude/src/claude_status.dart' show formatTokenCount, permissionModeLabel, shortModelLabel;
import 'package:clide/builtin/claude/src/claude_status.dart' show ClaudeUsage, formatTokenCount, permissionModeLabel, shortModelLabel;
import 'package:clide/builtin/claude/src/meta_sidebar/models.dart';
import 'package:clide/builtin/claude/src/transcript_reader.dart' show SessionStatus;
import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/widgets.dart';
class ActivityTabView extends StatelessWidget {
const ActivityTabView({super.key, required this.stats, required this.primaryStatus, required this.config});
const ActivityTabView({super.key, required this.stats, required this.primaryStatus, required this.config, this.usage});
final ClaudeStats stats;
final SessionStatus? primaryStatus;
final ClaudeConfig? config;
/// Parsed `/usage` output for the usage block, refreshed via the refresh
/// control (T-415). Null until the first refresh.
final ClaudeUsage? usage;
/// Publish a slash command for the primary pane to execute — the session
/// controls are the same code path as typing the command (D-6).
void _command(BuildContext context, String text) {
ClideKernel.of(context).messages.publish('builtin.claude', 'command', {'text': text});
}
@override
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
final latest = stats.latest;
final u = usage;
final sections = <MetaSection>[
if (u != null)
MetaSection('USAGE', [
if (u.session != null) MetaRow('session', u.session!),
if (u.week != null) MetaRow('week (all)', u.week!),
if (u.weekSonnet != null) MetaRow('week (sonnet)', u.weekSonnet!),
]),
if (latest != null)
MetaSection('TODAY', [
MetaRow('messages', '${latest.messageCount}'),
@@ -32,10 +51,47 @@ class ActivityTabView extends StatelessWidget {
if (latest != null) MetaSection('LIFETIME', [MetaRow('messages', '${stats.lifetimeMessages}'), MetaRow('sessions', '${stats.lifetimeSessions}')]),
..._runtimeSection(tokens),
];
if (sections.isEmpty) {
return metaPlaceholder('No activity recorded yet.');
}
return buildMetaTable(tokens, sections);
return ListView(
padding: const EdgeInsets.all(12),
children: [
// SESSION control strip (T-415): drives the primary session through
// the builtin.claude/command bus — identical to typing the command.
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ClideText('SESSION', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader),
),
Row(
children: [
_control(context, tokens, 'clear', 'trash', '/clear'),
_control(context, tokens, 'compact', 'arrows-in-simple', '/compact'),
_control(context, tokens, 'fork', 'git-branch', '/fork'),
_control(context, tokens, 'resume', 'clock-counter-clockwise', '/resume'),
const Spacer(),
_control(context, tokens, 'refresh usage', 'arrow-clockwise', '/usage'),
],
),
const SizedBox(height: 6),
if (sections.isEmpty) metaPlaceholder('No activity recorded yet.') else ...metaTableChildren(tokens, sections),
],
);
}
Widget _control(BuildContext context, SurfaceTokens tokens, String label, String glyph, String command) {
return Semantics(
button: true,
label: '$label session',
excludeSemantics: true,
onTap: () => _command(context, command),
child: ClideTappable(
tooltip: '$label · $command',
onTap: () => _command(context, command),
builder: (ctx, hovered, _) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
child: ClideIcon(PhosphorIcons.byName(glyph), size: 15, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
),
),
);
}
List<MetaSection> _runtimeSection(SurfaceTokens tokens) {
@@ -43,6 +99,7 @@ class ActivityTabView extends StatelessWidget {
final skills = config?.skills.length;
final rows = <MetaRow>[
if (st?.model != null) MetaRow('model', shortModelLabel(st!.model!), valueColor: tokens.globalFocus),
if (st?.effort != null) MetaRow('effort', st!.effort!),
if (st?.contextTokens != null) MetaRow('context', '${formatTokenCount(st!.contextTokens!)} ctx'),
if (st?.permissionMode != null) MetaRow('mode', permissionModeLabel(st!.permissionMode!)),
if (skills != null) MetaRow('skills', '$skills'),
@@ -44,7 +44,12 @@ Widget metaPlaceholder(String text) => Padding(
);
/// Key→value sections on the shared table geometry (Activity + Config).
Widget buildMetaTable(SurfaceTokens tokens, List<MetaSection> sections) {
Widget buildMetaTable(SurfaceTokens tokens, List<MetaSection> sections) =>
ListView(padding: const EdgeInsets.all(12), children: metaTableChildren(tokens, sections));
/// The table rows without the enclosing ListView, for tabs that compose extra
/// widgets around the sections (the Activity tab's control strip, T-415).
List<Widget> metaTableChildren(SurfaceTokens tokens, List<MetaSection> sections) {
final children = <Widget>[];
for (var i = 0; i < sections.length; i++) {
final s = sections[i];
@@ -74,5 +79,5 @@ Widget buildMetaTable(SurfaceTokens tokens, List<MetaSection> sections) {
);
}
}
return ListView(padding: const EdgeInsets.all(12), children: children);
return children;
}