synodic-client 0.0.1.dev63__tar.gz → 0.0.1.dev65__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/PKG-INFO +2 -2
  2. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/pyproject.toml +2 -2
  3. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/bootstrap.py +1 -1
  4. synodic_client-0.0.1.dev65/synodic_client/application/config_store.py +65 -0
  5. synodic_client-0.0.1.dev65/synodic_client/application/package_state.py +125 -0
  6. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/qt.py +5 -3
  7. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/schema.py +21 -0
  8. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/install.py +14 -7
  9. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/install_workers.py +0 -1
  10. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/projects.py +13 -6
  11. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/schema.py +0 -1
  12. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/screen.py +81 -52
  13. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/settings.py +15 -35
  14. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/tool_update_controller.py +99 -87
  15. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/tray.py +14 -28
  16. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/update_banner.py +25 -17
  17. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/update_controller.py +22 -27
  18. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/workers.py +15 -14
  19. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/resolution.py +0 -1
  20. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/schema.py +0 -5
  21. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/subprocess_patch.py +5 -5
  22. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/qt/test_gather_packages.py +37 -32
  23. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/qt/test_install_preview.py +6 -9
  24. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/qt/test_settings.py +36 -94
  25. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/qt/test_tray_window_show.py +24 -7
  26. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/qt/test_update_controller.py +30 -36
  27. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/test_config.py +0 -2
  28. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/test_resolution.py +0 -1
  29. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/LICENSE.md +0 -0
  30. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/README.md +0 -0
  31. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/__init__.py +0 -0
  32. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/__main__.py +0 -0
  33. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/__init__.py +0 -0
  34. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/data.py +0 -0
  35. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/icon.py +0 -0
  36. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/init.py +0 -0
  37. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/instance.py +0 -0
  38. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/__init__.py +0 -0
  39. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/action_card.py +0 -0
  40. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/card.py +0 -0
  41. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/log_panel.py +0 -0
  42. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/plugin_row.py +0 -0
  43. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/sidebar.py +0 -0
  44. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/screen/spinner.py +0 -0
  45. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/theme.py +0 -0
  46. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/application/uri.py +0 -0
  47. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/cli.py +0 -0
  48. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/client.py +0 -0
  49. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/config.py +0 -0
  50. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/logging.py +0 -0
  51. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/protocol.py +0 -0
  52. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/py.typed +0 -0
  53. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/startup.py +0 -0
  54. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/synodic_client/updater.py +0 -0
  55. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/__init__.py +0 -0
  56. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/conftest.py +0 -0
  57. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/__init__.py +0 -0
  58. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/qt/__init__.py +0 -0
  59. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/qt/conftest.py +0 -0
  60. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/qt/test_action_card.py +0 -0
  61. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/qt/test_log_panel.py +0 -0
  62. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/qt/test_logging.py +0 -0
  63. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/qt/test_preview_model.py +0 -0
  64. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/qt/test_sidebar.py +0 -0
  65. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/qt/test_update_banner.py +0 -0
  66. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/qt/test_update_feedback.py +0 -0
  67. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/test_cli.py +0 -0
  68. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/test_client_updater.py +0 -0
  69. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/test_client_version.py +0 -0
  70. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/test_examples.py +0 -0
  71. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/test_init.py +0 -0
  72. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/test_install.py +0 -0
  73. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/test_updater.py +0 -0
  74. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/test_uri.py +0 -0
  75. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/test_workers.py +0 -0
  76. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/windows/__init__.py +0 -0
  77. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/windows/conftest.py +0 -0
  78. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/windows/test_protocol.py +0 -0
  79. {synodic_client-0.0.1.dev63 → synodic_client-0.0.1.dev65}/tests/unit/windows/test_startup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: synodic_client
3
- Version: 0.0.1.dev63
3
+ Version: 0.0.1.dev65
4
4
  Author-Email: Synodic Software <contact@synodic.software>
5
5
  License: LGPL-3.0-or-later
6
6
  Project-URL: homepage, https://github.com/synodic/synodic-client
@@ -8,7 +8,7 @@ Project-URL: repository, https://github.com/synodic/synodic-client
8
8
  Requires-Python: <3.15,>=3.14
9
9
  Requires-Dist: pyside6>=6.10.2
10
10
  Requires-Dist: packaging>=26.0
11
- Requires-Dist: porringer>=0.2.1.dev77
11
+ Requires-Dist: porringer>=0.2.1.dev78
12
12
  Requires-Dist: qasync>=0.28.0
13
13
  Requires-Dist: velopack>=0.0.1444.dev49733
14
14
  Requires-Dist: typer>=0.24.1
@@ -10,12 +10,12 @@ requires-python = ">=3.14, <3.15"
10
10
  dependencies = [
11
11
  "pyside6>=6.10.2",
12
12
  "packaging>=26.0",
13
- "porringer>=0.2.1.dev77",
13
+ "porringer>=0.2.1.dev78",
14
14
  "qasync>=0.28.0",
15
15
  "velopack>=0.0.1444.dev49733",
16
16
  "typer>=0.24.1",
17
17
  ]
18
- version = "0.0.1.dev63"
18
+ version = "0.0.1.dev65"
19
19
 
20
20
  [project.license]
21
21
  text = "LGPL-3.0-or-later"
@@ -17,8 +17,8 @@ import sys
17
17
 
18
18
  from synodic_client.config import set_dev_mode
19
19
  from synodic_client.logging import configure_logging
20
- from synodic_client.subprocess_patch import apply as _apply_subprocess_patch
21
20
  from synodic_client.protocol import extract_uri_from_args
21
+ from synodic_client.subprocess_patch import apply as _apply_subprocess_patch
22
22
  from synodic_client.updater import initialize_velopack
23
23
 
24
24
  # Parse flags early so logging uses the right filename and level.
@@ -0,0 +1,65 @@
1
+ """Centralized configuration store.
2
+
3
+ Provides a single source of truth for :class:`ResolvedConfig` so that
4
+ every consumer (ToolsView, SettingsWindow, UpdateController,
5
+ ToolUpdateOrchestrator) always reads the same snapshot and receives
6
+ change notifications through a Qt signal.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from PySide6.QtCore import QObject, Signal
12
+
13
+ from synodic_client.resolution import ResolvedConfig, resolve_config, update_user_config
14
+
15
+
16
+ class ConfigStore(QObject):
17
+ """Observable wrapper around :class:`ResolvedConfig`.
18
+
19
+ All config mutations go through :meth:`update` (which persists to
20
+ disk) or :meth:`set` (which replaces without persisting). Both
21
+ emit :attr:`changed` so every connected consumer stays in sync.
22
+
23
+ Typical usage::
24
+
25
+ store = ConfigStore(initial_config)
26
+ store.changed.connect(some_consumer.on_config_changed)
27
+ store.update(auto_apply=False) # persists + emits
28
+ """
29
+
30
+ changed = Signal(object)
31
+ """Emitted with the new ``ResolvedConfig`` after every mutation."""
32
+
33
+ def __init__(self, config: ResolvedConfig | None = None, parent: QObject | None = None) -> None:
34
+ """Create a new store, optionally seeded with *config*."""
35
+ super().__init__(parent)
36
+ self._config = config if config is not None else resolve_config()
37
+
38
+ @property
39
+ def config(self) -> ResolvedConfig:
40
+ """The current configuration snapshot."""
41
+ return self._config
42
+
43
+ def update(self, **changes: object) -> ResolvedConfig:
44
+ """Persist *changes* to disk and broadcast the new config.
45
+
46
+ Wraps :func:`~synodic_client.resolution.update_user_config`.
47
+
48
+ Args:
49
+ **changes: Field-name / value pairs forwarded to
50
+ :func:`update_user_config`.
51
+
52
+ Returns:
53
+ The fresh :class:`ResolvedConfig`.
54
+ """
55
+ self._config = update_user_config(**changes)
56
+ self.changed.emit(self._config)
57
+ return self._config
58
+
59
+ def set(self, config: ResolvedConfig) -> None:
60
+ """Replace the config without persisting and notify listeners.
61
+
62
+ Use for externally resolved configs (e.g. passed at startup).
63
+ """
64
+ self._config = config
65
+ self.changed.emit(self._config)
@@ -0,0 +1,125 @@
1
+ """Shared package update state registry.
2
+
3
+ Provides :class:`PackageStateStore`, a centralised record of
4
+ per-package update status used by both ToolsView and ProjectsView
5
+ so that version/update information discovered in one view is
6
+ immediately available to the other.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ from PySide6.QtCore import QObject, Signal
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class PackageState:
18
+ """Canonical update state for a single package.
19
+
20
+ Keyed by ``(signal_key, name)`` inside :class:`PackageStateStore`.
21
+ """
22
+
23
+ name: str
24
+ """Package name (e.g. ``"ruff"``)."""
25
+
26
+ installed_version: str = ''
27
+ """Currently installed version, or empty if unknown."""
28
+
29
+ available_version: str = ''
30
+ """Latest available version, or empty if unknown."""
31
+
32
+ has_update: bool = False
33
+ """Whether the package has a newer version available."""
34
+
35
+
36
+ class PackageStateStore(QObject):
37
+ """Shared registry of package update states across views.
38
+
39
+ Both ToolsView and ProjectsView write discovered update information
40
+ into the store; each view can then read the canonical state
41
+ regardless of which view made the discovery.
42
+
43
+ :attr:`state_changed` is emitted whenever new data arrives so
44
+ listeners can refresh their badges/labels without polling.
45
+ """
46
+
47
+ state_changed = Signal()
48
+ """Emitted whenever any package state is created or modified."""
49
+
50
+ def __init__(self, parent: QObject | None = None) -> None:
51
+ """Initialise with an empty registry."""
52
+ super().__init__(parent)
53
+ self._data: dict[str, dict[str, PackageState]] = {}
54
+
55
+ # -- Bulk write (ToolsView check_updates) ----------------------------
56
+
57
+ def set_check_results(self, available: dict[str, dict[str, str]]) -> None:
58
+ """Populate from ``check_updates`` results.
59
+
60
+ *available* maps ``{signal_key: {package_name: latest_version}}``
61
+ — the same shape previously stored in
62
+ ``ToolsView._updates_available``.
63
+ """
64
+ self._data.clear()
65
+ for key, packages in available.items():
66
+ for pkg_name, latest in packages.items():
67
+ self._data.setdefault(key, {})[pkg_name] = PackageState(
68
+ name=pkg_name,
69
+ available_version=latest,
70
+ has_update=True,
71
+ )
72
+ self.state_changed.emit()
73
+
74
+ # -- Single-action write (ProjectsView dry-run) ----------------------
75
+
76
+ def record_action_result(
77
+ self,
78
+ signal_key: str,
79
+ pkg_name: str,
80
+ *,
81
+ installed_version: str = '',
82
+ available_version: str = '',
83
+ has_update: bool = False,
84
+ ) -> None:
85
+ """Record a single dry-run result.
86
+
87
+ Merges with any existing state so that data discovered by
88
+ different views accumulates rather than overwrites.
89
+ """
90
+ existing = self._data.get(signal_key, {}).get(pkg_name)
91
+ state = PackageState(
92
+ name=pkg_name,
93
+ installed_version=installed_version or (existing.installed_version if existing else ''),
94
+ available_version=available_version or (existing.available_version if existing else ''),
95
+ has_update=has_update or (existing.has_update if existing else False),
96
+ )
97
+ self._data.setdefault(signal_key, {})[pkg_name] = state
98
+ self.state_changed.emit()
99
+
100
+ # -- Read API --------------------------------------------------------
101
+
102
+ def get_updates(self, signal_key: str) -> dict[str, str]:
103
+ """Return ``{package_name: latest_version}`` for packages with updates.
104
+
105
+ Drop-in replacement for ``_updates_available.get(key, {})``.
106
+ """
107
+ bucket = self._data.get(signal_key, {})
108
+ return {name: s.available_version for name, s in bucket.items() if s.has_update}
109
+
110
+ def has_updates_for(self, signal_key: str) -> bool:
111
+ """Return whether any package under *signal_key* has an update."""
112
+ return any(s.has_update for s in self._data.get(signal_key, {}).values())
113
+
114
+ def get(self, signal_key: str, pkg_name: str) -> PackageState | None:
115
+ """Return the state for a specific package, or ``None``."""
116
+ return self._data.get(signal_key, {}).get(pkg_name)
117
+
118
+ @property
119
+ def has_data(self) -> bool:
120
+ """Return whether any update data has been recorded."""
121
+ return bool(self._data)
122
+
123
+ def clear(self) -> None:
124
+ """Remove all recorded state."""
125
+ self._data.clear()
@@ -15,6 +15,7 @@ from porringer.schema import LocalConfiguration
15
15
  from PySide6.QtCore import QEvent, QObject, Qt, QTimer
16
16
  from PySide6.QtWidgets import QApplication, QWidget
17
17
 
18
+ from synodic_client.application.config_store import ConfigStore
18
19
  from synodic_client.application.icon import app_icon
19
20
  from synodic_client.application.init import run_startup_preamble
20
21
  from synodic_client.application.instance import SingleInstance
@@ -216,8 +217,9 @@ def application(*, uri: str | None = None, dev_mode: bool = False, debug: bool =
216
217
  sys.exit(0)
217
218
  instance.start_server()
218
219
 
219
- _screen = Screen(porringer, config)
220
- _tray = TrayScreen(app, client, _screen.window, config=config)
220
+ _store = ConfigStore(config)
221
+ _screen = Screen(porringer, _store)
222
+ _tray = TrayScreen(app, client, _screen.window, store=_store)
221
223
 
222
224
  # Keep install preview windows alive until the app exits
223
225
  _install_windows: list[InstallPreviewWindow] = []
@@ -227,7 +229,7 @@ def application(*, uri: str | None = None, dev_mode: bool = False, debug: bool =
227
229
  window = InstallPreviewWindow(
228
230
  porringer,
229
231
  manifest_url,
230
- config=config,
232
+ config=_store.config,
231
233
  )
232
234
  _install_windows.append(window)
233
235
  window.show()
@@ -51,3 +51,24 @@ class ToolUpdateResult:
51
51
  failed: int = 0
52
52
  updated_packages: set[str] = field(default_factory=set)
53
53
  """Package names that were successfully upgraded."""
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class UpdateTarget:
58
+ """Identifies the scope of a manual tool update.
59
+
60
+ Passed to the shared completion handler so it can clear the correct
61
+ updating state and derive timestamp keys. ``None`` (the default in
62
+ the handler) means the update was periodic / automatic.
63
+
64
+ When *package* is empty the update targeted an entire plugin;
65
+ otherwise it targeted one specific package within the plugin.
66
+ *plugin* always carries the signal key (possibly composite
67
+ ``"plugin:tag"``).
68
+ """
69
+
70
+ plugin: str
71
+ """Signal key for the plugin (may be composite ``"name:tag"``)."""
72
+
73
+ package: str = ''
74
+ """Package name, or empty when the whole plugin was updated."""
@@ -43,6 +43,7 @@ from PySide6.QtWidgets import (
43
43
  QWidget,
44
44
  )
45
45
 
46
+ from synodic_client.application.package_state import PackageStateStore
46
47
  from synodic_client.application.screen import skip_reason_label
47
48
  from synodic_client.application.screen.action_card import ActionCardList, action_key
48
49
  from synodic_client.application.screen.card import CardFrame
@@ -113,6 +114,7 @@ class SetupPreviewWidget(QWidget):
113
114
  *,
114
115
  show_close: bool = True,
115
116
  config: ResolvedConfig | None = None,
117
+ package_store: PackageStateStore | None = None,
116
118
  ) -> None:
117
119
  """Initialize the preview widget.
118
120
 
@@ -122,11 +124,13 @@ class SetupPreviewWidget(QWidget):
122
124
  show_close: Whether to show the Close button. Set ``False``
123
125
  when embedding inside a persistent view (e.g. a tab).
124
126
  config: Global configuration for per-manifest pre-release state.
127
+ package_store: Shared package update state registry.
125
128
  """
126
129
  super().__init__(parent)
127
130
  self._porringer = porringer
128
131
  self._show_close = show_close
129
132
  self._config = config
133
+ self._package_store = package_store
130
134
  self._discovered_plugins: DiscoveredPlugins | None = None
131
135
 
132
136
  self._model = PreviewModel()
@@ -293,7 +297,6 @@ class SetupPreviewWidget(QWidget):
293
297
  path_or_url: str,
294
298
  *,
295
299
  project_directory: Path | None = None,
296
- detect_updates: bool = True,
297
300
  ) -> None:
298
301
  """Load a manifest preview, or skip if the same manifest is already showing results.
299
302
 
@@ -305,7 +308,6 @@ class SetupPreviewWidget(QWidget):
305
308
  Args:
306
309
  path_or_url: Manifest path or URL.
307
310
  project_directory: Working directory for project sync actions.
308
- detect_updates: Query package indices for newer versions.
309
311
  """
310
312
  key = normalize_manifest_key(path_or_url)
311
313
 
@@ -345,7 +347,6 @@ class SetupPreviewWidget(QWidget):
345
347
  self._run_preview_task(
346
348
  path_or_url,
347
349
  project_directory=self._model.project_directory,
348
- detect_updates=detect_updates,
349
350
  prerelease_packages=overrides,
350
351
  ),
351
352
  )
@@ -474,7 +475,6 @@ class SetupPreviewWidget(QWidget):
474
475
  path_or_url: str,
475
476
  *,
476
477
  project_directory: Path | None = None,
477
- detect_updates: bool = True,
478
478
  prerelease_packages: set[str] | None = None,
479
479
  ) -> None:
480
480
  """Run the preview coroutine and route completion/errors."""
@@ -484,7 +484,6 @@ class SetupPreviewWidget(QWidget):
484
484
  path_or_url,
485
485
  config=PreviewConfig(
486
486
  project_directory=project_directory,
487
- detect_updates=detect_updates,
488
487
  prerelease_packages=prerelease_packages,
489
488
  ),
490
489
  callbacks=PreviewCallbacks(
@@ -640,6 +639,16 @@ class SetupPreviewWidget(QWidget):
640
639
  if card is not None:
641
640
  card.set_check_result(result)
642
641
 
642
+ # Record in shared store so ToolsView can reflect the update
643
+ if self._package_store is not None and action.installer and action.package:
644
+ self._package_store.record_action_result(
645
+ action.installer,
646
+ str(action.package.name),
647
+ installed_version=result.installed_version or '',
648
+ available_version=result.available_version or '',
649
+ has_update=result.skip_reason == SkipReason.UPDATE_AVAILABLE,
650
+ )
651
+
643
652
  # Update phase text
644
653
  m.checked_count += 1
645
654
  total = len(m.action_states)
@@ -987,11 +996,9 @@ class InstallPreviewWindow(QMainWindow):
987
996
  logger.info('Starting install preview for: %s', self._manifest_url)
988
997
  self._url_label.setText(f'<b>Manifest:</b> {self._manifest_url}')
989
998
 
990
- detect = self._config.detect_updates if self._config else True
991
999
  self._preview_widget.load(
992
1000
  self._manifest_url,
993
1001
  project_directory=self._project_directory,
994
- detect_updates=detect,
995
1002
  )
996
1003
 
997
1004
  # --- Callbacks ---
@@ -232,7 +232,6 @@ async def run_preview(
232
232
  paths=[manifest_path],
233
233
  dry_run=True,
234
234
  project_directory=cfg.project_directory,
235
- detect_updates=cfg.detect_updates,
236
235
  prerelease_packages=cfg.prerelease_packages,
237
236
  )
238
237
  state = _DispatchState()
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import logging
7
7
  from pathlib import Path
8
+ from typing import TYPE_CHECKING
8
9
 
9
10
  from porringer.api import API
10
11
  from porringer.backend.command.core.discovery import DiscoveredPlugins
@@ -19,12 +20,15 @@ from PySide6.QtWidgets import (
19
20
  )
20
21
 
21
22
  from synodic_client.application.data import DataCoordinator
23
+ from synodic_client.application.package_state import PackageStateStore
22
24
  from synodic_client.application.screen.install import SetupPreviewWidget
23
25
  from synodic_client.application.screen.schema import PreviewPhase
24
26
  from synodic_client.application.screen.sidebar import ManifestSidebar
25
27
  from synodic_client.application.screen.spinner import LoadingIndicator
26
28
  from synodic_client.application.theme import COMPACT_MARGINS
27
- from synodic_client.resolution import ResolvedConfig
29
+
30
+ if TYPE_CHECKING:
31
+ from synodic_client.application.config_store import ConfigStore
28
32
 
29
33
  logger = logging.getLogger(__name__)
30
34
 
@@ -41,24 +45,27 @@ class ProjectsView(QWidget):
41
45
  def __init__(
42
46
  self,
43
47
  porringer: API,
44
- config: ResolvedConfig,
48
+ store: ConfigStore,
45
49
  parent: QWidget | None = None,
46
50
  *,
47
51
  coordinator: DataCoordinator | None = None,
52
+ package_store: PackageStateStore | None = None,
48
53
  ) -> None:
49
54
  """Initialize the projects view.
50
55
 
51
56
  Args:
52
57
  porringer: The porringer API instance.
53
- config: Resolved configuration.
58
+ store: The centralised :class:`ConfigStore`.
54
59
  parent: Optional parent widget.
55
60
  coordinator: Shared data coordinator for validated directory
56
61
  data.
62
+ package_store: Shared package update state registry.
57
63
  """
58
64
  super().__init__(parent)
59
65
  self._porringer = porringer
60
- self._config = config
66
+ self._store = store
61
67
  self._coordinator = coordinator
68
+ self._package_store = package_store
62
69
  self._refresh_in_progress = False
63
70
  self._pending_select: Path | None = None
64
71
  self._widgets: dict[Path, SetupPreviewWidget] = {}
@@ -163,7 +170,6 @@ class ProjectsView(QWidget):
163
170
  widget.load(
164
171
  str(path),
165
172
  project_directory=path if path.is_dir() else path.parent,
166
- detect_updates=self._config.detect_updates,
167
173
  )
168
174
 
169
175
  except Exception:
@@ -196,7 +202,8 @@ class ProjectsView(QWidget):
196
202
  self._porringer,
197
203
  self,
198
204
  show_close=False,
199
- config=self._config,
205
+ config=self._store.config,
206
+ package_store=self._package_store,
200
207
  )
201
208
  widget._discovered_plugins = discovered
202
209
  widget.install_finished.connect(self._on_install_finished)
@@ -334,7 +334,6 @@ class PreviewConfig:
334
334
  """Optional execution parameters for :func:`run_preview`."""
335
335
 
336
336
  project_directory: Path | None = None
337
- detect_updates: bool = True
338
337
  prerelease_packages: set[str] | None = None
339
338
 
340
339