runspec-console 0.44.0__tar.gz → 0.44.2__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 (96) hide show
  1. {runspec_console-0.44.0 → runspec_console-0.44.2}/CHANGELOG.md +31 -0
  2. {runspec_console-0.44.0 → runspec_console-0.44.2}/PKG-INFO +1 -1
  3. {runspec_console-0.44.0 → runspec_console-0.44.2}/pyproject.toml +1 -1
  4. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/bridge.py +28 -9
  5. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_chat_bridge.py +62 -3
  6. {runspec_console-0.44.0 → runspec_console-0.44.2}/.gitignore +0 -0
  7. {runspec_console-0.44.0 → runspec_console-0.44.2}/IMPL_NOTES.md +0 -0
  8. {runspec_console-0.44.0 → runspec_console-0.44.2}/README.md +0 -0
  9. {runspec_console-0.44.0 → runspec_console-0.44.2}/SECURITY.md +0 -0
  10. {runspec_console-0.44.0 → runspec_console-0.44.2}/dev_build.py +0 -0
  11. {runspec_console-0.44.0 → runspec_console-0.44.2}/examples/console-adapter-plugin/.github/prompts/create-console-plugin.prompt.md +0 -0
  12. {runspec_console-0.44.0 → runspec_console-0.44.2}/examples/console-adapter-plugin/AGENTS.md +0 -0
  13. {runspec_console-0.44.0 → runspec_console-0.44.2}/examples/console-adapter-plugin/README.md +0 -0
  14. {runspec_console-0.44.0 → runspec_console-0.44.2}/examples/console-adapter-plugin/console_adapter_plugin/__init__.py +0 -0
  15. {runspec_console-0.44.0 → runspec_console-0.44.2}/examples/console-adapter-plugin/console_adapter_plugin/adapter.py +0 -0
  16. {runspec_console-0.44.0 → runspec_console-0.44.2}/examples/console-adapter-plugin/pyproject.toml +0 -0
  17. {runspec_console-0.44.0 → runspec_console-0.44.2}/examples/console-adapter-plugin/tests/test_adapter_conformance.py +0 -0
  18. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/__init__.py +0 -0
  19. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/adapters/__init__.py +0 -0
  20. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/adapters/anthropic.py +0 -0
  21. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/adapters/base.py +0 -0
  22. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/adapters/bedrock.py +0 -0
  23. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/adapters/langserve.py +0 -0
  24. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/adapters/openai.py +0 -0
  25. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/adapters/testing.py +0 -0
  26. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/app.py +0 -0
  27. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/catalog.py +0 -0
  28. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/config.py +0 -0
  29. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/discovery.py +0 -0
  30. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/events.py +0 -0
  31. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/executor.py +0 -0
  32. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/filters_store.py +0 -0
  33. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/fs_browse.py +0 -0
  34. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/fs_transfer.py +0 -0
  35. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/hosts.py +0 -0
  36. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/notify.py +0 -0
  37. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/runspec.toml +0 -0
  38. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/sources/__init__.py +0 -0
  39. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/sources/chat.py +0 -0
  40. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/sources/chat_transport.py +0 -0
  41. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/sources/notifications.py +0 -0
  42. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/sources/outlook.py +0 -0
  43. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/ssh_pool.py +0 -0
  44. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/tools/__init__.py +0 -0
  45. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/tools/generate_ssh_key.py +0 -0
  46. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/trigger_runner.py +0 -0
  47. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/triggers.py +0 -0
  48. {runspec_console-0.44.0 → runspec_console-0.44.2}/runspec_console/version.py +0 -0
  49. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/__init__.py +0 -0
  50. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_adapter_testing.py +0 -0
  51. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_analytics.py +0 -0
  52. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_anthropic_adapter.py +0 -0
  53. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_autonomy_gate.py +0 -0
  54. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_bridge_adapter.py +0 -0
  55. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_catalog.py +0 -0
  56. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_chat_history.py +0 -0
  57. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_chat_source.py +0 -0
  58. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_chat_transport.py +0 -0
  59. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_command_tools.py +0 -0
  60. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_config.py +0 -0
  61. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_copy_ssh_id.py +0 -0
  62. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_devtools.py +0 -0
  63. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_discovery_globals.py +0 -0
  64. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_dispatch_queue.py +0 -0
  65. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_draft_send.py +0 -0
  66. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_events.py +0 -0
  67. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_executor_ssh.py +0 -0
  68. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_filters_store.py +0 -0
  69. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_fs_browse.py +0 -0
  70. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_fs_transfer.py +0 -0
  71. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_key_pubkey.py +0 -0
  72. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_langserve_adapter.py +0 -0
  73. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_list_dir_unsaved.py +0 -0
  74. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_logs_management.py +0 -0
  75. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_notifications.py +0 -0
  76. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_notify.py +0 -0
  77. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_outlook.py +0 -0
  78. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_path_normalize.py +0 -0
  79. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_plugin_adapters.py +0 -0
  80. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_positional_args.py +0 -0
  81. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_prompt_caching.py +0 -0
  82. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_refresh_cycle.py +0 -0
  83. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_relay_filter.py +0 -0
  84. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_remote_command_injection.py +0 -0
  85. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_rotate_ssh_key.py +0 -0
  86. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_save_config_pool.py +0 -0
  87. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_secret_args.py +0 -0
  88. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_shutdown_abort.py +0 -0
  89. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_ssh_logging.py +0 -0
  90. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_ssh_pool.py +0 -0
  91. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_terminal.py +0 -0
  92. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_trigger_config.py +0 -0
  93. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_trigger_runner.py +0 -0
  94. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_triggers.py +0 -0
  95. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_usage.py +0 -0
  96. {runspec_console-0.44.0 → runspec_console-0.44.2}/tests/test_version.py +0 -0
@@ -8,6 +8,37 @@ Version numbers follow [Semantic Versioning](https://semver.org/).
8
8
  ---
9
9
 
10
10
 
11
+ ## [0.44.2] — 2026-06-12
12
+
13
+ ### Changed
14
+ - **Helpdesk inbox moved on top of the conversation.** In queue mode the inbox is
15
+ now a full-width strip above the conversation (was a cramped side column), so
16
+ each thread row carries a real summary line — status · visitor · assignee ·
17
+ last-message snippet (catalog requests prefixed) · time — and the open count
18
+ shows in the header. Still collapses (now up/down) to give the conversation the
19
+ full screen. Group chat is unchanged.
20
+
21
+ ---
22
+
23
+ ## [0.44.1] — 2026-06-12
24
+
25
+ ### Fixed
26
+ - **Helpdesk catalog re-published on every discovery refresh.** The periodic
27
+ refresh cycle re-sent (and re-logged `published N tool(s)`) each staffed
28
+ queue's catalog unconditionally — log spam plus redundant `catalog` frames to
29
+ the room. The publisher now tracks the last catalog sent per queue namespace
30
+ and only re-sends when its contents change. Subscribe still forces a send (a
31
+ fresh connection's hub has no catalog yet); a disconnect clears the cache so a
32
+ reconnect republishes from scratch.
33
+
34
+ ### Changed
35
+ - **Helpdesk inbox layout.** The queue inbox column is now collapsible (fold to
36
+ a thin rail to focus on one conversation full-width), and the presence sidebar
37
+ is hidden in queue mode — a helpdesk thread is 1:1 (you + the visitor), so that
38
+ width goes to the conversation. Group chat is unchanged.
39
+
40
+ ---
41
+
11
42
  ## [0.44.0] — 2026-06-12
12
43
 
13
44
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runspec-console
3
- Version: 0.44.0
3
+ Version: 0.44.2
4
4
  Requires-Python: >=3.11
5
5
  Requires-Dist: cryptography>=42.0
6
6
  Requires-Dist: paramiko>=3.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "runspec-console"
7
- version = "0.44.0"
7
+ version = "0.44.2"
8
8
  requires-python = ">=3.11"
9
9
  dependencies = [
10
10
  "pywebview>=5.0",
@@ -254,6 +254,10 @@ class Bridge:
254
254
  self._thread_states: dict[str, dict[str, Any]] = {}
255
255
  self._agent_rooms: dict[str, str] = {}
256
256
  self._catalog_index: dict[str, dict[str, dict[str, Any]]] = {}
257
+ # namespace → last catalog actually published, so the periodic refresh
258
+ # only re-sends (and logs) when the contents change. Cleared on disconnect
259
+ # so the next subscribe republishes from scratch.
260
+ self._published_catalogs: dict[str, list[dict[str, Any]]] = {}
257
261
  self._hosts: list[dict[str, Any]] = []
258
262
  # Named source-side stream filters (runspec_filters.toml). Loaded once at
259
263
  # startup and reloaded on save; resolved by name when a relay/download
@@ -5063,8 +5067,9 @@ class Bridge:
5063
5067
  backoff = self._CHAT_BACKOFF_START
5064
5068
  if queue:
5065
5069
  # Publish the queue's self-service catalog right after subscribing
5066
- # (and again whenever discovery refreshes _publish_catalogs).
5067
- self._publish_catalog(spec, source)
5070
+ # (forced a fresh connection's hub has no catalog yet; later
5071
+ # discovery refreshes republish only on change — _publish_catalogs).
5072
+ self._publish_catalog(spec, source, force=True)
5068
5073
  watcher = _RotaPresenceWatcher(self._build_clock_ok(), source.set_presence)
5069
5074
  watcher.start()
5070
5075
  self._dispatch(
@@ -5080,6 +5085,9 @@ class Bridge:
5080
5085
  with self._lock:
5081
5086
  self._chat_sources.pop(room, None)
5082
5087
  self._agent_rooms.pop(room, None)
5088
+ self._published_catalogs.pop(
5089
+ room, None
5090
+ ) # force a fresh publish on reconnect
5083
5091
  self._dispatch(
5084
5092
  "runspec:room_connection",
5085
5093
  {"room": room, "host": host, "connected": False, "queue": queue},
@@ -5091,23 +5099,34 @@ class Bridge:
5091
5099
  break
5092
5100
  backoff = min(backoff * 2, self._CHAT_BACKOFF_MAX)
5093
5101
 
5094
- def _publish_catalog(self, spec: dict[str, Any], source: Any) -> None:
5102
+ def _publish_catalog(
5103
+ self, spec: dict[str, Any], source: Any, *, force: bool = False
5104
+ ) -> None:
5095
5105
  """Build and publish one queue's self-service catalog from the discovery
5096
- cache (best-effort a failed publish never breaks the connection; the
5097
- next discovery refresh or reconnect republishes)."""
5106
+ cache. **Idempotent**: the catalog is only sent (and logged) when its
5107
+ contents changed since the last publish for this namespace — so the
5108
+ periodic discovery refresh doesn't re-emit an unchanged catalog every
5109
+ cycle. ``force`` overrides the diff (the subscribe path always sends, as
5110
+ a fresh connection's hub has no catalog yet). Best-effort — a failed
5111
+ publish never breaks the connection; the next refresh or reconnect
5112
+ republishes."""
5098
5113
  from .catalog import build_catalog
5099
5114
 
5100
5115
  try:
5116
+ key = spec["room"]
5101
5117
  with self._lock:
5102
5118
  runnables = list(self._runnables_cache)
5103
5119
  allowlist = spec.get("catalog_allowlist")
5104
5120
  tools, index = build_catalog(runnables, allowlist=allowlist)
5105
5121
  with self._lock:
5106
- self._catalog_index[spec["room"]] = index
5122
+ self._catalog_index[key] = index # enactment map — keep current
5123
+ unchanged = not force and self._published_catalogs.get(key) == tools
5124
+ if not unchanged:
5125
+ self._published_catalogs[key] = tools
5126
+ if unchanged:
5127
+ return # nothing to send — the room already has this catalog
5107
5128
  source.send_catalog(tools)
5108
- logger.info(
5109
- "chat: published %d tool(s) to %r catalog", len(tools), spec["room"]
5110
- )
5129
+ logger.info("chat: published %d tool(s) to %r catalog", len(tools), key)
5111
5130
  except Exception:
5112
5131
  logger.exception("chat: catalog publish for %r failed", spec.get("room"))
5113
5132
 
@@ -237,6 +237,7 @@ def _supervisor_stub():
237
237
  s._chat_sources = {}
238
238
  s._agent_rooms = {}
239
239
  s._thread_states = {}
240
+ s._published_catalogs = {}
240
241
  s._trigger_engine = None
241
242
  s.dispatched: list[tuple[str, dict]] = []
242
243
  s._dispatch = lambda ev, detail: s.dispatched.append((ev, detail))
@@ -246,8 +247,10 @@ def _supervisor_stub():
246
247
  s._on_chat_event = types.MethodType(Bridge._on_chat_event, s)
247
248
  s._feed_engine_for = types.MethodType(Bridge._feed_engine_for, s)
248
249
  s._chat_supervise = types.MethodType(Bridge._chat_supervise, s)
249
- s._publish_catalog = lambda spec, source: s.published.append(spec["room"])
250
250
  s.published: list[str] = []
251
+ s._publish_catalog = lambda spec, source, *, force=False: s.published.append(
252
+ spec["room"]
253
+ )
251
254
  return s
252
255
 
253
256
 
@@ -294,9 +297,9 @@ def test_chat_supervisor_agent_mode_publishes_catalog_and_registers():
294
297
  registered: list[dict[str, str]] = []
295
298
  orig_publish = s._publish_catalog
296
299
 
297
- def publish_and_snapshot(sp, source):
300
+ def publish_and_snapshot(sp, source, *, force=False):
298
301
  registered.append(dict(s._agent_rooms)) # registered before publishing
299
- orig_publish(sp, source)
302
+ orig_publish(sp, source, force=force)
300
303
 
301
304
  s._publish_catalog = publish_and_snapshot
302
305
  s._chat_stop.clear()
@@ -406,6 +409,62 @@ def test_send_room_message_unstaffed_conversation_fails():
406
409
  assert out["ok"] is False and "not connected" in out["error"]
407
410
 
408
411
 
412
+ # ── catalog publish dedup ──────────────────────────────────────────────────────
413
+
414
+
415
+ class _CatalogSource:
416
+ def __init__(self) -> None:
417
+ self.sends: list[list[dict]] = []
418
+
419
+ def send_catalog(self, tools: list[dict]) -> None:
420
+ self.sends.append(tools)
421
+
422
+
423
+ def _catalog_stub(runnables: list[dict[str, Any]]):
424
+ s = types.SimpleNamespace()
425
+ s._lock = threading.Lock()
426
+ s._runnables_cache = runnables
427
+ s._catalog_index = {}
428
+ s._published_catalogs = {}
429
+ s._publish_catalog = types.MethodType(Bridge._publish_catalog, s)
430
+ return s
431
+
432
+
433
+ def test_publish_catalog_skips_unchanged_then_sends_on_change():
434
+ rn = {
435
+ "name": "reset-password",
436
+ "host": "web01",
437
+ "description": "",
438
+ "autonomy": "confirm",
439
+ "args": [],
440
+ "requireCommand": False,
441
+ "rawSpec": {"discoverable": ["self-service"]},
442
+ }
443
+ s = _catalog_stub([rn])
444
+ spec = {"room": "helpdesk/it", "queue": "it"}
445
+ src = _CatalogSource()
446
+
447
+ s._publish_catalog(spec, src) # first publish → sends
448
+ s._publish_catalog(spec, src) # discovery refresh, nothing changed → skipped
449
+ assert len(src.sends) == 1
450
+ assert [t["name"] for t in src.sends[0]] == ["reset-password"]
451
+
452
+ # A new self-service runnable appears → the next refresh re-sends.
453
+ s._runnables_cache = [rn, {**rn, "name": "request-vm"}]
454
+ s._publish_catalog(spec, src)
455
+ assert len(src.sends) == 2
456
+ assert [t["name"] for t in src.sends[1]] == ["reset-password", "request-vm"]
457
+
458
+
459
+ def test_publish_catalog_force_always_sends():
460
+ s = _catalog_stub([])
461
+ spec = {"room": "helpdesk/it", "queue": "it"}
462
+ src = _CatalogSource()
463
+ s._publish_catalog(spec, src, force=True)
464
+ s._publish_catalog(spec, src, force=True) # reconnect → resend even if empty
465
+ assert src.sends == [[], []]
466
+
467
+
409
468
  # ── thread controls (claim / resolve / reopen / close) ─────────────────────────
410
469
 
411
470