fix bottom input box alignment when status bar hidden (T-298)

The composer (and any bottom-most pane content) ran flush into the
window's bottom resize-drag strip when the status bar was hidden, since
the bar normally covers that edge. Reserve ClideResizeBorder.edgeThickness
as a bottom inset in RootLayout whenever the status bar is not visible, so
the interaction zone bottom-anchors consistently either way.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:36:13 +02:00
co-authored by Claude Opus 4.8
parent cbeb2c2d5a
commit 76346122aa
6 changed files with 113 additions and 2 deletions
+18
View File
@@ -2270,3 +2270,21 @@ Fix (both, per user direction):
Verification: standalone reproducer with both the native chooser AND the filter size warnings gone, an unrelated GLib-GIO CRITICAL still passes through (filter is scoped, not a blanket mute). `make build-linux` green. CHANGELOG updated under Fixed.', NULL, '2026-06-09 21:21:04', '2026-06-09 21:21:04', '2026-06-09 21:21:04', NULL, '872947bf48bd74c3fb6942ec90149ac5', 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-287', 'status', 'in_progress', 'done', NULL, '2026-06-09 21:21:08', '2026-06-09 21:21:08', '2026-06-09 21:21:08', NULL, '931bae221c6780445f00b547da62c8cb', 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-298', 'status', 'backlog', 'ready', NULL, '2026-06-09 21:21:30', '2026-06-09 21:21:30', '2026-06-09 21:21:30', NULL, '9aeac5d0a8d6dfeb6adb96ed63dda4eb', 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-298', 'status', 'ready', 'in_progress', NULL, '2026-06-09 21:22:26', '2026-06-09 21:22:26', '2026-06-09 21:22:26', NULL, 'b14a5b998bd76cda5bb558fc0faa4be9', 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-304', 'parent_id', NULL, 'T-276', NULL, '2026-06-09 21:22:31', '2026-06-09 21:22:31', '2026-06-09 21:22:31', NULL, '0b04f300702a4b9a46d5b591f15e82af', 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-304', 'description', NULL, 'The logo-mark spinner shown on in-progress activity/holder cards in the Claude conversation is too small to read as a spinner — it reads as a static speck. Enlarge it so the running state is legible at a glance.
**Where**
- `ClideSpinner` (lib/widgets/src/clide_spinner.dart) defaults to size 14; renders the logo SVG at width/height = size.
- `ClideStatusIndicator` (lib/widgets/src/clide_status_indicator.dart) default size 14; maps runningClideSpinner, successcheck, errorcross at the same size.
- Call sites: holder_card.dart:117 and :199 pass `size: 12` the small value the user is seeing.
**Direction (settle in review)**
- Bump the spinner size on the activity cards (the `size: 12` call sites, and/or the indicator default) to something clearly legible pull a concrete value from the ui-design control-geometry tokens rather than a magic number.
- Keep the running spinner, success check, and error cross visually balanced at the new size (they share `size`), so the card doesn''t jump when the state settles.
- Check the other ClideSpinner/StatusIndicator consumers (status surfaces) so the bump doesn''t bloat unrelated spots may warrant sizing the cards explicitly rather than changing the shared default.
**Acceptance**
- The in-progress spinner on conversation activity cards is comfortably distinguishable as a spinning indicator; success/error glyphs stay aligned at the same footprint.', NULL, '2026-06-09 21:22:44', '2026-06-09 21:22:44', '2026-06-09 21:22:44', NULL, '1aec6212db45140ee906592f9ce125ef', 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-280', 'status', 'backlog', 'ready', NULL, '2026-06-09 21:32:29', '2026-06-09 21:32:29', '2026-06-09 21:32:29', NULL, '0fcac3a23b0303186fefb36827374616', 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-298', 'status', 'in_progress', 'done', NULL, '2026-06-09 21:36:05', '2026-06-09 21:36:05', '2026-06-09 21:36:05', NULL, 'ecf870d28771c1368654195d93ec4dec', 1) ON CONFLICT(hash) DO NOTHING;
+46
View File
@@ -3737,3 +3737,49 @@ Expected: the input box sits correctly flush to the window bottom (or with consi
Notes:
- Audit the bottom-of-window layout stack (composer / interaction zone + status bar). The composer should bottom-anchor independently of the status bar''s visibility.
- Related to the bottom interaction zone work (D-78) and may interact with T-297 (scroll on input resize). Also relevant once the status-bar collapse toggles (T-294) can change what''s in the bar.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-09 16:06:41', '2026-06-09 21:21:30', NULL, 'cc9ab1dee5d58863b3277c896e3e4461', 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-298', 'bug', 'T-276', 'Bottom input box is misaligned when the status bar is hidden', 'The bottom input/prompt box (the composer, shown with the edit-pencil affordance on the right) is aligned weird when the status bar is not visible.
Symptom: with the status bar hidden, the input box''s alignment/padding looks off (see screenshot single-line box spanning full width, content not sitting where it should relative to the window bottom).
Likely cause: the composer''s vertical alignment / bottom padding assumes the status bar is present below it and reserves/relies on that space. When the status bar is hidden, that assumption breaks and the box no longer sits flush/centered correctly.
Expected: the input box sits correctly flush to the window bottom (or with consistent padding) regardless of whether the status bar is visible. Layout should not depend on the status bar being present.
Notes:
- Audit the bottom-of-window layout stack (composer / interaction zone + status bar). The composer should bottom-anchor independently of the status bar''s visibility.
- Related to the bottom interaction zone work (D-78) and may interact with T-297 (scroll on input resize). Also relevant once the status-bar collapse toggles (T-294) can change what''s in the bar.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-09 16:06:41', '2026-06-09 21:22:26', NULL, 'c5773b9623efafd5e269a1618f243f66', 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-304', 'bug', NULL, 'Activity-card spinner too small to make out — enlarge it', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-09 21:22:28', '2026-06-09 21:22:28', NULL, '95e2fdd86dbe98e3f911637bcb844072', 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-304', 'bug', 'T-276', 'Activity-card spinner too small to make out — enlarge it', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-09 21:22:28', '2026-06-09 21:22:31', NULL, '20c85ebff9e0a45f7e27ee099b7a2563', 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-304', 'bug', 'T-276', 'Activity-card spinner too small to make out — enlarge it', 'The logo-mark spinner shown on in-progress activity/holder cards in the Claude conversation is too small to read as a spinner — it reads as a static speck. Enlarge it so the running state is legible at a glance.
**Where**
- `ClideSpinner` (lib/widgets/src/clide_spinner.dart) defaults to size 14; renders the logo SVG at width/height = size.
- `ClideStatusIndicator` (lib/widgets/src/clide_status_indicator.dart) default size 14; maps runningClideSpinner, successcheck, errorcross at the same size.
- Call sites: holder_card.dart:117 and :199 pass `size: 12` the small value the user is seeing.
**Direction (settle in review)**
- Bump the spinner size on the activity cards (the `size: 12` call sites, and/or the indicator default) to something clearly legible pull a concrete value from the ui-design control-geometry tokens rather than a magic number.
- Keep the running spinner, success check, and error cross visually balanced at the new size (they share `size`), so the card doesn''t jump when the state settles.
- Check the other ClideSpinner/StatusIndicator consumers (status surfaces) so the bump doesn''t bloat unrelated spots may warrant sizing the cards explicitly rather than changing the shared default.
**Acceptance**
- The in-progress spinner on conversation activity cards is comfortably distinguishable as a spinning indicator; success/error glyphs stay aligned at the same footprint.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-09 21:22:28', '2026-06-09 21:22:44', NULL, '4d335acecf697af397d391dab086bd5d', 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-280', 'bug', NULL, 'app_test ''Open Folder non-repo'' hangs 10min — runAsync/ReceivePort teardown wedge', 'Pre-existing (reproduces at base commit 3a78dfa, predates the T-267 conversation-rendering epic). The widget test `test/app_test.dart` "Open Folder on a non-repo path surfaces the ''no git repo'' dialog" times out after 10 minutes; teardown is wedged on `_RawReceivePort._handleMessage`.
Bisected (each a 45s-timeout repro, all on this box):
- A bare `Process.run` inside `tester.runAsync` (no app, no extensions) hangs `Process.run`-in-`runAsync` leaks its exit ReceivePort here.
- `pumpApp` + empty `runAsync`, and `pumpApp` + a 1.5s real delay both PASS (boot + runAsync alone is fine).
- The full openFolder tap flow hangs even when project validation is stubbed to a synchronous, pure-Dart `.git` walk (no subprocess) AND `runAsync` is removed so the wedge is not solely the git subprocess; something in the booted-app + extensions + open-folder command path holds a native port that teardown waits on forever.
Quarantined with `skip:` so the suite/gate stays green. Real fix: find the leaked native async resource (likely a Process/Isolate/FakeDaemonClient port reachable from the open-folder command or app boot under the test harness) and ensure it''s drained/cancelled before teardown or drive the "no git repo" assertion without booting the resource. Then remove the skip.', 'ready', 'high', NULL, NULL, NULL, '2026-06-08 11:29:22', '2026-06-09 21:32:29', NULL, '9a47d2033458934d77d1110e1ac98cfe', 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-298', 'bug', 'T-276', 'Bottom input box is misaligned when the status bar is hidden', 'The bottom input/prompt box (the composer, shown with the edit-pencil affordance on the right) is aligned weird when the status bar is not visible.
Symptom: with the status bar hidden, the input box''s alignment/padding looks off (see screenshot single-line box spanning full width, content not sitting where it should relative to the window bottom).
Likely cause: the composer''s vertical alignment / bottom padding assumes the status bar is present below it and reserves/relies on that space. When the status bar is hidden, that assumption breaks and the box no longer sits flush/centered correctly.
Expected: the input box sits correctly flush to the window bottom (or with consistent padding) regardless of whether the status bar is visible. Layout should not depend on the status bar being present.
Notes:
- Audit the bottom-of-window layout stack (composer / interaction zone + status bar). The composer should bottom-anchor independently of the status bar''s visibility.
- Related to the bottom interaction zone work (D-78) and may interact with T-297 (scroll on input resize). Also relevant once the status-bar collapse toggles (T-294) can change what''s in the bar.', 'done', 'medium', NULL, NULL, NULL, '2026-06-09 16:06:41', '2026-06-09 21:36:05', NULL, '162be954ae0cc51fa9db0c36b0173cbe', 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);
+5
View File
@@ -247,6 +247,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
### Fixed
- **The composer no longer jams against the window bottom when the status bar
is hidden.** With the bar gone, the bottom-most pane content used to run flush
into the window's resize-drag edge; the layout now reserves that edge so the
input box bottom-anchors consistently whether or not the status bar shows.
(T-298)
- **Open Workspace no longer spews `GLib-GIO-CRITICAL` to the console.** The
folder picker now uses the portal-backed `GtkFileChooserNative` (out-of-process
in sandboxed/Flatpak builds), and a narrowly-scoped GLib log filter swallows the
+12 -1
View File
@@ -234,7 +234,7 @@ class RootLayout extends StatelessWidget {
final dockMax = (((MediaQuery.of(ctx).size.height) - statusHeight) * 0.5).clamp(80.0, double.infinity).toDouble();
final dockHeight = dockVisible ? ((a.sizeOf(Slots.dock) ?? 200).clamp(0.0, dockMax)).toDouble() : 0.0;
return Column(
final column = Column(
children: [
Expanded(
child: Row(
@@ -310,6 +310,17 @@ class RootLayout extends StatelessWidget {
),
],
);
// When the status bar is hidden it no longer occupies the window's
// bottom edge, so the bottom-most content (the Claude composer, an
// editor, a terminal) would otherwise run flush into the resize-drag
// strip and look jammed against the window bottom (T-298). Reserve a
// matching inset so the interaction zone bottom-anchors consistently,
// independent of status-bar visibility.
if (statusVisible) return column;
return Padding(
padding: const EdgeInsets.only(bottom: ClideResizeBorder.edgeThickness),
child: column,
);
},
);
}
+6 -1
View File
@@ -7,7 +7,12 @@ class ClideResizeBorder extends StatelessWidget {
final WindowControls windowControls;
final Widget child;
static const double _edge = 6;
/// Thickness of the window-edge resize-drag strips. Exposed so layouts can
/// reserve a matching inset for bottom-most content that would otherwise sit
/// under the bottom drag zone when no chrome (e.g. the status bar) covers it
/// (T-298).
static const double edgeThickness = 6;
static const double _edge = edgeThickness;
static const double _corner = 12;
@override
+26
View File
@@ -113,6 +113,32 @@ void main() {
expect(find.byType(ClideIconRail), findsNWidgets(2));
});
testWidgets('RootLayout reserves the resize-border inset for bottom content when the status bar is hidden (T-298)', (tester) async {
registerTabs();
// Status bar visible: the bar covers the window's bottom edge, so no extra
// inset is reserved — the content column is not bottom-padded.
await pumpLayout(tester);
expect(find.byType(StatusbarHost), findsOneWidget);
expect(
find.byWidgetPredicate((w) => w is Padding && w.padding == const EdgeInsets.only(bottom: ClideResizeBorder.edgeThickness)),
findsNothing,
reason: 'no resize inset is needed while the status bar occupies the bottom edge',
);
// Hide the status bar: nothing covers the bottom resize-drag strip, so the
// bottom-most content (the composer) must clear it via a reserved inset.
f.services.arrangement.setVisible(Slots.statusbar, false);
await tester.pump();
expect(find.byType(StatusbarHost), findsNothing);
expect(
find.byWidgetPredicate((w) => w is Padding && w.padding == const EdgeInsets.only(bottom: ClideResizeBorder.edgeThickness)),
findsOneWidget,
reason: 'the interaction zone must bottom-anchor clear of the resize border when no status bar covers it',
);
expect(tester.takeException(), isNull);
});
testWidgets('RootLayout collapses side panels into spines with the active-tab label', (tester) async {
registerTabs();
f.services.panels.activateTab(Slots.sidebar, 'files.tree');