@team-agent/installer 0.1.7 → 0.1.10
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 +6 -1
- package/README.zh.md +4 -1
- package/package.json +1 -2
- package/schemas/team.schema.json +1 -1
- package/skills/team-agent/SKILL.md +8 -2
- package/src/team_agent/runtime.py +599 -27
- package/src/team_agent/spec.py +1 -1
- package/tests/run_tests.py +0 -5651
|
@@ -54,6 +54,8 @@ TMUX_PANE_FORMAT = (
|
|
|
54
54
|
"#{pane_current_path}\t#{session_attached}"
|
|
55
55
|
)
|
|
56
56
|
HEALTH_STATUSES = {"RUNNING", "IDLE", "AWAITING_APPROVAL", "BLOCKED", "ERROR", "DONE"}
|
|
57
|
+
GHOSTTY_DISPLAY_BACKENDS = {"ghostty", "ghostty_window", "ghostty_workspace"}
|
|
58
|
+
GHOSTTY_WORKSPACE_PANES_PER_WINDOW = 3
|
|
57
59
|
PEEK_MAX_LINES = 80
|
|
58
60
|
PEEK_SEARCH_SCAN_LINES = 300
|
|
59
61
|
PEEK_MAX_MATCHES = 5
|
|
@@ -226,6 +228,22 @@ def _spec_team_dir(spec_path: Path, workspace: Path) -> Path:
|
|
|
226
228
|
return workspace.resolve() / ".team" / "current"
|
|
227
229
|
|
|
228
230
|
|
|
231
|
+
def _is_team_doc_dir(team_dir: Path) -> bool:
|
|
232
|
+
return (team_dir / "TEAM.md").exists() and (team_dir / "agents").is_dir()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _compile_team_dir_spec(team_dir: Path, workspace: Path) -> dict[str, Any]:
|
|
236
|
+
from team_agent.compiler import compile_team
|
|
237
|
+
|
|
238
|
+
spec_path = team_dir / "team.spec.yaml"
|
|
239
|
+
compiled = compile_team(team_dir, spec_path)
|
|
240
|
+
if compiled["spec"].get("context", {}).get("state_file") == "team_state.md":
|
|
241
|
+
state_file = str(team_dir.relative_to(workspace) / "team_state.md") if team_dir.is_relative_to(workspace) else "team_state.md"
|
|
242
|
+
compiled["spec"]["context"]["state_file"] = state_file
|
|
243
|
+
spec_path.write_text(dumps(compiled["spec"]), encoding="utf-8")
|
|
244
|
+
return compiled
|
|
245
|
+
|
|
246
|
+
|
|
229
247
|
def _attach_team_profile_dirs(spec: dict[str, Any], spec_path: Path, workspace: Path | None = None, team_dir: Path | None = None) -> None:
|
|
230
248
|
workspace = workspace.resolve() if workspace else workspace_from_spec(spec, spec_path)
|
|
231
249
|
team_dir = team_dir.resolve() if team_dir else _spec_team_dir(spec_path, workspace)
|
|
@@ -456,10 +474,16 @@ def launch(
|
|
|
456
474
|
timeout_s=1.5,
|
|
457
475
|
exclude_session_ids=known_session_ids,
|
|
458
476
|
)
|
|
459
|
-
if state.get("display_backend") in
|
|
477
|
+
if state.get("display_backend") in GHOSTTY_DISPLAY_BACKENDS:
|
|
460
478
|
display_jobs.append((agent["id"], agent))
|
|
461
479
|
started.append({"agent_id": agent["id"], "provider": agent["provider"], "window": agent["id"]})
|
|
462
|
-
for agent_id, display in _open_worker_displays(
|
|
480
|
+
for agent_id, display in _open_worker_displays(
|
|
481
|
+
workspace,
|
|
482
|
+
session_name,
|
|
483
|
+
display_jobs,
|
|
484
|
+
event_log,
|
|
485
|
+
state.get("display_backend", "none"),
|
|
486
|
+
).items():
|
|
463
487
|
if agent_id in state["agents"]:
|
|
464
488
|
state["agents"][agent_id]["display"] = display
|
|
465
489
|
save_runtime_state(workspace, state)
|
|
@@ -1753,15 +1777,21 @@ def shutdown(workspace: Path, keep_logs: bool = True) -> dict[str, Any]:
|
|
|
1753
1777
|
if proc.returncode == 0:
|
|
1754
1778
|
log_path.write_text(proc.stdout, encoding="utf-8")
|
|
1755
1779
|
captured.append(str(log_path))
|
|
1780
|
+
_close_ghostty_workspace(state, event_log)
|
|
1756
1781
|
for agent_id, agent_state in state.get("agents", {}).items():
|
|
1757
1782
|
_close_ghostty_display(agent_id, agent_state, event_log)
|
|
1758
1783
|
closed_displays.add(agent_id)
|
|
1759
1784
|
proc = run_cmd(["tmux", "kill-session", "-t", session_name], timeout=10)
|
|
1760
1785
|
if proc.returncode != 0:
|
|
1761
|
-
|
|
1762
|
-
|
|
1786
|
+
if "can't find session" in proc.stderr:
|
|
1787
|
+
event_log.write("shutdown.idempotent", session=session_name, reason="session disappeared before kill")
|
|
1788
|
+
else:
|
|
1789
|
+
raise RuntimeError(f"tmux kill-session failed: {proc.stderr.strip()}")
|
|
1790
|
+
else:
|
|
1791
|
+
event_log.write("shutdown.kill_session", session=session_name, keep_logs=keep_logs, captured=captured)
|
|
1763
1792
|
else:
|
|
1764
1793
|
event_log.write("shutdown.idempotent", session=session_name, reason="session missing")
|
|
1794
|
+
_close_ghostty_workspace(state, event_log)
|
|
1765
1795
|
for agent_id, agent_state in state.get("agents", {}).items():
|
|
1766
1796
|
if agent_id not in closed_displays:
|
|
1767
1797
|
_close_ghostty_display(agent_id, agent_state, event_log)
|
|
@@ -1792,10 +1822,16 @@ def shutdown(workspace: Path, keep_logs: bool = True) -> dict[str, Any]:
|
|
|
1792
1822
|
def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None) -> dict[str, Any]:
|
|
1793
1823
|
state = _select_restart_state(workspace, team)
|
|
1794
1824
|
spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
|
|
1795
|
-
if not spec_path.exists():
|
|
1796
|
-
raise RuntimeError(f"missing spec for restart: {spec_path}")
|
|
1797
|
-
spec = load_spec(spec_path)
|
|
1798
1825
|
team_dir = Path(str(state.get("team_dir"))) if state.get("team_dir") else _spec_team_dir(spec_path, workspace)
|
|
1826
|
+
if _is_team_doc_dir(team_dir):
|
|
1827
|
+
compiled = _compile_team_dir_spec(team_dir, workspace)
|
|
1828
|
+
spec = compiled["spec"]
|
|
1829
|
+
spec_path = team_dir / "team.spec.yaml"
|
|
1830
|
+
state["spec_path"] = str(spec_path)
|
|
1831
|
+
else:
|
|
1832
|
+
if not spec_path.exists():
|
|
1833
|
+
raise RuntimeError(f"missing spec for restart: {spec_path}")
|
|
1834
|
+
spec = load_spec(spec_path)
|
|
1799
1835
|
_attach_team_profile_dirs(spec, spec_path, workspace, team_dir)
|
|
1800
1836
|
ensure_workspace_dirs(workspace)
|
|
1801
1837
|
event_log = EventLog(workspace)
|
|
@@ -1810,6 +1846,9 @@ def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None)
|
|
|
1810
1846
|
raise RuntimeError(_tmux_session_conflict_error(session_name))
|
|
1811
1847
|
runtime_cfg = _effective_runtime_config(spec.get("runtime", {}))
|
|
1812
1848
|
display_backend = spec.get("runtime", {}).get("display_backend", state.get("display_backend", "none"))
|
|
1849
|
+
_close_ghostty_workspace(state, event_log)
|
|
1850
|
+
for agent_id, agent_state in state.get("agents", {}).items():
|
|
1851
|
+
_close_ghostty_display(agent_id, agent_state, event_log)
|
|
1813
1852
|
state["display_backend"] = display_backend
|
|
1814
1853
|
restart_agents = [
|
|
1815
1854
|
agent
|
|
@@ -1995,7 +2034,7 @@ def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None)
|
|
|
1995
2034
|
timeout_s=1.5,
|
|
1996
2035
|
exclude_session_ids=known_session_ids,
|
|
1997
2036
|
)
|
|
1998
|
-
if display_backend in
|
|
2037
|
+
if display_backend in GHOSTTY_DISPLAY_BACKENDS:
|
|
1999
2038
|
display_jobs.append((agent["id"], agent))
|
|
2000
2039
|
new_agents[agent["id"]] = agent_state
|
|
2001
2040
|
restarted.append(
|
|
@@ -2006,7 +2045,7 @@ def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None)
|
|
|
2006
2045
|
"display_target": None,
|
|
2007
2046
|
}
|
|
2008
2047
|
)
|
|
2009
|
-
display_results = _open_worker_displays(workspace, session_name, display_jobs, event_log)
|
|
2048
|
+
display_results = _open_worker_displays(workspace, session_name, display_jobs, event_log, display_backend)
|
|
2010
2049
|
for agent_id, display in display_results.items():
|
|
2011
2050
|
if agent_id in new_agents:
|
|
2012
2051
|
new_agents[agent_id]["display"] = display
|
|
@@ -2072,6 +2111,10 @@ def _start_agent_unlocked(workspace: Path, agent_id: str, force: bool, open_disp
|
|
|
2072
2111
|
display = agent_state.get("display") or {}
|
|
2073
2112
|
if display.get("status") != "opened":
|
|
2074
2113
|
agent_state["display"] = _open_ghostty_worker_window(workspace, session_name, agent_id, agent, event_log)
|
|
2114
|
+
elif open_display and state.get("display_backend") == "ghostty_workspace":
|
|
2115
|
+
display = agent_state.get("display") or {}
|
|
2116
|
+
if display.get("status") != "opened":
|
|
2117
|
+
agent_state["display"] = _open_ghostty_workspace_agent_display(session_name, agent_id, agent, display, event_log)
|
|
2075
2118
|
state["agents"][agent_id] = agent_state
|
|
2076
2119
|
save_runtime_state(workspace, state)
|
|
2077
2120
|
write_team_state(workspace, spec, state)
|
|
@@ -2248,6 +2291,14 @@ def _start_agent_unlocked(workspace: Path, agent_id: str, force: bool, open_disp
|
|
|
2248
2291
|
_capture_agent_session(workspace, agent_id, agent_state, event_log, timeout_s=1.5, exclude_session_ids=known_session_ids)
|
|
2249
2292
|
if open_display and state.get("display_backend") in {"ghostty", "ghostty_window"}:
|
|
2250
2293
|
agent_state["display"] = _open_ghostty_worker_window(workspace, session_name, agent_id, agent, event_log)
|
|
2294
|
+
elif open_display and state.get("display_backend") == "ghostty_workspace":
|
|
2295
|
+
agent_state["display"] = _open_ghostty_workspace_agent_display(
|
|
2296
|
+
session_name,
|
|
2297
|
+
agent_id,
|
|
2298
|
+
agent,
|
|
2299
|
+
previous.get("display") or {},
|
|
2300
|
+
event_log,
|
|
2301
|
+
)
|
|
2251
2302
|
state["agents"][agent_id] = agent_state
|
|
2252
2303
|
save_runtime_state(workspace, state)
|
|
2253
2304
|
store = MessageStore(workspace)
|
|
@@ -3030,7 +3081,7 @@ def preflight(team_dir: Path) -> dict[str, Any]:
|
|
|
3030
3081
|
ok = ok and bool(tmux_path)
|
|
3031
3082
|
ghostty = _ghostty_command()
|
|
3032
3083
|
ghostty_check = {"name": "ghostty", "ok": bool(ghostty), "path": ghostty, "required": False}
|
|
3033
|
-
if spec and spec.get("runtime", {}).get("display_backend") in
|
|
3084
|
+
if spec and spec.get("runtime", {}).get("display_backend") in GHOSTTY_DISPLAY_BACKENDS:
|
|
3034
3085
|
ghostty_check["required"] = True
|
|
3035
3086
|
ok = ok and bool(ghostty)
|
|
3036
3087
|
checks.append(ghostty_check)
|
|
@@ -3105,18 +3156,12 @@ def quick_start(
|
|
|
3105
3156
|
fresh: bool = False,
|
|
3106
3157
|
team_id: str | None = None,
|
|
3107
3158
|
) -> dict[str, Any]:
|
|
3108
|
-
from team_agent.compiler import compile_team
|
|
3109
|
-
|
|
3110
3159
|
team_dir = _prepare_quick_start_team(agents_dir.resolve(), Path.cwd().resolve(), name, team_id=team_id)
|
|
3111
3160
|
workspace = team_workspace(team_dir)
|
|
3112
3161
|
ensure_workspace_dirs(workspace)
|
|
3113
3162
|
_ensure_profiles_for_roles(team_dir)
|
|
3163
|
+
compiled = _compile_team_dir_spec(team_dir, workspace)
|
|
3114
3164
|
spec_path = team_dir / "team.spec.yaml"
|
|
3115
|
-
compiled = compile_team(team_dir, spec_path)
|
|
3116
|
-
if compiled["spec"].get("context", {}).get("state_file") == "team_state.md":
|
|
3117
|
-
state_file = str(team_dir.relative_to(workspace) / "team_state.md") if team_dir.is_relative_to(workspace) else "team_state.md"
|
|
3118
|
-
compiled["spec"]["context"]["state_file"] = state_file
|
|
3119
|
-
spec_path.write_text(dumps(compiled["spec"]), encoding="utf-8")
|
|
3120
3165
|
existing = _quick_start_existing_context(workspace, compiled["spec"]["runtime"]["session_name"])
|
|
3121
3166
|
if existing and not fresh:
|
|
3122
3167
|
return {
|
|
@@ -4508,14 +4553,34 @@ def _ghostty_command() -> str | None:
|
|
|
4508
4553
|
)
|
|
4509
4554
|
|
|
4510
4555
|
|
|
4556
|
+
def _ghostty_app_exists() -> bool:
|
|
4557
|
+
return Path("/Applications/Ghostty.app").exists()
|
|
4558
|
+
|
|
4559
|
+
|
|
4560
|
+
def _ghostty_pids_by_title(title: str, wait_s: float = 0.0) -> list[int]:
|
|
4561
|
+
deadline = time.monotonic() + max(wait_s, 0.0)
|
|
4562
|
+
while True:
|
|
4563
|
+
pgrep = run_cmd(["pgrep", "-f", f"--title={title}"], timeout=5)
|
|
4564
|
+
if pgrep.returncode == 0:
|
|
4565
|
+
pids = [int(pid) for pid in pgrep.stdout.split() if pid.isdigit()]
|
|
4566
|
+
if pids:
|
|
4567
|
+
return pids
|
|
4568
|
+
if time.monotonic() >= deadline:
|
|
4569
|
+
return []
|
|
4570
|
+
time.sleep(0.2)
|
|
4571
|
+
|
|
4572
|
+
|
|
4511
4573
|
def _open_worker_displays(
|
|
4512
4574
|
workspace: Path,
|
|
4513
4575
|
session_name: str,
|
|
4514
4576
|
jobs: list[tuple[str, dict[str, Any]]],
|
|
4515
4577
|
event_log: EventLog,
|
|
4578
|
+
display_backend: str = "ghostty_window",
|
|
4516
4579
|
) -> dict[str, dict[str, Any]]:
|
|
4517
4580
|
if not jobs:
|
|
4518
4581
|
return {}
|
|
4582
|
+
if display_backend == "ghostty_workspace":
|
|
4583
|
+
return _open_ghostty_workspace(workspace, session_name, jobs, event_log)
|
|
4519
4584
|
if len(jobs) == 1:
|
|
4520
4585
|
agent_id, agent = jobs[0]
|
|
4521
4586
|
return {agent_id: _open_ghostty_worker_window(workspace, session_name, agent_id, agent, event_log)}
|
|
@@ -4550,7 +4615,7 @@ def _open_ghostty_worker_window(
|
|
|
4550
4615
|
agent: dict[str, Any],
|
|
4551
4616
|
event_log: EventLog,
|
|
4552
4617
|
) -> dict[str, Any]:
|
|
4553
|
-
if not
|
|
4618
|
+
if not _ghostty_app_exists():
|
|
4554
4619
|
blocker = {
|
|
4555
4620
|
"backend": "ghostty_window",
|
|
4556
4621
|
"status": "blocked",
|
|
@@ -4592,15 +4657,130 @@ def _open_ghostty_worker_window(
|
|
|
4592
4657
|
if proc.returncode != 0:
|
|
4593
4658
|
display["reason"] = proc.stderr.strip() or proc.stdout.strip() or "open Ghostty.app failed"
|
|
4594
4659
|
else:
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
if pgrep.returncode == 0:
|
|
4598
|
-
display["pids"] = [int(pid) for pid in pgrep.stdout.split() if pid.isdigit()]
|
|
4599
|
-
display["pid"] = display["pids"][0] if display["pids"] else None
|
|
4660
|
+
display["pids"] = _ghostty_pids_by_title(title, wait_s=3.0)
|
|
4661
|
+
display["pid"] = display["pids"][0] if display["pids"] else None
|
|
4600
4662
|
event_log.write("display.ghostty_window", agent_id=agent["id"], **display)
|
|
4601
4663
|
return display
|
|
4602
4664
|
|
|
4603
4665
|
|
|
4666
|
+
def _open_ghostty_workspace(
|
|
4667
|
+
workspace: Path,
|
|
4668
|
+
session_name: str,
|
|
4669
|
+
jobs: list[tuple[str, dict[str, Any]]],
|
|
4670
|
+
event_log: EventLog,
|
|
4671
|
+
) -> dict[str, dict[str, Any]]:
|
|
4672
|
+
if not _ghostty_app_exists():
|
|
4673
|
+
return _ghostty_workspace_blocked(jobs, event_log, "ghostty_app_missing")
|
|
4674
|
+
aggregator_session = _ghostty_workspace_aggregator_name(session_name)
|
|
4675
|
+
linked_results = _prepare_ghostty_workspace_linked_sessions(session_name, jobs)
|
|
4676
|
+
displays: dict[str, dict[str, Any]] = {}
|
|
4677
|
+
linked_jobs: list[tuple[str, dict[str, Any], str]] = []
|
|
4678
|
+
for agent_id, agent in jobs:
|
|
4679
|
+
linked = linked_results.get(agent_id, {})
|
|
4680
|
+
linked_session = linked.get("linked_session") or _ghostty_display_session_name(session_name, agent_id)
|
|
4681
|
+
if linked.get("ok"):
|
|
4682
|
+
linked_jobs.append((agent_id, agent, linked_session))
|
|
4683
|
+
continue
|
|
4684
|
+
displays.update(
|
|
4685
|
+
_ghostty_workspace_blocked(
|
|
4686
|
+
[(agent_id, agent)],
|
|
4687
|
+
event_log,
|
|
4688
|
+
linked.get("reason", "display_session_create_failed"),
|
|
4689
|
+
aggregator_session=aggregator_session,
|
|
4690
|
+
linked_sessions={agent_id: linked_session},
|
|
4691
|
+
error=linked.get("error"),
|
|
4692
|
+
target=f"{session_name}:{agent_id}",
|
|
4693
|
+
)
|
|
4694
|
+
)
|
|
4695
|
+
if not linked_jobs:
|
|
4696
|
+
return displays
|
|
4697
|
+
prepared = _prepare_ghostty_workspace_aggregator(aggregator_session, linked_jobs)
|
|
4698
|
+
if not prepared["ok"]:
|
|
4699
|
+
_kill_ghostty_workspace_linked_sessions([linked_session for _agent_id, _agent, linked_session in linked_jobs])
|
|
4700
|
+
displays.update(
|
|
4701
|
+
_ghostty_workspace_blocked(
|
|
4702
|
+
[(agent_id, agent) for agent_id, agent, _linked_session in linked_jobs],
|
|
4703
|
+
event_log,
|
|
4704
|
+
prepared["reason"],
|
|
4705
|
+
aggregator_session=aggregator_session,
|
|
4706
|
+
linked_sessions={agent_id: linked_session for agent_id, _agent, linked_session in linked_jobs},
|
|
4707
|
+
error=prepared.get("error"),
|
|
4708
|
+
target=prepared.get("target"),
|
|
4709
|
+
)
|
|
4710
|
+
)
|
|
4711
|
+
return displays
|
|
4712
|
+
title = f"team-agent:{session_name}:workspace"
|
|
4713
|
+
launch_args = _ghostty_attach_args(aggregator_session, title)
|
|
4714
|
+
proc = run_cmd(launch_args, timeout=10)
|
|
4715
|
+
if proc.returncode != 0:
|
|
4716
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4717
|
+
_kill_ghostty_workspace_linked_sessions([linked_session for _agent_id, _agent, linked_session in linked_jobs])
|
|
4718
|
+
displays.update(
|
|
4719
|
+
_ghostty_workspace_blocked(
|
|
4720
|
+
[(agent_id, agent) for agent_id, agent, _linked_session in linked_jobs],
|
|
4721
|
+
event_log,
|
|
4722
|
+
"open Ghostty.app failed",
|
|
4723
|
+
aggregator_session=aggregator_session,
|
|
4724
|
+
linked_sessions={agent_id: linked_session for agent_id, _agent, linked_session in linked_jobs},
|
|
4725
|
+
error=proc.stderr.strip() or proc.stdout.strip(),
|
|
4726
|
+
)
|
|
4727
|
+
)
|
|
4728
|
+
return displays
|
|
4729
|
+
pids = _ghostty_pids_by_title(title, wait_s=3.0)
|
|
4730
|
+
panes = {pane["agent_id"]: pane for pane in prepared["panes"]}
|
|
4731
|
+
for agent_id, agent, linked_session in linked_jobs:
|
|
4732
|
+
pane = panes.get(agent_id, {})
|
|
4733
|
+
display = {
|
|
4734
|
+
"backend": "ghostty_workspace",
|
|
4735
|
+
"status": "opened",
|
|
4736
|
+
"title": title,
|
|
4737
|
+
"pane_title": pane.get("title") or _ghostty_workspace_pane_title(agent),
|
|
4738
|
+
"target": f"{session_name}:{agent_id}",
|
|
4739
|
+
"linked_session": linked_session,
|
|
4740
|
+
"aggregator_session": aggregator_session,
|
|
4741
|
+
"display_session": aggregator_session,
|
|
4742
|
+
"workspace_window": pane.get("window_name"),
|
|
4743
|
+
"pane_id": pane.get("pane_id"),
|
|
4744
|
+
"launch_args": launch_args,
|
|
4745
|
+
"pid": pids[0] if pids else None,
|
|
4746
|
+
"pids": pids,
|
|
4747
|
+
"tty": None,
|
|
4748
|
+
"fallback": "tmux_headless",
|
|
4749
|
+
"note": "Ghostty opens one aggregator tmux session; each pane attaches to a distinct linked session pinned to one base worker window, so runtime injection remains session:agent_id addressed.",
|
|
4750
|
+
}
|
|
4751
|
+
event_log.write("display.ghostty_workspace", agent_id=agent_id, **display)
|
|
4752
|
+
displays[agent_id] = display
|
|
4753
|
+
return displays
|
|
4754
|
+
|
|
4755
|
+
|
|
4756
|
+
def _ghostty_workspace_blocked(
|
|
4757
|
+
jobs: list[tuple[str, dict[str, Any]]],
|
|
4758
|
+
event_log: EventLog,
|
|
4759
|
+
reason: str,
|
|
4760
|
+
aggregator_session: str | None = None,
|
|
4761
|
+
linked_sessions: dict[str, str] | None = None,
|
|
4762
|
+
error: str | None = None,
|
|
4763
|
+
target: str | None = None,
|
|
4764
|
+
) -> dict[str, dict[str, Any]]:
|
|
4765
|
+
displays: dict[str, dict[str, Any]] = {}
|
|
4766
|
+
for agent_id, _agent in jobs:
|
|
4767
|
+
linked_session = (linked_sessions or {}).get(agent_id)
|
|
4768
|
+
display = {
|
|
4769
|
+
"backend": "ghostty_workspace",
|
|
4770
|
+
"status": "blocked",
|
|
4771
|
+
"reason": reason,
|
|
4772
|
+
"error": error,
|
|
4773
|
+
"target": target or f"{agent_id}",
|
|
4774
|
+
"linked_session": linked_session,
|
|
4775
|
+
"aggregator_session": aggregator_session,
|
|
4776
|
+
"display_session": aggregator_session,
|
|
4777
|
+
"fallback": "tmux_headless",
|
|
4778
|
+
}
|
|
4779
|
+
event_log.write("display.ghostty_workspace_blocked", agent_id=agent_id, **display)
|
|
4780
|
+
displays[agent_id] = display
|
|
4781
|
+
return displays
|
|
4782
|
+
|
|
4783
|
+
|
|
4604
4784
|
def _ghostty_display_session_name(session_name: str, window_name: str) -> str:
|
|
4605
4785
|
raw = f"{session_name}:{window_name}"
|
|
4606
4786
|
digest = hashlib.sha1(raw.encode("utf-8")).hexdigest()[:8]
|
|
@@ -4628,6 +4808,336 @@ def _prepare_ghostty_display_session(session_name: str, window_name: str, displa
|
|
|
4628
4808
|
return {"ok": True, "display_session": display_session}
|
|
4629
4809
|
|
|
4630
4810
|
|
|
4811
|
+
def _ghostty_workspace_aggregator_name(session_name: str) -> str:
|
|
4812
|
+
raw = f"{session_name}:workspace"
|
|
4813
|
+
digest = hashlib.sha1(raw.encode("utf-8")).hexdigest()[:8]
|
|
4814
|
+
safe_session = re.sub(r"[^A-Za-z0-9_.-]", "_", session_name)[:80].strip("._-") or "team"
|
|
4815
|
+
return f"{safe_session}__display__workspace__{digest}"
|
|
4816
|
+
|
|
4817
|
+
|
|
4818
|
+
def _ghostty_workspace_window_name(index: int) -> str:
|
|
4819
|
+
return "overview" if index == 0 else f"overview-{index + 1}"
|
|
4820
|
+
|
|
4821
|
+
|
|
4822
|
+
def _ghostty_workspace_pane_command(linked_session: str) -> str:
|
|
4823
|
+
return f"TMUX= tmux attach-session -t {shlex.quote(linked_session)}"
|
|
4824
|
+
|
|
4825
|
+
|
|
4826
|
+
def _ghostty_workspace_pane_title(agent: dict[str, Any]) -> str:
|
|
4827
|
+
return f"team-agent:{agent['id']}:{agent.get('role', '')}"
|
|
4828
|
+
|
|
4829
|
+
|
|
4830
|
+
def _prepare_ghostty_workspace_linked_sessions(
|
|
4831
|
+
session_name: str,
|
|
4832
|
+
jobs: list[tuple[str, dict[str, Any]]],
|
|
4833
|
+
) -> dict[str, dict[str, Any]]:
|
|
4834
|
+
def prepare(agent_id: str) -> dict[str, Any]:
|
|
4835
|
+
linked_session = _ghostty_display_session_name(session_name, agent_id)
|
|
4836
|
+
result = _prepare_ghostty_display_session(session_name, agent_id, linked_session)
|
|
4837
|
+
result["linked_session"] = linked_session
|
|
4838
|
+
return result
|
|
4839
|
+
|
|
4840
|
+
if len(jobs) == 1:
|
|
4841
|
+
agent_id, _agent = jobs[0]
|
|
4842
|
+
return {agent_id: prepare(agent_id)}
|
|
4843
|
+
results: dict[str, dict[str, Any]] = {}
|
|
4844
|
+
max_workers = min(4, len(jobs))
|
|
4845
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
4846
|
+
futures = {executor.submit(prepare, agent_id): agent_id for agent_id, _agent in jobs}
|
|
4847
|
+
for future in as_completed(futures):
|
|
4848
|
+
agent_id = futures[future]
|
|
4849
|
+
try:
|
|
4850
|
+
results[agent_id] = future.result()
|
|
4851
|
+
except Exception as exc:
|
|
4852
|
+
results[agent_id] = {
|
|
4853
|
+
"ok": False,
|
|
4854
|
+
"reason": "display_session_create_exception",
|
|
4855
|
+
"error": str(exc),
|
|
4856
|
+
"linked_session": _ghostty_display_session_name(session_name, agent_id),
|
|
4857
|
+
}
|
|
4858
|
+
return results
|
|
4859
|
+
|
|
4860
|
+
|
|
4861
|
+
def _prepare_ghostty_workspace_aggregator(
|
|
4862
|
+
aggregator_session: str,
|
|
4863
|
+
linked_jobs: list[tuple[str, dict[str, Any], str]],
|
|
4864
|
+
) -> dict[str, Any]:
|
|
4865
|
+
if _tmux_session_exists(aggregator_session):
|
|
4866
|
+
proc = run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4867
|
+
if proc.returncode != 0:
|
|
4868
|
+
return {"ok": False, "reason": "display_session_cleanup_failed", "error": proc.stderr.strip()}
|
|
4869
|
+
|
|
4870
|
+
def fail(reason: str, proc: Any | None = None, target: str | None = None) -> dict[str, Any]:
|
|
4871
|
+
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4872
|
+
result = {"ok": False, "reason": reason}
|
|
4873
|
+
if proc is not None:
|
|
4874
|
+
result["error"] = proc.stderr.strip()
|
|
4875
|
+
if target:
|
|
4876
|
+
result["target"] = target
|
|
4877
|
+
return result
|
|
4878
|
+
|
|
4879
|
+
panes: list[dict[str, Any]] = []
|
|
4880
|
+
for window_index, start in enumerate(range(0, len(linked_jobs), GHOSTTY_WORKSPACE_PANES_PER_WINDOW)):
|
|
4881
|
+
window_name = _ghostty_workspace_window_name(window_index)
|
|
4882
|
+
window_jobs = linked_jobs[start : start + GHOSTTY_WORKSPACE_PANES_PER_WINDOW]
|
|
4883
|
+
first_agent_id, first_agent, first_linked_session = window_jobs[0]
|
|
4884
|
+
if window_index == 0:
|
|
4885
|
+
proc = run_cmd(
|
|
4886
|
+
[
|
|
4887
|
+
"tmux",
|
|
4888
|
+
"new-session",
|
|
4889
|
+
"-d",
|
|
4890
|
+
"-P",
|
|
4891
|
+
"-F",
|
|
4892
|
+
"#{pane_id}",
|
|
4893
|
+
"-s",
|
|
4894
|
+
aggregator_session,
|
|
4895
|
+
"-n",
|
|
4896
|
+
window_name,
|
|
4897
|
+
_ghostty_workspace_pane_command(first_linked_session),
|
|
4898
|
+
],
|
|
4899
|
+
timeout=10,
|
|
4900
|
+
)
|
|
4901
|
+
if proc.returncode != 0:
|
|
4902
|
+
return {"ok": False, "reason": "display_session_create_failed", "error": proc.stderr.strip()}
|
|
4903
|
+
else:
|
|
4904
|
+
proc = run_cmd(
|
|
4905
|
+
[
|
|
4906
|
+
"tmux",
|
|
4907
|
+
"new-window",
|
|
4908
|
+
"-t",
|
|
4909
|
+
aggregator_session,
|
|
4910
|
+
"-n",
|
|
4911
|
+
window_name,
|
|
4912
|
+
"-P",
|
|
4913
|
+
"-F",
|
|
4914
|
+
"#{pane_id}",
|
|
4915
|
+
_ghostty_workspace_pane_command(first_linked_session),
|
|
4916
|
+
],
|
|
4917
|
+
timeout=10,
|
|
4918
|
+
)
|
|
4919
|
+
if proc.returncode != 0:
|
|
4920
|
+
return fail("display_session_window_create_failed", proc, first_linked_session)
|
|
4921
|
+
first_pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.0"
|
|
4922
|
+
first_title = _ghostty_workspace_pane_title(first_agent)
|
|
4923
|
+
title_result = _set_ghostty_workspace_pane_title(first_pane_id, first_title)
|
|
4924
|
+
if not title_result["ok"]:
|
|
4925
|
+
return fail(title_result["reason"], target=first_pane_id)
|
|
4926
|
+
panes.append(
|
|
4927
|
+
{
|
|
4928
|
+
"agent_id": first_agent_id,
|
|
4929
|
+
"pane_id": first_pane_id,
|
|
4930
|
+
"title": first_title,
|
|
4931
|
+
"linked_session": first_linked_session,
|
|
4932
|
+
"window_name": window_name,
|
|
4933
|
+
}
|
|
4934
|
+
)
|
|
4935
|
+
|
|
4936
|
+
proc = run_cmd(["tmux", "set-window-option", "-t", f"{aggregator_session}:{window_name}", "remain-on-exit", "on"], timeout=10)
|
|
4937
|
+
if proc.returncode != 0:
|
|
4938
|
+
return fail("display_session_remain_on_exit_failed", proc)
|
|
4939
|
+
|
|
4940
|
+
for index, (agent_id, agent, linked_session) in enumerate(window_jobs[1:], start=1):
|
|
4941
|
+
proc = run_cmd(
|
|
4942
|
+
[
|
|
4943
|
+
"tmux",
|
|
4944
|
+
"split-window",
|
|
4945
|
+
"-t",
|
|
4946
|
+
f"{aggregator_session}:{window_name}",
|
|
4947
|
+
"-h",
|
|
4948
|
+
"-P",
|
|
4949
|
+
"-F",
|
|
4950
|
+
"#{pane_id}",
|
|
4951
|
+
_ghostty_workspace_pane_command(linked_session),
|
|
4952
|
+
],
|
|
4953
|
+
timeout=10,
|
|
4954
|
+
)
|
|
4955
|
+
if proc.returncode != 0:
|
|
4956
|
+
return fail("display_session_split_failed", proc, linked_session)
|
|
4957
|
+
pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.{index}"
|
|
4958
|
+
title = _ghostty_workspace_pane_title(agent)
|
|
4959
|
+
title_result = _set_ghostty_workspace_pane_title(pane_id, title)
|
|
4960
|
+
if not title_result["ok"]:
|
|
4961
|
+
return fail(title_result["reason"], target=pane_id)
|
|
4962
|
+
panes.append(
|
|
4963
|
+
{
|
|
4964
|
+
"agent_id": agent_id,
|
|
4965
|
+
"pane_id": pane_id,
|
|
4966
|
+
"title": title,
|
|
4967
|
+
"linked_session": linked_session,
|
|
4968
|
+
"window_name": window_name,
|
|
4969
|
+
}
|
|
4970
|
+
)
|
|
4971
|
+
|
|
4972
|
+
proc = run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:{window_name}", "even-horizontal"], timeout=10)
|
|
4973
|
+
if proc.returncode != 0:
|
|
4974
|
+
return fail("display_session_layout_failed", proc)
|
|
4975
|
+
|
|
4976
|
+
proc = run_cmd(["tmux", "set-option", "-t", aggregator_session, "mouse", "on"], timeout=10)
|
|
4977
|
+
if proc.returncode != 0:
|
|
4978
|
+
return fail("display_session_mouse_failed", proc)
|
|
4979
|
+
run_cmd(["tmux", "select-window", "-t", f"{aggregator_session}:{_ghostty_workspace_window_name(0)}"], timeout=10)
|
|
4980
|
+
return {"ok": True, "aggregator_session": aggregator_session, "panes": panes}
|
|
4981
|
+
|
|
4982
|
+
|
|
4983
|
+
def _set_ghostty_workspace_pane_title(pane_id: str, title: str) -> dict[str, Any]:
|
|
4984
|
+
proc = run_cmd(["tmux", "select-pane", "-t", pane_id, "-T", title], timeout=10)
|
|
4985
|
+
if proc.returncode != 0:
|
|
4986
|
+
return {"ok": False, "reason": "display_session_pane_title_failed", "error": proc.stderr.strip()}
|
|
4987
|
+
return {"ok": True}
|
|
4988
|
+
|
|
4989
|
+
|
|
4990
|
+
def _tmux_stdout_last_line(stdout: str) -> str | None:
|
|
4991
|
+
lines = [line.strip() for line in stdout.splitlines() if line.strip()]
|
|
4992
|
+
return lines[-1] if lines else None
|
|
4993
|
+
|
|
4994
|
+
|
|
4995
|
+
def _open_ghostty_workspace_agent_display(
|
|
4996
|
+
session_name: str,
|
|
4997
|
+
agent_id: str,
|
|
4998
|
+
agent: dict[str, Any],
|
|
4999
|
+
previous_display: dict[str, Any],
|
|
5000
|
+
event_log: EventLog,
|
|
5001
|
+
) -> dict[str, Any]:
|
|
5002
|
+
if not _ghostty_app_exists():
|
|
5003
|
+
return _ghostty_workspace_blocked(
|
|
5004
|
+
[(agent_id, agent)],
|
|
5005
|
+
event_log,
|
|
5006
|
+
"ghostty_app_missing",
|
|
5007
|
+
aggregator_session=_ghostty_workspace_aggregator_name(session_name),
|
|
5008
|
+
linked_sessions={agent_id: _ghostty_display_session_name(session_name, agent_id)},
|
|
5009
|
+
target=f"{session_name}:{agent_id}",
|
|
5010
|
+
)[agent_id]
|
|
5011
|
+
aggregator_session = str(
|
|
5012
|
+
previous_display.get("aggregator_session")
|
|
5013
|
+
or previous_display.get("display_session")
|
|
5014
|
+
or _ghostty_workspace_aggregator_name(session_name)
|
|
5015
|
+
)
|
|
5016
|
+
linked_session = _ghostty_display_session_name(session_name, agent_id)
|
|
5017
|
+
prepared = _prepare_ghostty_display_session(session_name, agent_id, linked_session)
|
|
5018
|
+
if not prepared["ok"]:
|
|
5019
|
+
return _ghostty_workspace_blocked(
|
|
5020
|
+
[(agent_id, agent)],
|
|
5021
|
+
event_log,
|
|
5022
|
+
prepared["reason"],
|
|
5023
|
+
aggregator_session=aggregator_session,
|
|
5024
|
+
linked_sessions={agent_id: linked_session},
|
|
5025
|
+
error=prepared.get("error"),
|
|
5026
|
+
target=f"{session_name}:{agent_id}",
|
|
5027
|
+
)[agent_id]
|
|
5028
|
+
if not _tmux_session_exists(aggregator_session):
|
|
5029
|
+
return _ghostty_workspace_partial_update_display(
|
|
5030
|
+
session_name,
|
|
5031
|
+
agent_id,
|
|
5032
|
+
agent,
|
|
5033
|
+
event_log,
|
|
5034
|
+
reason="aggregator_session_missing",
|
|
5035
|
+
note="pane refresh requires full team restart",
|
|
5036
|
+
)
|
|
5037
|
+
|
|
5038
|
+
pane_title = _ghostty_workspace_pane_title(agent)
|
|
5039
|
+
command = _ghostty_workspace_pane_command(linked_session)
|
|
5040
|
+
pane_id = str(previous_display.get("pane_id") or "")
|
|
5041
|
+
workspace_window = str(previous_display.get("workspace_window") or _ghostty_workspace_window_name(0))
|
|
5042
|
+
refreshed = False
|
|
5043
|
+
if pane_id:
|
|
5044
|
+
proc = run_cmd(["tmux", "respawn-pane", "-k", "-t", pane_id, command], timeout=10)
|
|
5045
|
+
refreshed = proc.returncode == 0
|
|
5046
|
+
if not refreshed:
|
|
5047
|
+
proc = run_cmd(
|
|
5048
|
+
[
|
|
5049
|
+
"tmux",
|
|
5050
|
+
"split-window",
|
|
5051
|
+
"-t",
|
|
5052
|
+
f"{aggregator_session}:{workspace_window}",
|
|
5053
|
+
"-h",
|
|
5054
|
+
"-P",
|
|
5055
|
+
"-F",
|
|
5056
|
+
"#{pane_id}",
|
|
5057
|
+
command,
|
|
5058
|
+
],
|
|
5059
|
+
timeout=10,
|
|
5060
|
+
)
|
|
5061
|
+
if proc.returncode != 0:
|
|
5062
|
+
return _ghostty_workspace_partial_update_display(
|
|
5063
|
+
session_name,
|
|
5064
|
+
agent_id,
|
|
5065
|
+
agent,
|
|
5066
|
+
event_log,
|
|
5067
|
+
reason="aggregator_pane_refresh_failed",
|
|
5068
|
+
note=proc.stderr.strip() or "pane refresh requires full team restart",
|
|
5069
|
+
)
|
|
5070
|
+
pane_id = _tmux_stdout_last_line(proc.stdout) or pane_id
|
|
5071
|
+
title_result = _set_ghostty_workspace_pane_title(pane_id, pane_title)
|
|
5072
|
+
if not title_result["ok"]:
|
|
5073
|
+
return _ghostty_workspace_partial_update_display(
|
|
5074
|
+
session_name,
|
|
5075
|
+
agent_id,
|
|
5076
|
+
agent,
|
|
5077
|
+
event_log,
|
|
5078
|
+
reason=title_result["reason"],
|
|
5079
|
+
note=title_result.get("error") or "pane refresh requires full team restart",
|
|
5080
|
+
)
|
|
5081
|
+
run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:{workspace_window}", "even-horizontal"], timeout=10)
|
|
5082
|
+
title = str(previous_display.get("title") or f"team-agent:{session_name}:workspace")
|
|
5083
|
+
pids = [int(pid) for pid in previous_display.get("pids", []) if str(pid).isdigit()]
|
|
5084
|
+
display = {
|
|
5085
|
+
"backend": "ghostty_workspace",
|
|
5086
|
+
"status": "opened",
|
|
5087
|
+
"title": title,
|
|
5088
|
+
"pane_title": pane_title,
|
|
5089
|
+
"target": f"{session_name}:{agent_id}",
|
|
5090
|
+
"linked_session": linked_session,
|
|
5091
|
+
"aggregator_session": aggregator_session,
|
|
5092
|
+
"display_session": aggregator_session,
|
|
5093
|
+
"workspace_window": workspace_window,
|
|
5094
|
+
"pane_id": pane_id,
|
|
5095
|
+
"pid": pids[0] if pids else None,
|
|
5096
|
+
"pids": pids,
|
|
5097
|
+
"tty": None,
|
|
5098
|
+
"fallback": "tmux_headless",
|
|
5099
|
+
"note": "Refreshed this worker's Ghostty workspace pane by respawning it against a distinct linked session.",
|
|
5100
|
+
}
|
|
5101
|
+
event_log.write("display.ghostty_workspace", agent_id=agent_id, **display)
|
|
5102
|
+
return display
|
|
5103
|
+
|
|
5104
|
+
|
|
5105
|
+
def _ghostty_workspace_partial_update_display(
|
|
5106
|
+
session_name: str,
|
|
5107
|
+
agent_id: str,
|
|
5108
|
+
agent: dict[str, Any],
|
|
5109
|
+
event_log: EventLog,
|
|
5110
|
+
reason: str = "partial_update_requires_team_restart",
|
|
5111
|
+
note: str = "pane refresh requires full team restart",
|
|
5112
|
+
) -> dict[str, Any]:
|
|
5113
|
+
aggregator_session = _ghostty_workspace_aggregator_name(session_name)
|
|
5114
|
+
display = {
|
|
5115
|
+
"backend": "ghostty_workspace",
|
|
5116
|
+
"status": "blocked",
|
|
5117
|
+
"reason": reason,
|
|
5118
|
+
"target": f"{session_name}:{agent_id}",
|
|
5119
|
+
"linked_session": _ghostty_display_session_name(session_name, agent_id),
|
|
5120
|
+
"aggregator_session": aggregator_session,
|
|
5121
|
+
"display_session": aggregator_session,
|
|
5122
|
+
"pane_title": _ghostty_workspace_pane_title(agent),
|
|
5123
|
+
"fallback": "tmux_headless",
|
|
5124
|
+
"note": note,
|
|
5125
|
+
"action": "restart the team to rebuild the Ghostty workspace layout",
|
|
5126
|
+
}
|
|
5127
|
+
event_log.write("display.ghostty_workspace_partial_update", agent_id=agent_id, **display)
|
|
5128
|
+
return display
|
|
5129
|
+
|
|
5130
|
+
|
|
5131
|
+
def _kill_ghostty_workspace_linked_sessions(linked_sessions: list[str]) -> list[str]:
|
|
5132
|
+
killed: list[str] = []
|
|
5133
|
+
for linked_session in dict.fromkeys(linked_sessions):
|
|
5134
|
+
if _tmux_session_exists(linked_session):
|
|
5135
|
+
proc = run_cmd(["tmux", "kill-session", "-t", linked_session], timeout=10)
|
|
5136
|
+
if proc.returncode == 0:
|
|
5137
|
+
killed.append(linked_session)
|
|
5138
|
+
return killed
|
|
5139
|
+
|
|
5140
|
+
|
|
4631
5141
|
def _ghostty_attach_args(display_session: str, title: str) -> list[str]:
|
|
4632
5142
|
return [
|
|
4633
5143
|
"open",
|
|
@@ -4643,7 +5153,11 @@ def _ghostty_attach_args(display_session: str, title: str) -> list[str]:
|
|
|
4643
5153
|
]
|
|
4644
5154
|
|
|
4645
5155
|
|
|
4646
|
-
def _close_ghostty_display(
|
|
5156
|
+
def _close_ghostty_display(
|
|
5157
|
+
agent_id: str,
|
|
5158
|
+
agent_state: dict[str, Any],
|
|
5159
|
+
event_log: EventLog,
|
|
5160
|
+
) -> None:
|
|
4647
5161
|
display = agent_state.get("display") or {}
|
|
4648
5162
|
if display.get("backend") != "ghostty_window":
|
|
4649
5163
|
return
|
|
@@ -4651,9 +5165,7 @@ def _close_ghostty_display(agent_id: str, agent_state: dict[str, Any], event_log
|
|
|
4651
5165
|
pids = [str(pid) for pid in display.get("pids", []) if str(pid).isdigit()]
|
|
4652
5166
|
title = display.get("title")
|
|
4653
5167
|
if not pids and title:
|
|
4654
|
-
|
|
4655
|
-
if pgrep.returncode == 0:
|
|
4656
|
-
pids = [pid for pid in pgrep.stdout.split() if pid.isdigit()]
|
|
5168
|
+
pids = [str(pid) for pid in _ghostty_pids_by_title(str(title))]
|
|
4657
5169
|
killed: list[str] = []
|
|
4658
5170
|
for pid in pids:
|
|
4659
5171
|
proc = run_cmd(["kill", pid], timeout=5)
|
|
@@ -4674,6 +5186,66 @@ def _close_ghostty_display(agent_id: str, agent_state: dict[str, Any], event_log
|
|
|
4674
5186
|
)
|
|
4675
5187
|
|
|
4676
5188
|
|
|
5189
|
+
def _close_ghostty_workspace(state: dict[str, Any], event_log: EventLog) -> None:
|
|
5190
|
+
displays = [
|
|
5191
|
+
(agent_id, agent_state.get("display") or {})
|
|
5192
|
+
for agent_id, agent_state in state.get("agents", {}).items()
|
|
5193
|
+
if (agent_state.get("display") or {}).get("backend") == "ghostty_workspace"
|
|
5194
|
+
]
|
|
5195
|
+
if not displays:
|
|
5196
|
+
return
|
|
5197
|
+
aggregator_session = next(
|
|
5198
|
+
(
|
|
5199
|
+
str(display.get("aggregator_session") or display.get("display_session"))
|
|
5200
|
+
for _agent_id, display in displays
|
|
5201
|
+
if display.get("aggregator_session") or display.get("display_session")
|
|
5202
|
+
),
|
|
5203
|
+
None,
|
|
5204
|
+
)
|
|
5205
|
+
title = next((str(display.get("title")) for _agent_id, display in displays if display.get("title")), None)
|
|
5206
|
+
pids = {
|
|
5207
|
+
str(pid)
|
|
5208
|
+
for _agent_id, display in displays
|
|
5209
|
+
for pid in display.get("pids", [])
|
|
5210
|
+
if str(pid).isdigit()
|
|
5211
|
+
}
|
|
5212
|
+
if not pids and title:
|
|
5213
|
+
pids = {str(pid) for pid in _ghostty_pids_by_title(str(title))}
|
|
5214
|
+
|
|
5215
|
+
aggregator_closed = False
|
|
5216
|
+
if aggregator_session and _tmux_session_exists(aggregator_session):
|
|
5217
|
+
proc = run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
5218
|
+
if proc.returncode == 0:
|
|
5219
|
+
aggregator_closed = True
|
|
5220
|
+
else:
|
|
5221
|
+
event_log.write(
|
|
5222
|
+
"display.ghostty_workspace_close_failed",
|
|
5223
|
+
aggregator_session=aggregator_session,
|
|
5224
|
+
error=proc.stderr.strip(),
|
|
5225
|
+
)
|
|
5226
|
+
|
|
5227
|
+
linked_sessions = [
|
|
5228
|
+
str(display.get("linked_session"))
|
|
5229
|
+
for _agent_id, display in displays
|
|
5230
|
+
if display.get("linked_session")
|
|
5231
|
+
]
|
|
5232
|
+
linked_closed = _kill_ghostty_workspace_linked_sessions(linked_sessions)
|
|
5233
|
+
|
|
5234
|
+
killed: list[str] = []
|
|
5235
|
+
for pid in sorted(pids):
|
|
5236
|
+
proc = run_cmd(["kill", pid], timeout=5)
|
|
5237
|
+
if proc.returncode == 0:
|
|
5238
|
+
killed.append(pid)
|
|
5239
|
+
event_log.write(
|
|
5240
|
+
"display.ghostty_workspace_closed",
|
|
5241
|
+
pids=killed,
|
|
5242
|
+
title=title,
|
|
5243
|
+
aggregator_session=aggregator_session,
|
|
5244
|
+
linked_sessions=linked_closed,
|
|
5245
|
+
aggregator_closed=aggregator_closed,
|
|
5246
|
+
)
|
|
5247
|
+
|
|
5248
|
+
|
|
4677
5249
|
def get_adapter_or_raise(name: str) -> str:
|
|
4678
5250
|
if name == "tmux" and not shutil_which("tmux"):
|
|
4679
5251
|
raise RuntimeError("tmux is not installed; install tmux 3.3+ before launch")
|