From 86a52cd7313a86a007af8c3b878ebd6caee7c791 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 9 Jun 2026 19:24:19 +0200 Subject: [PATCH] finish T-235 (persisted activity fold level); close T-132 via Q-34 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T-235: app-scoped kActivityFoldLevelKey + a claude.activity.fold-level command that cycles none→tools→thinking→everything; ClaudePane + team_panel_host read it and re-fold live via the settings notifier. Unit tests for the helpers. T-132 cleanup: the one blocked item (account/team token budget) is detached (T-158), reframed as Q-34 'how + when to surface the budget given upstream doesn't expose it', with T-158 as its backlog resolver. T-132 closed — all doable work delivered. Co-Authored-By: Claude Opus 4.8 (1M context) --- .pql/changelog/ticket_history/2026-06.sql | 42 +++++++++++++++++ .pql/changelog/tickets/2026-06.sql | 46 +++++++++++++++++++ CHANGELOG.md | 4 ++ governance/README.md | 1 + governance/questions/architecture.md | 6 +++ lib/builtin/claude/src/activity_cluster.dart | 12 +++++ lib/builtin/claude/src/claude_pane.dart | 10 ++++ lib/builtin/claude/src/extension.dart | 22 +++++++++ lib/builtin/claude/src/team_panel_host.dart | 13 +++++- .../builtin/claude/activity_cluster_test.dart | 18 ++++++++ 10 files changed, 173 insertions(+), 1 deletion(-) diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 8d9ad86f..66ff3e01 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -2050,3 +2050,45 @@ Source: plan iridescent-tinkering-umbrella (2026-05-22), grounded in an-idea.md STATUS CORRECTED (2026-06-09): not superseded — substantially DELIVERED. 32/34 children done; the native Claude pane (transcript rendering, select/copy, no terminal), tmux team tiles + meta sidebar, composer, and D-75 all shipped. Re-flagged from ''ready'' (misleading — no startable work) to in_progress. Remaining, both non-blocking, kept under this epic: T-158 (team/account token budget — blocked on upstream /usage exposure) and T-235 (persist activity-card fold level, T-230 follow-up). Close when those land.', NULL, '2026-06-09 17:06:47', '2026-06-09 17:06:47', '2026-06-09 17:06:47', NULL, 'a11411cd79f8fa9b93a74693676d02f0', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-132', 'status', 'ready', 'in_progress', NULL, '2026-06-09 17:06:47', '2026-06-09 17:06:47', '2026-06-09 17:06:47', NULL, '8f39c5ef2d84d66fd67b8b0bce0c0fe3', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-235', 'description', 'Follow-up to T-230. The activity-card fold level is implemented as a switchable ConversationView.foldLevel parameter (FoldLevel none/tools/thinking/everything; groupConversation re-groups per level, unit-tested), but the live Claude pane currently uses the L1 default with no way for the user to change it. Finish acceptance #4: (1) read the fold level from a persisted user setting (via ctx.settings / the kernel settings service — see how other builtins read settings), defaulting to L1; (2) add a control to change it (e.g. a command ''claude.activity.fold-level'' cycling none->L1->L2->L3, and/or an entry in the Claude/settings UI); (3) thread it into the ConversationView built in claude_pane.dart (and team_panel_host.dart). The grouping + card + a11y are already done in activity_cluster.dart + conversation_view.dart.', 'Follow-up to T-230. The activity-card fold level is implemented as a switchable ConversationView.foldLevel parameter (FoldLevel none/tools/thinking/everything; groupConversation re-groups per level, unit-tested), but the live Claude pane currently uses the L1 default with no way for the user to change it. Finish acceptance #4: (1) read the fold level from a persisted user setting (via ctx.settings / the kernel settings service — see how other builtins read settings), defaulting to L1; (2) add a control to change it (e.g. a command ''claude.activity.fold-level'' cycling none->L1->L2->L3, and/or an entry in the Claude/settings UI); (3) thread it into the ConversationView built in claude_pane.dart (and team_panel_host.dart). The grouping + card + a11y are already done in activity_cluster.dart + conversation_view.dart. + +DONE 2026-06-09. Persisted app-scoped setting kActivityFoldLevelKey (app.claude.activityFoldLevel); command claude.activity.fold-level cycles none→tools→thinking→everything (foldLevelFromName/nextFoldLevel in activity_cluster.dart); ClaudePane + team_panel_host read the setting and re-fold live via the settings notifier. Tests: foldLevelFromName/nextFoldLevel in activity_cluster_test.', NULL, '2026-06-09 17:16:33', '2026-06-09 17:16:33', '2026-06-09 17:16:33', NULL, 'd405dc094770c506152a1c1733e8d311', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-235', 'status', 'backlog', 'done', NULL, '2026-06-09 17:16:34', '2026-06-09 17:16:34', '2026-06-09 17:16:34', NULL, '0f918f8f93f76772c2eb987acdbc349c', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-158', 'parent_id', 'T-132', NULL, NULL, '2026-06-09 17:16:34', '2026-06-09 17:16:34', '2026-06-09 17:16:34', NULL, 'cbc56070c7311b51c3709c7958d71d4f', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-158', 'description', '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''.', '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).', NULL, '2026-06-09 17:16:34', '2026-06-09 17:16:34', '2026-06-09 17:16:34', NULL, 'cdaf44fb19cf2dce450fee6e35b7c8cf', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-132', 'description', 'Replace the terminal-based Claude pane with native rendering driven by Claude Code''s transcript JSONL (with cross-widget text selection), and surface experimental tmux agent teams (lead + teammates) as tiled native panels plus a team-meta sidebar. The terminal is retained only as a general IDE tool. + +Why: +1. No cross-widget text selection/copy in clide''s native widgets today (regression vs a terminal). +2. PTY/TUI rendering is the fragile, OS-variant part of the Claude pane. +3. Claude Code''s experimental tmux team mode (lead + N teammates, each a tmux pane) is the flagship capability to surface as real GUI panels, not scraped TUI. + +Direction: run Claude headless in tmux; render its conversation natively from the transcript JSONL; lead panel (left, with composer) + teammate tiles (right) in a responsive auto-wrap grid (1->2->3 cols); team-meta sidebar with roster + token budget. + +Principle: Claude-centric first, CLI-first (D-6) a strong second. Accept isolated, version-pinned coupling to Claude Code internal contracts where it serves the Claude integration; preserve CLI/event surfaces where sensible. + +Acceptance: all child tickets done; D-record landed; Claude pane renders natively with working select+copy and no terminal; a tmux team surfaces as lead + auto-wrapping teammate tiles + meta sidebar; all fragile CC-internals parsing isolated and version-pinned. + +Source: plan iridescent-tinkering-umbrella (2026-05-22), grounded in an-idea.md + read-only validation of the transcript schema this session. + +STATUS CORRECTED (2026-06-09): not superseded — substantially DELIVERED. 32/34 children done; the native Claude pane (transcript rendering, select/copy, no terminal), tmux team tiles + meta sidebar, composer, and D-75 all shipped. Re-flagged from ''ready'' (misleading — no startable work) to in_progress. Remaining, both non-blocking, kept under this epic: T-158 (team/account token budget — blocked on upstream /usage exposure) and T-235 (persist activity-card fold level, T-230 follow-up). Close when those land.', 'Replace the terminal-based Claude pane with native rendering driven by Claude Code''s transcript JSONL (with cross-widget text selection), and surface experimental tmux agent teams (lead + teammates) as tiled native panels plus a team-meta sidebar. The terminal is retained only as a general IDE tool. + +Why: +1. No cross-widget text selection/copy in clide''s native widgets today (regression vs a terminal). +2. PTY/TUI rendering is the fragile, OS-variant part of the Claude pane. +3. Claude Code''s experimental tmux team mode (lead + N teammates, each a tmux pane) is the flagship capability to surface as real GUI panels, not scraped TUI. + +Direction: run Claude headless in tmux; render its conversation natively from the transcript JSONL; lead panel (left, with composer) + teammate tiles (right) in a responsive auto-wrap grid (1->2->3 cols); team-meta sidebar with roster + token budget. + +Principle: Claude-centric first, CLI-first (D-6) a strong second. Accept isolated, version-pinned coupling to Claude Code internal contracts where it serves the Claude integration; preserve CLI/event surfaces where sensible. + +Acceptance: all child tickets done; D-record landed; Claude pane renders natively with working select+copy and no terminal; a tmux team surfaces as lead + auto-wrapping teammate tiles + meta sidebar; all fragile CC-internals parsing isolated and version-pinned. + +Source: plan iridescent-tinkering-umbrella (2026-05-22), grounded in an-idea.md + read-only validation of the transcript schema this session. + +STATUS CORRECTED (2026-06-09): not superseded — substantially DELIVERED. 32/34 children done; the native Claude pane (transcript rendering, select/copy, no terminal), tmux team tiles + meta sidebar, composer, and D-75 all shipped. Re-flagged from ''ready'' (misleading — no startable work) to in_progress. Remaining, both non-blocking, kept under this epic: T-158 (team/account token budget — blocked on upstream /usage exposure) and T-235 (persist activity-card fold level, T-230 follow-up). Close when those land. + +CLOSED 2026-06-09: all doable work delivered (native Claude pane, transcript rendering, select/copy, no terminal, tmux team tiles + meta sidebar, composer, activity-card folding incl. the persisted fold level T-235, D-75). The one blocked item — account/team token budget — was detached (T-158) and reframed as Q-34 (how + when to surface it given upstream doesn''t expose the data); T-158 is the resolver, parked in the backlog. Nothing actionable remains under this epic.', NULL, '2026-06-09 17:23:25', '2026-06-09 17:23:25', '2026-06-09 17:23:25', NULL, '19f585ff8564f76c5d823a3455831636', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-132', 'status', 'in_progress', 'done', NULL, '2026-06-09 17:23:25', '2026-06-09 17:23:25', '2026-06-09 17:23:25', NULL, '6ad60f480bedf1a382cfb9af3d415060', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 2ba8b729..ef69a268 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -3464,3 +3464,49 @@ Acceptance: all child tickets done; D-record landed; Claude pane renders nativel Source: plan iridescent-tinkering-umbrella (2026-05-22), grounded in an-idea.md + read-only validation of the transcript schema this session. STATUS CORRECTED (2026-06-09): not superseded — substantially DELIVERED. 32/34 children done; the native Claude pane (transcript rendering, select/copy, no terminal), tmux team tiles + meta sidebar, composer, and D-75 all shipped. Re-flagged from ''ready'' (misleading — no startable work) to in_progress. Remaining, both non-blocking, kept under this epic: T-158 (team/account token budget — blocked on upstream /usage exposure) and T-235 (persist activity-card fold level, T-230 follow-up). Close when those land.', 'in_progress', 'high', NULL, NULL, NULL, '2026-05-22 15:45:15', '2026-06-09 17:06:47', NULL, '31d40a9794b4de3443e0802563fb0bfc', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-235', 'task', 'T-132', 'Persist + expose the activity-card fold level (T-230 follow-up)', 'Follow-up to T-230. The activity-card fold level is implemented as a switchable ConversationView.foldLevel parameter (FoldLevel none/tools/thinking/everything; groupConversation re-groups per level, unit-tested), but the live Claude pane currently uses the L1 default with no way for the user to change it. Finish acceptance #4: (1) read the fold level from a persisted user setting (via ctx.settings / the kernel settings service — see how other builtins read settings), defaulting to L1; (2) add a control to change it (e.g. a command ''claude.activity.fold-level'' cycling none->L1->L2->L3, and/or an entry in the Claude/settings UI); (3) thread it into the ConversationView built in claude_pane.dart (and team_panel_host.dart). The grouping + card + a11y are already done in activity_cluster.dart + conversation_view.dart. + +DONE 2026-06-09. Persisted app-scoped setting kActivityFoldLevelKey (app.claude.activityFoldLevel); command claude.activity.fold-level cycles none→tools→thinking→everything (foldLevelFromName/nextFoldLevel in activity_cluster.dart); ClaudePane + team_panel_host read the setting and re-fold live via the settings notifier. Tests: foldLevelFromName/nextFoldLevel in activity_cluster_test.', 'backlog', 'low', NULL, NULL, NULL, '2026-06-03 15:17:29', '2026-06-09 17:16:33', NULL, '40fd4797e688503fdc24c27b5d8ae5ba', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-235', 'task', 'T-132', 'Persist + expose the activity-card fold level (T-230 follow-up)', 'Follow-up to T-230. The activity-card fold level is implemented as a switchable ConversationView.foldLevel parameter (FoldLevel none/tools/thinking/everything; groupConversation re-groups per level, unit-tested), but the live Claude pane currently uses the L1 default with no way for the user to change it. Finish acceptance #4: (1) read the fold level from a persisted user setting (via ctx.settings / the kernel settings service — see how other builtins read settings), defaulting to L1; (2) add a control to change it (e.g. a command ''claude.activity.fold-level'' cycling none->L1->L2->L3, and/or an entry in the Claude/settings UI); (3) thread it into the ConversationView built in claude_pane.dart (and team_panel_host.dart). The grouping + card + a11y are already done in activity_cluster.dart + conversation_view.dart. + +DONE 2026-06-09. Persisted app-scoped setting kActivityFoldLevelKey (app.claude.activityFoldLevel); command claude.activity.fold-level cycles none→tools→thinking→everything (foldLevelFromName/nextFoldLevel in activity_cluster.dart); ClaudePane + team_panel_host read the setting and re-fold live via the settings notifier. Tests: foldLevelFromName/nextFoldLevel in activity_cluster_test.', 'done', 'low', NULL, NULL, NULL, '2026-06-03 15:17:29', '2026-06-09 17:16:34', NULL, '877368262db5a391fe1b37deb354155d', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-158', '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''.', 'backlog', 'low', NULL, NULL, NULL, '2026-05-23 20:48:38', '2026-06-09 17:16:34', NULL, 'ac1714a8fedebb110163a4c2205d9944', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-158', '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).', 'backlog', 'low', NULL, NULL, NULL, '2026-05-23 20:48:38', '2026-06-09 17:16:34', NULL, 'e137a4e7751d48048823fec2fd3d6d2a', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-132', 'epic', NULL, 'Native Claude integration: transcript-rendered conversation + tmux agent-team panels', 'Replace the terminal-based Claude pane with native rendering driven by Claude Code''s transcript JSONL (with cross-widget text selection), and surface experimental tmux agent teams (lead + teammates) as tiled native panels plus a team-meta sidebar. The terminal is retained only as a general IDE tool. + +Why: +1. No cross-widget text selection/copy in clide''s native widgets today (regression vs a terminal). +2. PTY/TUI rendering is the fragile, OS-variant part of the Claude pane. +3. Claude Code''s experimental tmux team mode (lead + N teammates, each a tmux pane) is the flagship capability to surface as real GUI panels, not scraped TUI. + +Direction: run Claude headless in tmux; render its conversation natively from the transcript JSONL; lead panel (left, with composer) + teammate tiles (right) in a responsive auto-wrap grid (1->2->3 cols); team-meta sidebar with roster + token budget. + +Principle: Claude-centric first, CLI-first (D-6) a strong second. Accept isolated, version-pinned coupling to Claude Code internal contracts where it serves the Claude integration; preserve CLI/event surfaces where sensible. + +Acceptance: all child tickets done; D-record landed; Claude pane renders natively with working select+copy and no terminal; a tmux team surfaces as lead + auto-wrapping teammate tiles + meta sidebar; all fragile CC-internals parsing isolated and version-pinned. + +Source: plan iridescent-tinkering-umbrella (2026-05-22), grounded in an-idea.md + read-only validation of the transcript schema this session. + +STATUS CORRECTED (2026-06-09): not superseded — substantially DELIVERED. 32/34 children done; the native Claude pane (transcript rendering, select/copy, no terminal), tmux team tiles + meta sidebar, composer, and D-75 all shipped. Re-flagged from ''ready'' (misleading — no startable work) to in_progress. Remaining, both non-blocking, kept under this epic: T-158 (team/account token budget — blocked on upstream /usage exposure) and T-235 (persist activity-card fold level, T-230 follow-up). Close when those land. + +CLOSED 2026-06-09: all doable work delivered (native Claude pane, transcript rendering, select/copy, no terminal, tmux team tiles + meta sidebar, composer, activity-card folding incl. the persisted fold level T-235, D-75). The one blocked item — account/team token budget — was detached (T-158) and reframed as Q-34 (how + when to surface it given upstream doesn''t expose the data); T-158 is the resolver, parked in the backlog. Nothing actionable remains under this epic.', 'in_progress', 'high', NULL, NULL, NULL, '2026-05-22 15:45:15', '2026-06-09 17:23:25', NULL, '199062edd21d86aabf28297978ccf36c', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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 (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-132', 'epic', NULL, 'Native Claude integration: transcript-rendered conversation + tmux agent-team panels', 'Replace the terminal-based Claude pane with native rendering driven by Claude Code''s transcript JSONL (with cross-widget text selection), and surface experimental tmux agent teams (lead + teammates) as tiled native panels plus a team-meta sidebar. The terminal is retained only as a general IDE tool. + +Why: +1. No cross-widget text selection/copy in clide''s native widgets today (regression vs a terminal). +2. PTY/TUI rendering is the fragile, OS-variant part of the Claude pane. +3. Claude Code''s experimental tmux team mode (lead + N teammates, each a tmux pane) is the flagship capability to surface as real GUI panels, not scraped TUI. + +Direction: run Claude headless in tmux; render its conversation natively from the transcript JSONL; lead panel (left, with composer) + teammate tiles (right) in a responsive auto-wrap grid (1->2->3 cols); team-meta sidebar with roster + token budget. + +Principle: Claude-centric first, CLI-first (D-6) a strong second. Accept isolated, version-pinned coupling to Claude Code internal contracts where it serves the Claude integration; preserve CLI/event surfaces where sensible. + +Acceptance: all child tickets done; D-record landed; Claude pane renders natively with working select+copy and no terminal; a tmux team surfaces as lead + auto-wrapping teammate tiles + meta sidebar; all fragile CC-internals parsing isolated and version-pinned. + +Source: plan iridescent-tinkering-umbrella (2026-05-22), grounded in an-idea.md + read-only validation of the transcript schema this session. + +STATUS CORRECTED (2026-06-09): not superseded — substantially DELIVERED. 32/34 children done; the native Claude pane (transcript rendering, select/copy, no terminal), tmux team tiles + meta sidebar, composer, and D-75 all shipped. Re-flagged from ''ready'' (misleading — no startable work) to in_progress. Remaining, both non-blocking, kept under this epic: T-158 (team/account token budget — blocked on upstream /usage exposure) and T-235 (persist activity-card fold level, T-230 follow-up). Close when those land. + +CLOSED 2026-06-09: all doable work delivered (native Claude pane, transcript rendering, select/copy, no terminal, tmux team tiles + meta sidebar, composer, activity-card folding incl. the persisted fold level T-235, D-75). The one blocked item — account/team token budget — was detached (T-158) and reframed as Q-34 (how + when to surface it given upstream doesn''t expose the data); T-158 is the resolver, parked in the backlog. Nothing actionable remains under this epic.', 'done', 'high', NULL, NULL, NULL, '2026-05-22 15:45:15', '2026-06-09 17:23:25', NULL, 'bb12e25f5a36b67bcb1c4dddb4a750d1', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_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); diff --git a/CHANGELOG.md b/CHANGELOG.md index d913a764..f8a2c3e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Added +- **The activity-card fold level is now adjustable and persists.** A + `claude.activity.fold-level` command cycles how aggressively meta steps fold + (none → tools → thinking → everything); the choice is saved app-wide and the + Claude pane and team tiles re-fold live. (T-235) - **Consecutive edits to one file fold into a single card.** A run of same-file edits collapses to one `# edits` holder instead of a stack — every edit reachable on expand; a different file or an interleaving step splits it. The diff --git a/governance/README.md b/governance/README.md index 86c9c1a9..989ff34b 100644 --- a/governance/README.md +++ b/governance/README.md @@ -157,6 +157,7 @@ You might also want, project-permitting: - [Q-29: Branch picker location](questions/architecture.md#q-29-branch-picker-location) — _architecture_ - [Q-30: Focus behavior when editor is dirty and viewer is peeked](questions/architecture.md#q-30-focus-behavior-when-editor-is-dirty-and-viewer-is-peeked) — _architecture_ - [Q-31: XWayland fallback for frameless — proper Wayland protocol needed](questions/architecture.md#q-31-xwayland-fallback-for-frameless--proper-wayland-protocol-needed) — _architecture_ +- [Q-34: How + when to surface the account/team token budget given upstream doesn't expose it](questions/architecture.md#q-34-how--when-to-surface-the-accountteam-token-budget-given-upstream-doesnt-expose-it) — _architecture_ ## Resolved questions diff --git a/governance/questions/architecture.md b/governance/questions/architecture.md index 3fd233ef..a0466b3c 100644 --- a/governance/questions/architecture.md +++ b/governance/questions/architecture.md @@ -146,4 +146,10 @@ ticket persistence. - **Context:** Transport choice affects discovery and lifecycle. SSE/WS need a port and a published URL, which collides with the `XDG_RUNTIME_DIR` Unix-socket model used for the CLI; we'd likely publish the URL alongside the socket path (env var or `XDG_RUNTIME_DIR` discovery file). stdio is process-per-client and works for clients that prefer process-spawn over network. Decision interacts with [Q-32](#q-32-mcp-tool-surface--minimum-slash-ide-or-extended-clide-tools) — if the surface stays at the `/ide` minimum, SSE alone is sufficient. - **Source:** [D-68](../decisions/architecture.md#d-68-dual-integration-surface--bash-cli-primary-mcp-secondary). +### Q-34: How + when to surface the account/team token budget given upstream doesn't expose it +- **Status:** Open +- **Question:** The team-meta sidebar should show the account/subscription usage budget (5-hour + weekly limits, % used, reset times), but Claude Code does not expose this data programmatically under subscription (OAuth) auth (verified 2026-05-23, claude 2.1.150): `/usage` is TUI-only, `stats-cache.json` holds activity counts only, the stream-json `rate_limit_event` is undocumented and needs a billed turn, and `claude auth status --json` shows plan only. So: what path do we take, and when do we act? Options — (a) wait for upstream `claude usage --json` / a `/v1/organizations/{org}/usage/subscription` endpoint (open feature request anthropics/claude-code#44328); (b) an API-key usage path for key-auth users only; (c) scrape the undocumented `rate_limit_event` on a billed turn (fragile, version-pinned); (d) ship a partial budget (activity counts) and defer true limits. +- **Context:** The only unshipped piece of the otherwise-complete native-Claude epic (T-132). Blocked on data availability, not on clide work — hence a question (when/how to revisit) rather than active scope. Option (c) interacts with D-75's "version-pinned coupling to CC internals" posture. Resolved by T-158 when a viable path lands. +- **Source:** 2026-06-09 — split out of T-132 / T-158 (was "blocked on upstream"); project memory `claude-usage-budget-not-exposed`, GitHub anthropics/claude-code#44328. + --- diff --git a/lib/builtin/claude/src/activity_cluster.dart b/lib/builtin/claude/src/activity_cluster.dart index 12df048c..0b07ca9e 100644 --- a/lib/builtin/claude/src/activity_cluster.dart +++ b/lib/builtin/claude/src/activity_cluster.dart @@ -30,6 +30,18 @@ enum FoldLevel { everything, } +/// Settings key for the persisted activity-card fold level (T-235). App-scoped: +/// a personal viewing preference, not per-repo. +const String kActivityFoldLevelKey = 'app.claude.activityFoldLevel'; + +/// Parse a stored fold-level name back to a [FoldLevel], defaulting to L1 +/// ([FoldLevel.tools]) for null/unknown values. +FoldLevel foldLevelFromName(String? name) => FoldLevel.values.firstWhere((l) => l.name == name, orElse: () => FoldLevel.tools); + +/// The next fold level in the cycle none → tools → thinking → everything → none +/// (T-235), for the toggle command. +FoldLevel nextFoldLevel(FoldLevel level) => FoldLevel.values[(level.index + 1) % FoldLevel.values.length]; + /// A unit the conversation view renders: either a single first-class item or /// a folded run of meta items. sealed class RenderGroup { diff --git a/lib/builtin/claude/src/claude_pane.dart b/lib/builtin/claude/src/claude_pane.dart index 3a2d1cba..7b57911a 100644 --- a/lib/builtin/claude/src/claude_pane.dart +++ b/lib/builtin/claude/src/claude_pane.dart @@ -10,6 +10,7 @@ import 'claude_composer.dart'; import 'claude_config.dart'; import 'claude_status.dart'; import 'clipboard_paste.dart'; +import 'activity_cluster.dart' show foldLevelFromName, kActivityFoldLevelKey; import 'conversation_controller.dart'; import 'conversation_view.dart'; import 'permission_mode_control.dart'; @@ -152,12 +153,20 @@ class _ClaudePaneState extends State { // GlobalKey and spawns once, so without this it would keep the previous // repo's session after a switch (T-269). _projectSub = ClideKernel.of(context).events.on().listen(_onProjectChanged); + // Re-fold the conversation when the activity fold-level setting changes + // (claude.activity.fold-level command, T-235). + ClideKernel.of(context).settings.addListener(_onSettingsChanged); } } + void _onSettingsChanged() { + if (mounted) setState(() {}); + } + @override void dispose() { activeClaudeConfig?.removeListener(_onConfigChanged); + _kernel()?.settings.removeListener(_onSettingsChanged); _projectSub?.cancel(); _projectSub = null; _statusSub?.cancel(); @@ -493,6 +502,7 @@ class _ClaudePaneState extends State { onTap: _focusComposerOnTap, child: ConversationView( controller: _conversation!, + foldLevel: foldLevelFromName(_kernel()?.settings.get(kActivityFoldLevelKey)), hiddenToolUseIds: _session?.promptedToolUseIds ?? const {}, toolUseOutcomes: _session?.toolUseOutcomes ?? const {}, emptyState: ClaudeBanner( diff --git a/lib/builtin/claude/src/extension.dart b/lib/builtin/claude/src/extension.dart index c1d8cd0d..9ff900ed 100644 --- a/lib/builtin/claude/src/extension.dart +++ b/lib/builtin/claude/src/extension.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:clide/clide.dart'; +import 'package:clide/builtin/claude/src/activity_cluster.dart' show foldLevelFromName, kActivityFoldLevelKey, nextFoldLevel; import 'package:clide/builtin/claude/src/claude_config.dart'; import 'package:clide/builtin/claude/src/claude_status.dart' show nextSafePermissionMode; import 'package:clide/builtin/claude/src/claude_session_host.dart'; @@ -76,6 +77,15 @@ class ClaudeExtension extends ClideExtension { title: 'Claude: session storage (disk usage + cleanup)', run: _manageStorage, ), + // T-235: cycle how aggressively the activity card folds meta steps + // (none → tools → thinking → everything), persisted app-wide. The panes + // read kActivityFoldLevelKey and rebuild via the settings notifier. + CommandContribution( + id: 'claude.activity.fold-level', + command: 'claude.activity.fold-level', + title: 'Claude: cycle activity fold level', + run: _cycleFoldLevel, + ), // T-171: agent roster controls (D-6 CLI/UI parity). // Usage: clide claude.agent.show CommandContribution( @@ -403,6 +413,18 @@ class ClaudeExtension extends ClideExtension { /// this when they want a hard reset — after a Claude wedge or to start /// completely fresh. All sessions are torn down through the orchestrator /// (D-77); the primary will re-spawn and resume on the next pane build. + /// Cycle the persisted activity fold level (T-235). Reads the current value + /// from settings, advances none → tools → thinking → everything → none, and + /// writes it back; the panes listen to settings and re-fold live. + Future _cycleFoldLevel(List args) async { + final ctx = _ctx; + if (ctx == null) return IpcResponse.ok(id: '', data: const {}); + final current = foldLevelFromName(ctx.settings.get(kActivityFoldLevelKey)); + final next = nextFoldLevel(current); + await ctx.settings.set(kActivityFoldLevelKey, next.name); + return IpcResponse.ok(id: '', data: {'foldLevel': next.name}); + } + Future _killAllSessions(List args) async { final orch = _orchestrator; if (orch == null) return IpcResponse.ok(id: '', data: const {}); diff --git a/lib/builtin/claude/src/team_panel_host.dart b/lib/builtin/claude/src/team_panel_host.dart index 1db1f4dc..c8111f47 100644 --- a/lib/builtin/claude/src/team_panel_host.dart +++ b/lib/builtin/claude/src/team_panel_host.dart @@ -11,6 +11,7 @@ library; import 'dart:async'; import 'package:clide/builtin/claude/src/conversation_controller.dart'; +import 'package:clide/builtin/claude/src/activity_cluster.dart' show foldLevelFromName, kActivityFoldLevelKey; import 'package:clide/builtin/claude/src/conversation_view.dart'; import 'package:clide/builtin/claude/src/transcript_publisher.dart'; import 'package:clide/kernel/kernel.dart'; @@ -202,7 +203,17 @@ class _TeammateTile extends StatelessWidget { ], ), ), - Expanded(child: ConversationView(controller: controller, wrapInSelectionArea: false)), + Expanded( + // Re-fold live with the activity fold-level setting (T-235). + child: ListenableBuilder( + listenable: ClideKernel.of(context).settings, + builder: (ctx, _) => ConversationView( + controller: controller, + wrapInSelectionArea: false, + foldLevel: foldLevelFromName(ClideKernel.of(ctx).settings.get(kActivityFoldLevelKey)), + ), + ), + ), ], ), ); diff --git a/test/builtin/claude/activity_cluster_test.dart b/test/builtin/claude/activity_cluster_test.dart index f6f620e1..a47a04ee 100644 --- a/test/builtin/claude/activity_cluster_test.dart +++ b/test/builtin/claude/activity_cluster_test.dart @@ -90,6 +90,24 @@ void main() { }); }); + group('fold-level persistence helpers (T-235)', () { + test('foldLevelFromName parses known names, defaults to L1 (tools)', () { + expect(foldLevelFromName('none'), FoldLevel.none); + expect(foldLevelFromName('tools'), FoldLevel.tools); + expect(foldLevelFromName('thinking'), FoldLevel.thinking); + expect(foldLevelFromName('everything'), FoldLevel.everything); + expect(foldLevelFromName(null), FoldLevel.tools); + expect(foldLevelFromName('bogus'), FoldLevel.tools); + }); + + test('nextFoldLevel cycles none → tools → thinking → everything → none', () { + expect(nextFoldLevel(FoldLevel.none), FoldLevel.tools); + expect(nextFoldLevel(FoldLevel.tools), FoldLevel.thinking); + expect(nextFoldLevel(FoldLevel.thinking), FoldLevel.everything); + expect(nextFoldLevel(FoldLevel.everything), FoldLevel.none); + }); + }); + group('editFilePath', () { test('reads the file of an edit tool-use; null otherwise', () { expect(editFilePath(_edit('1', '/a/b.dart')), '/a/b.dart');