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:
@@ -6220,3 +6220,5 @@ own budget view rather than send the user to the Activity tab.
|
|||||||
Scope is now exactly the "RECOMMENDED SCOPE" above (team_tab.dart MetaSection +
|
Scope is now exactly the "RECOMMENDED SCOPE" above (team_tab.dart MetaSection +
|
||||||
thread _usage from claude_meta_sidebar.dart + team.section.usage i18n key,
|
thread _usage from claude_meta_sidebar.dart + team.section.usage i18n key,
|
||||||
en/nl). This resolves Q-34. Ready to pick up; not yet started.', NULL, '2026-06-26 07:01:13', '2026-06-26 07:01:13.929', '2026-06-26 07:01:13.929', NULL, 'b94a593c3db76b3a9705d8111f3d6b9b', 2) ON CONFLICT(hash) DO NOTHING;
|
en/nl). This resolves Q-34. Ready to pick up; not yet started.', NULL, '2026-06-26 07:01:13', '2026-06-26 07:01:13.929', '2026-06-26 07:01:13.929', NULL, 'b94a593c3db76b3a9705d8111f3d6b9b', 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 ('06FB0TNQM62FKQQD0B9B80PFY4', 'status', 'ready', 'in_progress', NULL, '2026-06-26 07:09:18', '2026-06-26 07:09:18.285', '2026-06-26 07:09:18.285', NULL, 'fae5181a90e789f07eab3a12939d08f4', 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 ('06FB0TNQM62FKQQD0B9B80PFY4', 'status', 'in_progress', 'done', NULL, '2026-06-26 09:31:41', '2026-06-26 09:31:41.267', '2026-06-26 09:31:41.267', NULL, 'b1bc1ec1f57601ffd2c4cee1a756b610', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
|||||||
@@ -7671,3 +7671,191 @@ own budget view rather than send the user to the Activity tab.
|
|||||||
Scope is now exactly the "RECOMMENDED SCOPE" above (team_tab.dart MetaSection +
|
Scope is now exactly the "RECOMMENDED SCOPE" above (team_tab.dart MetaSection +
|
||||||
thread _usage from claude_meta_sidebar.dart + team.section.usage i18n key,
|
thread _usage from claude_meta_sidebar.dart + team.section.usage i18n key,
|
||||||
en/nl). This resolves Q-34. Ready to pick up; not yet started.', 'ready', 'low', NULL, NULL, NULL, '2026-05-23 20:48:38', '2026-06-26 07:01:13.929', NULL, '5232c82a5d4727de012507305f87626a', 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);
|
en/nl). This resolves Q-34. Ready to pick up; not yet started.', 'ready', 'low', NULL, NULL, NULL, '2026-05-23 20:48:38', '2026-06-26 07:01:13.929', NULL, '5232c82a5d4727de012507305f87626a', 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 ('06FB0TNQM62FKQQD0B9B80PFY4', 'task', NULL, 'Team sidebar: account/team token budget (blocked on upstream)', 'Show the account/subscription usage budget (5-hour + weekly limits, % used, reset times) in the Claude meta sidebar (T-141). BLOCKED: this data is not programmatically exposed under subscription (OAuth) auth as of claude 2.1.150 — verified empirically + via docs (2026-05-23). /usage is TUI-only (headless ''claude -p /usage'' returns only a one-liner); stats-cache.json has activity counts only; the stream-json rate_limit_event is undocumented + needs a billed turn; ''claude auth status --json'' shows plan only. A ''claude usage --json'' + a /v1/organizations/{org}/usage/subscription endpoint are an OPEN, unshipped feature request (GitHub anthropics/claude-code#44328). Revisit when #44328 ships or an API-key usage path exists. See project memory ''claude-usage-budget-not-exposed''.
|
||||||
|
|
||||||
|
2026-06-09: detached from T-132 (which is otherwise complete) and made the RESOLVER ticket for Q-34 (how + when to surface the budget given upstream doesn''t expose it). Stays in the backlog; revisit when a viable data path lands (upstream claude usage --json / endpoint per anthropics/claude-code#44328, or an API-key usage path).
|
||||||
|
|
||||||
|
UNBLOCKED (2026-06-12, T-415): probed claude 2.1.175 — a forwarded /usage IS
|
||||||
|
answered headless in stream-json (free, num_turns 0) with parseable text
|
||||||
|
(session %, week % all-models, week % Sonnet). The Activity tab now renders it
|
||||||
|
via parseUsageText + a user-initiated refresh control. Remaining scope for this
|
||||||
|
ticket would be per-member/team budget split, if still wanted.
|
||||||
|
|
||||||
|
─────────────────────────────────────────────
|
||||||
|
REFINED 2026-06-26
|
||||||
|
|
||||||
|
## Grounding (current state)
|
||||||
|
- T-415 SHIPPED the account-budget display. `/usage` is forwarded headless
|
||||||
|
(free, num_turns 0), parsed by `parseUsageText` → `ClaudeUsage` (session %,
|
||||||
|
week all-models %, week Sonnet %) in
|
||||||
|
lib/builtin/claude/src/claude_status.dart, rendered in the Activity tab
|
||||||
|
(meta_sidebar/activity_tab.dart, USAGE `MetaSection`) with a user-initiated
|
||||||
|
refresh that publishes `/usage` on the `builtin.claude/command` channel.
|
||||||
|
claude_meta_sidebar.dart binds the PRIMARY session''s synthetic text items and
|
||||||
|
parses them into `_usage`.
|
||||||
|
|
||||||
|
## KEY FINDING — a literal "per-member split" is not meaningful today
|
||||||
|
- Claude''s usage budget is per-ACCOUNT (subscription/OAuth), not per-session.
|
||||||
|
Every clide session in a workspace (primary, secondaries, managed teammates)
|
||||||
|
spawns against the SAME `~/.claude` login — session_orchestrator spawns with
|
||||||
|
no per-session CLAUDE_* auth override and all transcripts live under one
|
||||||
|
project dir. So the whole team draws down ONE shared 5h/weekly budget.
|
||||||
|
- Rendering the same numbers on each roster row would mislead, not inform. True
|
||||||
|
per-member budgets need per-SESSION account login, which even T-476 (per-REPO
|
||||||
|
login) does NOT provide — within one workspace/team the account is shared.
|
||||||
|
Treat per-member-different-account as out of scope unless per-session
|
||||||
|
multi-account auth ever exists.
|
||||||
|
|
||||||
|
## RECOMMENDED SCOPE (shippable, honest) — resolves Q-34
|
||||||
|
Surface the existing account-wide budget in the TEAM tab as a single "Account"
|
||||||
|
section (NOT per-member): the team sidebar then answers "how much budget is left
|
||||||
|
for the account the whole team shares." Satisfies the ticket title without the
|
||||||
|
false split.
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
1. meta_sidebar/team_tab.dart (TeamTabView.build): add a `MetaSection`
|
||||||
|
(header i18n key `team.section.usage`) rendering the same 3 `MetaRow`s the
|
||||||
|
Activity tab uses (session / week-all / week-sonnet), shown only when usage
|
||||||
|
!= null, with a one-line caption that it is account-wide / shared across the
|
||||||
|
team (so it isn''t read as per-member).
|
||||||
|
2. claude_meta_sidebar.dart: pass the existing `_usage` (ClaudeUsage?) into
|
||||||
|
TeamTabView (it already passes `members`). No new fetch — the primary''s
|
||||||
|
`/usage` already IS the account budget.
|
||||||
|
3. Optional: a refresh control in the team section mirroring the Activity tab
|
||||||
|
(publish `/usage` on `builtin.claude/command`), or just rely on the Activity
|
||||||
|
tab''s refresh keeping `_usage` current.
|
||||||
|
|
||||||
|
Files to touch: meta_sidebar/team_tab.dart, claude_meta_sidebar.dart (thread
|
||||||
|
usage), meta_sidebar/models.dart (reuse MetaSection/MetaRow),
|
||||||
|
assets/i18n/en_us + nl_nl/builtin.claude.json (new `team.section.usage` [+
|
||||||
|
caption] key — keep en/nl parity for the a11y i18n gate).
|
||||||
|
|
||||||
|
## ALTERNATIVE PATHS (pick one before building)
|
||||||
|
A. Shared-account section in the team tab (recommended above).
|
||||||
|
B. Close T-158 as substantially DONE by T-415 — the budget is already visible in
|
||||||
|
the Activity tab; a team-tab duplicate may be redundant. Q-34 answered either
|
||||||
|
way.
|
||||||
|
C. Keep the true per-member split as a FUTURE item, explicitly blocked on a
|
||||||
|
(currently non-existent) per-session multi-account login; park T-158 behind
|
||||||
|
it.
|
||||||
|
|
||||||
|
## ACCEPTANCE (for option A)
|
||||||
|
- Team tab shows ONE account-budget section (session / week / week-sonnet %)
|
||||||
|
when a `/usage` result is available, clearly labelled account-wide / shared.
|
||||||
|
- No per-member duplication of the same numbers.
|
||||||
|
- No new network/headless call beyond the existing user-initiated `/usage`
|
||||||
|
refresh (D-64 / POLICY: explicit user action only).
|
||||||
|
- en/nl parity for any new i18n keys.
|
||||||
|
|
||||||
|
## REFERENCES
|
||||||
|
T-415 (usage parse + Activity render); Q-34 (this resolves it); T-476 (per-REPO,
|
||||||
|
not per-session, account login — does NOT enable per-member budgets);
|
||||||
|
`parseUsageText` / `ClaudeUsage` (claude_status.dart); activity_tab.dart USAGE
|
||||||
|
block; team_tab.dart roster; claude_meta_sidebar.dart usage binding.
|
||||||
|
|
||||||
|
─────────────────────────────────────────────
|
||||||
|
DECISION 2026-06-26: go with option A.
|
||||||
|
|
||||||
|
Build the single shared-account budget section in the TEAM tab (account-wide,
|
||||||
|
clearly labelled — NOT per-member). Options B (close as done-by-T-415) and C
|
||||||
|
(per-member, deferred behind per-session multi-account) are declined: per-member
|
||||||
|
is not meaningful under one shared account, and the team tab should carry its
|
||||||
|
own budget view rather than send the user to the Activity tab.
|
||||||
|
|
||||||
|
Scope is now exactly the "RECOMMENDED SCOPE" above (team_tab.dart MetaSection +
|
||||||
|
thread _usage from claude_meta_sidebar.dart + team.section.usage i18n key,
|
||||||
|
en/nl). This resolves Q-34. Ready to pick up; not yet started.', 'in_progress', 'low', NULL, NULL, NULL, '2026-05-23 20:48:38', '2026-06-26 07:09:18.285', NULL, 'c1e4e9e1c6c7f410c641359902fcbc65', 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 ('06FB0TNQM62FKQQD0B9B80PFY4', 'task', NULL, 'Team sidebar: account/team token budget (blocked on upstream)', 'Show the account/subscription usage budget (5-hour + weekly limits, % used, reset times) in the Claude meta sidebar (T-141). BLOCKED: this data is not programmatically exposed under subscription (OAuth) auth as of claude 2.1.150 — verified empirically + via docs (2026-05-23). /usage is TUI-only (headless ''claude -p /usage'' returns only a one-liner); stats-cache.json has activity counts only; the stream-json rate_limit_event is undocumented + needs a billed turn; ''claude auth status --json'' shows plan only. A ''claude usage --json'' + a /v1/organizations/{org}/usage/subscription endpoint are an OPEN, unshipped feature request (GitHub anthropics/claude-code#44328). Revisit when #44328 ships or an API-key usage path exists. See project memory ''claude-usage-budget-not-exposed''.
|
||||||
|
|
||||||
|
2026-06-09: detached from T-132 (which is otherwise complete) and made the RESOLVER ticket for Q-34 (how + when to surface the budget given upstream doesn''t expose it). Stays in the backlog; revisit when a viable data path lands (upstream claude usage --json / endpoint per anthropics/claude-code#44328, or an API-key usage path).
|
||||||
|
|
||||||
|
UNBLOCKED (2026-06-12, T-415): probed claude 2.1.175 — a forwarded /usage IS
|
||||||
|
answered headless in stream-json (free, num_turns 0) with parseable text
|
||||||
|
(session %, week % all-models, week % Sonnet). The Activity tab now renders it
|
||||||
|
via parseUsageText + a user-initiated refresh control. Remaining scope for this
|
||||||
|
ticket would be per-member/team budget split, if still wanted.
|
||||||
|
|
||||||
|
─────────────────────────────────────────────
|
||||||
|
REFINED 2026-06-26
|
||||||
|
|
||||||
|
## Grounding (current state)
|
||||||
|
- T-415 SHIPPED the account-budget display. `/usage` is forwarded headless
|
||||||
|
(free, num_turns 0), parsed by `parseUsageText` → `ClaudeUsage` (session %,
|
||||||
|
week all-models %, week Sonnet %) in
|
||||||
|
lib/builtin/claude/src/claude_status.dart, rendered in the Activity tab
|
||||||
|
(meta_sidebar/activity_tab.dart, USAGE `MetaSection`) with a user-initiated
|
||||||
|
refresh that publishes `/usage` on the `builtin.claude/command` channel.
|
||||||
|
claude_meta_sidebar.dart binds the PRIMARY session''s synthetic text items and
|
||||||
|
parses them into `_usage`.
|
||||||
|
|
||||||
|
## KEY FINDING — a literal "per-member split" is not meaningful today
|
||||||
|
- Claude''s usage budget is per-ACCOUNT (subscription/OAuth), not per-session.
|
||||||
|
Every clide session in a workspace (primary, secondaries, managed teammates)
|
||||||
|
spawns against the SAME `~/.claude` login — session_orchestrator spawns with
|
||||||
|
no per-session CLAUDE_* auth override and all transcripts live under one
|
||||||
|
project dir. So the whole team draws down ONE shared 5h/weekly budget.
|
||||||
|
- Rendering the same numbers on each roster row would mislead, not inform. True
|
||||||
|
per-member budgets need per-SESSION account login, which even T-476 (per-REPO
|
||||||
|
login) does NOT provide — within one workspace/team the account is shared.
|
||||||
|
Treat per-member-different-account as out of scope unless per-session
|
||||||
|
multi-account auth ever exists.
|
||||||
|
|
||||||
|
## RECOMMENDED SCOPE (shippable, honest) — resolves Q-34
|
||||||
|
Surface the existing account-wide budget in the TEAM tab as a single "Account"
|
||||||
|
section (NOT per-member): the team sidebar then answers "how much budget is left
|
||||||
|
for the account the whole team shares." Satisfies the ticket title without the
|
||||||
|
false split.
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
1. meta_sidebar/team_tab.dart (TeamTabView.build): add a `MetaSection`
|
||||||
|
(header i18n key `team.section.usage`) rendering the same 3 `MetaRow`s the
|
||||||
|
Activity tab uses (session / week-all / week-sonnet), shown only when usage
|
||||||
|
!= null, with a one-line caption that it is account-wide / shared across the
|
||||||
|
team (so it isn''t read as per-member).
|
||||||
|
2. claude_meta_sidebar.dart: pass the existing `_usage` (ClaudeUsage?) into
|
||||||
|
TeamTabView (it already passes `members`). No new fetch — the primary''s
|
||||||
|
`/usage` already IS the account budget.
|
||||||
|
3. Optional: a refresh control in the team section mirroring the Activity tab
|
||||||
|
(publish `/usage` on `builtin.claude/command`), or just rely on the Activity
|
||||||
|
tab''s refresh keeping `_usage` current.
|
||||||
|
|
||||||
|
Files to touch: meta_sidebar/team_tab.dart, claude_meta_sidebar.dart (thread
|
||||||
|
usage), meta_sidebar/models.dart (reuse MetaSection/MetaRow),
|
||||||
|
assets/i18n/en_us + nl_nl/builtin.claude.json (new `team.section.usage` [+
|
||||||
|
caption] key — keep en/nl parity for the a11y i18n gate).
|
||||||
|
|
||||||
|
## ALTERNATIVE PATHS (pick one before building)
|
||||||
|
A. Shared-account section in the team tab (recommended above).
|
||||||
|
B. Close T-158 as substantially DONE by T-415 — the budget is already visible in
|
||||||
|
the Activity tab; a team-tab duplicate may be redundant. Q-34 answered either
|
||||||
|
way.
|
||||||
|
C. Keep the true per-member split as a FUTURE item, explicitly blocked on a
|
||||||
|
(currently non-existent) per-session multi-account login; park T-158 behind
|
||||||
|
it.
|
||||||
|
|
||||||
|
## ACCEPTANCE (for option A)
|
||||||
|
- Team tab shows ONE account-budget section (session / week / week-sonnet %)
|
||||||
|
when a `/usage` result is available, clearly labelled account-wide / shared.
|
||||||
|
- No per-member duplication of the same numbers.
|
||||||
|
- No new network/headless call beyond the existing user-initiated `/usage`
|
||||||
|
refresh (D-64 / POLICY: explicit user action only).
|
||||||
|
- en/nl parity for any new i18n keys.
|
||||||
|
|
||||||
|
## REFERENCES
|
||||||
|
T-415 (usage parse + Activity render); Q-34 (this resolves it); T-476 (per-REPO,
|
||||||
|
not per-session, account login — does NOT enable per-member budgets);
|
||||||
|
`parseUsageText` / `ClaudeUsage` (claude_status.dart); activity_tab.dart USAGE
|
||||||
|
block; team_tab.dart roster; claude_meta_sidebar.dart usage binding.
|
||||||
|
|
||||||
|
─────────────────────────────────────────────
|
||||||
|
DECISION 2026-06-26: go with option A.
|
||||||
|
|
||||||
|
Build the single shared-account budget section in the TEAM tab (account-wide,
|
||||||
|
clearly labelled — NOT per-member). Options B (close as done-by-T-415) and C
|
||||||
|
(per-member, deferred behind per-session multi-account) are declined: per-member
|
||||||
|
is not meaningful under one shared account, and the team tab should carry its
|
||||||
|
own budget view rather than send the user to the Activity tab.
|
||||||
|
|
||||||
|
Scope is now exactly the "RECOMMENDED SCOPE" above (team_tab.dart MetaSection +
|
||||||
|
thread _usage from claude_meta_sidebar.dart + team.section.usage i18n key,
|
||||||
|
en/nl). This resolves Q-34. Ready to pick up; not yet started.', 'done', 'low', NULL, NULL, NULL, '2026-05-23 20:48:38', '2026-06-26 09:31:41.267', NULL, 'a96387776f48125b0def220885e1f27b', 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);
|
||||||
|
|||||||
@@ -22,9 +22,15 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
|||||||
fleshed out to full token + syntax coverage and ships alongside a
|
fleshed out to full token + syntax coverage and ships alongside a
|
||||||
contrast-hardened `summer-night-hc`, both selectable in Settings → Appearance.
|
contrast-hardened `summer-night-hc`, both selectable in Settings → Appearance.
|
||||||
(T-478)
|
(T-478)
|
||||||
|
- **Account budget in the Team tab.** The Claude meta sidebar's Team tab now
|
||||||
|
shows the shared account `/usage` budget as a single labelled card (usage is
|
||||||
|
per-account, not per-member). (T-158)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- **Claude meta sidebar facelift.** The Activity, Team, and Config tabs render
|
||||||
|
their sections as elevated cards with small-caps headers, matching the
|
||||||
|
settings overlay's card design. (T-158)
|
||||||
- **Bundled themes and Tier-0 i18n namespaces resolve from one canonical list
|
- **Bundled themes and Tier-0 i18n namespaces resolve from one canonical list
|
||||||
each.** The app, testmode harness, and contrast/i18n gates iterate the shared
|
each.** The app, testmode harness, and contrast/i18n gates iterate the shared
|
||||||
lists instead of drifting copies; a new theme or catalog is validated
|
lists instead of drifting copies; a new theme or catalog is validated
|
||||||
|
|||||||
@@ -194,6 +194,8 @@
|
|||||||
"taskRow.reassign": { "translation": "Reassign task" },
|
"taskRow.reassign": { "translation": "Reassign task" },
|
||||||
"team.empty": { "translation": "No team active." },
|
"team.empty": { "translation": "No team active." },
|
||||||
"team.section.tasks": { "translation": "TASKS" },
|
"team.section.tasks": { "translation": "TASKS" },
|
||||||
|
"team.section.usage": { "translation": "Account" },
|
||||||
|
"team.usage.shared": { "translation": "Shared across the team" },
|
||||||
"tabStrip.activity": { "translation": "Activity" },
|
"tabStrip.activity": { "translation": "Activity" },
|
||||||
"tabStrip.team": { "translation": "Team" },
|
"tabStrip.team": { "translation": "Team" },
|
||||||
"tabStrip.team.count": { "translation": "Team · {count}" },
|
"tabStrip.team.count": { "translation": "Team · {count}" },
|
||||||
|
|||||||
@@ -194,6 +194,8 @@
|
|||||||
"taskRow.reassign": { "translation": "Taak opnieuw toewijzen" },
|
"taskRow.reassign": { "translation": "Taak opnieuw toewijzen" },
|
||||||
"team.empty": { "translation": "Geen team actief." },
|
"team.empty": { "translation": "Geen team actief." },
|
||||||
"team.section.tasks": { "translation": "TAKEN" },
|
"team.section.tasks": { "translation": "TAKEN" },
|
||||||
|
"team.section.usage": { "translation": "Account" },
|
||||||
|
"team.usage.shared": { "translation": "Gedeeld met het hele team" },
|
||||||
"tabStrip.activity": { "translation": "Activiteit" },
|
"tabStrip.activity": { "translation": "Activiteit" },
|
||||||
"tabStrip.team": { "translation": "Team" },
|
"tabStrip.team": { "translation": "Team" },
|
||||||
"tabStrip.team.count": { "translation": "Team · {count}" },
|
"tabStrip.team.count": { "translation": "Team · {count}" },
|
||||||
|
|||||||
@@ -19,9 +19,10 @@
|
|||||||
/// `meta_sidebar/` (T-395 split). Activity and Config render on the same
|
/// `meta_sidebar/` (T-395 split). Activity and Config render on the same
|
||||||
/// table geometry (`buildMetaTable`) so switching tabs doesn't visually jump.
|
/// table geometry (`buildMetaTable`) so switching tabs doesn't visually jump.
|
||||||
///
|
///
|
||||||
/// The account/team token budget is intentionally absent: it isn't
|
/// The account budget now surfaces from a forwarded `/usage` (T-415): the
|
||||||
/// programmatically exposed under subscription auth (see project memory /
|
/// Activity tab renders it, and the Team tab repeats it as a single shared
|
||||||
/// GitHub anthropics/claude-code#44328).
|
/// ACCOUNT card (T-158) — usage is per-account (one `~/.claude` login), so it's
|
||||||
|
/// shown once, not split per member.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
@@ -312,6 +313,7 @@ class _ClaudeMetaSidebarState extends State<ClaudeMetaSidebar> {
|
|||||||
members: _members,
|
members: _members,
|
||||||
memberStatus: _memberStatus,
|
memberStatus: _memberStatus,
|
||||||
orchestrator: _orchestrator,
|
orchestrator: _orchestrator,
|
||||||
|
usage: _usage,
|
||||||
tasks: _tasks,
|
tasks: _tasks,
|
||||||
injectingAgentId: _injectingAgentId,
|
injectingAgentId: _injectingAgentId,
|
||||||
injectController: _injectCtl,
|
injectController: _injectCtl,
|
||||||
|
|||||||
@@ -87,59 +87,61 @@ class ActivityTabView extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
// SESSION control strip (T-415): drives the primary session through
|
// SESSION control strip (T-415): drives the primary session through
|
||||||
// the builtin.claude/command bus — identical to typing the command.
|
// the builtin.claude/command bus — identical to typing the command.
|
||||||
Padding(
|
// Carded to match the settings overlay (T-158 facelift).
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
metaSectionHeader(context, tokens, ClideSettings.i18n.string(context, 'activity.section.session', namespace: 'builtin.claude', placeholder: 'SESSION')),
|
||||||
child: ClideText(
|
metaCard(tokens, [
|
||||||
ClideSettings.i18n.string(context, 'activity.section.session', namespace: 'builtin.claude', placeholder: 'SESSION'),
|
Padding(
|
||||||
fontSize: clideFontSmall,
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||||
color: tokens.sidebarSectionHeader,
|
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(
|
if (sections.isNotEmpty) const SizedBox(height: 16),
|
||||||
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.isEmpty)
|
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
|
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 effort = status?.effort ?? settings['effortLevel']?.toString() ?? 'default';
|
||||||
|
|
||||||
final children = <Widget>[
|
final children = <Widget>[
|
||||||
// Pinned SETTINGS control panel — not collapsible.
|
// Pinned SETTINGS control panel — not collapsible. Carded to match the
|
||||||
Padding(
|
// settings overlay (T-158 facelift).
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
metaSectionHeader(context, tokens, ClideSettings.i18n.string(context, 'config.section.settings', namespace: 'builtin.claude', placeholder: 'SETTINGS')),
|
||||||
child: ClideText(
|
metaCard(tokens, [
|
||||||
ClideSettings.i18n.string(context, 'config.section.settings', namespace: 'builtin.claude', placeholder: 'SETTINGS'),
|
SettingControlRow(
|
||||||
fontSize: clideFontSmall,
|
label: ClideSettings.i18n.string(context, 'config.row.model', namespace: 'builtin.claude', placeholder: 'model'),
|
||||||
color: tokens.sidebarSectionHeader,
|
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(
|
||||||
SettingControlRow(
|
label: ClideSettings.i18n.string(context, 'config.row.effort', namespace: 'builtin.claude', placeholder: 'effort'),
|
||||||
label: ClideSettings.i18n.string(context, 'config.row.model', namespace: 'builtin.claude', placeholder: 'model'),
|
value: effort,
|
||||||
value: model,
|
options: kEffortLevels,
|
||||||
valueColor: tokens.globalFocus,
|
isActive: (o) => o.value == effort,
|
||||||
options: (models == null || models!.isEmpty) ? kFallbackModels : models!,
|
command: 'effort',
|
||||||
isActive: (o) => o.value == model || model.toLowerCase().contains(o.value.toLowerCase()),
|
),
|
||||||
command: 'model',
|
SettingControlRow(
|
||||||
),
|
label: ClideSettings.i18n.string(context, 'config.row.permissionMode', namespace: 'builtin.claude', placeholder: 'permission mode'),
|
||||||
SettingControlRow(
|
value: permissionModeLabel(mode),
|
||||||
label: ClideSettings.i18n.string(context, 'config.row.effort', namespace: 'builtin.claude', placeholder: 'effort'),
|
options: kPermissionModes,
|
||||||
value: effort,
|
isActive: (o) => o.value == mode,
|
||||||
options: kEffortLevels,
|
command: 'permissions',
|
||||||
isActive: (o) => o.value == effort,
|
),
|
||||||
command: 'effort',
|
_configRow(tokens, ClideSettings.i18n.string(context, 'config.row.outputStyle', namespace: 'builtin.claude', placeholder: 'output style'), outputStyle),
|
||||||
),
|
_configRow(
|
||||||
SettingControlRow(
|
tokens,
|
||||||
label: ClideSettings.i18n.string(context, 'config.row.permissionMode', namespace: 'builtin.claude', placeholder: 'permission mode'),
|
ClideSettings.i18n.string(context, 'config.row.source', namespace: 'builtin.claude', placeholder: 'source'),
|
||||||
value: permissionModeLabel(mode),
|
ClideSettings.i18n.string(context, 'config.row.source.value', namespace: 'builtin.claude', placeholder: '~/.claude + .claude'),
|
||||||
options: kPermissionModes,
|
),
|
||||||
isActive: (o) => o.value == mode,
|
]),
|
||||||
command: 'permissions',
|
const SizedBox(height: 12),
|
||||||
),
|
|
||||||
_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'),
|
|
||||||
),
|
|
||||||
|
|
||||||
// ---- Accordion sections ----
|
// ---- Accordion sections ----
|
||||||
for (final section in ConfigSection.values) _accordion(context, tokens, cfg, section),
|
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.
|
/// One read-only key→value row in the pinned SETTINGS table.
|
||||||
Widget _configRow(SurfaceTokens tokens, String label, String value, {Color? valueColor}) {
|
Widget _configRow(SurfaceTokens tokens, String label, String value, {Color? valueColor}) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: kMetaRowPitch),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -324,7 +321,7 @@ class _SettingControlRowState extends State<SettingControlRow> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tokens = ClideSettings.theme.of(context).surface;
|
final tokens = ClideSettings.theme.of(context).surface;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: kMetaRowPitch),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -43,41 +43,64 @@ Widget metaPlaceholder(String text) => Padding(
|
|||||||
child: ClideText(text, muted: true, fontSize: kMetaFont),
|
child: ClideText(text, muted: true, fontSize: kMetaFont),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Key→value sections on the shared table geometry (Activity + Config).
|
/// Small-caps section header (mono, uppercase) — the same treatment the settings
|
||||||
Widget buildMetaTable(SurfaceTokens tokens, List<MetaSection> sections) =>
|
/// overlay uses (settings_category_view `_SectionCard`), so the Claude meta
|
||||||
ListView(padding: const EdgeInsets.all(12), children: metaTableChildren(tokens, sections));
|
/// 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
|
/// An elevated card (the settings card surface) wrapping divider-separated
|
||||||
/// widgets around the sections (the Activity tab's control strip, T-415).
|
/// [rows]: `panelHeader` fill, `dividerColor` hairline border, 6px radius.
|
||||||
List<Widget> metaTableChildren(SurfaceTokens tokens, List<MetaSection> sections) {
|
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>[];
|
final children = <Widget>[];
|
||||||
for (var i = 0; i < sections.length; i++) {
|
for (var i = 0; i < sections.length; i++) {
|
||||||
final s = sections[i];
|
final s = sections[i];
|
||||||
children.add(
|
children.add(
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: i == 0 ? 0 : 18, bottom: 8),
|
padding: EdgeInsets.only(top: i == 0 ? 0 : 16),
|
||||||
child: ClideText(s.header, fontSize: clideFontSmall, color: tokens.sidebarSectionHeader),
|
child: metaSectionHeader(context, tokens, s.header),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
for (final r in s.rows) {
|
children.add(metaCard(tokens, [for (final r in s.rows) metaCardRow(tokens, r)]));
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,13 @@
|
|||||||
/// Stateless and props-driven; the parent owns the member list, inject
|
/// Stateless and props-driven; the parent owns the member list, inject
|
||||||
/// state, and orchestrator wiring. Split out of claude_meta_sidebar.dart
|
/// state, and orchestrator wiring. Split out of claude_meta_sidebar.dart
|
||||||
/// (T-395).
|
/// (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;
|
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/models.dart';
|
||||||
import 'package:clide/builtin/claude/src/meta_sidebar/roster_row.dart';
|
import 'package:clide/builtin/claude/src/meta_sidebar/roster_row.dart';
|
||||||
import 'package:clide/builtin/claude/src/meta_sidebar/task_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.onSetPermissionMode,
|
||||||
required this.onFork,
|
required this.onFork,
|
||||||
required this.onOpenChatPane,
|
required this.onOpenChatPane,
|
||||||
|
this.usage,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<TeamMemberJoined> members;
|
final List<TeamMemberJoined> members;
|
||||||
@@ -46,27 +52,32 @@ class TeamTabView extends StatelessWidget {
|
|||||||
final void Function(String memberName) onFork;
|
final void Function(String memberName) onFork;
|
||||||
final VoidCallback onOpenChatPane;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tokens = ClideSettings.theme.of(context).surface;
|
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>[
|
final children = <Widget>[
|
||||||
for (final m in members)
|
..._accountSection(context, tokens),
|
||||||
AgentRosterRow(
|
if (members.isEmpty)
|
||||||
key: ValueKey(m.agentId),
|
metaPlaceholder(ClideSettings.i18n.string(context, 'team.empty', namespace: 'builtin.claude', placeholder: 'No team active.'))
|
||||||
member: m,
|
else
|
||||||
status: memberStatus[m.agentId],
|
for (final m in members)
|
||||||
orchestrator: orchestrator,
|
AgentRosterRow(
|
||||||
injectingAgentId: injectingAgentId,
|
key: ValueKey(m.agentId),
|
||||||
injectController: injectController,
|
member: m,
|
||||||
onToggleInject: onToggleInject,
|
status: memberStatus[m.agentId],
|
||||||
onInjectSubmit: onInjectSubmit,
|
orchestrator: orchestrator,
|
||||||
onClose: onClose,
|
injectingAgentId: injectingAgentId,
|
||||||
onSetPermissionMode: onSetPermissionMode,
|
injectController: injectController,
|
||||||
onFork: onFork,
|
onToggleInject: onToggleInject,
|
||||||
),
|
onInjectSubmit: onInjectSubmit,
|
||||||
|
onClose: onClose,
|
||||||
|
onSetPermissionMode: onSetPermissionMode,
|
||||||
|
onFork: onFork,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (tasks.isNotEmpty) {
|
if (tasks.isNotEmpty) {
|
||||||
@@ -85,16 +96,48 @@ class TeamTabView extends StatelessWidget {
|
|||||||
return ListView(padding: const EdgeInsets.all(12), children: children);
|
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) {
|
Widget _taskSection(BuildContext context, SurfaceTokens tokens) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ClideText(
|
metaSectionHeader(context, tokens, ClideSettings.i18n.string(context, 'team.section.tasks', namespace: 'builtin.claude', placeholder: 'TASKS')),
|
||||||
ClideSettings.i18n.string(context, 'team.section.tasks', namespace: 'builtin.claude', placeholder: 'TASKS'),
|
|
||||||
fontSize: clideFontSmall,
|
|
||||||
color: tokens.globalTextMuted,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
for (final t in tasks) TaskRow(task: t, members: members, broker: orchestrator?.broker),
|
for (final t in tasks) TaskRow(task: t, members: members, broker: orchestrator?.broker),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/// T-158: the Team tab carries the shared account budget — a single ACCOUNT
|
||||||
|
/// card from the forwarded `/usage`, NOT a per-member split (usage is
|
||||||
|
/// per-account: every team session shares one ~/.claude login).
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:clide/builtin/claude/src/claude_status.dart' show ClaudeUsage;
|
||||||
|
import 'package:clide/builtin/claude/src/meta_sidebar/team_tab.dart';
|
||||||
|
import 'package:clide/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../../helpers/kernel_fixture.dart';
|
||||||
|
import '../../helpers/widget_harness.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late KernelFixture f;
|
||||||
|
late TextEditingController inject;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
f = await KernelFixture.create();
|
||||||
|
inject = TextEditingController();
|
||||||
|
});
|
||||||
|
tearDown(() {
|
||||||
|
inject.dispose();
|
||||||
|
f.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
// No team members + no catalog seeded, so labels fall back to their English
|
||||||
|
// placeholders ('ACCOUNT' is the uppercased section header).
|
||||||
|
TeamTabView view({ClaudeUsage? usage}) => TeamTabView(
|
||||||
|
members: const [],
|
||||||
|
memberStatus: const {},
|
||||||
|
orchestrator: null,
|
||||||
|
tasks: const [],
|
||||||
|
injectingAgentId: null,
|
||||||
|
injectController: inject,
|
||||||
|
onToggleInject: (_) {},
|
||||||
|
onInjectSubmit: (_, _) {},
|
||||||
|
onClose: (_) {},
|
||||||
|
onSetPermissionMode: (_, _) {},
|
||||||
|
onFork: (_) {},
|
||||||
|
onOpenChatPane: () {},
|
||||||
|
usage: usage,
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('renders the shared ACCOUNT budget card when usage is present', (tester) async {
|
||||||
|
await tester.pumpWidget(harness(f, view(usage: const ClaudeUsage(session: '15% used', week: '53% used', weekSonnet: '0% used'))));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('ACCOUNT'), findsOneWidget);
|
||||||
|
expect(find.text('15% used'), findsOneWidget);
|
||||||
|
expect(find.text('53% used'), findsOneWidget);
|
||||||
|
expect(find.text('0% used'), findsOneWidget);
|
||||||
|
// Labelled shared, not per-member.
|
||||||
|
expect(find.text('Shared across the team'), findsOneWidget);
|
||||||
|
// Solo (no members) still shows the budget above the empty-team notice.
|
||||||
|
expect(find.text('No team active.'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('omits the ACCOUNT card when no usage has been fetched', (tester) async {
|
||||||
|
await tester.pumpWidget(harness(f, view()));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('ACCOUNT'), findsNothing);
|
||||||
|
expect(find.text('Shared across the team'), findsNothing);
|
||||||
|
expect(find.text('No team active.'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('omits the ACCOUNT card when the usage result is empty', (tester) async {
|
||||||
|
await tester.pumpWidget(harness(f, view(usage: const ClaudeUsage())));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('ACCOUNT'), findsNothing);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user