give the status-bar context slot a flexible share
StatusbarHost laid every item at intrinsic width, so once the focused-pane context line grew long (a model/mode/context/skills summary) the row's content exceeded the bar width and overflowed instead of letting the slot shrink. Add an opt-in flex factor to StatusItemContribution; the host wraps flex>0 items in Flexible(loose) so they yield width when the bar is tight, and the marquee then receives a bounded viewport and scrolls. Drops the fixed maxWidth cap on the pane-context item. T-160. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2275,3 +2275,6 @@ Fix direction: give the in-pane context slot a flexible/bounded width in the sta
|
||||
|
||||
---
|
||||
Refinement (2026-05-30, /whats-next): pulled into the T-167/T-168 batch and sequenced FIRST. T-168 adds live cost / permission-mode / context / rate-limit to this same in-pane status slot; the Flexible/marquee fix here must land before T-168 lengthens the line. After this fix, T-168 can safely add fields to the slot.', NULL, '2026-05-30 10:44:10', '2026-05-30 10:44:10', '2026-05-30 10:44:10', NULL, '4ad98d4a7db42e295fba6dff0988ad3b', 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-167', 'status', 'backlog', 'in_progress', NULL, '2026-05-30 10:48:32', '2026-05-30 10:48:32', '2026-05-30 10:48:32', NULL, '229c9cb92eb8554dbba51ca65e1eeba9', 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-168', 'status', 'backlog', 'in_progress', NULL, '2026-05-30 10:48:32', '2026-05-30 10:48:32', '2026-05-30 10:48:32', NULL, '4469254d98eba45964ca51d5855e5014', 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-160', 'status', 'backlog', 'in_progress', NULL, '2026-05-30 10:48:32', '2026-05-30 10:48:32', '2026-05-30 10:48:32', NULL, 'e410b6f78d0e4d96bc37f3a351d72767', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -2789,3 +2789,30 @@ Fix direction: give the in-pane context slot a flexible/bounded width in the sta
|
||||
|
||||
---
|
||||
Refinement (2026-05-30, /whats-next): pulled into the T-167/T-168 batch and sequenced FIRST. T-168 adds live cost / permission-mode / context / rate-limit to this same in-pane status slot; the Flexible/marquee fix here must land before T-168 lengthens the line. After this fix, T-168 can safely add fields to the slot.', 'backlog', 'medium', NULL, NULL, NULL, '2026-05-24 09:13:42', '2026-05-30 10:44:10', NULL, '5ed66d193e73c29141a8525a44a242f7', 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-168', 'task', 'T-164', 'P1: native rendering leveled up — typed event cards + streaming + live status', 'Exploit the structured stream the transcript-tail never cleanly gave. Per-tool cards from tool_use input (Edit/Write → diff, Bash → command+output, Read/Grep → file/query); pair each tool_result to its tool_use (with is_error) so diffs/failures render in place; stream assistant text + thinking blocks token-by-token via --include-partial-messages; surface live model / permission-mode / context-tokens / cost and rate_limit_event straight off the init + result events (retires the ClaudeConfig probe + status-bar guessing for the active pane); render structured errors as events. Acceptance: a session with edits/bash/reads shows the right card per tool with paired results; replies stream; the status line reflects live model/mode/context/cost from events; widget + fixture tests. Blocked by T-165.
|
||||
|
||||
---
|
||||
Refinement (2026-05-30, /whats-next) — five seam decisions resolved:
|
||||
|
||||
1. tool_use/result pairing: add a Map<String, AssistantToolUse> toolUseById index to ConversationController. The result card looks up its tool_use and renders the Edit/Write diff (reuse prompt_card''s _editBody style) or the is_error failure IN PLACE in the result card. Files: conversation_controller.dart (index), conversation_view.dart (_toolUse / _toolResult bodies — they currently render indented JSON only and can''t see each other).
|
||||
|
||||
2. Per-tool card bodies: Edit/Write → diff; Bash → command+output; Read/Grep → file/query. Selected by tool name off the paired tool_use input.
|
||||
|
||||
3. Partial-message streaming: pass --include-partial-messages in ClaudeStreamJsonProcess.start(); accumulate partials keyed by message.id in StreamJsonSession._onLine BEFORE emitting to the controller, so one item updates in place (no duplicate AssistantTextMessage items). Thinking blocks stream the same way. Do NOT add a new streaming item type in the controller.
|
||||
|
||||
4. Live cost/context (was an open gap, defaulted): extend SessionStatus (transcript_reader.dart) with cost + contextWindow fields; extract them from result events in stream_json_session.dart::_statusFromEvent (currently only handles system/init) — total_cost_usd + modelUsage.<model>.contextWindow per the spike doc docs/spikes/cc-stream-json-control-protocol-2.1.150.md.
|
||||
|
||||
5. rate_limit_event + the focus status line: surface live model/mode/context/cost/rate-limit in the in-pane status slot. This DEPENDS ON T-160 (status-bar overflow / marquee) which is pulled into this batch and sequenced FIRST — do not lengthen the status line until T-160''s Flexible/marquee fix lands. ClaudeConfig.ensureProbe() is retired only for the ACTIVE-pane status; the probe survives for slash-command typeahead.
|
||||
|
||||
Files an implementer touches: conversation_view.dart, conversation_controller.dart, transcript_reader.dart, stream_json_session.dart, claude_pane.dart, plus test/builtin/claude/*. Commit the in-flight prompt_card.dart change first (disjoint surface — permission-card Write-description dedup).
|
||||
|
||||
Sequencing within the batch: T-160 → T-168.', 'in_progress', 'medium', NULL, NULL, 'D-77', '2026-05-24 16:26:39', '2026-05-30 10:48:32', NULL, '24a7adcd7c1e8cb3f9c4e042c43aa9b2', 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-167', 'task', 'T-164', 'P1: session persistence + /clear + /resume on stream-json (--resume)', 'Move session lifecycle onto the stream-json model. New session: --session-id <uuid>; resume an existing one: --resume <id> (T-161 confirmed --session-id refuses an existing id). Re-implement the clide-owned commands on this model: /clear = new fresh session, /resume = picker then resume the picked id. Retire the tmux session lifecycle for Claude (kill/hasSession/reap, new-session -A) and the --session-id-in-use handling once the transport no longer uses tmux. Acceptance: restart resumes the primary''s session; /clear starts empty; /resume switches to a picked session; all without tmux; tests for the new/resume argv selection and the command flows. Supersedes the mechanics of T-156/T-161. Blocked by T-165. Amends D-41 (tmux persistence → --resume).', 'in_progress', 'high', NULL, NULL, 'D-77', '2026-05-24 16:26:30', '2026-05-30 10:48:32', NULL, '4c9095cfb7c8caa57c4b720582a73ef7', 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-160', 'bug', 'T-132', 'Status bar overflows instead of marquee-scrolling the in-pane slot', 'Observed via make run (2026-05-24): the bottom status bar shows a RenderFlex ''overflowed by 77 PIXELS'' stripe after the in-pane context line (opus 4.7 · default · 21k ctx · 10 skills); the ClideMarquee never engages.
|
||||
|
||||
Cause: StatusbarHost (lib/app.dart ~1143) lays items in a Row [left items..., Spacer(), right items...], each at INTRINSIC width with no Flexible. PaneContextStatusItem wraps its text in ConstrainedBox(maxWidth:360) + ClideMarquee, but because the text (~280px) is under 360 the slot takes content width, so the marquee''s viewport == its content and it never scrolls. T-154 appended ''· N skills'', lengthening the line enough that git-branch + context slot + right items exceed the bar width -> the Row overflows (~77px) instead of the slot shrinking + marquee scrolling.
|
||||
|
||||
Fix direction: give the in-pane context slot a flexible/bounded width in the status bar (e.g. wrap it in Flexible(fit: loose), or have the bar allot it a shrinkable share) so it yields width when the bar is tight and ClideMarquee scrolls within the allotted width. Verify the interaction with the Spacer and the right-aligned items (priority>=100), and reconsider the fixed maxWidth:360. Acceptance: at narrow widths the in-pane slot marquee-scrolls and the status bar never shows an overflow stripe; widget test at a constrained width. Introduced by T-150 (marquee) + T-154 (longer line).
|
||||
|
||||
---
|
||||
Refinement (2026-05-30, /whats-next): pulled into the T-167/T-168 batch and sequenced FIRST. T-168 adds live cost / permission-mode / context / rate-limit to this same in-pane status slot; the Flexible/marquee fix here must land before T-168 lengthens the line. After this fix, T-168 can safely add fields to the slot.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-05-24 09:13:42', '2026-05-30 10:48:32', NULL, 'be801b09fd2aeb3a4bb71b782499f9a7', 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);
|
||||
|
||||
@@ -18,6 +18,9 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Status bar no longer overflows when the focused-pane context line is long
|
||||
— the in-pane slot now takes a flexible share of the bar and marquee-scrolls
|
||||
within it instead of pushing the row past its width.
|
||||
- Write/Edit permission cards no longer print the file path twice — the
|
||||
description line is suppressed when it just repeats `file_path`.
|
||||
- Resumed Claude session no longer starts with an empty pane — `claude
|
||||
|
||||
+7
-1
@@ -1140,10 +1140,16 @@ class StatusbarHost extends StatelessWidget {
|
||||
color: tokens.chromeBackground,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
alignment: Alignment.center,
|
||||
// Left items with flex > 0 are wrapped in Flexible(loose) so they
|
||||
// yield width when the bar is tight; their contained ClideMarquee
|
||||
// then scrolls within the allotted bounds (T-160). Items with
|
||||
// flex == 0 (default) stay at intrinsic width. Right-side items
|
||||
// (priority >= 100) are always intrinsic-width.
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
for (final item in left) item.build(ctx),
|
||||
for (final item in left)
|
||||
if (item.flex > 0) Flexible(flex: item.flex, fit: FlexFit.loose, child: item.build(ctx)) else item.build(ctx),
|
||||
const Spacer(),
|
||||
for (final item in right) item.build(ctx),
|
||||
],
|
||||
|
||||
@@ -11,9 +11,6 @@ import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Max width the slot occupies in the status bar before marquee kicks in.
|
||||
const double _slotMaxWidth = 360;
|
||||
|
||||
/// Fixed slot height — panes can render anything, but not blow up the bar.
|
||||
const double _slotHeight = 16;
|
||||
|
||||
@@ -28,14 +25,15 @@ class PaneContextStatusItem extends StatelessWidget {
|
||||
builder: (ctx, _) {
|
||||
final widget = focus.activeStatusWidget;
|
||||
if (widget == null) return const SizedBox.shrink();
|
||||
// The parent StatusbarHost wraps this item in Flexible(loose) so the
|
||||
// Row hands us a bounded maxWidth (T-160). ClideMarquee receives that
|
||||
// constraint via LayoutBuilder and scrolls when content exceeds it —
|
||||
// no ConstrainedBox(maxWidth) cap needed here.
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: SizedBox(
|
||||
height: _slotHeight,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: _slotMaxWidth),
|
||||
child: ClideMarquee(child: widget),
|
||||
),
|
||||
child: ClideMarquee(child: widget),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -61,6 +61,7 @@ class StatusItemContribution extends ContributionPoint {
|
||||
required this.build,
|
||||
this.priority = 0,
|
||||
this.listenable,
|
||||
this.flex = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -68,6 +69,13 @@ class StatusItemContribution extends ContributionPoint {
|
||||
final WidgetBuilder build;
|
||||
final int priority;
|
||||
final Listenable? listenable;
|
||||
|
||||
/// When > 0, the status bar wraps this item in
|
||||
/// `Flexible(flex: flex, fit: FlexFit.loose)` so it yields width when the
|
||||
/// bar is tight and any contained [ClideMarquee] can scroll (T-160).
|
||||
/// Defaults to 0 (intrinsic/non-flex). Only meaningful for left-side items
|
||||
/// (priority < 100); right-side items are always intrinsic-width.
|
||||
final int flex;
|
||||
}
|
||||
|
||||
/// A button in the main toolbar.
|
||||
|
||||
@@ -1,16 +1,54 @@
|
||||
/// Tests for the focus-driven status-bar slot item (T-150): it renders
|
||||
/// the focused pane's status widget and clears when focus moves to a
|
||||
/// pane with no status.
|
||||
/// Tests for the focus-driven status-bar slot item (T-150 / T-160): it
|
||||
/// renders the focused pane's status widget and clears when focus moves
|
||||
/// to a pane with no status; at a constrained width the slot never
|
||||
/// overflows and ClideMarquee gets a bounded viewport.
|
||||
library;
|
||||
|
||||
import 'package:clide/builtin/claude/src/pane_context_status.dart';
|
||||
import 'package:clide/kernel/src/panels/slot_id.dart';
|
||||
import 'package:clide/kernel/kernel.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';
|
||||
|
||||
/// Wraps [child] in a Row with Flexible(loose), mirroring the layout that
|
||||
/// StatusbarHost applies to left-side flex items (T-160), inside a
|
||||
/// [SizedBox] of the given [width].
|
||||
///
|
||||
/// Deliberately does NOT use the shared [harness] here: that one mounts an
|
||||
/// `Overlay(canSizeOverlay: true)` over a zero-size MediaQuery, which runs an
|
||||
/// intrinsic-sizing pass and hands children unbounded width — so a `Flexible`
|
||||
/// never bounds the marquee and this test could not observe the fix. We mount
|
||||
/// the minimum theme/kernel tree under a tight, [Align]ed [SizedBox] so the
|
||||
/// `Flexible` gets a real bounded width from the surrounding Row.
|
||||
Widget _narrowRow(KernelFixture f, double width, Widget child) => Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ClideKernel(
|
||||
services: f.services,
|
||||
child: ClideTheme(
|
||||
controller: f.services.theme,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: 24,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(flex: 1, fit: FlexFit.loose, child: child),
|
||||
const SizedBox(width: 20), // simulated right-side items
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void main() {
|
||||
late KernelFixture f;
|
||||
setUp(() async => f = await KernelFixture.create());
|
||||
@@ -32,4 +70,53 @@ void main() {
|
||||
await tester.pump();
|
||||
expect(find.text('S'), findsNothing);
|
||||
});
|
||||
|
||||
// T-160: at a narrow bar width the slot must never overflow and
|
||||
// ClideMarquee must receive a bounded (scrollable) viewport.
|
||||
testWidgets('no RenderFlex overflow at narrow width — ClideMarquee is bounded', (tester) async {
|
||||
// A long status line similar to what T-154 added:
|
||||
// "opus 4.7 · default · 21k ctx · 10 skills" — roughly 280 px of text.
|
||||
const longStatus = Text(
|
||||
'opus 4.7 · default · 21k ctx · 10 skills',
|
||||
textDirection: TextDirection.ltr,
|
||||
softWrap: false,
|
||||
);
|
||||
|
||||
// Pump at 200 px wide — narrower than the status content so the marquee
|
||||
// must receive a bounded viewport < content width and start scrolling.
|
||||
const barWidth = 200.0;
|
||||
await tester.pumpWidget(_narrowRow(f, barWidth, const PaneContextStatusItem()));
|
||||
await tester.pump(); // first frame — nothing focused yet
|
||||
|
||||
final focus = f.services.focus;
|
||||
focus.setActive(slot: Slots.workspace, contributionId: 'pane.z');
|
||||
focus.setStatusWidget('pane.z', longStatus);
|
||||
await tester.pump(); // status widget appears
|
||||
await tester.pump(); // ClideMarquee _measure post-frame callback
|
||||
|
||||
// 1. No overflow exception.
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
// 2. The ClideMarquee is in the tree.
|
||||
expect(find.byType(ClideMarquee), findsOneWidget);
|
||||
|
||||
// 3. The ClideMarquee render box is bounded — its width must be less than
|
||||
// barWidth (the slot shrinks to fit, not past the container).
|
||||
final marqueeSize = tester.getSize(find.byType(ClideMarquee));
|
||||
expect(marqueeSize.width, lessThan(barWidth));
|
||||
expect(marqueeSize.width, greaterThan(0));
|
||||
|
||||
// Dispose the ticker by tearing down the widget tree.
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
});
|
||||
|
||||
testWidgets('no overflow when no status is active — slot is shrunk', (tester) async {
|
||||
const barWidth = 200.0;
|
||||
await tester.pumpWidget(_narrowRow(f, barWidth, const PaneContextStatusItem()));
|
||||
await tester.pump();
|
||||
|
||||
// No focused pane → SizedBox.shrink path. No overflow, no marquee.
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.byType(ClideMarquee), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user