zettcode-engine 0.4.0__tar.gz → 0.4.1__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 (91) hide show
  1. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/PKG-INFO +1 -1
  2. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/core.py +1 -1
  3. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/plugins/loader.py +63 -12
  4. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/plugins/plugin.py +6 -0
  5. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/plugins/switches.py +3 -2
  6. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/version.py +1 -1
  7. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/.gitignore +0 -0
  8. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/pyproject.toml +0 -0
  9. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/_compat.py +0 -0
  10. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/budget.py +0 -0
  11. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/config.py +0 -0
  12. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/config_edit.py +0 -0
  13. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/__init__.py +0 -0
  14. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/agent/__init__.py +0 -0
  15. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/agent/base.py +0 -0
  16. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/agent/context.py +0 -0
  17. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/agent/exports.py +0 -0
  18. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/agent/interaction.py +0 -0
  19. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/agent/sessions.py +0 -0
  20. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/agent/settings.py +0 -0
  21. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/agent/turns.py +0 -0
  22. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/dialogue/__init__.py +0 -0
  23. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/dialogue/ask.py +0 -0
  24. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/dialogue/side.py +0 -0
  25. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/dialogue/title.py +0 -0
  26. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/export/__init__.py +0 -0
  27. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/export/trace/script.js +0 -0
  28. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/export/trace/style.css +0 -0
  29. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/export/trace/template.html +0 -0
  30. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/extensions/__init__.py +0 -0
  31. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/extensions/capabilities.py +0 -0
  32. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/extensions/compaction.py +0 -0
  33. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/extensions/context.py +0 -0
  34. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/extensions/files.py +0 -0
  35. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/extensions/guidance.py +0 -0
  36. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/extensions/harness.py +0 -0
  37. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/extensions/harness_switch.py +0 -0
  38. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/extensions/mcp_notice.py +0 -0
  39. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/extensions/relay.py +0 -0
  40. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/extensions/subagents.py +0 -0
  41. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/extensions/tool_output.py +0 -0
  42. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/extensions/truncation.py +0 -0
  43. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/extensions/usage.py +0 -0
  44. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/harness_paths.py +0 -0
  45. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/permission/__init__.py +0 -0
  46. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/permission/approval.py +0 -0
  47. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/permission/readonly.py +0 -0
  48. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/permission/sandbox.py +0 -0
  49. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/pipeline.py +0 -0
  50. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/plugins/__init__.py +0 -0
  51. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/plugins/commands.py +0 -0
  52. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/plugins/context.py +0 -0
  53. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/plugins/extension.py +0 -0
  54. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/plugins/graph.py +0 -0
  55. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/plugins/groups.py +0 -0
  56. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/plugins/mixins.py +0 -0
  57. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/plugins/services.py +0 -0
  58. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/plugins/settings.py +0 -0
  59. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/plugins/state.py +0 -0
  60. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/py.typed +0 -0
  61. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/server_tools.py +0 -0
  62. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/storage/__init__.py +0 -0
  63. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/storage/layout.py +0 -0
  64. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/storage/ledger.py +0 -0
  65. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/storage/locks.py +0 -0
  66. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/storage/metadata.py +0 -0
  67. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/storage/records.py +0 -0
  68. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/storage/store.py +0 -0
  69. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/tools/__init__.py +0 -0
  70. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/tools/boundary.py +0 -0
  71. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/tools/files.py +0 -0
  72. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/tools/images.py +0 -0
  73. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/tools/output.py +0 -0
  74. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/tools/shell.py +0 -0
  75. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/engine/tools/workspace.py +0 -0
  76. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/error_text.py +0 -0
  77. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/filelock.py +0 -0
  78. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/logging_setup.py +0 -0
  79. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/mention.py +0 -0
  80. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/modes.py +0 -0
  81. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/paths.py +0 -0
  82. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/plugin_tui.py +0 -0
  83. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/processes.py +0 -0
  84. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/prompt.py +0 -0
  85. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/providers.py +0 -0
  86. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/py.typed +0 -0
  87. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/registry.py +0 -0
  88. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/relay.py +0 -0
  89. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/statusline_parts.py +0 -0
  90. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/wire.py +0 -0
  91. {zettcode_engine-0.4.0 → zettcode_engine-0.4.1}/src/zettcode/wording.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: zettcode-engine
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: The agent core of ZettCode: the runtime over zett-agent, and the leaf types it is written on
5
5
  Project-URL: Repository, https://github.com/Chang-LeHung/zettcode
6
6
  Author: Chang-LeHung
@@ -106,7 +106,7 @@ def _load_engine_plugins(config: ZettCodeConfig, shared: Mapping[str, object]) -
106
106
  compaction, the MCP client) is :mod:`zettcode.sdk.builtins`, which passes its own loader here. A runtime built
107
107
  this way still honours ``[plugins]`` for the distributions and single files a reader installed.
108
108
  """
109
- return load_plugins(config, builtins=(), shared=shared)
109
+ return load_plugins(config, builtins=(), shared=shared, agent=False)
110
110
 
111
111
 
112
112
  def build_system_prompt(config: ZettCodeConfig, *, now: datetime | None = None) -> str:
@@ -65,6 +65,30 @@ PLUGIN_ENTRY_POINT_GROUP = "zettcode.plugins"
65
65
  #: load them: they would import the whole terminal to offer it nothing.
66
66
  TERMINAL_PART_GROUP = "zettcode.terminal"
67
67
 
68
+ #: Entry-point group the program publishes the plugins that compose the agent under (the tools, the session log, the
69
+ #: permission, the compaction, ...). Every loader reads it, and none of it can be switched off: these are the program as
70
+ #: much as a compiled-in plugin is, and a configuration key such as ``[skills] enabled`` is what turns a feature off.
71
+ #: They are activated by ``(rank, name)`` and then by ``deps``.
72
+ AGENT_PART_GROUP = "zettcode.agent"
73
+
74
+ #: Who may publish into the program's own groups. Being unswitchable and ahead of everything is a privilege of the
75
+ #: program, not of whoever declares the group in a `pyproject.toml`: a point from any other distribution is an
76
+ #: ordinary plugin whatever group it is in. A point that says nothing of its distribution (a test's stand-in) passes.
77
+ PROGRAM_DISTRIBUTIONS: dict[str, frozenset[str]] = {
78
+ AGENT_PART_GROUP: frozenset({"zettcode-sdk"}),
79
+ TERMINAL_PART_GROUP: frozenset({"zettcode-app", "zettcode-commands", "zettcode-pets", "zettcode-presenters"}),
80
+ }
81
+
82
+
83
+ def part_group(point: EntryPoint) -> str | None:
84
+ """Return the program group ``point`` is a part of, or ``None`` when it is an ordinary plugin."""
85
+ group = getattr(point, "group", None)
86
+ allowed = PROGRAM_DISTRIBUTIONS.get(group) if group else None
87
+ if allowed is None:
88
+ return None
89
+ dist = _distribution(point)
90
+ return group if dist is None or dist in allowed else None
91
+
68
92
 
69
93
  def installed_entry_points() -> tuple[EntryPoint, ...]:
70
94
  """Return every plugin entry point installed in this environment.
@@ -74,9 +98,10 @@ def installed_entry_points() -> tuple[EntryPoint, ...]:
74
98
  """
75
99
  from importlib.metadata import entry_points
76
100
 
101
+ agent = sorted(entry_points(group=AGENT_PART_GROUP), key=lambda point: point.name)
77
102
  parts = sorted(entry_points(group=TERMINAL_PART_GROUP), key=lambda point: point.name)
78
103
  plugins = sorted(entry_points(group=PLUGIN_ENTRY_POINT_GROUP), key=lambda point: point.name)
79
- return (*parts, *plugins)
104
+ return (*agent, *parts, *plugins)
80
105
 
81
106
 
82
107
  def installed_files(directory: Path) -> tuple[Path, ...]:
@@ -142,6 +167,7 @@ def load_plugins(
142
167
  builtins: Sequence[Plugin[Any]],
143
168
  shared: Mapping[str, object] | None = None,
144
169
  required: Sequence[str] = (),
170
+ agent: bool = True,
145
171
  ) -> Plugins:
146
172
  """Load the builtin plugins and every enabled distribution into one bundle.
147
173
 
@@ -151,9 +177,9 @@ def load_plugins(
151
177
  entry_points: Entry points to load; ``None`` reads the installed ones.
152
178
  Tests pass their own, so discovery can be exercised without
153
179
  installing a distribution.
154
- builtins: Plugins compiled into the repository, which the engine does not ship: the caller passes
155
- :data:`zettcode.sdk.builtins.AGENT_PLUGINS` for the program's agent, and ``()`` to load only what a
156
- reader installed.
180
+ builtins: Plugins compiled into the repository, which the engine does not ship; ``()`` for none. The program's
181
+ agent is not passed here: it is found through the ``zettcode.agent`` entry-point group, unless ``agent`` is
182
+ false. A caller that cannot discover (a test) may pass :data:`zettcode.sdk.builtins.AGENT_PLUGINS`.
157
183
  shared: Objects the runtime hands over by name before any plugin activates (see
158
184
  :mod:`zettcode.engine.plugins.services`); a plugin reads them with ``container.service``, and they
159
185
  stay findable on the returned bundle. A plugin cannot provide a name that is taken here.
@@ -193,6 +219,8 @@ class _Candidate:
193
219
  error: BaseException | None = None
194
220
  #: Whether it came from the program's own entry-point group, which is ahead of every installed plugin.
195
221
  part: bool = False
222
+ #: Whether it is one of the plugins that compose the agent (``zettcode.agent``): a part that is also built in.
223
+ agent: bool = False
196
224
 
197
225
  @property
198
226
  def name(self) -> str:
@@ -213,6 +241,7 @@ def run(
213
241
  entry_points: Iterable[EntryPoint] | None,
214
242
  required: Sequence[str] = (),
215
243
  terminal: bool = False,
244
+ agent: bool = True,
216
245
  ) -> Plugins:
217
246
  """Feed the builtins, the enabled distributions and the single-file plugins to ``loader``, and return what it bundled.
218
247
 
@@ -241,8 +270,10 @@ def run(
241
270
  # which that switch is not about) and the plugins the program requires.
242
271
  points = tuple(entry_points) if entry_points is not None else installed_entry_points()
243
272
  # The parts of the program that need a screen are for a loader that has one.
244
- points = tuple(point for point in points if terminal or getattr(point, "group", None) != TERMINAL_PART_GROUP)
245
- parts = {point.name for point in points if getattr(point, "group", None) == TERMINAL_PART_GROUP}
273
+ points = tuple(point for point in points if terminal or part_group(point) != TERMINAL_PART_GROUP)
274
+ points = tuple(point for point in points if agent or part_group(point) != AGENT_PART_GROUP)
275
+ parts = {point.name for point in points if part_group(point) is not None}
276
+ agent_names = {point.name for point in points if part_group(point) == AGENT_PART_GROUP}
246
277
  # The configuration file's own list and the switch file's, merged for the one caller that loads. The switch
247
278
  # module cannot be imported here (it imports this one), so the union is written out; `off_names` is the same
248
279
  # rule for the front ends.
@@ -272,7 +303,9 @@ def run(
272
303
  sources[path.stem] = partial(_from_file, loader, path)
273
304
  held = {origin for origin in sources if origin in disabled or (not config.plugins_enabled and origin not in parts)}
274
305
  for origin in sources:
275
- if origin not in held:
306
+ # A name the caller already passed as a built-in is that plugin; the discovered twin of it is the same part
307
+ # offered twice, not a second plugin to refuse as a duplicate.
308
+ if origin not in held and origin not in loader.builtins:
276
309
  candidates.extend(sources[origin]())
277
310
  # A name on that list can only reach a distribution or a file, unless the program needs it: the plugins it lists
278
311
  # as builtin loaded above, and the required ones, with what they need, are pulled in below. A line that names one
@@ -282,7 +315,9 @@ def run(
282
315
  "plugins.required",
283
316
  extra={"plugin": origin, "reason": "part of the program; [plugins] disable does not reach it"},
284
317
  )
285
- protected = set(required)
318
+ # The plugins that compose the agent are protected like the required ones, but a failure of one is recorded and
319
+ # does not stop the call: that is what they were as compiled-in plugins.
320
+ protected = set(required) | agent_names
286
321
  while True:
287
322
  by_name = {candidate.name: candidate for candidate in candidates}
288
323
  wanted: set[str] = set()
@@ -294,7 +329,9 @@ def run(
294
329
  wanted.add(name)
295
330
  if name in by_name:
296
331
  queue.extend(by_name[name].deps)
297
- pulled = sorted(origin for origin in held if origin in wanted)
332
+ pulled = sorted(
333
+ origin for origin in held if origin in wanted and not (origin in parts and origin in loader.builtins)
334
+ )
298
335
  if not pulled:
299
336
  break
300
337
  for origin in pulled:
@@ -320,6 +357,13 @@ def run(
320
357
  "plugins.name_taken",
321
358
  extra={"plugin": name, "who": who, "reason": f"{owners[name]} already has the name {name!r}"},
322
359
  )
360
+ # A plugin of the program is the program's own whether or not it loads, and whether or not it is ever activated: one
361
+ # refused for a need that failed still goes on the list a switch is refused against.
362
+ loader.builtins.extend(c.origin for c in candidates if c.builtin and c.origin not in loader.builtins)
363
+ # Within the program's own groups the stated rank decides, then the name; the stable sort keeps everything else.
364
+ at = [i for i, c in enumerate(candidates) if c.agent]
365
+ for slot, candidate in zip(at, sorted((candidates[i] for i in at), key=_rank), strict=True):
366
+ candidates[slot] = candidate
323
367
  ordered, refused = graph.order([graph.Node(c.name, c.deps) for c in candidates])
324
368
  loaded: set[int] = set()
325
369
  for index in sorted(refused):
@@ -374,15 +418,22 @@ def _described(point: EntryPoint) -> str:
374
418
  return f"{point.name!r} ({dist or 'unknown distribution'}" + (f", {value})" if value else ")")
375
419
 
376
420
 
421
+ def _rank(candidate: _Candidate) -> tuple[int, str]:
422
+ """Sort key of a part inside its group: its declared rank, then its name; a failure sorts first, by origin."""
423
+ return (candidate.plugin.rank if candidate.plugin is not None else -1, candidate.origin)
424
+
425
+
377
426
  def _from_point(loader: Loader, point: EntryPoint) -> list[_Candidate]:
378
427
  """Resolve one entry point into a candidate; a failure is a candidate that carries it."""
379
- part = getattr(point, "group", None) == TERMINAL_PART_GROUP
428
+ group = part_group(point)
429
+ part = group is not None
430
+ agent = group == AGENT_PART_GROUP
380
431
  try:
381
- return [_Candidate(_instantiate(point), point.name, part=part)]
432
+ return [_Candidate(_instantiate(point), point.name, builtin=agent, part=part, agent=agent)]
382
433
  except KeyboardInterrupt:
383
434
  raise
384
435
  except BaseException as error:
385
- return [_Candidate(None, point.name, error=error, part=part)]
436
+ return [_Candidate(None, point.name, builtin=agent, error=error, part=part, agent=agent)]
386
437
 
387
438
 
388
439
  def _from_file(loader: Loader, path: Path) -> list[_Candidate]:
@@ -93,6 +93,12 @@ class Plugin(PluginAgentMixin, Generic[_C], ABC):
93
93
  #: The names of the plugins this one needs. The loader activates a plugin after every plugin it lists, and
94
94
  #: leaves it out, reported, when one of them is absent, turned off, failed, or the lists go round in a circle.
95
95
  deps: ClassVar[tuple[str, ...]] = ()
96
+ #: Where a plugin stands among the others of its entry-point group when the loader has no ``deps`` to go by: parts
97
+ #: are activated by ``(rank, name)`` within their group, so a group whose extensions share a priority (and so
98
+ #: keep their registration order) is activated in a stated order and not in the order entry points happen to sort.
99
+ #: The program's to set: ``zettcode.sdk.builtins`` writes it for its own plugins, and a plugin of anyone else has
100
+ #: no use for it, since it is only read for the parts of the program.
101
+ rank: ClassVar[int] = 0
96
102
  priority: ClassVar[int] = 100
97
103
  #: The configuration key that decides whether a compiled-in plugin does its work, or ``None`` when there is none:
98
104
  #: a plugin the reader cannot turn off is left out of the list rather than shown without a switch.
@@ -129,8 +129,9 @@ def catalog(
129
129
  # A part for a screen is listed by a process that has one. A bundle that holds none of it (the SDK, the
130
130
  # server) is not offering switches for things it never loads — unless one is switched off, which is the one
131
131
  # state in which a part is absent from a bundle that would have it.
132
- if getattr(point, "group", None) == loader.TERMINAL_PART_GROUP and point.name not in off:
133
- continue
132
+ if loader.part_group(point) is not None:
133
+ if point.name not in off:
134
+ continue
134
135
  installed.append(_installed(point.name, point.name, config, off=off))
135
136
  # A single-file plugin that is switched off is not in the bundle (the loader never imported it), so the folder is
136
137
  # what says it exists: it stays on the page, off, where it can be switched back on.
@@ -8,4 +8,4 @@ of the dependency graph: everything else may import `zettcode.version`, nothing
8
8
  from __future__ import annotations
9
9
 
10
10
  #: The version of the program and of every distribution built beside it.
11
- __version__ = "0.4.0"
11
+ __version__ = "0.4.1"