From 7c2eae42f1628a2217cc524067f63bf53c2dd133 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Mon, 18 May 2026 13:22:55 +0200 Subject: [PATCH] fix theme picker integration test; one bake for build-time facts (T-116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test was awaiting services.commands.execute('theme.pick') whose Future doesn't complete until the dialog is dismissed — deadlock. Fire-and-forget around pumpAndSettle, then tap Cancel, then await the original future. Also tear the widget tree down before services.dispose() so listening widgets unsubscribe first. Pre-existing layout overflow in the welcome _StatusLine surfaced when running the test at narrower viewports. Switched to a whole- row FittedBox(scaleDown) — uniform shrink on narrow screens, no-op at standard widths. User flagged the hardcoded 'clide 2.0.0-dev' string. Replaced with one generated lib/src/build_info.g.dart (gitignored, regenerated by `make gen-build-info` from pubspec.yaml + git short SHA + UTC clock). The same target re-syncs assets/licenses.yaml self.version in place — no second source. Every make build/run/test depends on it implicitly. Welcome status line now reads `clideVersion`. Stale fontSize literals in welcome_view swept to typography constants; clideFontMeta=13, clideFontDialogTitle=16, clideFontWelcomeBanner=52 added to fill gaps in the scale. Co-Authored-By: Claude --- .claude/skills/git-commit/SKILL.md | 5 +- .gitignore | 5 ++ .pql/changelog/ticket_history/2026-05.sql | 3 + .pql/changelog/tickets/2026-05.sql | 17 +++++ CHANGELOG.md | 13 ++++ Makefile | 41 +++++++---- assets/licenses.yaml | 5 +- ci/test_integration.sh | 6 -- integration_test/theme_picker_test.dart | 25 ++++++- lib/builtin/welcome/src/welcome_view.dart | 87 +++++++++++++---------- lib/clide.dart | 24 ++----- lib/widgets/src/typography.dart | 6 ++ 12 files changed, 156 insertions(+), 81 deletions(-) diff --git a/.claude/skills/git-commit/SKILL.md b/.claude/skills/git-commit/SKILL.md index 1329a255..95cfa6c6 100644 --- a/.claude/skills/git-commit/SKILL.md +++ b/.claude/skills/git-commit/SKILL.md @@ -93,9 +93,10 @@ Cutting a release is its own commit. In a single commit: 1. Move all entries from `## [Unreleased]` under a new heading `## [X.Y.Z] — YYYY-MM-DD`. 2. Leave an empty `## [Unreleased]` section at the top with its subsection skeleton ready. 3. Bump `pubspec.yaml` `version:` to `X.Y.Z` (drop the `-dev` suffix for the tag; re-add it on the next development commit if desired). -4. Commit subject: `release vX.Y.Z`. +4. Run `make gen-build-info` so `assets/licenses.yaml` `self.version:` re-syncs from pubspec (it's auto-rewritten by every build but commit the fresh state). Stage the resulting diff alongside step 3. +5. Commit subject: `release vX.Y.Z`. -`pubspec.yaml` is the single source of truth for the version — the Makefile reads it for ldflag stamping of the sidecar binary, and the Flutter app reads it for build info. Bumping `pubspec.yaml` and the changelog out of sync is the mistake this rule prevents. +`pubspec.yaml` is the single source of truth for the version. Every `make` build/run/test target regenerates `lib/src/build_info.g.dart` (gitignored) and rewrites `assets/licenses.yaml` `self.version:` from it — so the Flutter app sees the current version everywhere without manual sync. Bumping `pubspec.yaml` and the changelog out of sync is the mistake this rule prevents. ## Attribution trailer diff --git a/.gitignore b/.gitignore index 14a45e9d..698aa61d 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,11 @@ tools/ui/.serve.pid # Lives under test/ so the repo root stays uncluttered. /test/.test-output/ +# -- Build-time stamps generated by `make gen-build-info` ----------- +# version (from pubspec.yaml), commit (git short SHA), date (UTC). +# Regenerated on every make build/run/test target. +/lib/src/build_info.g.dart + # -- Test, coverage, profile output ------------------------------------ *.test *.out diff --git a/.pql/changelog/ticket_history/2026-05.sql b/.pql/changelog/ticket_history/2026-05.sql index 31373e74..2525dec0 100644 --- a/.pql/changelog/ticket_history/2026-05.sql +++ b/.pql/changelog/ticket_history/2026-05.sql @@ -1818,3 +1818,6 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, 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-115', 'status', 'in_progress', 'done', NULL, '2026-05-18 08:26:05', '2026-05-18 08:26:05', '2026-05-18 08:26:05', NULL, 'b8978359fc1b83ea0bcfc6cd5d34d5de', 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-115', 'status', 'done', 'done', NULL, '2026-05-18 09:30:59', '2026-05-18 09:30:59', '2026-05-18 09:30:59', NULL, '2039c525646caaf89a735454f7278162', 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-107', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 10:27:25', '2026-05-18 10:27:25', '2026-05-18 10:27:25', NULL, 'e40d2c9ddd9915ad2e32302bd555c99f', 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-107', 'status', 'in_progress', 'done', NULL, '2026-05-18 10:30:30', '2026-05-18 10:30:30', '2026-05-18 10:30:30', NULL, 'd5ecc2578b5b48991d07531eb2166529', 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-116', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 10:35:47', '2026-05-18 10:35:47', '2026-05-18 10:35:47', NULL, '59b70e93c913952d4f13b9ad2320e060', 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-116', 'status', 'in_progress', 'done', NULL, '2026-05-18 11:22:46', '2026-05-18 11:22:46', '2026-05-18 11:22:46', NULL, '16fd285587d7a37090d2a21f4307c8e8', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-05.sql b/.pql/changelog/tickets/2026-05.sql index 791a2cf1..9242b6c5 100644 --- a/.pql/changelog/tickets/2026-05.sql +++ b/.pql/changelog/tickets/2026-05.sql @@ -1969,3 +1969,20 @@ Done when: - No new public surface; everything stays library-private Source: T-107 / consultants.md "Code quality — Findings — [Major]".', 'backlog', 'low', NULL, NULL, NULL, '2026-05-18 10:29:02', '2026-05-18 10:29:02', NULL, '1ac51fb8f92a9e72903c16e31f076955', 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-107', 'task', 'T-97', 'decide lib/src/terminal/ status: formally vendor or clean to project bar', '~7k LOC forked from xterm.dart in an undeclared middle state. Carries commented-out `print()` debugging (`custom_text_edit.dart:244-275`), dangling TODOs (`parser.dart:110-113`, `keytab.dart:91`), a 1137-line `parser.dart`, and the only `// ignore: invalid_use_of_protected_member` in the repo (`terminal_view.dart:363`). Memory says "code under `lib/` is owned, not vendored." + +**Decide one path and execute:** +- **(a)** Formally vendor: relocate to `native/` or document as frozen in a D-record + `licenses.yaml`. Coverage carve-out documented. +- **(b)** Clean to the project bar: remove debug prints, resolve TODOs, justify or remove the protected-member suppression, consider splitting `parser.dart`. + +Source: consultants.md "Code quality — Findings — [Major]".', 'done', 'medium', NULL, NULL, NULL, '2026-05-17 18:47:55', '2026-05-18 10:30:30', NULL, '930012b3c62d18d9fe242c7d5879a347', 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-116', 'bug', 'T-97', 'integration_test/theme_picker_test.dart hangs pumpAndSettle on theme.pick', 'When running `flutter test integration_test/theme_picker_test.dart` against the real boot path, calling `services.commands.execute(''theme.pick'')` followed by `await tester.pumpAndSettle()` does not settle — the test hangs for 60+ seconds and never reaches the `Select theme` / `Cancel` assertions. Likely an animation loop or a never-completing Future in the modal mount path. + +Blocks T-103 from adding integration tests to `make push-check` (the other two integration tests pass after viewport + assertion fixes). + +**Acceptance:** +1. `flutter test integration_test/theme_picker_test.dart` completes in <10s. +2. Both assertions (`Select theme` visible after `theme.pick`; gone after `Cancel`) pass. +3. T-103''s `push-check-full` can include integration tests without the theme-picker hanging. + +Source: surfaced 2026-05-17 during T-103 work.', 'done', 'medium', NULL, NULL, NULL, '2026-05-17 19:14:31', '2026-05-18 11:22:46', NULL, 'ff3b728310bd265a57a60778417d026f', 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 154659b2..fb85f9a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. softer ink-tinted shadow (T-114). - Text-zoom (Ctrl +/-/0) is now a kernel `TextZoom` service and shows up in the palette as `View: Zoom In/Out/Reset Zoom` (T-114). +- `make gen-build-info` bakes `lib/src/build_info.g.dart` (version, + commit, date) and rewrites `assets/licenses.yaml` `self.version:` + from `pubspec.yaml` on every build/run/test target — one source of + truth, no manual sync, no `--dart-define` plumbing. - Panel splitters (sidebar / context / editor-split) are tab-focusable; arrow keys nudge by 10 px, Shift+arrow by 50 px (2% / 10% for the editor split). Exposed as slider Semantics nodes so screen readers @@ -182,6 +186,15 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Fixed +- Welcome status line no longer overflows on narrow viewports — + whole-row `FittedBox(scaleDown)` instead of fixed sibling widths. + Inline `fontSize:` literals replaced with the typography + constants. Version label reads `clideVersion` so the status line + stays in sync with `pubspec.yaml` (T-116). +- Integration test `theme_picker_test.dart` no longer deadlocks — + was `await`ing `services.commands.execute('theme.pick')` whose + Future doesn't complete until the dialog is dismissed. Now + fire-and-forget around `pumpAndSettle` (T-116). - `TerminalView.onTapUp` now actually fires on primary tap — was wired to a dead code path (T-93). Dead `onTapUp` surface on `TerminalGestureHandler` / `TerminalGestureDetector` removed. diff --git a/Makefile b/Makefile index 9ce9cce1..2ba26a10 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ else endif VERSION ?= $(shell awk -F': *' '/^version:/ {gsub(/[" ]/,"",$$2); print $$2; exit}' pubspec.yaml) +COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) .PHONY: help @@ -28,7 +29,7 @@ help: ## Show this help. # -- app (Flutter) ------------------------------------------------------- .PHONY: run -run: ## Launch the Flutter desktop app. +run: gen-build-info ## Launch the Flutter desktop app. ifeq ($(FLUTTER_OS),linux) GDK_BACKEND=x11 LD_LIBRARY_PATH=$(CURDIR)/native/linux-x64$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH} flutter run -d linux --dart-define=CLIDE_PROJECT=$(CURDIR) else @@ -64,7 +65,7 @@ pubget: ## flutter pub get. flutter pub get .PHONY: build-check -build-check: ## Verify native + Dart build compiles (no run). +build-check: gen-build-info ## Verify native + Dart build compiles (no run). ifeq ($(FLUTTER_OS),linux) LD_LIBRARY_PATH=$(CURDIR)/native/linux-x64$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH} flutter build linux else @@ -79,29 +80,45 @@ analyze: ## flutter analyze. format: ## dart format --set-exit-if-changed. dart format --set-exit-if-changed . +# The single bake of every build-time fact derived from pubspec.yaml +# + git + clock. Runs implicitly as a prereq of every target that +# compiles or executes Dart code so nobody has to remember it. +# +# Outputs: +# - lib/src/build_info.g.dart — gitignored, fresh on every build. +# Exposes `clideVersion` (from pubspec), `clideCommit` (git short +# SHA), `clideDate` (UTC now) for the app to read directly. +# - assets/licenses.yaml `self.version:` — rewritten in place so the +# bundled license manifest never drifts from pubspec. (Tracked in +# git; the rewrite is a no-op when in sync.) +.PHONY: gen-build-info +gen-build-info: + @printf '// GENERATED — do not edit. Regenerated by `make` on every\n// build/run/test (see gen-build-info in Makefile). The version\n// field is read from pubspec.yaml `version:` — the single source\n// of truth for the release number; commit + date stamp at run time.\nconst String clideVersion = '"'"'%s'"'"';\nconst String clideCommit = '"'"'%s'"'"';\nconst String clideDate = '"'"'%s'"'"';\n' "$(VERSION)" "$(COMMIT)" "$(DATE)" > lib/src/build_info.g.dart + @awk -v v="$(VERSION)" '/^self:/ {in_self=1} in_self && /^[[:space:]]+version:/ {sub(/version:.*/, "version: \"" v "\""); in_self=0} {print}' assets/licenses.yaml > assets/licenses.yaml.tmp && mv assets/licenses.yaml.tmp assets/licenses.yaml + .PHONY: verify -verify: analyze format decisions-validate changelog-gate ## No-tests sweep — analyze + format + decisions-validate + changelog-gate. For mid-edit "are the gates green?" checks; `push-check` is the full pre-push pipeline. +verify: gen-build-info analyze format decisions-validate changelog-gate ## No-tests sweep — gen-build-info + analyze + format + decisions-validate + changelog-gate. For mid-edit "are the gates green?" checks; `push-check` is the full pre-push pipeline. .PHONY: t -t: ## Run one test path with tee'd output. Usage: make t T=test/path/to/spec.dart +t: gen-build-info ## Run one test path with tee'd output. Usage: make t T=test/path/to/spec.dart @mkdir -p test/.test-output @if [ -z "$(T)" ]; then echo "usage: make t T=test/path/to/spec.dart" >&2; exit 2; fi flutter test $(T) 2>&1 | tee test/.test-output/last.log .PHONY: test -test: ## Fast: analyze + format + unit + widget + golden (<60s). +test: gen-build-info ## Fast: analyze + format + unit + widget + golden (<60s). ci/test.sh .PHONY: test-core -test-core: ## Core subsystem tests (IPC, PTY, git, pane registry). +test-core: gen-build-info ## Core subsystem tests (IPC, PTY, git, pane registry). ci/test_core.sh .PHONY: test-a11y -test-a11y: ## A11y contract (semantic coverage + keyboard + contrast + i18n). +test-a11y: gen-build-info ## A11y contract (semantic coverage + keyboard + contrast + i18n). ci/test_a11y.sh .PHONY: test-integration -test-integration: ## Integration tests (real app boot; xvfb on headless Linux). +test-integration: gen-build-info ## Integration tests (real app boot; xvfb on headless Linux). ci/test_integration.sh .PHONY: test-e2e @@ -141,15 +158,15 @@ ui-smoke: ## Build + serve + run Playwright smoke + stop. @sh -c 'trap "tools/ui/stop.sh >/dev/null 2>&1" EXIT; cd tools/ui && npx playwright test smoke.spec.ts' .PHONY: build -build: ## flutter build for the current OS. +build: gen-build-info ## flutter build for the current OS. flutter build $(FLUTTER_OS) .PHONY: build-linux -build-linux: ## flutter build linux (desktop bundle). +build-linux: gen-build-info ## flutter build linux (desktop bundle). flutter build linux .PHONY: build-macos -build-macos: ## flutter build macos (desktop bundle). +build-macos: gen-build-info ## flutter build macos (desktop bundle). flutter build macos # -- install / uninstall ----------------------------------------------------- @@ -263,7 +280,7 @@ decisions-validate: ## Parser dry-run over governance/{decisions,questions,rejec push-check: decisions-validate test-core test test-a11y coverage-gate changelog-gate ## Pre-push gate (fast — <2 min target). .PHONY: push-check-full -push-check-full: push-check test-integration smoke-bundle ## Pre-release gate (push-check + integration + smoke; slower; skips theme_picker per T-116). +push-check-full: push-check test-integration smoke-bundle ## Pre-release gate (push-check + integration + smoke; slower). .PHONY: hooks hooks: ## Install the repo's git hooks. diff --git a/assets/licenses.yaml b/assets/licenses.yaml index f976b6c7..6772a9ce 100644 --- a/assets/licenses.yaml +++ b/assets/licenses.yaml @@ -36,7 +36,10 @@ schema_version: 1 # About screen can read it at runtime. self: name: clide - version: "2.0.0-dev" + # Auto-synced from pubspec.yaml `version:` by `make gen-build-info` + # (runs implicitly on every build/run/test). Don't hand-edit; bump + # pubspec instead. + version: "2.1.0" homepage: https://github.com/postmeridiem/clide license: MIT license_file: assets/LICENSE diff --git a/ci/test_integration.sh b/ci/test_integration.sh index 2ccb9b1b..b2fec5c9 100755 --- a/ci/test_integration.sh +++ b/ci/test_integration.sh @@ -3,16 +3,10 @@ # start" regression gate. Flutter integration tests prefer one file at # a time on desktop; we iterate to avoid the "Unable to start the app" # error that hits when they run as a batch. -# -# Skips: theme_picker_test.dart — pumpAndSettle hangs on theme.pick -# (T-116). Restore once that's fixed. set -euo pipefail cd "$(dirname "$0")/.." for f in integration_test/*_test.dart; do - case "$f" in - integration_test/theme_picker_test.dart) echo "==> integration_test: $f (SKIPPED — T-116)"; continue ;; - esac echo "==> integration_test: $f" flutter test "$f" done diff --git a/integration_test/theme_picker_test.dart b/integration_test/theme_picker_test.dart index eb21267d..3a5ab9ad 100644 --- a/integration_test/theme_picker_test.dart +++ b/integration_test/theme_picker_test.dart @@ -6,6 +6,7 @@ import 'package:clide/builtin/theme_picker/theme_picker.dart'; import 'package:clide/builtin/welcome/welcome.dart'; import 'package:clide/kernel/kernel.dart'; import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -39,21 +40,39 @@ void main() { ..register(ThemePickerExtension()); await services.extensions.activateAll(); + // Larger viewport so the welcome screen's _StatusLine row doesn't + // overflow on the default ~800x600 — that overflow throws a layout + // assertion that fails the test before we get to theme.pick. + tester.view.physicalSize = const Size(1200, 800); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + await tester.pumpWidget(ClideApp(services: services)); await tester.pumpAndSettle(); - // Invoke the command. - await services.commands.execute('theme.pick'); + // Fire-and-forget: theme.pick's run handler awaits + // ctx.dialog.show(...), whose Future doesn't complete until the + // dialog is dismissed. Awaiting here would deadlock the test + // before the dialog ever mounts. + final pending = services.commands.execute('theme.pick'); await tester.pumpAndSettle(); expect(find.text('Select theme'), findsOneWidget); expect(find.text('Cancel'), findsOneWidget); - // Dismiss via Cancel. + // Dismiss via Cancel — this completes the pending future above. await tester.tap(find.text('Cancel')); await tester.pumpAndSettle(); expect(find.text('Select theme'), findsNothing); + await pending; + // Tear the widget tree down BEFORE disposing services so widgets + // that listen to kernel notifiers (KeymapService, etc.) unsubscribe + // first. Disposing services while the tree is mounted triggers + // "ChangeNotifier used after dispose" during teardown rebuilds. + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pumpAndSettle(); await services.dispose(); }); } diff --git a/lib/builtin/welcome/src/welcome_view.dart b/lib/builtin/welcome/src/welcome_view.dart index f756c730..57ea6c6e 100644 --- a/lib/builtin/welcome/src/welcome_view.dart +++ b/lib/builtin/welcome/src/welcome_view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:clide/clide.dart' show clideVersion; import 'package:clide/kernel/kernel.dart'; import 'package:clide/widgets/widgets.dart'; import 'package:flutter/services.dart' show MissingPluginException; @@ -90,7 +91,7 @@ class _TipsCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClideText('TIPS', fontSize: 12, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), + ClideText('TIPS', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), const SizedBox(height: 14), _tipRow(firstRow), const SizedBox(height: 8), @@ -108,8 +109,8 @@ class _TipsCard extends StatelessWidget { Expanded( child: Row( children: [ - Expanded(child: ClideText(tips[i].$1, fontSize: 13, color: tokens.globalTextMuted)), - ClideText(tips[i].$2, fontSize: 12, color: tokens.globalForeground, fontFamily: clideMonoFamily), + Expanded(child: ClideText(tips[i].$1, fontSize: clideFontMeta, color: tokens.globalTextMuted)), + ClideText(tips[i].$2, fontSize: clideFontSmall, color: tokens.globalForeground, fontFamily: clideMonoFamily), ], ), ), @@ -134,8 +135,8 @@ class _Header extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClideText('clide', fontSize: 52, fontWeight: FontWeight.w300, color: tokens.globalForeground), - ClideText('IDE for Claude Code CLI', muted: true, fontSize: 16), + ClideText('clide', fontSize: clideFontWelcomeBanner, fontWeight: FontWeight.w300, color: tokens.globalForeground), + ClideText('IDE for Claude Code CLI', muted: true, fontSize: clideFontDialogTitle), ], ), ], @@ -153,7 +154,7 @@ class _StartColumn extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClideText('START', fontSize: 12, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), + ClideText('START', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), const SizedBox(height: 20), _ActionRow( icon: PhosphorIcons.folder, @@ -236,8 +237,8 @@ class _ActionRow extends StatelessWidget { children: [ ClideIcon(icon, size: 18, color: tokens.globalTextMuted), const SizedBox(width: 14), - Expanded(child: ClideText(label, fontSize: 15, color: tokens.globalForeground)), - if (shortcut != null) ClideText(shortcut!, fontSize: 13, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), + Expanded(child: ClideText(label, fontSize: clideFontBody, color: tokens.globalForeground)), + if (shortcut != null) ClideText(shortcut!, fontSize: clideFontMeta, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), ], ), ), @@ -259,10 +260,10 @@ class _RecentColumn extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClideText('RECENT', fontSize: 12, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), + ClideText('RECENT', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), const SizedBox(height: 20), if (recents.isEmpty) - const ClideText('No recent projects.', muted: true, fontSize: 14) + const ClideText('No recent projects.', muted: true, fontSize: clideFontCaption) else for (final r in recents) _RecentRow( @@ -312,18 +313,18 @@ class _RecentRow extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClideText(project.name, fontSize: 15, fontWeight: FontWeight.w500), + ClideText(project.name, fontSize: clideFontBody, fontWeight: FontWeight.w500), const SizedBox(height: 3), Row( children: [ Flexible( child: ClideText(project.relativePath, - muted: true, fontSize: 13, fontFamily: clideMonoFamily, maxLines: 1, overflow: TextOverflow.ellipsis)), + muted: true, fontSize: clideFontMeta, fontFamily: clideMonoFamily, maxLines: 1, overflow: TextOverflow.ellipsis)), if (project.branch != null) ...[ - ClideText(' · ', muted: true, fontSize: 13), + ClideText(' · ', muted: true, fontSize: clideFontMeta), ClideIcon(PhosphorIcons.gitBranch, size: 11, color: tokens.globalTextMuted), const SizedBox(width: 3), - ClideText(project.branch!, muted: true, fontSize: 13, fontFamily: clideMonoFamily), + ClideText(project.branch!, muted: true, fontSize: clideFontMeta, fontFamily: clideMonoFamily), ], ], ), @@ -337,7 +338,7 @@ class _RecentRow extends StatelessWidget { onTap: onToggleSticky, ), const SizedBox(width: 12), - ClideText(project.timeAgo, muted: true, fontSize: 13), + ClideText(project.timeAgo, muted: true, fontSize: clideFontMeta), ], ), ), @@ -390,20 +391,29 @@ class _StatusLine extends StatelessWidget { listenable: kernel.toolchain, builder: (ctx, _) { final tc = kernel.toolchain; - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ClideText('clide 2.0.0-dev', muted: true, fontSize: 12, fontFamily: clideMonoFamily), - ClideText(' · ', muted: true, fontSize: 12), - if (!tc.resolved) - ClideText('checking…', muted: true, fontSize: 12, fontFamily: clideMonoFamily) - else if (tc.allOk) - ClideText('application ok', fontSize: 12, fontFamily: clideMonoFamily, color: tokens.statusSuccess) - else - ClideText(tc.missing.map((t) => '$t not found').join(' · '), fontSize: 12, fontFamily: clideMonoFamily, color: tokens.statusWarning), - ClideText(' · ', muted: true, fontSize: 12), - _ThemeLink(tokens: tokens, kernel: kernel, themeName: themeName), - ], + // FittedBox+scaleDown shrinks the row uniformly on narrow + // viewports rather than overflowing — at standard widths it's + // a no-op. The status line is decorative chrome; keep it on + // one line by accepting a tiny font on very narrow screens. + return FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ClideText('clide $clideVersion', muted: true, fontSize: clideFontSmall, fontFamily: clideMonoFamily), + ClideText(' · ', muted: true, fontSize: clideFontSmall), + if (!tc.resolved) + ClideText('checking…', muted: true, fontSize: clideFontSmall, fontFamily: clideMonoFamily) + else if (tc.allOk) + ClideText('application ok', fontSize: clideFontSmall, fontFamily: clideMonoFamily, color: tokens.statusSuccess) + else + ClideText(tc.missing.map((t) => '$t not found').join(' · '), + fontSize: clideFontSmall, fontFamily: clideMonoFamily, color: tokens.statusWarning), + ClideText(' · ', muted: true, fontSize: clideFontSmall), + _ThemeLink(tokens: tokens, kernel: kernel, themeName: themeName), + ], + ), ); }, ); @@ -423,8 +433,8 @@ class _ThemeLink extends StatelessWidget { builder: (ctx, hovered, _) => Row( mainAxisSize: MainAxisSize.min, children: [ - ClideText('theme: ', muted: true, fontSize: 12, fontFamily: clideMonoFamily), - ClideText(themeName, fontSize: 12, fontFamily: clideMonoFamily, color: hovered ? tokens.globalForeground : tokens.globalFocus), + ClideText('theme: ', muted: true, fontSize: clideFontSmall, fontFamily: clideMonoFamily), + ClideText(themeName, fontSize: clideFontSmall, fontFamily: clideMonoFamily, color: hovered ? tokens.globalForeground : tokens.globalFocus), ], ), ); @@ -489,9 +499,9 @@ class _OpenProjectDialogState extends State<_OpenProjectDialog> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const ClideText('Open project', fontSize: 16, fontWeight: FontWeight.w600), + const ClideText('Open project', fontSize: clideFontDialogTitle, fontWeight: FontWeight.w600), const SizedBox(height: 4), - const ClideText('Enter the path to a git repository.', muted: true, fontSize: 13), + const ClideText('Enter the path to a git repository.', muted: true, fontSize: clideFontMeta), const SizedBox(height: 16), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), @@ -503,7 +513,8 @@ class _OpenProjectDialogState extends State<_OpenProjectDialog> { child: EditableText( controller: _controller, focusNode: _focus, - style: TextStyle(color: tokens.globalForeground, fontSize: 14, fontFamily: clideMonoFamily, fontFamilyFallback: clideMonoFamilyFallback), + style: TextStyle( + color: tokens.globalForeground, fontSize: clideFontCaption, fontFamily: clideMonoFamily, fontFamilyFallback: clideMonoFamilyFallback), cursorColor: tokens.globalForeground, backgroundCursorColor: tokens.globalTextMuted, onSubmitted: (_) => unawaited(_submit()), @@ -511,7 +522,7 @@ class _OpenProjectDialogState extends State<_OpenProjectDialog> { ), if (_error != null) ...[ const SizedBox(height: 8), - ClideText(_error!, color: tokens.statusError, fontSize: 12), + ClideText(_error!, color: tokens.statusError, fontSize: clideFontSmall), ], const SizedBox(height: 16), Row( @@ -548,14 +559,14 @@ class _NotARepoDialog extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const ClideText('No git repo found', fontSize: 16, fontWeight: FontWeight.w600), + const ClideText('No git repo found', fontSize: clideFontDialogTitle, fontWeight: FontWeight.w600), const SizedBox(height: 8), - ClideText(path, muted: true, fontSize: 13), + ClideText(path, muted: true, fontSize: clideFontMeta), const SizedBox(height: 8), const ClideText( 'A clide project root requires a git repository.', muted: true, - fontSize: 13, + fontSize: clideFontMeta, ), const SizedBox(height: 16), Row( diff --git a/lib/clide.dart b/lib/clide.dart index b3e731e8..1c4d67d5 100644 --- a/lib/clide.dart +++ b/lib/clide.dart @@ -27,22 +27,8 @@ export 'src/ipc/schema_v1.dart'; export 'src/panes/event_sink.dart'; export 'src/panes/pane.dart' show Pane, PaneKind; -/// Build-time-stamped version string. -/// -/// The Makefile's `build` target passes `--define=clideVersion=…`, -/// stamping `pubspec.yaml`'s `version:` plus the git short SHA and -/// dirty marker. -const clideVersion = String.fromEnvironment( - 'clideVersion', - defaultValue: '2.0.0-dev', -); - -const clideCommit = String.fromEnvironment( - 'clideCommit', - defaultValue: 'unknown', -); - -const clideDate = String.fromEnvironment( - 'clideDate', - defaultValue: 'unknown', -); +// clideVersion / clideCommit / clideDate live in lib/src/build_info.g.dart, +// regenerated by every `make` build/run/test target from pubspec.yaml + +// git short SHA + current UTC time. See `gen-build-info` in the +// Makefile. +export 'src/build_info.g.dart' show clideVersion, clideCommit, clideDate; diff --git a/lib/widgets/src/typography.dart b/lib/widgets/src/typography.dart index 7d21a148..6cf36760 100644 --- a/lib/widgets/src/typography.dart +++ b/lib/widgets/src/typography.dart @@ -56,8 +56,14 @@ const String clideMonoFamily = 'JetBrainsMono'; const double clideFontBody = 15; const double clideFontCaption = 14; const double clideFontMono = 14; +const double clideFontMeta = 13; const double clideFontSmall = 12; const double clideFontBadge = 11; +// Larger semantic sizes that aren't body text. clideFontDialogTitle is +// the modal/dialog heading; clideFontWelcomeBanner is the oversized +// "clide" mark on the welcome screen — one-off but worth naming. +const double clideFontDialogTitle = 16; +const double clideFontWelcomeBanner = 52; const double clideLineHeight = 1.25; /// System fallback chain. Ordered by platform prevalence + quality of