feat(claude): shared account budget in Team tab + meta-sidebar card facelift (T-158)
Implements T-158 option A: the forwarded /usage budget (already parsed for the Activity tab, T-415) now also renders in the Team tab as a single ACCOUNT card, captioned "shared across the team". Usage is per-account — every team session shares one ~/.claude login — so it's shown once, not split per member (which would just repeat identical numbers). Facelift: the Activity, Team, and Config tabs now render their sections as elevated cards (panelHeader fill, dividerColor hairline, 6px radius) under small-caps mono headers, matching the settings overlay's card design (settings_category_view). Shared helpers — metaSectionHeader / metaCard / metaCardRow — live in meta_sidebar/models.dart; the SESSION control strip and the pinned Config SETTINGS block adopt the same card. The shared ClideAccordion (also used by decisions/tickets) is left untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,9 +19,10 @@
|
||||
/// `meta_sidebar/` (T-395 split). Activity and Config render on the same
|
||||
/// table geometry (`buildMetaTable`) so switching tabs doesn't visually jump.
|
||||
///
|
||||
/// The account/team token budget is intentionally absent: it isn't
|
||||
/// programmatically exposed under subscription auth (see project memory /
|
||||
/// GitHub anthropics/claude-code#44328).
|
||||
/// The account budget now surfaces from a forwarded `/usage` (T-415): the
|
||||
/// Activity tab renders it, and the Team tab repeats it as a single shared
|
||||
/// ACCOUNT card (T-158) — usage is per-account (one `~/.claude` login), so it's
|
||||
/// shown once, not split per member.
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
@@ -312,6 +313,7 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
||||
members: _members,
|
||||
memberStatus: _memberStatus,
|
||||
orchestrator: _orchestrator,
|
||||
usage: _usage,
|
||||
tasks: _tasks,
|
||||
injectingAgentId: _injectingAgentId,
|
||||
injectController: _injectCtl,
|
||||
|
||||
@@ -87,59 +87,61 @@ class ActivityTabView extends StatelessWidget {
|
||||
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(
|
||||
ClideSettings.i18n.string(context, 'activity.section.session', namespace: 'builtin.claude', placeholder: 'SESSION'),
|
||||
fontSize: clideFontSmall,
|
||||
color: tokens.sidebarSectionHeader,
|
||||
// Carded to match the settings overlay (T-158 facelift).
|
||||
metaSectionHeader(context, tokens, ClideSettings.i18n.string(context, 'activity.section.session', namespace: 'builtin.claude', placeholder: 'SESSION')),
|
||||
metaCard(tokens, [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
_control(
|
||||
context,
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'activity.control.clear', namespace: 'builtin.claude', placeholder: 'clear'),
|
||||
'trash',
|
||||
'/clear',
|
||||
),
|
||||
_control(
|
||||
context,
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'activity.control.compact', namespace: 'builtin.claude', placeholder: 'compact'),
|
||||
'arrows-in-simple',
|
||||
'/compact',
|
||||
),
|
||||
_control(
|
||||
context,
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'activity.control.fork', namespace: 'builtin.claude', placeholder: 'fork'),
|
||||
'git-branch',
|
||||
'/fork',
|
||||
),
|
||||
_control(
|
||||
context,
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'activity.control.resume', namespace: 'builtin.claude', placeholder: 'resume'),
|
||||
'clock-counter-clockwise',
|
||||
'/resume',
|
||||
),
|
||||
const Spacer(),
|
||||
_control(
|
||||
context,
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'activity.control.refreshUsage', namespace: 'builtin.claude', placeholder: 'refresh usage'),
|
||||
'arrow-clockwise',
|
||||
'/usage',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_control(
|
||||
context,
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'activity.control.clear', namespace: 'builtin.claude', placeholder: 'clear'),
|
||||
'trash',
|
||||
'/clear',
|
||||
),
|
||||
_control(
|
||||
context,
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'activity.control.compact', namespace: 'builtin.claude', placeholder: 'compact'),
|
||||
'arrows-in-simple',
|
||||
'/compact',
|
||||
),
|
||||
_control(
|
||||
context,
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'activity.control.fork', namespace: 'builtin.claude', placeholder: 'fork'),
|
||||
'git-branch',
|
||||
'/fork',
|
||||
),
|
||||
_control(
|
||||
context,
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'activity.control.resume', namespace: 'builtin.claude', placeholder: 'resume'),
|
||||
'clock-counter-clockwise',
|
||||
'/resume',
|
||||
),
|
||||
const Spacer(),
|
||||
_control(
|
||||
context,
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'activity.control.refreshUsage', namespace: 'builtin.claude', placeholder: 'refresh usage'),
|
||||
'arrow-clockwise',
|
||||
'/usage',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
]),
|
||||
if (sections.isNotEmpty) const SizedBox(height: 16),
|
||||
if (sections.isEmpty)
|
||||
metaPlaceholder(ClideSettings.i18n.string(context, 'activity.empty', namespace: 'builtin.claude', placeholder: 'No activity recorded yet.'))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: metaPlaceholder(ClideSettings.i18n.string(context, 'activity.empty', namespace: 'builtin.claude', placeholder: 'No activity recorded yet.')),
|
||||
)
|
||||
else
|
||||
...metaTableChildren(tokens, sections),
|
||||
...metaTableChildren(context, tokens, sections),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,43 +53,40 @@ class ConfigTabView extends StatelessWidget {
|
||||
final effort = status?.effort ?? settings['effortLevel']?.toString() ?? 'default';
|
||||
|
||||
final children = <Widget>[
|
||||
// Pinned SETTINGS control panel — not collapsible.
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'config.section.settings', namespace: 'builtin.claude', placeholder: 'SETTINGS'),
|
||||
fontSize: clideFontSmall,
|
||||
color: tokens.sidebarSectionHeader,
|
||||
// Pinned SETTINGS control panel — not collapsible. Carded to match the
|
||||
// settings overlay (T-158 facelift).
|
||||
metaSectionHeader(context, tokens, ClideSettings.i18n.string(context, 'config.section.settings', namespace: 'builtin.claude', placeholder: 'SETTINGS')),
|
||||
metaCard(tokens, [
|
||||
SettingControlRow(
|
||||
label: ClideSettings.i18n.string(context, 'config.row.model', namespace: 'builtin.claude', placeholder: 'model'),
|
||||
value: model,
|
||||
valueColor: tokens.globalFocus,
|
||||
options: (models == null || models!.isEmpty) ? kFallbackModels : models!,
|
||||
isActive: (o) => o.value == model || model.toLowerCase().contains(o.value.toLowerCase()),
|
||||
command: 'model',
|
||||
),
|
||||
),
|
||||
SettingControlRow(
|
||||
label: ClideSettings.i18n.string(context, 'config.row.model', namespace: 'builtin.claude', placeholder: 'model'),
|
||||
value: model,
|
||||
valueColor: tokens.globalFocus,
|
||||
options: (models == null || models!.isEmpty) ? kFallbackModels : models!,
|
||||
isActive: (o) => o.value == model || model.toLowerCase().contains(o.value.toLowerCase()),
|
||||
command: 'model',
|
||||
),
|
||||
SettingControlRow(
|
||||
label: ClideSettings.i18n.string(context, 'config.row.effort', namespace: 'builtin.claude', placeholder: 'effort'),
|
||||
value: effort,
|
||||
options: kEffortLevels,
|
||||
isActive: (o) => o.value == effort,
|
||||
command: 'effort',
|
||||
),
|
||||
SettingControlRow(
|
||||
label: ClideSettings.i18n.string(context, 'config.row.permissionMode', namespace: 'builtin.claude', placeholder: 'permission mode'),
|
||||
value: permissionModeLabel(mode),
|
||||
options: kPermissionModes,
|
||||
isActive: (o) => o.value == mode,
|
||||
command: 'permissions',
|
||||
),
|
||||
_configRow(tokens, ClideSettings.i18n.string(context, 'config.row.outputStyle', namespace: 'builtin.claude', placeholder: 'output style'), outputStyle),
|
||||
_configRow(
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'config.row.source', namespace: 'builtin.claude', placeholder: 'source'),
|
||||
ClideSettings.i18n.string(context, 'config.row.source.value', namespace: 'builtin.claude', placeholder: '~/.claude + .claude'),
|
||||
),
|
||||
SettingControlRow(
|
||||
label: ClideSettings.i18n.string(context, 'config.row.effort', namespace: 'builtin.claude', placeholder: 'effort'),
|
||||
value: effort,
|
||||
options: kEffortLevels,
|
||||
isActive: (o) => o.value == effort,
|
||||
command: 'effort',
|
||||
),
|
||||
SettingControlRow(
|
||||
label: ClideSettings.i18n.string(context, 'config.row.permissionMode', namespace: 'builtin.claude', placeholder: 'permission mode'),
|
||||
value: permissionModeLabel(mode),
|
||||
options: kPermissionModes,
|
||||
isActive: (o) => o.value == mode,
|
||||
command: 'permissions',
|
||||
),
|
||||
_configRow(tokens, ClideSettings.i18n.string(context, 'config.row.outputStyle', namespace: 'builtin.claude', placeholder: 'output style'), outputStyle),
|
||||
_configRow(
|
||||
tokens,
|
||||
ClideSettings.i18n.string(context, 'config.row.source', namespace: 'builtin.claude', placeholder: 'source'),
|
||||
ClideSettings.i18n.string(context, 'config.row.source.value', namespace: 'builtin.claude', placeholder: '~/.claude + .claude'),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ---- Accordion sections ----
|
||||
for (final section in ConfigSection.values) _accordion(context, tokens, cfg, section),
|
||||
@@ -116,7 +113,7 @@ class ConfigTabView extends StatelessWidget {
|
||||
/// One read-only key→value row in the pinned SETTINGS table.
|
||||
Widget _configRow(SurfaceTokens tokens, String label, String value, {Color? valueColor}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: kMetaRowPitch),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -324,7 +321,7 @@ class _SettingControlRowState extends State<SettingControlRow> {
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideSettings.theme.of(context).surface;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: kMetaRowPitch),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
|
||||
@@ -43,41 +43,64 @@ Widget metaPlaceholder(String text) => Padding(
|
||||
child: ClideText(text, muted: true, fontSize: kMetaFont),
|
||||
);
|
||||
|
||||
/// Key→value sections on the shared table geometry (Activity + Config).
|
||||
Widget buildMetaTable(SurfaceTokens tokens, List<MetaSection> sections) =>
|
||||
ListView(padding: const EdgeInsets.all(12), children: metaTableChildren(tokens, sections));
|
||||
/// Small-caps section header (mono, uppercase) — the same treatment the settings
|
||||
/// overlay uses (settings_category_view `_SectionCard`), so the Claude meta
|
||||
/// sidebar and the settings modal read as one card system (T-158 facelift).
|
||||
Widget metaSectionHeader(BuildContext context, SurfaceTokens tokens, String label) => Padding(
|
||||
padding: const EdgeInsets.only(left: 2, bottom: 6),
|
||||
child: ClideText(label.toUpperCase(), fontSize: clideFontCaption, color: tokens.sidebarSectionHeader, fontFamily: ClideSettings.fonts.monoOf(context)),
|
||||
);
|
||||
|
||||
/// 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) {
|
||||
/// An elevated card (the settings card surface) wrapping divider-separated
|
||||
/// [rows]: `panelHeader` fill, `dividerColor` hairline border, 6px radius.
|
||||
Widget metaCard(SurfaceTokens tokens, List<Widget> rows) => ClideSurface(
|
||||
color: tokens.panelHeader,
|
||||
border: tokens.dividerColor,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (var i = 0; i < rows.length; i++) ...[if (i > 0) const ClideDivider(), rows[i]],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
/// One label→value row sized for the card interior — the shared label column
|
||||
/// then the value, uniform with the settings field rows.
|
||||
Widget metaCardRow(SurfaceTokens tokens, MetaRow r) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: kMetaLabelColumnWidth,
|
||||
child: ClideText(r.label, muted: true, fontSize: kMetaFont),
|
||||
),
|
||||
Expanded(
|
||||
child: ClideText(r.value, fontSize: kMetaFont, color: r.valueColor ?? tokens.globalForeground),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
/// Key→value sections rendered as carded blocks (Activity + Config).
|
||||
Widget buildMetaTable(BuildContext context, SurfaceTokens tokens, List<MetaSection> sections) =>
|
||||
ListView(padding: const EdgeInsets.all(12), children: metaTableChildren(context, tokens, sections));
|
||||
|
||||
/// The carded sections without the enclosing ListView, for tabs that compose
|
||||
/// extra widgets around them (the Activity tab's control strip, T-415): a
|
||||
/// small-caps header above an elevated card of label→value rows.
|
||||
List<Widget> metaTableChildren(BuildContext context, SurfaceTokens tokens, List<MetaSection> sections) {
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
final s = sections[i];
|
||||
children.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: i == 0 ? 0 : 18, bottom: 8),
|
||||
child: ClideText(s.header, fontSize: clideFontSmall, color: tokens.sidebarSectionHeader),
|
||||
padding: EdgeInsets.only(top: i == 0 ? 0 : 16),
|
||||
child: metaSectionHeader(context, tokens, s.header),
|
||||
),
|
||||
);
|
||||
for (final r in s.rows) {
|
||||
children.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: kMetaRowPitch),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: kMetaLabelColumnWidth,
|
||||
child: ClideText(r.label, muted: true, fontSize: kMetaFont),
|
||||
),
|
||||
Expanded(
|
||||
child: ClideText(r.value, fontSize: kMetaFont, color: r.valueColor ?? tokens.globalForeground),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
children.add(metaCard(tokens, [for (final r in s.rows) metaCardRow(tokens, r)]));
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
/// Stateless and props-driven; the parent owns the member list, inject
|
||||
/// state, and orchestrator wiring. Split out of claude_meta_sidebar.dart
|
||||
/// (T-395).
|
||||
///
|
||||
/// Also carries the shared account budget (T-158): the `/usage` figures are
|
||||
/// per-ACCOUNT — every team session shares one `~/.claude` login, so this is a
|
||||
/// single shared budget shown once and labelled as such, NOT a per-member split.
|
||||
library;
|
||||
|
||||
import 'package:clide/builtin/claude/src/claude_status.dart' show ClaudeUsage;
|
||||
import 'package:clide/builtin/claude/src/meta_sidebar/models.dart';
|
||||
import 'package:clide/builtin/claude/src/meta_sidebar/roster_row.dart';
|
||||
import 'package:clide/builtin/claude/src/meta_sidebar/task_row.dart';
|
||||
@@ -31,6 +36,7 @@ class TeamTabView extends StatelessWidget {
|
||||
required this.onSetPermissionMode,
|
||||
required this.onFork,
|
||||
required this.onOpenChatPane,
|
||||
this.usage,
|
||||
});
|
||||
|
||||
final List<TeamMemberJoined> members;
|
||||
@@ -46,27 +52,32 @@ class TeamTabView extends StatelessWidget {
|
||||
final void Function(String memberName) onFork;
|
||||
final VoidCallback onOpenChatPane;
|
||||
|
||||
/// Parsed `/usage` budget for the account this team shares (T-158). Null until
|
||||
/// the first `/usage` refresh (driven from the Activity tab control).
|
||||
final ClaudeUsage? usage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideSettings.theme.of(context).surface;
|
||||
if (members.isEmpty) {
|
||||
return metaPlaceholder(ClideSettings.i18n.string(context, 'team.empty', namespace: 'builtin.claude', placeholder: 'No team active.'));
|
||||
}
|
||||
final children = <Widget>[
|
||||
for (final m in members)
|
||||
AgentRosterRow(
|
||||
key: ValueKey(m.agentId),
|
||||
member: m,
|
||||
status: memberStatus[m.agentId],
|
||||
orchestrator: orchestrator,
|
||||
injectingAgentId: injectingAgentId,
|
||||
injectController: injectController,
|
||||
onToggleInject: onToggleInject,
|
||||
onInjectSubmit: onInjectSubmit,
|
||||
onClose: onClose,
|
||||
onSetPermissionMode: onSetPermissionMode,
|
||||
onFork: onFork,
|
||||
),
|
||||
..._accountSection(context, tokens),
|
||||
if (members.isEmpty)
|
||||
metaPlaceholder(ClideSettings.i18n.string(context, 'team.empty', namespace: 'builtin.claude', placeholder: 'No team active.'))
|
||||
else
|
||||
for (final m in members)
|
||||
AgentRosterRow(
|
||||
key: ValueKey(m.agentId),
|
||||
member: m,
|
||||
status: memberStatus[m.agentId],
|
||||
orchestrator: orchestrator,
|
||||
injectingAgentId: injectingAgentId,
|
||||
injectController: injectController,
|
||||
onToggleInject: onToggleInject,
|
||||
onInjectSubmit: onInjectSubmit,
|
||||
onClose: onClose,
|
||||
onSetPermissionMode: onSetPermissionMode,
|
||||
onFork: onFork,
|
||||
),
|
||||
];
|
||||
|
||||
if (tasks.isNotEmpty) {
|
||||
@@ -85,16 +96,48 @@ class TeamTabView extends StatelessWidget {
|
||||
return ListView(padding: const EdgeInsets.all(12), children: children);
|
||||
}
|
||||
|
||||
/// The shared account-budget card (T-158): one ACCOUNT section with the three
|
||||
/// `/usage` figures and a caption that it is account-wide, not per-member.
|
||||
/// Empty when no `/usage` result has arrived yet.
|
||||
List<Widget> _accountSection(BuildContext context, SurfaceTokens tokens) {
|
||||
final u = usage;
|
||||
if (u == null || (u.session == null && u.week == null && u.weekSonnet == null)) return const [];
|
||||
return [
|
||||
metaSectionHeader(context, tokens, ClideSettings.i18n.string(context, 'team.section.usage', namespace: 'builtin.claude', placeholder: 'ACCOUNT')),
|
||||
metaCard(tokens, [
|
||||
if (u.session != null)
|
||||
metaCardRow(
|
||||
tokens,
|
||||
MetaRow(ClideSettings.i18n.string(context, 'activity.row.session', namespace: 'builtin.claude', placeholder: 'session'), u.session!),
|
||||
),
|
||||
if (u.week != null)
|
||||
metaCardRow(
|
||||
tokens,
|
||||
MetaRow(ClideSettings.i18n.string(context, 'activity.row.weekAll', namespace: 'builtin.claude', placeholder: 'week (all)'), u.week!),
|
||||
),
|
||||
if (u.weekSonnet != null)
|
||||
metaCardRow(
|
||||
tokens,
|
||||
MetaRow(ClideSettings.i18n.string(context, 'activity.row.weekSonnet', namespace: 'builtin.claude', placeholder: 'week (sonnet)'), u.weekSonnet!),
|
||||
),
|
||||
]),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 2, top: 4),
|
||||
child: ClideText(
|
||||
ClideSettings.i18n.string(context, 'team.usage.shared', namespace: 'builtin.claude', placeholder: 'Shared across the team'),
|
||||
muted: true,
|
||||
fontSize: clideFontCaption,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _taskSection(BuildContext context, SurfaceTokens tokens) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClideText(
|
||||
ClideSettings.i18n.string(context, 'team.section.tasks', namespace: 'builtin.claude', placeholder: 'TASKS'),
|
||||
fontSize: clideFontSmall,
|
||||
color: tokens.globalTextMuted,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
metaSectionHeader(context, tokens, ClideSettings.i18n.string(context, 'team.section.tasks', namespace: 'builtin.claude', placeholder: 'TASKS')),
|
||||
for (final t in tasks) TaskRow(task: t, members: members, broker: orchestrator?.broker),
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user