cdx-manager 0.18.5 → 0.18.7
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 +14 -7
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_18_6.md +8 -0
- package/changelogs/CHANGELOGS_0_18_7.md +5 -0
- package/checksums/release-archives.json +22 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +4 -2
- package/src/cli_args.py +2 -1
- package/src/commands/launch.py +56 -5
- package/src/commands/maintenance.py +2 -0
- package/src/commands/settings.py +1 -0
- package/src/commands/tray.py +7 -0
- package/src/provider_runtime.py +18 -3
- package/src/session_service.py +2 -0
- package/src/session_status.py +4 -1
- package/src/tray_logics.py +41 -19
- package/src/tray_plugins.py +4 -4
- package/src/update_manager.py +1 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<br clear="left"/>
|
|
6
6
|
|
|
7
|
-
[](LICENSE) ](LICENSE)  
|
|
8
8
|
|
|
9
9
|
**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.
|
|
10
10
|
|
|
@@ -377,12 +377,13 @@ It is off by default because turning it on writes into the provider's own
|
|
|
377
377
|
configuration and, on Codex, puts an approval prompt in front of you. That is
|
|
378
378
|
not something to do to sessions you already own without being asked.
|
|
379
379
|
|
|
380
|
-
|
|
381
|
-
notification includes at most 180 characters
|
|
382
|
-
a permission request may quote the
|
|
383
|
-
it wants to do. That text can be
|
|
384
|
-
|
|
385
|
-
|
|
380
|
+
New sessions inherit response previews when agent alerts were accepted during
|
|
381
|
+
tray installation. A completion notification includes at most 180 characters
|
|
382
|
+
of the agent's final response, and a permission request may quote the
|
|
383
|
+
provider's own one-line description of what it wants to do. That text can be
|
|
384
|
+
visible on a lock screen; disable it per session with
|
|
385
|
+
`cdx set <name> --notify-preview off`. Previews never read transcripts, and the
|
|
386
|
+
command a permission request is about is never included.
|
|
386
387
|
|
|
387
388
|
Both providers are subscribed to the same two hooks: `Stop` when a turn ends and
|
|
388
389
|
`PermissionRequest` when one blocks on your approval. Claude Code is also
|
|
@@ -465,6 +466,8 @@ Interactive launches and resumes set the terminal window title to `session — f
|
|
|
465
466
|
|
|
466
467
|
The title is only written when stdout is a terminal. Nothing is emitted for `--json` launches, redirected or piped output, `cdx run` and other headless runs, login flows, or Ollama sessions. Session names and directories are stripped of escape and control characters before the title is emitted, and cdx does not restore the previous title when the session ends.
|
|
467
468
|
|
|
469
|
+
Claude Code writes its own animated title while an action runs, and it redraws far more often than cdx re-asserts the shared title. So interactive Claude launches and resumes started by cdx set the Claude-specific `CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1` in the provider environment, which stops Claude from setting and clearing the title and leaves `session — folder` stable while Claude is busy. Claude's in-TUI activity indicator is unaffected, the value is forced so an ambient setting in your shell cannot hand the title back, and headless `cdx run`, Claude authentication flows, and every non-Claude provider keep their previous environment.
|
|
470
|
+
|
|
468
471
|
### Launch History
|
|
469
472
|
|
|
470
473
|
Every interactive `cdx <name>` launch is recorded under `CDX_HOME`, including success/failure, duration, cwd, launch settings, and transcript path.
|
|
@@ -1046,6 +1049,10 @@ Without `--yes`, an interactive confirmation is required. `cdx` sends an idempot
|
|
|
1046
1049
|
|
|
1047
1050
|
Contribution guidelines are available in [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
|
1048
1051
|
|
|
1052
|
+
Working on the tray companion? [`docs/tray-local-testing.md`](docs/tray-local-testing.md)
|
|
1053
|
+
covers the local loops, from rendering the menu to stdout to running a real icon
|
|
1054
|
+
in the menu bar.
|
|
1055
|
+
|
|
1049
1056
|
---
|
|
1050
1057
|
|
|
1051
1058
|
## License
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.18.
|
|
1
|
+
0.18.7
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# CDX Manager 0.18.6
|
|
2
|
+
|
|
3
|
+
## Features
|
|
4
|
+
|
|
5
|
+
- Launches can use an explicit `--dir`; each run records its directory for status and tray surfaces.
|
|
6
|
+
- The Logics tray card derives repositories from active runs and shows basename-only rows.
|
|
7
|
+
- `cdx update` names its install prefix, while `cdx tray doctor` reports the companion CDX command and hook-store alignment.
|
|
8
|
+
- Windows Terminal, GNOME Terminal, and Konsole open new tabs where their documented interfaces support it; other terminals retain the working window fallback.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# CDX Manager 0.18.7
|
|
2
|
+
|
|
3
|
+
## Fixes
|
|
4
|
+
|
|
5
|
+
- Interactive Claude sessions keep the cdx `session — folder` terminal title while Claude is running an action, instead of losing it to Claude's own animated title. Headless runs, Claude authentication flows, and other providers are unchanged.
|
|
@@ -246,6 +246,28 @@
|
|
|
246
246
|
"x86_64-pc-windows-msvc": "cc33c0f5a53472c6c1c72f458f33052abc09b6fe4ff5dfb5edae497c234bb2d0",
|
|
247
247
|
"x86_64-unknown-linux-musl": "d8fb9958112329487bd91be7f7505f18f886676f4309e30aa3877340e42c6c15"
|
|
248
248
|
}
|
|
249
|
+
},
|
|
250
|
+
"v0.18.5": {
|
|
251
|
+
"github_tarball_sha256": "d4251f1bdeb5aba31308cf749bf51733435bf2559f50a923ce090f0940c59876",
|
|
252
|
+
"github_zip_sha256": "36bc9d4e0a3cad306ccd405d7b4e4091418e21fe897b5ebcad4e275133615837",
|
|
253
|
+
"tray_assets": {
|
|
254
|
+
"aarch64-apple-darwin": "b0eb6653c3bbe1aba93d8b1966377e379eecea063ad01a10a11cdfd52d48e5ac",
|
|
255
|
+
"aarch64-unknown-linux-musl": "0cbca29099f1d320e917cb60e463fe0eb8fef9b78396db32520b8574ae4bb4a3",
|
|
256
|
+
"x86_64-apple-darwin": "a3e3d485552191f2456573dfbfee4259e430e7432b6d307eea00d5c527c86011",
|
|
257
|
+
"x86_64-pc-windows-msvc": "31a1743b0b6ec420a85c1679e93fcbd32c982a36e080a59d563276204682f152",
|
|
258
|
+
"x86_64-unknown-linux-musl": "49fba26d3deeab1e46af542528a5c71262e6c4d82898fcf989d7222a09a11d59"
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
"v0.18.6": {
|
|
262
|
+
"github_tarball_sha256": "31111eda2c31399b3fea14d80d10c0a2d10f15aab93a77a60939c86c564971ca",
|
|
263
|
+
"github_zip_sha256": "a3d78348416b3494645fa9f0467144c7e76b40d8862be959d125eb3d13805b15",
|
|
264
|
+
"tray_assets": {
|
|
265
|
+
"aarch64-apple-darwin": "aeb9267f72c3ee7426672d2e1fe98c0e39e0df795bbad6832299d4176c21022e",
|
|
266
|
+
"aarch64-unknown-linux-musl": "846969612d9a0e82f5f6cacc05aedc33153c1c325b8af95f63c8a2a5c2217261",
|
|
267
|
+
"x86_64-apple-darwin": "da5b4d3994fff9e616401e5a4cd01d68d216ddbeb6d41d96d67bbc789ef85118",
|
|
268
|
+
"x86_64-pc-windows-msvc": "296f760a7e10b7ef05936157b9de2b9d3b5e13f7ea2f57b7616705352ea97bd2",
|
|
269
|
+
"x86_64-unknown-linux-musl": "b360ea3ec14ab3aad0ccffcf477dee4bdadf6c1d4f150ef1cd13c7d293ae6829"
|
|
270
|
+
}
|
|
249
271
|
}
|
|
250
272
|
}
|
|
251
273
|
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
|
@@ -619,8 +619,10 @@ def main(argv, options=None):
|
|
|
619
619
|
if handler:
|
|
620
620
|
return handler(rest, ctx)
|
|
621
621
|
|
|
622
|
-
|
|
623
|
-
|
|
622
|
+
from .commands.launch import _parse_launch_args
|
|
623
|
+
directory, launch_args = _parse_launch_args(rest)
|
|
624
|
+
if all(arg in ("--json", "-r", "--resume") for arg in launch_args):
|
|
625
|
+
return handle_launch(command, ctx, resume=("-r" in launch_args or "--resume" in launch_args), directory=directory)
|
|
624
626
|
|
|
625
627
|
raise CdxError(f"Unknown command: {command}. Use cdx --help.")
|
|
626
628
|
|
package/src/cli_args.py
CHANGED
|
@@ -43,7 +43,7 @@ RESUME_USAGE = "Usage: cdx resume <name> [--json]"
|
|
|
43
43
|
CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--json]"
|
|
44
44
|
SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json"
|
|
45
45
|
NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
|
|
46
|
-
RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--detach] [--failover] [--refresh] --json"
|
|
46
|
+
RUN_USAGE = "Usage: cdx run [session] (--cwd|--dir) PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--detach] [--failover] [--refresh] --json"
|
|
47
47
|
RUN_JSON_REQUIRED = "cdx run: --json is required."
|
|
48
48
|
RUN_TARGET_REQUIRED = "cdx run: specify a session name or --provider PROVIDER."
|
|
49
49
|
RUN_SESSION_PROVIDER_CONFLICT = "cdx run: cannot specify both a session name and --provider."
|
|
@@ -585,6 +585,7 @@ def _parse_run_effort(reasoning_effort, power):
|
|
|
585
585
|
def _parse_run_args(args):
|
|
586
586
|
parsed = _parse_run_flag_args(args, {
|
|
587
587
|
"--cwd": {"key": "cwd", "type": "str", "default": None},
|
|
588
|
+
"--dir": {"key": "cwd", "type": "str", "default": None},
|
|
588
589
|
"--prompt-file": {"key": "prompt_file", "type": "str", "default": None},
|
|
589
590
|
"--prompt": {"key": "prompt", "type": "str", "default": None},
|
|
590
591
|
"--provider": {"key": "provider", "type": "str", "default": None, "transform": _parse_run_provider},
|
package/src/commands/launch.py
CHANGED
|
@@ -28,6 +28,54 @@ from ..errors import CdxError
|
|
|
28
28
|
from ..provider_runtime import _ensure_session_authentication, _get_auth_home, _run_interactive_provider_command
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def _parse_launch_args(args):
|
|
32
|
+
"""Return the per-launch directory and ordinary launch flags."""
|
|
33
|
+
directory = None
|
|
34
|
+
cleaned = []
|
|
35
|
+
index = 0
|
|
36
|
+
while index < len(args):
|
|
37
|
+
arg = args[index]
|
|
38
|
+
if arg == "--dir":
|
|
39
|
+
if directory is not None or index + 1 >= len(args):
|
|
40
|
+
raise CdxError("Usage: cdx <name> [--dir PATH] [--resume] [--json]")
|
|
41
|
+
directory = args[index + 1]
|
|
42
|
+
index += 2
|
|
43
|
+
continue
|
|
44
|
+
if arg.startswith("--dir="):
|
|
45
|
+
if directory is not None or not arg.split("=", 1)[1]:
|
|
46
|
+
raise CdxError("Usage: cdx <name> [--dir PATH] [--resume] [--json]")
|
|
47
|
+
directory = arg.split("=", 1)[1]
|
|
48
|
+
index += 1
|
|
49
|
+
continue
|
|
50
|
+
cleaned.append(arg)
|
|
51
|
+
index += 1
|
|
52
|
+
return directory, cleaned
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _launch_directory(session, ctx, explicit, json_flag):
|
|
56
|
+
cwd = os.path.realpath(explicit) if explicit else (ctx.get("cwd") or os.getcwd())
|
|
57
|
+
if explicit and not os.path.isdir(cwd):
|
|
58
|
+
raise CdxError(f"Invalid directory: {explicit or cwd}")
|
|
59
|
+
if explicit or json_flag or not ctx["stdin_is_tty"] or cwd not in {os.path.realpath(os.path.expanduser("~")), os.path.realpath(os.path.sep)}:
|
|
60
|
+
return cwd
|
|
61
|
+
recent = []
|
|
62
|
+
for entry in ctx["service"]["get_launch_history"](session["name"], limit=20):
|
|
63
|
+
path = entry.get("cwd")
|
|
64
|
+
if path and os.path.isdir(path) and path not in recent:
|
|
65
|
+
recent.append(path)
|
|
66
|
+
choices = recent + [cwd]
|
|
67
|
+
if len(choices) == 1:
|
|
68
|
+
return cwd
|
|
69
|
+
ctx["out"]("Choose launch directory:\n" + "".join(f" {index + 1}. {path}\n" for index, path in enumerate(choices)))
|
|
70
|
+
answer = (ctx["options"].get("input") or input)(f"Directory [1-{len(choices)}] (default {len(choices)}): ").strip()
|
|
71
|
+
if not answer:
|
|
72
|
+
return cwd
|
|
73
|
+
try:
|
|
74
|
+
return choices[int(answer) - 1]
|
|
75
|
+
except (ValueError, IndexError):
|
|
76
|
+
raise CdxError("Launch cancelled: choose a listed directory.") from None
|
|
77
|
+
|
|
78
|
+
|
|
31
79
|
def _format_resume_capability(capability, use_color=False):
|
|
32
80
|
name = capability["session"]
|
|
33
81
|
provider = capability["provider"]
|
|
@@ -74,12 +122,13 @@ def handle_can_resume(rest, ctx):
|
|
|
74
122
|
return 0
|
|
75
123
|
|
|
76
124
|
def handle_resume(rest, ctx):
|
|
125
|
+
directory, rest = _parse_launch_args(rest)
|
|
77
126
|
json_flag, args = _parse_json_flag(rest)
|
|
78
127
|
if len(args) != 1:
|
|
79
128
|
raise CdxError(RESUME_USAGE)
|
|
80
|
-
return handle_launch(args[0], ctx, resume=True, force_json=json_flag)
|
|
129
|
+
return handle_launch(args[0], ctx, resume=True, force_json=json_flag, directory=directory)
|
|
81
130
|
|
|
82
|
-
def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None):
|
|
131
|
+
def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None, directory=None):
|
|
83
132
|
json_flag = "--json" in ctx.get("raw_args", ctx["options"].get("raw_args", []))
|
|
84
133
|
if force_json is not None:
|
|
85
134
|
json_flag = force_json
|
|
@@ -91,6 +140,9 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
|
|
|
91
140
|
raise CdxError(
|
|
92
141
|
f"Provider {session['provider']} does not support native resume through cdx."
|
|
93
142
|
)
|
|
143
|
+
cwd = _launch_directory(session, ctx, directory, json_flag)
|
|
144
|
+
if not json_flag and not ctx["stdin_is_tty"]:
|
|
145
|
+
ctx["out"](f"Using directory: {cwd}\n")
|
|
94
146
|
_ensure_session_authentication(
|
|
95
147
|
session,
|
|
96
148
|
ctx["service"],
|
|
@@ -127,13 +179,12 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
|
|
|
127
179
|
# a real turn — so it gets no instruction it does not need.
|
|
128
180
|
notice += " — approve them once in Codex to enable"
|
|
129
181
|
ctx["out"](f"{_info(notice, ctx['use_color'])}\n")
|
|
130
|
-
cwd = ctx.get("cwd") or os.getcwd()
|
|
131
182
|
runtime_run_id = None
|
|
132
183
|
|
|
133
184
|
def runtime_lifecycle(event, info):
|
|
134
185
|
nonlocal runtime_run_id
|
|
135
186
|
if event == "started":
|
|
136
|
-
runtime = ctx["service"]["start_session_runtime"](session["name"], info)
|
|
187
|
+
runtime = ctx["service"]["start_session_runtime"](session["name"], {**info, "cwd": cwd})
|
|
137
188
|
runtime_run_id = runtime.get("runId")
|
|
138
189
|
elif event == "finished" and runtime_run_id:
|
|
139
190
|
ctx["service"]["finish_session_runtime"](
|
|
@@ -176,7 +227,7 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
|
|
|
176
227
|
**run_info,
|
|
177
228
|
})
|
|
178
229
|
if json_flag:
|
|
179
|
-
extra = {"session": ctx["service"]["get_session"](session["name"])}
|
|
230
|
+
extra = {"session": ctx["service"]["get_session"](session["name"]), "cwd": cwd}
|
|
180
231
|
if capability:
|
|
181
232
|
extra["resume"] = capability
|
|
182
233
|
_write_json(ctx, _json_success("resume" if resume else "launch", message, warnings=warnings, **extra))
|
|
@@ -774,10 +774,12 @@ def handle_update(rest, ctx):
|
|
|
774
774
|
current_version=current_version,
|
|
775
775
|
target_version=target_version,
|
|
776
776
|
mode=plan["mode"],
|
|
777
|
+
prefix=plan["prefix"],
|
|
777
778
|
steps=results,
|
|
778
779
|
))
|
|
779
780
|
return 0
|
|
780
781
|
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
782
|
+
ctx["out"](f"Install prefix: {plan['prefix']}\n")
|
|
781
783
|
for warning in warnings:
|
|
782
784
|
ctx["out"](f"{_warn(warning['message'], ctx['use_color'])}\n")
|
|
783
785
|
return 0
|
package/src/commands/settings.py
CHANGED
|
@@ -58,6 +58,7 @@ def _notification_setting_notice(settings, sessions, action):
|
|
|
58
58
|
notices.append(f"Agent alerts updated for {names} — this provider does not support notification hooks.")
|
|
59
59
|
return "\n".join(notices)
|
|
60
60
|
if "notify_preview" in settings:
|
|
61
|
+
names = _format_bulk_launch_summary(sessions)
|
|
61
62
|
if action == "set" and settings["notify_preview"]:
|
|
62
63
|
return f"Response previews enabled for {names} — completion text may be visible on the lock screen."
|
|
63
64
|
return f"Response previews disabled for {names}."
|
package/src/commands/tray.py
CHANGED
|
@@ -5,12 +5,14 @@ and `autostart` act on recorded state and say plainly when there is none, and
|
|
|
5
5
|
`doctor` reads all of it without changing any of it.
|
|
6
6
|
"""
|
|
7
7
|
import os
|
|
8
|
+
import shutil
|
|
8
9
|
import subprocess
|
|
9
10
|
from datetime import datetime, timezone
|
|
10
11
|
|
|
11
12
|
from ..cli_args import TRAY_USAGE, _parse_flag_args
|
|
12
13
|
from ..cli_helpers import _json_success, _write_json
|
|
13
14
|
from ..cli_render import _dim, _pad_table, _style, _warn
|
|
15
|
+
from ..config import get_cdx_home
|
|
14
16
|
from ..errors import CdxError
|
|
15
17
|
from ..tray_alerts import alerts_enabled, set_alerts
|
|
16
18
|
from ..tray_autostart import disable as autostart_disable
|
|
@@ -127,6 +129,7 @@ def _tray_status(args, ctx):
|
|
|
127
129
|
ctx["service"]["base_dir"],
|
|
128
130
|
env=ctx.get("env"),
|
|
129
131
|
cache=None if parsed["refresh"] else _plugin_cache(ctx),
|
|
132
|
+
directories=[row.get("cwd") for row in rows if row.get("active") and row.get("cwd")],
|
|
130
133
|
)
|
|
131
134
|
snapshot = build_snapshot(
|
|
132
135
|
rows,
|
|
@@ -591,6 +594,8 @@ def _tray_doctor(args, ctx):
|
|
|
591
594
|
instance = companion_instance(env=env)
|
|
592
595
|
desktop = desktop_capability(env=env)
|
|
593
596
|
toasts = toast_capability(env=env)
|
|
597
|
+
companion_cdx = (env or {}).get("CDX_TRAY_CDX") or shutil.which("cdx", path=(env or {}).get("PATH")) or "cdx"
|
|
598
|
+
hook_store = get_cdx_home(env)
|
|
594
599
|
|
|
595
600
|
checks = [
|
|
596
601
|
("companion", "installed" if state else ("override" if executable else "absent"),
|
|
@@ -608,6 +613,8 @@ def _tray_doctor(args, ctx):
|
|
|
608
613
|
("autostart", "on" if autostart["enabled"] else "off", autostart["artifact"] or "unsupported"),
|
|
609
614
|
("alerts", "on" if alerts_enabled(base_dir) else "muted",
|
|
610
615
|
"cdx tray alerts on|off"),
|
|
616
|
+
("companion_cdx", "override" if (env or {}).get("CDX_TRAY_CDX") else "path", companion_cdx),
|
|
617
|
+
("hook_store", "aligned" if os.path.realpath(hook_store) == os.path.realpath(base_dir) else "mismatched", hook_store),
|
|
611
618
|
("update", "interrupted" if interrupted_update(base_dir) else "clean",
|
|
612
619
|
"a staged companion was never promoted; run: cdx tray install"
|
|
613
620
|
if interrupted_update(base_dir) else "-"),
|
package/src/provider_runtime.py
CHANGED
|
@@ -108,6 +108,9 @@ def headless_permission_disables_network(provider, permission):
|
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
REDACTED_PROMPT_ARG = "[prompt redacted]"
|
|
111
|
+
# Documented Claude Code switch that stops it from setting and clearing the
|
|
112
|
+
# terminal title, leaving cdx's shared title keeper as the only writer.
|
|
113
|
+
CLAUDE_DISABLE_TERMINAL_TITLE_ENV = "CLAUDE_CODE_DISABLE_TERMINAL_TITLE"
|
|
111
114
|
CLAUDE_CLI_MODEL_ALIASES = {
|
|
112
115
|
"claude-sonnet": "sonnet",
|
|
113
116
|
"claude-opus": "opus",
|
|
@@ -148,11 +151,23 @@ def _anthropic_credentials_path(auth_home):
|
|
|
148
151
|
return os.path.join(auth_home, "credentials", f"{_anthropic_profile_name()}.json")
|
|
149
152
|
|
|
150
153
|
|
|
151
|
-
def _claude_env(base_env, auth_home):
|
|
154
|
+
def _claude_env(base_env, auth_home, own_terminal_title=False):
|
|
155
|
+
"""Build the Claude provider environment for one cdx spawn.
|
|
156
|
+
|
|
157
|
+
`own_terminal_title` is set by the interactive launch/resume paths, where
|
|
158
|
+
cdx's title keeper owns the `session — folder` title. Claude Code redraws an
|
|
159
|
+
animated busy title far more often than the keeper re-emits, so we tell it
|
|
160
|
+
to leave the title alone; its in-TUI activity indicator is unaffected. The
|
|
161
|
+
value is forced rather than defaulted so an ambient `0` in the operator's
|
|
162
|
+
shell cannot hand the title back to Claude. Headless and auth spawns do not
|
|
163
|
+
own a title and keep their previous environment.
|
|
164
|
+
"""
|
|
152
165
|
env = {**base_env, **_home_env_overrides(auth_home)}
|
|
153
166
|
env.pop("CLAUDE_CONFIG_DIR", None)
|
|
154
167
|
env.pop("CODEX_HOME", None)
|
|
155
168
|
env.setdefault("ANTHROPIC_PROFILE", _anthropic_profile_name())
|
|
169
|
+
if own_terminal_title:
|
|
170
|
+
env[CLAUDE_DISABLE_TERMINAL_TITLE_ENV] = "1"
|
|
156
171
|
return env
|
|
157
172
|
|
|
158
173
|
|
|
@@ -547,7 +562,7 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
547
562
|
if initial_prompt:
|
|
548
563
|
args.append(initial_prompt)
|
|
549
564
|
auth_home = _get_auth_home(session)
|
|
550
|
-
claude_env = _claude_env(env, auth_home)
|
|
565
|
+
claude_env = _claude_env(env, auth_home, own_terminal_title=True)
|
|
551
566
|
oauth_token = _read_claude_launch_oauth_token(auth_home)
|
|
552
567
|
if oauth_token:
|
|
553
568
|
claude_env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
@@ -693,7 +708,7 @@ def _build_resume_spec(session, cwd=None, env_override=None, capture_transcript=
|
|
|
693
708
|
if resume_prompt:
|
|
694
709
|
args.append(resume_prompt)
|
|
695
710
|
auth_home = _get_auth_home(session)
|
|
696
|
-
claude_env = _claude_env(env, auth_home)
|
|
711
|
+
claude_env = _claude_env(env, auth_home, own_terminal_title=True)
|
|
697
712
|
oauth_token = _read_claude_launch_oauth_token(auth_home)
|
|
698
713
|
if oauth_token:
|
|
699
714
|
claude_env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
package/src/session_service.py
CHANGED
|
@@ -629,6 +629,7 @@ def _launch_defaults_for(base_dir, provider):
|
|
|
629
629
|
try:
|
|
630
630
|
if supports_agent_alerts(provider) and alerts_default(base_dir):
|
|
631
631
|
launch["notify"] = True
|
|
632
|
+
launch["notify_preview"] = True
|
|
632
633
|
except OSError:
|
|
633
634
|
# An unreadable default is no default. Creating the session matters
|
|
634
635
|
# more than inheriting a preference.
|
|
@@ -739,6 +740,7 @@ def start_session_runtime(store, name, payload=None):
|
|
|
739
740
|
"pid": payload.get("pid") or os.getpid(),
|
|
740
741
|
"command": payload.get("command"),
|
|
741
742
|
"label": payload.get("label"),
|
|
743
|
+
"cwd": payload.get("cwd"),
|
|
742
744
|
"transcriptPath": payload.get("transcript_path") or payload.get("transcriptPath"),
|
|
743
745
|
}
|
|
744
746
|
|
package/src/session_status.py
CHANGED
|
@@ -268,6 +268,7 @@ def _status_row_from_session(store, s):
|
|
|
268
268
|
status = s.get("lastStatus")
|
|
269
269
|
enabled = s.get("enabled", True) is not False
|
|
270
270
|
row_status = status if enabled else None
|
|
271
|
+
runtime = _session_runtime(store, s["name"])
|
|
271
272
|
return {
|
|
272
273
|
"session_name": s["name"],
|
|
273
274
|
"label": s.get("label"),
|
|
@@ -279,7 +280,8 @@ def _status_row_from_session(store, s):
|
|
|
279
280
|
# ignored `--priority` entirely.
|
|
280
281
|
"priority": _row_priority(s),
|
|
281
282
|
"reasoning_effort": _row_reasoning_effort(s),
|
|
282
|
-
"active": bool(
|
|
283
|
+
"active": bool(runtime) if enabled else False,
|
|
284
|
+
"cwd": runtime.get("cwd") if runtime else None,
|
|
283
285
|
"status": "enabled" if enabled else "disabled",
|
|
284
286
|
"auth_status": (s.get("auth") or {}).get("status") or "unknown",
|
|
285
287
|
"auth_checked_at": _to_local_iso((s.get("auth") or {}).get("lastCheckedAt")),
|
|
@@ -313,6 +315,7 @@ def format_list_rows(store):
|
|
|
313
315
|
"provider": s["provider"] if has_multiple else None,
|
|
314
316
|
"enabled": s.get("enabled", True) is not False,
|
|
315
317
|
"active": bool(_session_runtime(store, s["name"])) if s.get("enabled", True) is not False else False,
|
|
318
|
+
"cwd": (_session_runtime(store, s["name"]) or {}).get("cwd"),
|
|
316
319
|
"enabled_status": "disabled" if s.get("enabled", True) is False else "enabled",
|
|
317
320
|
"status": s.get("lastStatus"),
|
|
318
321
|
"launch": s.get("launch") or {},
|
package/src/tray_logics.py
CHANGED
|
@@ -12,6 +12,7 @@ task, because it is what to do instead. A tray menu that listed everything would
|
|
|
12
12
|
be a worse `logics-manager status`, run more often.
|
|
13
13
|
"""
|
|
14
14
|
import json
|
|
15
|
+
import os
|
|
15
16
|
import subprocess
|
|
16
17
|
import time
|
|
17
18
|
|
|
@@ -21,7 +22,7 @@ from .tray_plugins import ADAPTER_TIMEOUT_SECONDS, CARD_TTL_SECONDS
|
|
|
21
22
|
_PRIORITY_ORDER = {"high": 0, "medium": 1, "low": 2}
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
def logics_card(env=None, now=None, cache=None, runner=None, executable=None):
|
|
25
|
+
def logics_card(env=None, now=None, cache=None, runner=None, executable=None, directories=None):
|
|
25
26
|
"""One card, or None when there is nothing truthful to show.
|
|
26
27
|
|
|
27
28
|
None covers every unhappy case on purpose — not installed, not a Logics
|
|
@@ -33,7 +34,7 @@ def logics_card(env=None, now=None, cache=None, runner=None, executable=None):
|
|
|
33
34
|
cached = _from_cache(cache, now)
|
|
34
35
|
if cached is not None:
|
|
35
36
|
return cached
|
|
36
|
-
status = _status(env=env, runner=runner, executable=executable)
|
|
37
|
+
status = _status(env=env, runner=runner, executable=executable, directories=directories)
|
|
37
38
|
card = _card_from_status(status)
|
|
38
39
|
_to_cache(cache, now, card)
|
|
39
40
|
return card
|
|
@@ -58,7 +59,16 @@ def _to_cache(cache, now, card):
|
|
|
58
59
|
cache["logics"] = {"at": now, "card": card}
|
|
59
60
|
|
|
60
61
|
|
|
61
|
-
def
|
|
62
|
+
def _repository_root(directory):
|
|
63
|
+
directory = os.path.realpath(directory or "")
|
|
64
|
+
while directory and directory != os.path.dirname(directory):
|
|
65
|
+
if os.path.exists(os.path.join(directory, ".git")):
|
|
66
|
+
return directory
|
|
67
|
+
directory = os.path.dirname(directory)
|
|
68
|
+
return directory if os.path.exists(os.path.join(directory, ".git")) else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _status(env=None, runner=None, executable=None, directories=None):
|
|
62
72
|
"""`logics-manager status --format json`, or None however it failed.
|
|
63
73
|
|
|
64
74
|
Resolved through CDX's own lookup rather than a path the tray knows: on a
|
|
@@ -68,26 +78,38 @@ def _status(env=None, runner=None, executable=None):
|
|
|
68
78
|
executable = executable or resolve_logics_manager(env or {})
|
|
69
79
|
if not executable:
|
|
70
80
|
return None
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
[executable, "status", "--format", "json"],
|
|
74
|
-
capture_output=True,
|
|
75
|
-
text=True,
|
|
76
|
-
timeout=ADAPTER_TIMEOUT_SECONDS,
|
|
77
|
-
check=False,
|
|
78
|
-
)
|
|
79
|
-
except (OSError, subprocess.SubprocessError):
|
|
80
|
-
return None
|
|
81
|
-
if getattr(completed, "returncode", 1) != 0:
|
|
82
|
-
return None
|
|
83
|
-
try:
|
|
84
|
-
payload = json.loads(completed.stdout)
|
|
85
|
-
except (TypeError, ValueError):
|
|
81
|
+
roots = sorted({root for root in (_repository_root(path) for path in (directories or [])) if root})
|
|
82
|
+
if directories is not None and not roots:
|
|
86
83
|
return None
|
|
87
|
-
|
|
84
|
+
payloads = []
|
|
85
|
+
for root in roots or [None]:
|
|
86
|
+
try:
|
|
87
|
+
completed = (runner or subprocess.run)(
|
|
88
|
+
[executable, "status", "--format", "json"], capture_output=True,
|
|
89
|
+
text=True, timeout=ADAPTER_TIMEOUT_SECONDS, check=False, **({"cwd": root} if root else {}),
|
|
90
|
+
)
|
|
91
|
+
payload = json.loads(completed.stdout) if getattr(completed, "returncode", 1) == 0 else None
|
|
92
|
+
except (OSError, subprocess.SubprocessError, TypeError, ValueError):
|
|
93
|
+
return None if not payloads else payloads
|
|
94
|
+
if isinstance(payload, dict):
|
|
95
|
+
payloads.append((root, payload))
|
|
96
|
+
return payloads if roots else (payloads[0][1] if payloads else None)
|
|
88
97
|
|
|
89
98
|
|
|
90
99
|
def _card_from_status(status):
|
|
100
|
+
if isinstance(status, list):
|
|
101
|
+
cards = [(root, _card_from_status(payload), len(payload.get("blocked_docs") or [])) for root, payload in status]
|
|
102
|
+
cards = [(root, card, blocked) for root, card, blocked in cards if card]
|
|
103
|
+
if not cards:
|
|
104
|
+
return None
|
|
105
|
+
root, card, _ = sorted(cards, key=lambda item: (-item[2], -len(item[1]["rows"]), item[0] or ""))[0]
|
|
106
|
+
repository_count = len(cards)
|
|
107
|
+
blocked_count = sum(item[2] for item in cards)
|
|
108
|
+
card["summary"] = f"{repository_count} repositories · {blocked_count} blocked"
|
|
109
|
+
prefix = os.path.basename(root or "")
|
|
110
|
+
for row in card["rows"]:
|
|
111
|
+
row["label"] = f"{prefix}: {row['label']}"
|
|
112
|
+
return card
|
|
91
113
|
if not isinstance(status, dict):
|
|
92
114
|
return None
|
|
93
115
|
blocked = [doc for doc in (status.get("blocked_docs") or []) if isinstance(doc, dict)]
|
package/src/tray_plugins.py
CHANGED
|
@@ -128,7 +128,7 @@ def _valid_action(action, plugin):
|
|
|
128
128
|
)
|
|
129
129
|
|
|
130
130
|
|
|
131
|
-
def collect_cards(base_dir, env=None, now=None, adapters=None, cache=None):
|
|
131
|
+
def collect_cards(base_dir, env=None, now=None, adapters=None, cache=None, directories=None):
|
|
132
132
|
"""Every enabled plugin's card, bounded, with failures dropped.
|
|
133
133
|
|
|
134
134
|
Never raises. An adapter that throws, hangs past its timeout, returns
|
|
@@ -143,7 +143,7 @@ def collect_cards(base_dir, env=None, now=None, adapters=None, cache=None):
|
|
|
143
143
|
if not adapter:
|
|
144
144
|
continue
|
|
145
145
|
try:
|
|
146
|
-
raw = adapter(env=env, now=now, cache=cache)
|
|
146
|
+
raw = adapter(env=env, now=now, cache=cache, directories=directories) if name == "logics" else adapter(env=env, now=now, cache=cache)
|
|
147
147
|
except Exception: # noqa: BLE001 - an integration cannot break the tray
|
|
148
148
|
continue
|
|
149
149
|
card = bounded_card(raw, name)
|
|
@@ -152,10 +152,10 @@ def collect_cards(base_dir, env=None, now=None, adapters=None, cache=None):
|
|
|
152
152
|
return cards
|
|
153
153
|
|
|
154
154
|
|
|
155
|
-
def _logics_adapter(env=None, now=None, cache=None):
|
|
155
|
+
def _logics_adapter(env=None, now=None, cache=None, directories=None):
|
|
156
156
|
from .tray_logics import logics_card
|
|
157
157
|
|
|
158
|
-
return logics_card(env=env, now=now, cache=cache)
|
|
158
|
+
return logics_card(env=env, now=now, cache=cache, directories=directories)
|
|
159
159
|
|
|
160
160
|
|
|
161
161
|
# The registry. A name here is the only way an adapter can run, and adding one
|
package/src/update_manager.py
CHANGED