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:
@@ -4353,3 +4353,5 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3P91QQQDT5J50F52FPCKM', 'status', 'backlog', 'ready', NULL, '2026-06-12 11:28:28', '2026-06-12 11:28:28', '2026-06-12 11:28:28', NULL, 'ccb4c4193c34ef5e2bd9e20d407ccda3', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ8QNGJFFK7G24CBWQAR2C', 'status', 'backlog', 'ready', NULL, '2026-06-12 11:28:32', '2026-06-12 11:28:32', '2026-06-12 11:28:32', NULL, '93622dd3b0a88598d1823685a0c2d3e6', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBQ595H08JFTRFSR90GSZQ0G', 'status', 'backlog', 'ready', NULL, '2026-06-12 11:28:38', '2026-06-12 11:28:38', '2026-06-12 11:28:38', NULL, '5729470fe53b18812c0338fa4f23a3c8', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBQ595H08JFTRFSR90GSZQ0G', 'status', 'ready', 'done', NULL, '2026-06-12 11:30:22', '2026-06-12 11:30:22', '2026-06-12 11:30:22', NULL, '6dd59c198c66791e49bad07edcc24882', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3P91QQQDT5J50F52FPCKM', 'status', 'ready', 'in_progress', NULL, '2026-06-12 11:30:55', '2026-06-12 11:30:55', '2026-06-12 11:30:55', NULL, 'c5a086c9415df9fbe4e7feacecc19c73', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -4777,3 +4777,7 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
|
||||
|
||||
SPIKE FIRST: capture what the stream-json wire actually emits during a Workflow run (tool_use input shape, progress/notification events, sidechain attribution for workflow-spawned agents) — same probe method as T-410''s. Scope the rendering to what the wire really carries; if progress only exists in the harness UI and not on the wire, document that limit and render what''s available (start/end + result). Filed from user request 2026-06-12.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-12 10:25:00', '2026-06-12 11:28:32', NULL, '07318aee4082e6af072c15605745a41d', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBQ595H08JFTRFSR90GSZQ0G', 'task', '06FBP3EZC7AJANXZVF3D91QYWM', 'Claude sidebar tab robot icon in claude orange', 'User request (2026-06-12, try-it-out): tint the Claude sidebar tab''s robot icon with the Claude accent (#D97757, claudeAccent) so the Claude surface is identifiable at a glance in the icon rail. Nominative use per the licenses.yaml trademark note (marks Claude''s own panel, not clide branding). If the TabContribution/icon-rail doesn''t support per-tab colour, add an optional iconColor to the contribution and honour it in the rail (active/inactive states: keep the accent when active, mute toward the normal inactive tone when not).', 'ready', 'low', NULL, NULL, NULL, '2026-06-12 11:26:14', '2026-06-12 11:28:38', NULL, 'bffe2b9dfafc4a13412ceb5c05700915', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBQ595H08JFTRFSR90GSZQ0G', 'task', '06FBP3EZC7AJANXZVF3D91QYWM', 'Claude sidebar tab robot icon in claude orange', 'User request (2026-06-12, try-it-out): tint the Claude sidebar tab''s robot icon with the Claude accent (#D97757, claudeAccent) so the Claude surface is identifiable at a glance in the icon rail. Nominative use per the licenses.yaml trademark note (marks Claude''s own panel, not clide branding). If the TabContribution/icon-rail doesn''t support per-tab colour, add an optional iconColor to the contribution and honour it in the rail (active/inactive states: keep the accent when active, mute toward the normal inactive tone when not).', 'done', 'low', NULL, NULL, NULL, '2026-06-12 11:26:14', '2026-06-12 11:30:22', NULL, '2f7fd997294bca6437808ba784624c47', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3P91QQQDT5J50F52FPCKM', 'story', '06FBP3EZC7AJANXZVF3D91QYWM', 'Activity tab: session controls + usage block (revisit T-158)', 'Two additions to the sidebar Activity tab. (1) Session controls row: clear / compact / fork / resume as icon buttons driving the exact same handlers as the slash commands (claude_pane._send routes) — D-6 parity for the session-mutating set. Confirm-on-destructive (/clear) via the existing dialog pattern.
|
||||
|
||||
(2) Usage/cost block: the version-keyed probes show /usage IS advertised in stream-json (init-2.1.16x.json) — probe what forwarded /usage returns in headless mode and, if parseable, render plan usage alongside the existing cost/context data from result events (T-168 fields already in SessionStatus). This revisits T-158 ''blocked on upstream'' — if the output is unusable, document that in T-158 and keep the cost/context-only block. Link T-158 either way.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-12 08:59:28', '2026-06-12 11:30:55', NULL, 'ea7652bcef396769e394a0d436920d8e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
|
||||
@@ -18,6 +18,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
|
||||
### Added
|
||||
|
||||
- **Session controls and live usage in the Claude sidebar Activity tab.** A
|
||||
SESSION strip offers clear/compact/fork/resume buttons (same code path as the
|
||||
typed commands), and a refresh control fetches `/usage` — plan usage renders
|
||||
as a USAGE block (session and weekly percentages). The runtime row now also
|
||||
shows the session's effort level. (T-415)
|
||||
- **The Claude sidebar Config tab is a live control panel.** Model, effort, and
|
||||
permission mode are popover controls showing the running session's values;
|
||||
picking an option drives the session through the same path as the typed slash
|
||||
|
||||
@@ -36,8 +36,9 @@ import 'package:clide/builtin/claude/src/meta_sidebar/tab_strip.dart';
|
||||
import 'package:clide/builtin/claude/src/meta_sidebar/team_tab.dart';
|
||||
import 'package:clide/builtin/claude/src/session_orchestrator.dart';
|
||||
import 'package:clide/builtin/claude/src/team_broker.dart' show TeamBroker, TeamTask;
|
||||
import 'package:clide/builtin/claude/src/claude_status.dart' show ClaudeUsage, parseUsageText;
|
||||
import 'package:clide/builtin/claude/src/transcript_publisher.dart' show ClaudeConversation;
|
||||
import 'package:clide/builtin/claude/src/transcript_reader.dart' show SessionStatus;
|
||||
import 'package:clide/builtin/claude/src/transcript_reader.dart' show AssistantTextMessage, ConversationItem, SessionStatus;
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
@@ -85,6 +86,8 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
StreamSubscription<Message>? _statusSub;
|
||||
StreamSubscription<Message>? _tabSub;
|
||||
StreamSubscription<SessionStatus>? _primarySub;
|
||||
StreamSubscription<ConversationItem>? _primaryItemsSub;
|
||||
ClaudeUsage? _usage;
|
||||
StreamSubscription<void>? _brokerChangeSub;
|
||||
Timer? _timer;
|
||||
late final Future<ClaudeStats> Function() _load;
|
||||
@@ -199,6 +202,8 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
final session = _orchestrator?.byId('primary')?.session;
|
||||
_primarySub?.cancel();
|
||||
_primarySub = null;
|
||||
_primaryItemsSub?.cancel();
|
||||
_primaryItemsSub = null;
|
||||
if (session == null) {
|
||||
if (_primaryStatus != null && mounted) setState(() => _primaryStatus = null);
|
||||
return;
|
||||
@@ -208,6 +213,14 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
_primarySub = session.statusStream.listen((s) {
|
||||
if (mounted) setState(() => _primaryStatus = s);
|
||||
});
|
||||
// Watch for /usage responses: CLI-local output arrives as synthetic
|
||||
// assistant text; when it parses as usage, the Activity block updates
|
||||
// (T-415). Driven by the refresh control publishing '/usage'.
|
||||
_primaryItemsSub = session.items.listen((item) {
|
||||
if (item is! AssistantTextMessage || !item.synthetic) return;
|
||||
final parsed = parseUsageText(item.text);
|
||||
if (parsed != null && mounted) setState(() => _usage = parsed);
|
||||
});
|
||||
}
|
||||
|
||||
void _onConfigChange() {
|
||||
@@ -257,6 +270,7 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
_statusSub?.cancel();
|
||||
_tabSub?.cancel();
|
||||
_primarySub?.cancel();
|
||||
_primaryItemsSub?.cancel();
|
||||
_brokerChangeSub?.cancel();
|
||||
_injectCtl.dispose();
|
||||
_config?.removeListener(_onConfigChange);
|
||||
@@ -272,7 +286,7 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
SidebarTabStrip(current: _tab, memberCount: _members.length, onPick: (t) => setState(() => _tab = t)),
|
||||
Expanded(
|
||||
child: switch (_tab) {
|
||||
SidebarTab.activity => ActivityTabView(stats: _stats, primaryStatus: _primaryStatus, config: _config),
|
||||
SidebarTab.activity => ActivityTabView(stats: _stats, primaryStatus: _primaryStatus, config: _config, usage: _usage),
|
||||
SidebarTab.team => TeamTabView(
|
||||
members: _members,
|
||||
memberStatus: _memberStatus,
|
||||
|
||||
@@ -92,3 +92,41 @@ String formatTokenCount(int n) {
|
||||
if (n >= 1000) return '${(n / 1000).round()}k';
|
||||
return '$n';
|
||||
}
|
||||
|
||||
/// Parsed `/usage` output (T-415). The CLI answers a forwarded `/usage`
|
||||
/// headless and free (probed 2.1.175, num_turns 0) with plain text:
|
||||
///
|
||||
/// Current session: 15% used · resets Jun 12, 3:39pm (Europe/Amsterdam)
|
||||
/// Current week (all models): 53% used · resets Jun 15, 6:59pm (…)
|
||||
/// Current week (Sonnet only): 0% used
|
||||
class ClaudeUsage {
|
||||
const ClaudeUsage({this.session, this.week, this.weekSonnet});
|
||||
|
||||
/// The value text per line (e.g. `15% used · resets Jun 12, 3:39pm`),
|
||||
/// timezone parenthetical stripped. Null when the line wasn't present.
|
||||
final String? session;
|
||||
final String? week;
|
||||
final String? weekSonnet;
|
||||
|
||||
bool get isEmpty => session == null && week == null && weekSonnet == null;
|
||||
}
|
||||
|
||||
/// Parse `/usage` response text into a [ClaudeUsage], or null when [text]
|
||||
/// isn't usage output. Tolerant of label drift: any `Current …: …% used`
|
||||
/// line is matched by its key phrase.
|
||||
ClaudeUsage? parseUsageText(String text) {
|
||||
if (!text.contains('% used')) return null;
|
||||
String? valueOf(String keyPhrase) {
|
||||
for (final line in text.split('\n')) {
|
||||
if (!line.contains(keyPhrase)) continue;
|
||||
final colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
// Strip the trailing timezone parenthetical — noise at sidebar width.
|
||||
return line.substring(colon + 1).replaceAll(RegExp(r'\s*\([^)]*\)\s*$'), '').trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final usage = ClaudeUsage(session: valueOf('Current session'), week: valueOf('(all models)'), weekSonnet: valueOf('(Sonnet only)'));
|
||||
return usage.isEmpty ? null : usage;
|
||||
}
|
||||
|
||||
@@ -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 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),
|
||||
],
|
||||
);
|
||||
}
|
||||
return buildMetaTable(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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user