diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 7fd22ad8..49e9e048 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -3512,3 +3512,4 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB58X0TFJ02YTMVPD0D9Q838', 'status', 'backlog', 'done', NULL, '2026-06-10 17:47:01', '2026-06-10 17:47:01', '2026-06-10 17:47:01', NULL, 'ba3d17ec4e668ade82b07d2bb848ab91', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5CW7JPT6BR2RWMNYVCXJ50', 'status', 'backlog', 'done', NULL, '2026-06-10 18:20:28', '2026-06-10 18:20:28', '2026-06-10 18:20:28', NULL, '213109fcd67b4375ebbd3b59c4a1ed04', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5HMYDXP62RKH3HP55T6AYG', 'status', 'backlog', 'review', NULL, '2026-06-10 18:24:41', '2026-06-10 18:24:41', '2026-06-10 18:24:41', NULL, 'c5a88f9594c44896a6a3d1a4b2418ed2', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5HMYDXP62RKH3HP55T6AYG', 'status', 'review', 'done', NULL, '2026-06-10 18:27:58', '2026-06-10 18:27:58', '2026-06-10 18:27:58', NULL, 'a9c72ab53b69f5ca6bf1fa4dd0ddfa05', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/ticket_idmap/2026-06.sql b/.pql/changelog/ticket_idmap/2026-06.sql index 173c1715..efe62e0a 100644 --- a/.pql/changelog/ticket_idmap/2026-06.sql +++ b/.pql/changelog/ticket_idmap/2026-06.sql @@ -174,3 +174,6 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4XCM5KBXDDSCWJ37GPYG3R', 'T-347', '2026-06-10 16:55:10', '2026-06-10 16:55:10', NULL, '9d40226dbc6136072d2d5d0eda71f141', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB50YE6S6YWNP2ZSFWES9B2W', 'T-348', '2026-06-10 17:10:42', '2026-06-10 17:10:42', NULL, '8bb5ad92551f272417840869a0774668', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB58X0TFJ02YTMVPD0D9Q838', 'T-349', '2026-06-10 17:45:28', '2026-06-10 17:45:28', NULL, '0b12f14fa83254ab113c870531628359', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5CW7JPT6BR2RWMNYVCXJ50', 'T-350', '2026-06-10 18:02:50', '2026-06-10 18:02:50', NULL, 'a5c0c22d84621b14a5208317414d6026', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5HMYDXP62RKH3HP55T6AYG', 'T-351', '2026-06-10 18:23:41', '2026-06-10 18:23:41', NULL, '9d2da44c16c5aa38c0a36e4b00ef5f15', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5M14B76B31654D959XM5AC', 'T-352', '2026-06-10 18:34:05', '2026-06-10 18:34:05', NULL, '3fe3e1d5fb7c0fbd084b45116575ad98', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 9bdbd649..2d422180 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -3229,3 +3229,11 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat Root cause is timing, not the protocol. linux/runner/clide_app.cc connects request_no_server_decorations() to the GtkWidget ''realize'' signal, but GTK''s Wayland backend only creates the wl_surface on MAP, not realize. So gdk_wayland_window_get_wl_surface() returns null and the function bails at its own ''if (surface == nullptr) return'' before creating the org_kde_kwin_server_decoration / requesting mode NONE. KWin 6 defaults to server-side decorations on Wayland unless that request lands → native title bar shows (double title bar with clide''s own chrome). Fix: also fire the request on the ''map'' signal (wl_surface is live by then); the realize handler still does the X11 gdk_window_set_decorations hint and harmlessly bails on the Wayland part (surface null) so no duplicate decoration object is created. If KWin 6 turns out not to honor the legacy KDE protocol, fall back to the standard xdg-decoration protocol (zxdg_decoration_manager_v1, set_mode CLIENT_SIDE).', 'review', 'high', NULL, NULL, NULL, '2026-06-10 18:23:41', '2026-06-10 18:24:41', NULL, '0d06f87ff4d71e2ee4e4821968b5e3df', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5HMYDXP62RKH3HP55T6AYG', 'bug', NULL, 'Frameless chrome: KWin keeps the title bar — decoration request fires before the wl_surface exists', 'On KDE Plasma 6 / KWin 6 (Wayland), clide still shows the compositor''s native title bar even though the frameless code (D-057, KDE server-decoration protocol) is compiled in (confirmed: with T-349 making wayland-client REQUIRED, a 2.3.0 build that runs at all has it). + +Root cause is timing, not the protocol. linux/runner/clide_app.cc connects request_no_server_decorations() to the GtkWidget ''realize'' signal, but GTK''s Wayland backend only creates the wl_surface on MAP, not realize. So gdk_wayland_window_get_wl_surface() returns null and the function bails at its own ''if (surface == nullptr) return'' before creating the org_kde_kwin_server_decoration / requesting mode NONE. KWin 6 defaults to server-side decorations on Wayland unless that request lands → native title bar shows (double title bar with clide''s own chrome). + +Fix: also fire the request on the ''map'' signal (wl_surface is live by then); the realize handler still does the X11 gdk_window_set_decorations hint and harmlessly bails on the Wayland part (surface null) so no duplicate decoration object is created. If KWin 6 turns out not to honor the legacy KDE protocol, fall back to the standard xdg-decoration protocol (zxdg_decoration_manager_v1, set_mode CLIENT_SIDE).', 'done', 'high', NULL, NULL, NULL, '2026-06-10 18:23:41', '2026-06-10 18:27:58', NULL, 'e61eeca3f6ccf42a334867c757fda759', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5M14B76B31654D959XM5AC', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Ticket sidebar errors on first load: pql runs before the workspace workDir is set', 'The tickets sidebar (and other pql panes) fail on first load when clide is desktop-launched: the daemon''s PqlClient is constructed with workDir = the boot CWD (the launch dir, e.g. ~), not the repo. The pane''s first pql.tickets.list fires before swapIpcServer reconfigures the dispatcher with the project''s workRoot, so pql runs in the wrong dir — against a stale/global pql.db — and errors (observed: ''ticket_deps.blocker_record_id missing — pql.db is from an earlier schema''). A manual refresh works because by then the workspace is open and the workDir is correct. + +This is a wrong-workDir timing issue, not db-busy (so the T-350 retry doesn''t catch it). Fix: the pql-backed panes refetch on ProjectOpened (which fires after the IPC server swaps to the project workRoot). Implemented for the tickets pane; the decisions/pql/search panes share the latent bug and should get the same refetch.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-10 18:34:05', '2026-06-10 18:34:05', NULL, 'a6d1821c06dbccb30c9f7e777aef7ef5', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 3db3252b..82ab6812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. SQLite lock under concurrent writes) is now retried a few times before surfacing, instead of leaving the pane on "pql … failed" until a manual refresh. (T-350) +- **Ticket and decision sidebars load on first open, not just after a manual + refresh.** On a desktop launch the daemon's pql workspace starts as the + launch directory, not the repo, so the panes' first fetch ran against the + wrong (or a stale-schema) DB and errored. They now re-fetch when the + workspace actually opens, by which point the pql workspace is the repo. (T-352) ## [2.3.0] — 2026-06-10 diff --git a/lib/builtin/decisions/src/decisions_view.dart b/lib/builtin/decisions/src/decisions_view.dart index 37df264c..2db9bb9c 100644 --- a/lib/builtin/decisions/src/decisions_view.dart +++ b/lib/builtin/decisions/src/decisions_view.dart @@ -23,6 +23,7 @@ class _DecisionsViewState extends State { StreamSubscription? _focusSub; StreamSubscription? _fileSub; StreamSubscription? _schedulerSub; + StreamSubscription? _projectSub; bool _refreshing = false; bool _pendingRefresh = false; @@ -37,6 +38,12 @@ class _DecisionsViewState extends State { .where((e) => e.subsystem == 'files' && e.kind == 'files.changed' && _isDecisionPath(e.data['path'] as String? ?? '')) .listen((_) => _refresh()); _schedulerSub = kernel.events.on().where((e) => e.tier == SchedulerTier.oneMinute).listen((_) => _refresh()); + // The first load can fire before the project's workspace is wired into + // the daemon (the boot workDir is the launch CWD, not the repo), so pql + // runs against the wrong/old DB and the list errors. Re-fetch once the + // workspace is actually open — ProjectOpened fires after the IPC server + // swaps to the project workRoot. (T-352) + _projectSub = kernel.events.on().listen((_) => _refresh()); } if (!_loading || _decisions.isNotEmpty) return; unawaited(_load()); @@ -88,6 +95,7 @@ class _DecisionsViewState extends State { _focusSub?.cancel(); _fileSub?.cancel(); _schedulerSub?.cancel(); + _projectSub?.cancel(); super.dispose(); } diff --git a/lib/builtin/tickets/src/tickets_view.dart b/lib/builtin/tickets/src/tickets_view.dart index 320fd2ce..9795f38e 100644 --- a/lib/builtin/tickets/src/tickets_view.dart +++ b/lib/builtin/tickets/src/tickets_view.dart @@ -37,6 +37,7 @@ class _TicketsViewState extends State { StreamSubscription? _focusSub; StreamSubscription? _schedulerSub; StreamSubscription? _changedSub; + StreamSubscription? _projectSub; bool _refreshing = false; bool _pendingRefresh = false; @@ -111,6 +112,12 @@ class _TicketsViewState extends State { })); }); _schedulerSub = kernel.events.on().where((e) => e.tier == SchedulerTier.oneMinute).listen((_) => _refresh()); + // The first load can fire before the project's workspace is wired into + // the daemon (the boot workDir is the launch CWD, not the repo), so pql + // runs against the wrong/old DB and the list errors. Re-fetch once the + // workspace is actually open — the daemon's pql workDir is correct by + // then (ProjectOpened fires after the IPC server swaps). (T-352) + _projectSub = kernel.events.on().listen((_) => _refresh()); } if (!_loading || _tickets.isNotEmpty) return; unawaited(_load()); @@ -121,6 +128,7 @@ class _TicketsViewState extends State { _focusSub?.cancel(); _changedSub?.cancel(); _schedulerSub?.cancel(); + _projectSub?.cancel(); super.dispose(); } diff --git a/test/builtin/decisions/decisions_view_test.dart b/test/builtin/decisions/decisions_view_test.dart index 7a6f9f9a..ef413257 100644 --- a/test/builtin/decisions/decisions_view_test.dart +++ b/test/builtin/decisions/decisions_view_test.dart @@ -546,6 +546,29 @@ void main() { }); }); + group('DecisionsView — workspace open triggers reload (T-352)', () { + testWidgets('ProjectOpened triggers _refresh', (tester) async { + // The first load can fire before the daemon's pql workDir is the repo; a + // ProjectOpened (fired after the IPC server swaps) must re-fetch. + int listCallCount = 0; + f.ipc.stub('pql.decisions.sync', (_) async => _ok(const {})); + f.ipc.stub('pql.decisions.list', (_) async { + listCallCount++; + return _ok({ + 'decisions': [_decision(id: 'D-$listCallCount', title: 'open $listCallCount')], + }); + }); + + await pumpView(tester); + expect(listCallCount, 1); + + f.services.events.emit(const ProjectOpened(path: '/repo')); + await pumpAsync(tester); + + expect(listCallCount, greaterThanOrEqualTo(2)); + }); + }); + group('DecisionsView — concurrent refresh guard', () { testWidgets('second refresh while one is running sets _pendingRefresh', (tester) async { final Completer firstListCompleter = Completer(); diff --git a/test/builtin/tickets/tickets_view_test.dart b/test/builtin/tickets/tickets_view_test.dart index 6a6889a6..4183bf7c 100644 --- a/test/builtin/tickets/tickets_view_test.dart +++ b/test/builtin/tickets/tickets_view_test.dart @@ -137,6 +137,21 @@ void main() { expect(calls, greaterThan(before)); }); + testWidgets('refetches when the workspace opens (T-352)', (tester) async { + // The first load can fire before the daemon's pql workDir is the repo; a + // ProjectOpened (fired after the IPC server swaps) must re-fetch. + var calls = 0; + f.ipc.stub('pql.tickets.list', (_) async { + calls++; + return _list([_t('T-1', 'Thing', 'backlog')]); + }); + await pumpView(tester); + final before = calls; + f.services.events.emit(const ProjectOpened(path: '/repo')); + await pumpAsync(tester); + expect(calls, greaterThan(before)); + }); + // -- Type-filter chips (T-343) ------------------------------------------ // The chips own one GestureDetector for both onTap (toggle) and onDoubleTap // (solo), so a single tap's onTap only fires after the ~300ms double-tap