cdx-manager 0.17.0 → 0.17.1

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CDX Manager
2
2
 
3
- [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.17.0-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
3
+ [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.17.1-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
4
4
 
5
5
  **Stop guessing which AI account still has quota.** `cdx` tracks the rate-limit window of every Codex and Claude account you own, tells you which one is usable right now, and launches it with isolated auth — in one command.
6
6
 
@@ -349,8 +349,8 @@ When a session's agent finishes a turn, or stops to wait for you, cdx raises a
349
349
  desktop notification naming the session and the repository:
350
350
 
351
351
  ```text
352
- ✓ work1 logics-manager · waiting for you
353
- ✓ codex-a cdx-manager · finished
352
+ ✓ work1 logics-manager · needs your attention
353
+ ✓ codex-a cdx-manager · turn complete
354
354
  ```
355
355
 
356
356
  Off until you ask for it, per session:
@@ -359,6 +359,7 @@ Off until you ask for it, per session:
359
359
  cdx set work1 --notify on # turn it on for one session
360
360
  cdx set --all --notify on # turn it on everywhere
361
361
  cdx set work1 --notify off # and back off; cdx removes what it installed
362
+ cdx set work1 --notify-preview on # include a short final-response preview
362
363
  ```
363
364
 
364
365
  Turning it on records the setting. The wiring happens at that session's **next
@@ -372,6 +373,16 @@ It is off by default because turning it on writes into the provider's own
372
373
  configuration and, on Codex, puts an approval prompt in front of you. That is
373
374
  not something to do to sessions you already own without being asked.
374
375
 
376
+ Response previews are separately off by default. When enabled, a completion
377
+ notification includes at most 180 characters of the agent's final response.
378
+ That text can be visible on a lock screen, so enable it only for sessions whose
379
+ output is safe to expose. Previews never read transcripts and are not shown for
380
+ permission requests.
381
+
382
+ `cdx notify` is the internal hook target, called by the provider; it is not the
383
+ setup command. `cdx ready` is separate: it notifies you when the next cooling-
384
+ down session becomes usable.
385
+
375
386
  Per platform: macOS uses `osascript`, Linux `notify-send`, Windows a toast, and
376
387
  WSL reaches the Windows notification centre through interop. On a host with no
377
388
  way to deliver one — a headless or SSH Linux session, or WSL with interop
@@ -489,7 +500,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
489
500
  | `cdx update [--check] [--yes] [--json] [--version TAG]` | Update cdx-manager using the installer that matches how it was installed |
490
501
  | `cdx update all [--yes] [--json]` | Check installed providers, RTK, and Ponytail across Codex profiles; show the plan and ask once before applying safe native/Homebrew updates and missing RTK/Ponytail setup |
491
502
  | `cdx ready [--refresh] [--json]` | Schedule an OS notification for the next cooling-down assistant that becomes ready, then return immediately |
492
- | `cdx notify` | Hook target: raises the desktop notification when a session's agent finishes a turn or waits for you. Called by the provider, not by you |
503
+ | `cdx notify` | Internal provider hook target for agent alerts; use `cdx set <name> --notify on` to enable it |
493
504
  | `cdx next [--json] [--refresh]` | Select the best next assistant using the same priority logic as `cdx status` |
494
505
  | `cdx select --provider PROVIDER [--min-reasoning-effort minimal\|low\|medium\|high\|xhigh] [--min-power minimal\|low\|medium\|high\|xhigh] [--require-ready] [--refresh] --json` | Select a suitable session for headless automation |
495
506
  | `cdx run [session] --cwd PATH (--prompt-file PATH\|--prompt TEXT\|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--reasoning-effort minimal\|low\|medium\|high\|xhigh] [--power minimal\|low\|medium\|high\|xhigh] [--permission MODE] [--timeout-seconds N] [--detach] [--failover] [--refresh] --json` | Run one headless task and return a stable JSON result; `--detach` returns the `run_id` at launch without waiting, `--failover` continues the task on the next account when this one hits its rate limit, `--prompt-file -` reads the prompt from stdin |
@@ -0,0 +1,29 @@
1
+ # CDX Manager 0.17.1
2
+
3
+ ## Fixes
4
+
5
+ ### Agent-alert setup now reports provider support accurately
6
+
7
+ `cdx set <name> --notify on` now distinguishes sessions where cdx can install
8
+ provider hooks (Claude and Codex) from providers that do not expose that
9
+ integration yet. A mixed bulk selection reports both outcomes instead of
10
+ promising a future hook installation that cannot happen.
11
+
12
+ The setting remains storable for unsupported providers, so bulk configuration
13
+ remains predictable and a future provider integration can honour the existing
14
+ opt-in.
15
+
16
+ ### Permission-request notifications constrain tool names
17
+
18
+ Tool names supplied by provider hook payloads now use the same printable,
19
+ single-line normalization as response previews and are capped at 80 characters.
20
+ This keeps permission notifications readable and prevents malformed payloads
21
+ from expanding or controlling desktop notification text.
22
+
23
+ ## Validation
24
+
25
+ - `npm run lint`
26
+ - `npm test`
27
+ - `npm run release:validate`
28
+ - `logics-manager lint --require-status`
29
+ - `logics-manager audit --legacy-cutoff-version 1.1.0 --group-by-doc`
@@ -184,6 +184,10 @@
184
184
  "v0.16.0": {
185
185
  "github_tarball_sha256": "bdcd2808922410b20567ba86589d49166c5c2b9404325195b28457df80ca3dad",
186
186
  "github_zip_sha256": "e89cb8c964304f0805d06da9d355d543f8a590e0febbf07e585a692ccf73ef99"
187
+ },
188
+ "v0.17.0": {
189
+ "github_tarball_sha256": "6f321106aa426590ce809788fab4f26401b4cc499a6c518a56a4f24f8bfb6c2e",
190
+ "github_zip_sha256": "72665a0006df40bb27301140b0ed6600a0d91b456facbb5a2aad2a917357ae75"
187
191
  }
188
192
  }
189
193
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "Terminal session manager for Codex and Claude accounts.",
5
5
  "license": "MIT",
6
6
  "author": "Alexandre Agostini",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cdx-manager"
7
- version = "0.17.0"
7
+ version = "0.17.1"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -19,13 +19,21 @@ from .notify import notification_channel, send_desktop_notification
19
19
  # Set on the provider process at launch; the hook runs as its child and inherits both.
20
20
  SESSION_ENV = "CDX_SESSION_NAME"
21
21
  ENABLED_ENV = "CDX_NOTIFY"
22
+ PREVIEW_ENV = "CDX_NOTIFY_PREVIEW"
22
23
 
23
24
  # Our entries are recognised by the command they run, not by a marker key of our
24
25
  # own: both providers validate this file against their own schema, and an extra
25
26
  # key they do not know is a rejection risk for no gain.
26
27
  HOOK_ARG = "notify"
27
28
 
28
- _WAITING_EVENTS = {"notification", "permissionrequest", "userpromptsubmit"}
29
+ _WAITING_EVENTS = {"notification", "permissionrequest"}
30
+ _PREVIEW_LIMIT = 180
31
+ _TOOL_NAME_LIMIT = 80
32
+
33
+
34
+ def supports_agent_alerts(provider):
35
+ """Whether cdx can provision provider hooks for this session."""
36
+ return provider in {"claude", "codex"}
29
37
 
30
38
 
31
39
  def resolve_hook_command(env=None):
@@ -70,9 +78,29 @@ def compose_notification(payload, env=None, cwd=None):
70
78
  return None
71
79
  directory = payload.get("cwd") or payload.get("workspace") or cwd or os.getcwd()
72
80
  event = str(payload.get("hook_event_name") or payload.get("eventName") or payload.get("type") or "").lower()
73
- state = "waiting for you" if event.replace("_", "") in _WAITING_EVENTS else "finished"
81
+ event = event.replace("_", "").replace("-", "")
82
+ state = "needs your attention" if event in _WAITING_EVENTS else "turn complete"
83
+ tool_name = _notification_text(payload.get("tool_name"), _TOOL_NAME_LIMIT)
84
+ if event == "permissionrequest" and tool_name:
85
+ state += f" ({tool_name})"
74
86
  where = os.path.basename(str(directory).rstrip("/\\")) or directory
75
- return f"✓ {session}", f"{where} · {state}"
87
+ preview = _notification_preview(payload, event, env)
88
+ return f"✓ {session}", f"{where} · {state}" + (f" — {preview}" if preview else "")
89
+
90
+
91
+ def _notification_preview(payload, event, env):
92
+ if event != "stop" or env.get(PREVIEW_ENV) != "1":
93
+ return ""
94
+ return _notification_text(payload.get("last_assistant_message"), _PREVIEW_LIMIT)
95
+
96
+
97
+ def _notification_text(value, limit):
98
+ if not isinstance(value, str):
99
+ return ""
100
+ value = " ".join("".join(char if char.isprintable() else " " for char in value).split())
101
+ if len(value) <= limit:
102
+ return value
103
+ return value[:limit - 1].rstrip() + "…"
76
104
 
77
105
 
78
106
  def handle_notify(rest, ctx):
@@ -82,9 +110,12 @@ def handle_notify(rest, ctx):
82
110
  stdin_text = None
83
111
  # "stdin" in ctx is the {"isTTY": ...} descriptor, not a stream.
84
112
  reader = ctx.get("prompt_stdin") or sys.stdin
113
+ env = os.environ if ctx.get("env") is None else ctx["env"]
114
+ if ctx.get("stdin_is_tty") and not env.get(SESSION_ENV):
115
+ ctx["out"]("cdx notify is called by provider hooks. Enable agent alerts with: cdx set <name> --notify on\n")
116
+ return 0
85
117
  if reader is not None and not ctx.get("stdin_is_tty"):
86
118
  stdin_text = reader.read()
87
- env = os.environ if ctx.get("env") is None else ctx["env"]
88
119
  composed = compose_notification(read_hook_payload(rest, stdin_text), env, ctx.get("cwd"))
89
120
  if composed:
90
121
  send_desktop_notification(*composed, spawn_sync=ctx.get("spawn_sync"), env=env)
@@ -98,6 +129,8 @@ def launch_notify_env(session, enabled, env=None):
98
129
  values = {SESSION_ENV: session["name"]}
99
130
  if not enabled:
100
131
  values[ENABLED_ENV] = "0"
132
+ elif (session.get("launch") or {}).get("notify_preview") is True:
133
+ values[PREVIEW_ENV] = "1"
101
134
  return values
102
135
 
103
136
 
@@ -130,6 +163,8 @@ def provision(auth_home, provider, enabled, env=None, spawn_sync=None, base_dir=
130
163
  # Nothing could be delivered on this host, so installing a hook would only
131
164
  # buy the user an approval prompt for a feature that shows nothing.
132
165
  enabled = False
166
+ if not supports_agent_alerts(provider):
167
+ return False
133
168
  command = resolve_hook_command(env)
134
169
  if provider == "claude":
135
170
  return _apply_hooks(hook_config_path(auth_home, provider), enabled, command)
@@ -224,7 +259,7 @@ def _write_codex_plugin(auth_home, command, base_dir=None):
224
259
  os.path.join(root, "hooks", "cdx-hooks.json"): {
225
260
  "hooks": {
226
261
  event: [{"hooks": [{"type": "command", "command": f"{command} {HOOK_ARG}"}]}]
227
- for event in ("Stop", "Notification")
262
+ for event in ("Stop", "PermissionRequest")
228
263
  },
229
264
  },
230
265
  }
package/src/cli.py CHANGED
@@ -231,8 +231,8 @@ def _print_help(use_color=False):
231
231
  f" {_style('cdx config <name> [--json]', '36', use_color)}",
232
232
  f" {_style('cdx configs [--json]', '36', use_color)}",
233
233
  f" {_style('cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]', '36', use_color)}",
234
- f" {_style('cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--priority 0..100] [--json]', '36', use_color)}",
235
- f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--model|--priority|--all) [--json]', '36', use_color)}",
234
+ f" {_style('cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--notify on|off] [--notify-preview on|off] [--model MODEL] [--priority 0..100] [--json]', '36', use_color)}",
235
+ f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--notify|--notify-preview|--model|--priority|--all) [--json]', '36', use_color)}",
236
236
  f" {_style('cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
237
237
  f" {_style('cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
238
238
  f" {_style('cdx last [--json]', '36', use_color)}",
@@ -259,7 +259,10 @@ def _print_help(use_color=False):
259
259
  f" {_style('cdx view [--json]', '36', use_color)}",
260
260
  f" {_style('cdx update [all] [--check] [--yes] [--json] [--version TAG]', '36', use_color)}",
261
261
  f" {_style('cdx ready [--refresh] [--json]', '36', use_color)}",
262
- f" {_style('cdx notify', '36', use_color)} # hook target, called by the provider",
262
+ "",
263
+ _style("Notifications:", "1", use_color),
264
+ f" {_style('cdx set <name> --notify on', '36', use_color)} # agent finishes or needs attention",
265
+ f" {_style('cdx ready', '36', use_color)} # next session to become ready",
263
266
  f" {_style('cdx <name> [--json]', '36', use_color)}",
264
267
  f" {_style('cdx --help', '36', use_color)}",
265
268
  f" {_style('cdx --version', '36', use_color)}",
package/src/cli_args.py CHANGED
@@ -249,6 +249,7 @@ def _parse_set_args(args):
249
249
  "--rtk": {"key": "rtk", "type": "str", "default": None, "transform": _parse_fast_value},
250
250
  "--logics": {"key": "logics", "type": "str", "default": None, "transform": _parse_fast_value},
251
251
  "--notify": {"key": "notify", "type": "str", "default": None, "transform": _parse_fast_value},
252
+ "--notify-preview": {"key": "notify_preview", "type": "str", "default": None, "transform": _parse_fast_value},
252
253
  "--model": {"key": "model", "type": "str", "default": None},
253
254
  "--fallback-model": {"key": "fallback_model", "type": "str", "default": None},
254
255
  "--budget": {"key": "budget", "type": "str", "default": None, "transform": _parse_budget_value},
@@ -270,7 +271,7 @@ def _parse_set_args(args):
270
271
  raise CdxError(SET_USAGE)
271
272
  settings = {
272
273
  key: parsed[key]
273
- for key in ("power", "permission", "fast", "rtk", "logics", "notify", "model", "fallback_model", "budget", "extra_args", "priority")
274
+ for key in ("power", "permission", "fast", "rtk", "logics", "notify", "notify_preview", "model", "fallback_model", "budget", "extra_args", "priority")
274
275
  if parsed[key] is not None
275
276
  }
276
277
  if not settings:
@@ -293,6 +294,7 @@ def _parse_unset_args(args):
293
294
  "--rtk": {"key": "rtk", "type": "bool", "default": False},
294
295
  "--logics": {"key": "logics", "type": "bool", "default": False},
295
296
  "--notify": {"key": "notify", "type": "bool", "default": False},
297
+ "--notify-preview": {"key": "notify_preview", "type": "bool", "default": False},
296
298
  "--model": {"key": "model", "type": "bool", "default": False},
297
299
  "--fallback-model": {"key": "fallback_model", "type": "bool", "default": False},
298
300
  "--budget": {"key": "budget", "type": "bool", "default": False},
@@ -314,7 +316,7 @@ def _parse_unset_args(args):
314
316
  if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
315
317
  raise CdxError(UNSET_USAGE)
316
318
  _UNSET_KEYS = (
317
- "power", "reasoning_effort", "permission", "fast", "rtk", "logics", "notify",
319
+ "power", "reasoning_effort", "permission", "fast", "rtk", "logics", "notify", "notify_preview",
318
320
  "model", "fallback_model", "budget", "extra_args", "priority",
319
321
  )
320
322
  keys = list(_UNSET_KEYS) if parsed["all"] else [key for key in _UNSET_KEYS if parsed[key]]
@@ -486,7 +486,8 @@ def _format_launch_config(session, use_color=False):
486
486
  ("fast", "Fast"),
487
487
  ("rtk", "RTK"),
488
488
  ("logics", "Logics"),
489
- ("notify", "Notify"),
489
+ ("notify", "Agent alerts"),
490
+ ("notify_preview", "Response preview"),
490
491
  ("model", "Model"),
491
492
  ("fallback_model", "Fallback"),
492
493
  ("budget", "Budget"),
@@ -509,18 +510,18 @@ def _format_launch_config(session, use_color=False):
509
510
  def _format_launch_settings_hint(name="<name>"):
510
511
  return (
511
512
  f"Set a value: cdx set {name} --power medium --permission auto "
512
- "--fast on --rtk on --logics on --notify on --model MODEL --priority 80"
513
+ "--fast on --rtk on --logics on --notify on --notify-preview on --model MODEL --priority 80"
513
514
  )
514
515
 
515
516
  def _format_launch_setting_value(launch, key, use_color=False):
516
- if key in ("fast", "rtk", "logics", "notify"):
517
+ if key in ("fast", "rtk", "logics", "notify", "notify_preview"):
517
518
  if launch.get(key) is True:
518
519
  return _style("on", "32", use_color)
519
520
  if launch.get(key) is False:
520
521
  return _style("off", "2", use_color)
521
522
  if key == "logics":
522
523
  return _dim("auto", use_color)
523
- if key == "notify":
524
+ if key in ("notify", "notify_preview"):
524
525
  return _style("off", "2", use_color)
525
526
  return _dim("default", use_color)
526
527
  value = launch.get(key)
package/src/cli_render.py CHANGED
@@ -175,6 +175,7 @@ def _format_sessions(service, use_color=False):
175
175
  f" {_style('cdx configs', '36', use_color)}",
176
176
  f" {_style('cdx stats', '36', use_color)}",
177
177
  f" {_style('cdx ready', '36', use_color)}",
178
+ f" {_style('cdx set <name> --notify on', '36', use_color)}",
178
179
  f" {_style('cdx perm all default', '36', use_color)}",
179
180
  f" {_style('cdx handoff <source> <target>', '36', use_color)}",
180
181
  f" {_style('cdx history', '36', use_color)}",
@@ -3,6 +3,7 @@
3
3
  Split out of cli_commands.py. Moved verbatim; re-exported by cli_commands.
4
4
  """
5
5
 
6
+ from ..agent_notify import supports_agent_alerts
6
7
  from ..cli_args import _parse_launch_setting_alias_args, _parse_set_args, _parse_unset_args
7
8
  from ..cli_helpers import _format_launch_config, _json_success, _write_json
8
9
  from ..cli_render import _success
@@ -40,6 +41,28 @@ def _format_bulk_launch_summary(sessions):
40
41
  return ", ".join(names)
41
42
  return ", ".join(names[:8]) + f", +{len(names) - 8} more"
42
43
 
44
+
45
+ def _notification_setting_notice(settings, sessions, action):
46
+ if "notify" in settings:
47
+ supported = [session for session in sessions if supports_agent_alerts(session["provider"])]
48
+ unsupported = [session for session in sessions if not supports_agent_alerts(session["provider"])]
49
+ notices = []
50
+ if supported:
51
+ names = _format_bulk_launch_summary(supported)
52
+ if action == "set" and settings["notify"]:
53
+ notices.append(f"Agent alerts enabled for {names} — hooks install on the next interactive launch.")
54
+ else:
55
+ notices.append(f"Agent alerts disabled for {names} — cdx removes its hooks on the next interactive launch.")
56
+ if unsupported:
57
+ names = _format_bulk_launch_summary(unsupported)
58
+ notices.append(f"Agent alerts updated for {names} — this provider does not support notification hooks.")
59
+ return "\n".join(notices)
60
+ if "notify_preview" in settings:
61
+ if action == "set" and settings["notify_preview"]:
62
+ return f"Response previews enabled for {names} — completion text may be visible on the lock screen."
63
+ return f"Response previews disabled for {names}."
64
+ return None
65
+
43
66
  def _apply_launch_settings(parsed, ctx, action="set"):
44
67
  targets = _resolve_bulk_launch_targets(parsed, ctx["service"])
45
68
  sessions = [
@@ -63,6 +86,9 @@ def _apply_launch_settings(parsed, ctx, action="set"):
63
86
  _write_json(ctx, payload)
64
87
  return 0
65
88
  ctx["out"](f"{_success(message, ctx['use_color'])}\n")
89
+ notice = _notification_setting_notice(parsed["settings"], sessions, action)
90
+ if notice:
91
+ ctx["out"](f"{notice}\n")
66
92
  if len(sessions) == 1:
67
93
  ctx["out"](f"{_format_launch_config(sessions[0], ctx['use_color'])}\n")
68
94
  return 0
@@ -90,6 +116,9 @@ def _clear_launch_settings(parsed, ctx, action="unset"):
90
116
  _write_json(ctx, payload)
91
117
  return 0
92
118
  ctx["out"](f"{_success(message, ctx['use_color'])}\n")
119
+ notice = _notification_setting_notice({key: False for key in parsed["keys"]}, sessions, action)
120
+ if notice:
121
+ ctx["out"](f"{notice}\n")
93
122
  if len(sessions) == 1:
94
123
  ctx["out"](f"{_format_launch_config(sessions[0], ctx['use_color'])}\n")
95
124
  return 0
@@ -126,7 +126,7 @@ def _format_launch_configs(sessions, use_color=False):
126
126
  for key in ("fallback_model", "budget")
127
127
  )
128
128
  has_extra = any((session.get("launch") or {}).get("extra_args") for session in sessions)
129
- headers = ["SESSION", "PROVIDER", "POWER", "PERMISSION", "FAST", "RTK", "LOGICS", "MODEL"]
129
+ headers = ["SESSION", "PROVIDER", "POWER", "PERMISSION", "FAST", "RTK", "LOGICS", "ALERTS", "MODEL"]
130
130
  if has_headless:
131
131
  headers += ["FALLBACK", "BUDGET"]
132
132
  if has_extra:
@@ -143,6 +143,7 @@ def _format_launch_configs(sessions, use_color=False):
143
143
  _format_launch_setting_value(launch, "fast", use_color),
144
144
  _format_launch_setting_value(launch, "rtk", use_color),
145
145
  _format_launch_setting_value(launch, "logics", use_color),
146
+ _format_launch_setting_value(launch, "notify", use_color),
146
147
  _format_launch_setting_value(launch, "model", use_color),
147
148
  ]
148
149
  if has_headless:
@@ -223,6 +223,18 @@ def _normalize_launch_settings(settings, mark_fast_service_tier=True):
223
223
  normalized["notify"] = False
224
224
  else:
225
225
  raise CdxError(f"Unsupported notify value: {settings['notify']}")
226
+ if "notify_preview" in settings and settings["notify_preview"] is not None:
227
+ value = settings["notify_preview"]
228
+ if isinstance(value, bool):
229
+ normalized["notify_preview"] = value
230
+ else:
231
+ text = str(value).strip().lower()
232
+ if text in ("on", "true", "1", "yes"):
233
+ normalized["notify_preview"] = True
234
+ elif text in ("off", "false", "0", "no"):
235
+ normalized["notify_preview"] = False
236
+ else:
237
+ raise CdxError(f"Unsupported notify preview value: {settings['notify_preview']}")
226
238
  if "model" in settings and settings["model"] is not None:
227
239
  model = str(settings["model"]).strip()
228
240
  if not model:
@@ -361,7 +373,7 @@ def unset_launch_settings(store, name, keys):
361
373
  if not keys:
362
374
  raise CdxError("At least one launch setting is required.")
363
375
  allowed = {
364
- "power", "reasoning_effort", "reasoningEffort", "permission", "fast", "rtk", "logics", "notify",
376
+ "power", "reasoning_effort", "reasoningEffort", "permission", "fast", "rtk", "logics", "notify", "notify_preview",
365
377
  "model", "fallback_model", "budget", "extra_args", "priority",
366
378
  }
367
379
  unknown = [key for key in keys if key not in allowed]