diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index dd94fb46..d196daa3 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -1829,3 +1829,5 @@ RETRACTED the cancellation — NOT a duplicate of T-236 (per user, 2026-06-09). DONE 2026-06-09 — satisfied by D-89 together with T-236. The lightbox-expansion + path-retention (filename a11y label / unchanged copyText) intent from T-254 is delivered by the shared ImageThumbnail → lightbox; the chosen presentation is inline (not a separate card) per the user. See T-236 / D-89 / commit.', NULL, '2026-06-09 15:55:06', '2026-06-09 15:55:06', '2026-06-09 15:55:06', NULL, 'c6090b1f5fdbdee4b56e6fd1ac39a9e2', 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-254', 'status', 'ready', 'done', NULL, '2026-06-09 15:55:06', '2026-06-09 15:55:06', '2026-06-09 15:55:06', NULL, 'e07372e7e9662712183808199ac27210', 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-297', 'status', 'backlog', 'ready', NULL, '2026-06-09 16:01:58', '2026-06-09 16:01:58', '2026-06-09 16:01:58', NULL, '12474d426c882ddf33efdd4fa6f86f95', 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-296', 'status', 'backlog', 'ready', NULL, '2026-06-09 16:02:14', '2026-06-09 16:02:14', '2026-06-09 16:02:14', NULL, '8f3930428725856dcfd8042261852398', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 3a8d0fb1..f13adfb6 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -3146,3 +3146,43 @@ Repro: Notes: - Likely the scroll controller doesn''t react to the composer/interaction-zone height change (no re-scroll on viewport-inset/size change). Audit the conversation view''s scroll handling around interaction-zone show/hide (D-78) and on keyboard/box resize. - Affects the Claude conversation panel.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-09 16:01:09', '2026-06-09 16:01:09', NULL, '5287b26bc163fd424808be2151796347', 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-297', 'bug', 'T-276', 'Conversation doesn''t re-scroll when the bottom input resizes (content hides behind it)', 'When the bottom input area changes height, the conversation view does not re-scroll to the new viewport bottom, so the last bit of content is left hidden behind the newly-sized box. + +Trigger: anything that resizes the bottom interaction zone (D-78) — e.g. opening a permission dialog or the AskUserQuestion UI, which replace/expand the composer. The taller box shrinks the conversation viewport from the bottom, but the scroll offset isn''t adjusted, so content that was at the bottom edge is now occluded. + +Expected: when the interaction zone grows, the conversation re-anchors so the previously-visible bottom content stays visible above the box (preserve bottom-anchoring / keep the tail in view). Symmetric on shrink — no leftover gap when the box collapses back. + +Repro: +1. Scroll the Claude conversation to the bottom (tail in view). +2. Trigger a permission prompt or AskUserQuestion (interaction zone expands). +3. Observed: a strip of the last message/card is hidden behind the enlarged input box. +4. Expected: view scrolls so that content remains fully visible above the box. + +Notes: +- Likely the scroll controller doesn''t react to the composer/interaction-zone height change (no re-scroll on viewport-inset/size change). Audit the conversation view''s scroll handling around interaction-zone show/hide (D-78) and on keyboard/box resize. +- Affects the Claude conversation panel.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-09 16:01:09', '2026-06-09 16:01:58', NULL, '6508dfb893a7d12cad13e8d9e7b03718', 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-296', 'task', 'T-276', 'Group consecutive edits to the same file into one collapsed card', 'When Claude makes multiple subsequent edits to the SAME file, bundle them into a single collapsed holder card instead of rendering each edit as its own card — mirroring how we already fold meta/activity runs. + +Behaviour: +- Detect a run of consecutive edits targeting the same file_path and group them into one ClideHolderCard (the shared container from T-266; see _ActivityCard in lib/builtin/claude/src/conversation_view.dart:681). +- Collapsed (default): one-line ticker of the latest edit + a count label. +- Show the count as "# edits" (e.g. "3 edits" / "1 edit"), NOT "# steps". The existing activity card uses stepLabel = ''$count steps'' at conversation_view.dart:683; this run wants an edits-flavoured label. +- Expanded: every individual edit, each with its full report — bundle, do NOT drop or summarise away any information. All per-edit detail must remain reachable on expand. + +Notes: +- Reuse the existing ClideHolderCard / folding machinery rather than building a new card. +- Grouping breaks when the file_path changes or a non-edit step interleaves (consecutive-same-file only), matching the ''subsequent edits to the same file'' wording. +- Parity with the existing meta/activity folding (T-230) — same collapse/expand affordance, just an edits-labelled run. + +WORKED EXAMPLE (from user screenshot, clide_markdown.dart run): the current stream renders 11 stacked cards — 3x "Edit clide_markdown.dart", then a folded "Read … 2 steps" holder, then 7x "Edit clide_markdown.dart". With this feature it collapses to THREE cards: +- [3 edits] (the first edit run) +- [2 steps] (the existing Read holder — unchanged; this is what splits the edit run) +- [7 edits] (the second edit run) +Confirms the split rule: an interleaving non-edit step (here the folded Read run) breaks the consecutive-same-file edit grouping into two separate edit cards. Same-file edits with nothing between them collapse into one "# edits" card. + +STATUS INDICATOR (live tick reuse): the bundled edits card carries ONE header status indicator that reuses the existing per-step success tick, driven by the latest edit''s state: +- edit in flight -> spinner +- edit completed -> success check (the current green tick) +- next edit starts -> back to spinner +- ...repeat, settling on check when the final edit in the run completes (or the error glyph if one fails). +So the collapsed card''s indicator animates spinner<->check as the run grows, rather than showing a static tick. Each individual edit keeps its own tick in the expanded list (unchanged); this is the aggregate indicator on the holder header/ticker. Mirror the same treatment for the existing "# steps" activity card if it doesn''t already do this.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-09 15:43:58', '2026-06-09 16:02:14', NULL, '79d8b599af75fc98290b7ab3751c077c', 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/lib/kernel/src/settings.dart b/lib/kernel/src/settings.dart index 0c2e9fcc..939a2ed7 100644 --- a/lib/kernel/src/settings.dart +++ b/lib/kernel/src/settings.dart @@ -14,6 +14,22 @@ class SettingsStore extends ChangeNotifier { final Map _appValues = {}; final Map _projectValues = {}; + // Writes are fire-and-forget (callers don't await `set`); if the store is + // disposed while one is mid-flight (app shutdown, a closing test), skip the + // post-write notify rather than asserting on a disposed ChangeNotifier. + bool _disposed = false; + + @override + void dispose() { + _disposed = true; + super.dispose(); + } + + void _safeNotify() { + if (_disposed) return; + notifyListeners(); + } + Future load() async { _appValues ..clear() @@ -22,7 +38,7 @@ class SettingsStore extends ChangeNotifier { if (projectDir != null) { _projectValues.addAll(await _readFile(_projectFile)); } - notifyListeners(); + _safeNotify(); } Future setProjectDir(Directory? dir) async { @@ -31,7 +47,7 @@ class SettingsStore extends ChangeNotifier { if (dir != null) { _projectValues.addAll(await _readFile(_projectFile)); } - notifyListeners(); + _safeNotify(); } File get _appFile => File('${appDir.path}/settings.yaml'); @@ -73,7 +89,7 @@ class SettingsStore extends ChangeNotifier { _appValues[key] = value; await _writeFile(_appFile, _appValues); } - notifyListeners(); + _safeNotify(); } Future> _readFile(File f) async { diff --git a/test/builtin/claude/image_thumbnail_test.dart b/test/builtin/claude/image_thumbnail_test.dart index 1e696dbe..f09fac20 100644 --- a/test/builtin/claude/image_thumbnail_test.dart +++ b/test/builtin/claude/image_thumbnail_test.dart @@ -3,7 +3,6 @@ library; import 'package:clide/builtin/claude/src/image_thumbnail.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../helpers/kernel_fixture.dart'; diff --git a/test/kernel/src/theme_persistence_test.dart b/test/kernel/src/theme_persistence_test.dart index 0710be6a..46bdb0d0 100644 --- a/test/kernel/src/theme_persistence_test.dart +++ b/test/kernel/src/theme_persistence_test.dart @@ -39,7 +39,7 @@ void main() { }); tearDown(() async { - await pumpEventQueue(); // flush any in-flight unawaited set() file writes + await pumpEventQueue(); // let in-flight writes settle (dispose then no-ops any straggler) settings.dispose(); theme.dispose(); for (final d in [appTmp, repoTmp]) {