@team-agent/installer 0.1.9 → 0.1.11
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 -1
- package/skills/team-agent/SKILL.md +9 -3
- package/src/team_agent/cli.py +2 -1
- package/src/team_agent/mcp_server.py +1 -1
- package/src/team_agent/runtime.py +304 -74
package/README.md
CHANGED
|
@@ -128,7 +128,12 @@ Lead: [proposes a team — refactor architect (Claude), code mover (Codex),
|
|
|
128
128
|
You: Go.
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
That's it.
|
|
131
|
+
That's it. With the default display, teammates appear in separate Ghostty
|
|
132
|
+
windows. If `display_backend: ghostty_workspace` is set, teammates appear in one
|
|
133
|
+
Ghostty window with tmux tabs/windows, up to three side-by-side panes per tab
|
|
134
|
+
(`4` workers => `3 + 1`, `8` workers => `3 + 3 + 2`). The lead reports
|
|
135
|
+
progress, raises decisions when needed, and shuts everything down when you say
|
|
136
|
+
so.
|
|
132
137
|
|
|
133
138
|
### Stop / resume
|
|
134
139
|
|
package/README.zh.md
CHANGED
|
@@ -124,7 +124,10 @@ Lead: [提议一个 team:重构架构师 (Claude)、代码搬运工 (Codex)、
|
|
|
124
124
|
你: 开始。
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
完事。默认显示模式下,队员会分别出现在独立 Ghostty 窗口。设置
|
|
128
|
+
`display_backend: ghostty_workspace` 时,队员会出现在同一个 Ghostty 窗口里,
|
|
129
|
+
用 tmux tab/window 分页,每个 tab 最多三列 pane:4 个队员是 `3 + 1`,8 个队员是
|
|
130
|
+
`3 + 3 + 2`。lead 推进工作、需要决策时停下来问你,你说"关掉"就关掉。
|
|
128
131
|
|
|
129
132
|
### 关闭 / 恢复
|
|
130
133
|
|
package/package.json
CHANGED
|
@@ -27,7 +27,7 @@ cat > .team/current/TEAM.md <<'EOF'
|
|
|
27
27
|
name: demo-team
|
|
28
28
|
objective: One worker handles bounded tasks and reports through Team Agent MCP.
|
|
29
29
|
dangerous_auto_approve: false
|
|
30
|
-
display_backend:
|
|
30
|
+
display_backend: ghostty_workspace
|
|
31
31
|
fast: false
|
|
32
32
|
provider_models:
|
|
33
33
|
codex: gpt-5.5
|
|
@@ -59,7 +59,13 @@ team-agent quick-start .team/current
|
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
YAML lists must be block style. Use `tools:\n - fs_read`; do not use `tools: [fs_read, mcp_team]`.
|
|
62
|
-
|
|
62
|
+
|
|
63
|
+
Display choices:
|
|
64
|
+
- `ghostty_workspace`: one Ghostty window. Workers are shown in tmux tabs/windows, up to 3 side-by-side panes per tab. Four workers become `3 + 1`; eight become `3 + 3 + 2`.
|
|
65
|
+
- `ghostty_window`: one Ghostty window per worker.
|
|
66
|
+
- `none`: headless/CI.
|
|
67
|
+
|
|
68
|
+
Omitting `display_backend` defaults to `ghostty_window`.
|
|
63
69
|
|
|
64
70
|
## Provider Prep
|
|
65
71
|
|
|
@@ -112,7 +118,7 @@ For diagnosis, run `team-agent profile show deepseek --workspace . --json`; neve
|
|
|
112
118
|
- `team-agent send --watch-result coder "Do the bounded task"` sends a direct worker message, returns after delivery, and lets the coordinator collect/report completion asynchronously.
|
|
113
119
|
- After `send --watch-result` succeeds, do not run `sleep`, `status`, `inbox`, or `collect` polling loops unless the user explicitly asks for diagnosis; the coordinator will notify the leader when the result arrives.
|
|
114
120
|
- `team-agent send --task task_initial "Start"` routes by task.
|
|
115
|
-
- `team-agent status` shows team, worker health, result-store counts, `session_id`, `captured_via`, and attribution confidence.
|
|
121
|
+
- `team-agent status` shows team, worker health, result-store counts, `session_id`, `captured_via`, and attribution confidence. `team-agent status --json` is compact and context-safe by default; use `team-agent status --detail --json` only for raw runtime-state diagnostics.
|
|
116
122
|
- `team-agent status coder` shows one worker.
|
|
117
123
|
- `team-agent approvals [coder]` shows structured pending approval prompts without copying worker terminal pages.
|
|
118
124
|
- `team-agent inbox coder` shows message history only. Final results are not in inbox.
|
package/src/team_agent/cli.py
CHANGED
|
@@ -135,6 +135,7 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
135
135
|
p = sub.add_parser("status", help="Show team runtime status")
|
|
136
136
|
p.add_argument("agent", nargs="?")
|
|
137
137
|
p.add_argument("--workspace", default=".")
|
|
138
|
+
p.add_argument("--detail", action="store_true", help="Include full raw runtime state in --json output")
|
|
138
139
|
add_json(p)
|
|
139
140
|
p.set_defaults(func=cmd_status)
|
|
140
141
|
|
|
@@ -504,7 +505,7 @@ def cmd_settle(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
504
505
|
|
|
505
506
|
def cmd_status(args: argparse.Namespace) -> dict[str, Any]:
|
|
506
507
|
if args.json:
|
|
507
|
-
return runtime.status(Path(args.workspace).resolve(), as_json=True)
|
|
508
|
+
return runtime.status(Path(args.workspace).resolve(), as_json=True, compact=not args.detail)
|
|
508
509
|
return runtime.format_status(Path(args.workspace).resolve(), args.agent)
|
|
509
510
|
|
|
510
511
|
|
|
@@ -170,7 +170,7 @@ class TeamOrchestratorTools:
|
|
|
170
170
|
return {"ok": True, "state_file": str(path)}
|
|
171
171
|
|
|
172
172
|
def get_team_status(self) -> dict[str, Any]:
|
|
173
|
-
return runtime.status(self.workspace, as_json=True)
|
|
173
|
+
return runtime.status(self.workspace, as_json=True, compact=True)
|
|
174
174
|
|
|
175
175
|
def request_human(self, question: str, task_id: str | None = None, agent_id: str | None = None) -> dict[str, Any]:
|
|
176
176
|
store = MessageStore(self.workspace)
|
|
@@ -55,6 +55,9 @@ TMUX_PANE_FORMAT = (
|
|
|
55
55
|
)
|
|
56
56
|
HEALTH_STATUSES = {"RUNNING", "IDLE", "AWAITING_APPROVAL", "BLOCKED", "ERROR", "DONE"}
|
|
57
57
|
GHOSTTY_DISPLAY_BACKENDS = {"ghostty", "ghostty_window", "ghostty_workspace"}
|
|
58
|
+
GHOSTTY_WORKSPACE_PANES_PER_WINDOW = 3
|
|
59
|
+
STATUS_TEXT_LIMIT = 240
|
|
60
|
+
STATUS_EVENT_LIMIT = 3
|
|
58
61
|
PEEK_MAX_LINES = 80
|
|
59
62
|
PEEK_SEARCH_SCAN_LINES = 300
|
|
60
63
|
PEEK_MAX_MATCHES = 5
|
|
@@ -227,6 +230,22 @@ def _spec_team_dir(spec_path: Path, workspace: Path) -> Path:
|
|
|
227
230
|
return workspace.resolve() / ".team" / "current"
|
|
228
231
|
|
|
229
232
|
|
|
233
|
+
def _is_team_doc_dir(team_dir: Path) -> bool:
|
|
234
|
+
return (team_dir / "TEAM.md").exists() and (team_dir / "agents").is_dir()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _compile_team_dir_spec(team_dir: Path, workspace: Path) -> dict[str, Any]:
|
|
238
|
+
from team_agent.compiler import compile_team
|
|
239
|
+
|
|
240
|
+
spec_path = team_dir / "team.spec.yaml"
|
|
241
|
+
compiled = compile_team(team_dir, spec_path)
|
|
242
|
+
if compiled["spec"].get("context", {}).get("state_file") == "team_state.md":
|
|
243
|
+
state_file = str(team_dir.relative_to(workspace) / "team_state.md") if team_dir.is_relative_to(workspace) else "team_state.md"
|
|
244
|
+
compiled["spec"]["context"]["state_file"] = state_file
|
|
245
|
+
spec_path.write_text(dumps(compiled["spec"]), encoding="utf-8")
|
|
246
|
+
return compiled
|
|
247
|
+
|
|
248
|
+
|
|
230
249
|
def _attach_team_profile_dirs(spec: dict[str, Any], spec_path: Path, workspace: Path | None = None, team_dir: Path | None = None) -> None:
|
|
231
250
|
workspace = workspace.resolve() if workspace else workspace_from_spec(spec, spec_path)
|
|
232
251
|
team_dir = team_dir.resolve() if team_dir else _spec_team_dir(spec_path, workspace)
|
|
@@ -618,7 +637,7 @@ def _quick_start_existing_context(workspace: Path, session_name: str) -> dict[st
|
|
|
618
637
|
return None
|
|
619
638
|
|
|
620
639
|
|
|
621
|
-
def status(workspace: Path, as_json: bool = False) -> dict[str, Any]:
|
|
640
|
+
def status(workspace: Path, as_json: bool = False, *, compact: bool = False) -> dict[str, Any]:
|
|
622
641
|
state = load_runtime_state(workspace)
|
|
623
642
|
store = MessageStore(workspace)
|
|
624
643
|
event_log = EventLog(workspace)
|
|
@@ -629,7 +648,7 @@ def status(workspace: Path, as_json: bool = False) -> dict[str, Any]:
|
|
|
629
648
|
save_runtime_state(workspace, state)
|
|
630
649
|
session_name = state.get("session_name")
|
|
631
650
|
tmux_exists = _tmux_session_exists(session_name) if session_name else False
|
|
632
|
-
|
|
651
|
+
result = {
|
|
633
652
|
"team": state.get("leader", {}).get("id", "leader"),
|
|
634
653
|
"session_name": session_name,
|
|
635
654
|
"tmux_session_present": tmux_exists,
|
|
@@ -641,6 +660,157 @@ def status(workspace: Path, as_json: bool = False) -> dict[str, Any]:
|
|
|
641
660
|
"results": store.result_counts(),
|
|
642
661
|
"last_events": EventLog(workspace).tail(10),
|
|
643
662
|
}
|
|
663
|
+
return _compact_status(result) if compact else result
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _compact_status(data: dict[str, Any]) -> dict[str, Any]:
|
|
667
|
+
return {
|
|
668
|
+
"team": data.get("team"),
|
|
669
|
+
"session_name": data.get("session_name"),
|
|
670
|
+
"tmux_session_present": data.get("tmux_session_present"),
|
|
671
|
+
"leader_receiver": _compact_mapping(
|
|
672
|
+
data.get("leader_receiver", {}),
|
|
673
|
+
{
|
|
674
|
+
"status",
|
|
675
|
+
"provider",
|
|
676
|
+
"mode",
|
|
677
|
+
"session_name",
|
|
678
|
+
"window_name",
|
|
679
|
+
"pane_id",
|
|
680
|
+
"pane_current_command",
|
|
681
|
+
},
|
|
682
|
+
),
|
|
683
|
+
"agents": {
|
|
684
|
+
agent_id: _compact_agent_state(agent_id, agent)
|
|
685
|
+
for agent_id, agent in (data.get("agents") or {}).items()
|
|
686
|
+
},
|
|
687
|
+
"agent_health": data.get("agent_health", {}),
|
|
688
|
+
"tasks": [_compact_task(task) for task in data.get("tasks", [])],
|
|
689
|
+
"messages": data.get("messages", {}),
|
|
690
|
+
"results": data.get("results", {}),
|
|
691
|
+
"last_events": [_compact_event(event) for event in data.get("last_events", [])[-STATUS_EVENT_LIMIT:]],
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _compact_agent_state(agent_id: str, agent: dict[str, Any]) -> dict[str, Any]:
|
|
696
|
+
display = agent.get("display") or {}
|
|
697
|
+
result = _compact_mapping(
|
|
698
|
+
agent,
|
|
699
|
+
{
|
|
700
|
+
"agent_id",
|
|
701
|
+
"status",
|
|
702
|
+
"provider",
|
|
703
|
+
"model",
|
|
704
|
+
"tmux_window_present",
|
|
705
|
+
"session_id",
|
|
706
|
+
"captured_via",
|
|
707
|
+
"attribution_confidence",
|
|
708
|
+
},
|
|
709
|
+
)
|
|
710
|
+
result.setdefault("agent_id", agent_id)
|
|
711
|
+
if display:
|
|
712
|
+
result["display"] = _compact_mapping(
|
|
713
|
+
display,
|
|
714
|
+
{
|
|
715
|
+
"backend",
|
|
716
|
+
"status",
|
|
717
|
+
"workspace_window",
|
|
718
|
+
"pane_id",
|
|
719
|
+
"pid",
|
|
720
|
+
"pids",
|
|
721
|
+
"reason",
|
|
722
|
+
},
|
|
723
|
+
)
|
|
724
|
+
return result
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _compact_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
728
|
+
return _compact_mapping(
|
|
729
|
+
task,
|
|
730
|
+
{
|
|
731
|
+
"id",
|
|
732
|
+
"title",
|
|
733
|
+
"status",
|
|
734
|
+
"assignee",
|
|
735
|
+
"type",
|
|
736
|
+
"risk",
|
|
737
|
+
"accepted_result_id",
|
|
738
|
+
"last_result_summary",
|
|
739
|
+
},
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _compact_event(event: dict[str, Any]) -> dict[str, Any]:
|
|
744
|
+
skipped = {"command", "payload", "launch_args", "content", "prompt", "developer_instructions"}
|
|
745
|
+
kept = {
|
|
746
|
+
"event",
|
|
747
|
+
"ts",
|
|
748
|
+
"agent_id",
|
|
749
|
+
"task_id",
|
|
750
|
+
"message_id",
|
|
751
|
+
"result_id",
|
|
752
|
+
"status",
|
|
753
|
+
"ok",
|
|
754
|
+
"reason",
|
|
755
|
+
"error",
|
|
756
|
+
"session",
|
|
757
|
+
"window",
|
|
758
|
+
"target",
|
|
759
|
+
"backend",
|
|
760
|
+
"workspace_window",
|
|
761
|
+
"pane_id",
|
|
762
|
+
"restart_mode",
|
|
763
|
+
"provider",
|
|
764
|
+
"delivery_status",
|
|
765
|
+
"warning",
|
|
766
|
+
"collected",
|
|
767
|
+
"notified",
|
|
768
|
+
"lock",
|
|
769
|
+
"waited_sec",
|
|
770
|
+
"once",
|
|
771
|
+
"pid",
|
|
772
|
+
}
|
|
773
|
+
result: dict[str, Any] = {}
|
|
774
|
+
for key, value in event.items():
|
|
775
|
+
if key in skipped or key not in kept | {"agents", "coordinator"}:
|
|
776
|
+
continue
|
|
777
|
+
if key == "agents" and isinstance(value, list):
|
|
778
|
+
result["agent_count"] = len(value)
|
|
779
|
+
result["agents"] = [
|
|
780
|
+
_compact_mapping(item, {"agent_id", "restart_mode", "session_id"})
|
|
781
|
+
for item in value[:8]
|
|
782
|
+
if isinstance(item, dict)
|
|
783
|
+
]
|
|
784
|
+
continue
|
|
785
|
+
result[key] = _compact_value(value)
|
|
786
|
+
return result
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _compact_mapping(source: Any, keys: set[str]) -> dict[str, Any]:
|
|
790
|
+
if not isinstance(source, dict):
|
|
791
|
+
return {}
|
|
792
|
+
return {key: _compact_value(source[key]) for key in keys if key in source}
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _compact_value(value: Any) -> Any:
|
|
796
|
+
if isinstance(value, str):
|
|
797
|
+
return value if len(value) <= STATUS_TEXT_LIMIT else value[: STATUS_TEXT_LIMIT - 1] + "…"
|
|
798
|
+
if isinstance(value, (int, float, bool)) or value is None:
|
|
799
|
+
return value
|
|
800
|
+
if isinstance(value, list):
|
|
801
|
+
if all(isinstance(item, (str, int, float, bool)) or item is None for item in value):
|
|
802
|
+
compact = [_compact_value(item) for item in value[:8]]
|
|
803
|
+
if len(value) > 8:
|
|
804
|
+
compact.append(f"... {len(value) - 8} more")
|
|
805
|
+
return compact
|
|
806
|
+
return f"{len(value)} item(s)"
|
|
807
|
+
if isinstance(value, dict):
|
|
808
|
+
return {
|
|
809
|
+
key: _compact_value(item)
|
|
810
|
+
for key, item in value.items()
|
|
811
|
+
if key not in {"command", "payload", "launch_args", "content", "prompt", "developer_instructions"}
|
|
812
|
+
}
|
|
813
|
+
return str(value)
|
|
644
814
|
|
|
645
815
|
|
|
646
816
|
def format_status(workspace: Path, agent_id: str | None = None) -> str:
|
|
@@ -1766,8 +1936,12 @@ def shutdown(workspace: Path, keep_logs: bool = True) -> dict[str, Any]:
|
|
|
1766
1936
|
closed_displays.add(agent_id)
|
|
1767
1937
|
proc = run_cmd(["tmux", "kill-session", "-t", session_name], timeout=10)
|
|
1768
1938
|
if proc.returncode != 0:
|
|
1769
|
-
|
|
1770
|
-
|
|
1939
|
+
if "can't find session" in proc.stderr:
|
|
1940
|
+
event_log.write("shutdown.idempotent", session=session_name, reason="session disappeared before kill")
|
|
1941
|
+
else:
|
|
1942
|
+
raise RuntimeError(f"tmux kill-session failed: {proc.stderr.strip()}")
|
|
1943
|
+
else:
|
|
1944
|
+
event_log.write("shutdown.kill_session", session=session_name, keep_logs=keep_logs, captured=captured)
|
|
1771
1945
|
else:
|
|
1772
1946
|
event_log.write("shutdown.idempotent", session=session_name, reason="session missing")
|
|
1773
1947
|
_close_ghostty_workspace(state, event_log)
|
|
@@ -1801,10 +1975,16 @@ def shutdown(workspace: Path, keep_logs: bool = True) -> dict[str, Any]:
|
|
|
1801
1975
|
def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None) -> dict[str, Any]:
|
|
1802
1976
|
state = _select_restart_state(workspace, team)
|
|
1803
1977
|
spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
|
|
1804
|
-
if not spec_path.exists():
|
|
1805
|
-
raise RuntimeError(f"missing spec for restart: {spec_path}")
|
|
1806
|
-
spec = load_spec(spec_path)
|
|
1807
1978
|
team_dir = Path(str(state.get("team_dir"))) if state.get("team_dir") else _spec_team_dir(spec_path, workspace)
|
|
1979
|
+
if _is_team_doc_dir(team_dir):
|
|
1980
|
+
compiled = _compile_team_dir_spec(team_dir, workspace)
|
|
1981
|
+
spec = compiled["spec"]
|
|
1982
|
+
spec_path = team_dir / "team.spec.yaml"
|
|
1983
|
+
state["spec_path"] = str(spec_path)
|
|
1984
|
+
else:
|
|
1985
|
+
if not spec_path.exists():
|
|
1986
|
+
raise RuntimeError(f"missing spec for restart: {spec_path}")
|
|
1987
|
+
spec = load_spec(spec_path)
|
|
1808
1988
|
_attach_team_profile_dirs(spec, spec_path, workspace, team_dir)
|
|
1809
1989
|
ensure_workspace_dirs(workspace)
|
|
1810
1990
|
event_log = EventLog(workspace)
|
|
@@ -1819,6 +1999,9 @@ def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None)
|
|
|
1819
1999
|
raise RuntimeError(_tmux_session_conflict_error(session_name))
|
|
1820
2000
|
runtime_cfg = _effective_runtime_config(spec.get("runtime", {}))
|
|
1821
2001
|
display_backend = spec.get("runtime", {}).get("display_backend", state.get("display_backend", "none"))
|
|
2002
|
+
_close_ghostty_workspace(state, event_log)
|
|
2003
|
+
for agent_id, agent_state in state.get("agents", {}).items():
|
|
2004
|
+
_close_ghostty_display(agent_id, agent_state, event_log)
|
|
1822
2005
|
state["display_backend"] = display_backend
|
|
1823
2006
|
restart_agents = [
|
|
1824
2007
|
agent
|
|
@@ -3126,18 +3309,12 @@ def quick_start(
|
|
|
3126
3309
|
fresh: bool = False,
|
|
3127
3310
|
team_id: str | None = None,
|
|
3128
3311
|
) -> dict[str, Any]:
|
|
3129
|
-
from team_agent.compiler import compile_team
|
|
3130
|
-
|
|
3131
3312
|
team_dir = _prepare_quick_start_team(agents_dir.resolve(), Path.cwd().resolve(), name, team_id=team_id)
|
|
3132
3313
|
workspace = team_workspace(team_dir)
|
|
3133
3314
|
ensure_workspace_dirs(workspace)
|
|
3134
3315
|
_ensure_profiles_for_roles(team_dir)
|
|
3316
|
+
compiled = _compile_team_dir_spec(team_dir, workspace)
|
|
3135
3317
|
spec_path = team_dir / "team.spec.yaml"
|
|
3136
|
-
compiled = compile_team(team_dir, spec_path)
|
|
3137
|
-
if compiled["spec"].get("context", {}).get("state_file") == "team_state.md":
|
|
3138
|
-
state_file = str(team_dir.relative_to(workspace) / "team_state.md") if team_dir.is_relative_to(workspace) else "team_state.md"
|
|
3139
|
-
compiled["spec"]["context"]["state_file"] = state_file
|
|
3140
|
-
spec_path.write_text(dumps(compiled["spec"]), encoding="utf-8")
|
|
3141
3318
|
existing = _quick_start_existing_context(workspace, compiled["spec"]["runtime"]["session_name"])
|
|
3142
3319
|
if existing and not fresh:
|
|
3143
3320
|
return {
|
|
@@ -4715,6 +4892,7 @@ def _open_ghostty_workspace(
|
|
|
4715
4892
|
"linked_session": linked_session,
|
|
4716
4893
|
"aggregator_session": aggregator_session,
|
|
4717
4894
|
"display_session": aggregator_session,
|
|
4895
|
+
"workspace_window": pane.get("window_name"),
|
|
4718
4896
|
"pane_id": pane.get("pane_id"),
|
|
4719
4897
|
"launch_args": launch_args,
|
|
4720
4898
|
"pid": pids[0] if pids else None,
|
|
@@ -4790,6 +4968,10 @@ def _ghostty_workspace_aggregator_name(session_name: str) -> str:
|
|
|
4790
4968
|
return f"{safe_session}__display__workspace__{digest}"
|
|
4791
4969
|
|
|
4792
4970
|
|
|
4971
|
+
def _ghostty_workspace_window_name(index: int) -> str:
|
|
4972
|
+
return "overview" if index == 0 else f"overview-{index + 1}"
|
|
4973
|
+
|
|
4974
|
+
|
|
4793
4975
|
def _ghostty_workspace_pane_command(linked_session: str) -> str:
|
|
4794
4976
|
return f"TMUX= tmux attach-session -t {shlex.quote(linked_session)}"
|
|
4795
4977
|
|
|
@@ -4833,75 +5015,121 @@ def _prepare_ghostty_workspace_aggregator(
|
|
|
4833
5015
|
aggregator_session: str,
|
|
4834
5016
|
linked_jobs: list[tuple[str, dict[str, Any], str]],
|
|
4835
5017
|
) -> dict[str, Any]:
|
|
4836
|
-
window_name = "overview"
|
|
4837
5018
|
if _tmux_session_exists(aggregator_session):
|
|
4838
5019
|
proc = run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4839
5020
|
if proc.returncode != 0:
|
|
4840
5021
|
return {"ok": False, "reason": "display_session_cleanup_failed", "error": proc.stderr.strip()}
|
|
4841
5022
|
|
|
4842
|
-
|
|
4843
|
-
first_agent_id, first_agent, first_linked_session = linked_jobs[0]
|
|
4844
|
-
proc = run_cmd(
|
|
4845
|
-
[
|
|
4846
|
-
"tmux",
|
|
4847
|
-
"new-session",
|
|
4848
|
-
"-d",
|
|
4849
|
-
"-P",
|
|
4850
|
-
"-F",
|
|
4851
|
-
"#{pane_id}",
|
|
4852
|
-
"-s",
|
|
4853
|
-
aggregator_session,
|
|
4854
|
-
"-n",
|
|
4855
|
-
window_name,
|
|
4856
|
-
_ghostty_workspace_pane_command(first_linked_session),
|
|
4857
|
-
],
|
|
4858
|
-
timeout=10,
|
|
4859
|
-
)
|
|
4860
|
-
if proc.returncode != 0:
|
|
4861
|
-
return {"ok": False, "reason": "display_session_create_failed", "error": proc.stderr.strip()}
|
|
4862
|
-
first_pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.0"
|
|
4863
|
-
first_title = _ghostty_workspace_pane_title(first_agent)
|
|
4864
|
-
title_result = _set_ghostty_workspace_pane_title(first_pane_id, first_title)
|
|
4865
|
-
if not title_result["ok"]:
|
|
5023
|
+
def fail(reason: str, proc: Any | None = None, target: str | None = None) -> dict[str, Any]:
|
|
4866
5024
|
run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
return {"ok": False, "reason": "display_session_remain_on_exit_failed", "error": proc.stderr.strip()}
|
|
5025
|
+
result = {"ok": False, "reason": reason}
|
|
5026
|
+
if proc is not None:
|
|
5027
|
+
result["error"] = proc.stderr.strip()
|
|
5028
|
+
if target:
|
|
5029
|
+
result["target"] = target
|
|
5030
|
+
return result
|
|
4874
5031
|
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
5032
|
+
panes: list[dict[str, Any]] = []
|
|
5033
|
+
for window_index, start in enumerate(range(0, len(linked_jobs), GHOSTTY_WORKSPACE_PANES_PER_WINDOW)):
|
|
5034
|
+
window_name = _ghostty_workspace_window_name(window_index)
|
|
5035
|
+
window_jobs = linked_jobs[start : start + GHOSTTY_WORKSPACE_PANES_PER_WINDOW]
|
|
5036
|
+
first_agent_id, first_agent, first_linked_session = window_jobs[0]
|
|
5037
|
+
if window_index == 0:
|
|
5038
|
+
proc = run_cmd(
|
|
5039
|
+
[
|
|
5040
|
+
"tmux",
|
|
5041
|
+
"new-session",
|
|
5042
|
+
"-d",
|
|
5043
|
+
"-P",
|
|
5044
|
+
"-F",
|
|
5045
|
+
"#{pane_id}",
|
|
5046
|
+
"-s",
|
|
5047
|
+
aggregator_session,
|
|
5048
|
+
"-n",
|
|
5049
|
+
window_name,
|
|
5050
|
+
_ghostty_workspace_pane_command(first_linked_session),
|
|
5051
|
+
],
|
|
5052
|
+
timeout=10,
|
|
5053
|
+
)
|
|
5054
|
+
if proc.returncode != 0:
|
|
5055
|
+
return {"ok": False, "reason": "display_session_create_failed", "error": proc.stderr.strip()}
|
|
5056
|
+
else:
|
|
5057
|
+
proc = run_cmd(
|
|
5058
|
+
[
|
|
5059
|
+
"tmux",
|
|
5060
|
+
"new-window",
|
|
5061
|
+
"-t",
|
|
5062
|
+
aggregator_session,
|
|
5063
|
+
"-n",
|
|
5064
|
+
window_name,
|
|
5065
|
+
"-P",
|
|
5066
|
+
"-F",
|
|
5067
|
+
"#{pane_id}",
|
|
5068
|
+
_ghostty_workspace_pane_command(first_linked_session),
|
|
5069
|
+
],
|
|
5070
|
+
timeout=10,
|
|
5071
|
+
)
|
|
5072
|
+
if proc.returncode != 0:
|
|
5073
|
+
return fail("display_session_window_create_failed", proc, first_linked_session)
|
|
5074
|
+
first_pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.0"
|
|
5075
|
+
first_title = _ghostty_workspace_pane_title(first_agent)
|
|
5076
|
+
title_result = _set_ghostty_workspace_pane_title(first_pane_id, first_title)
|
|
5077
|
+
if not title_result["ok"]:
|
|
5078
|
+
return fail(title_result["reason"], target=first_pane_id)
|
|
5079
|
+
panes.append(
|
|
5080
|
+
{
|
|
5081
|
+
"agent_id": first_agent_id,
|
|
5082
|
+
"pane_id": first_pane_id,
|
|
5083
|
+
"title": first_title,
|
|
5084
|
+
"linked_session": first_linked_session,
|
|
5085
|
+
"window_name": window_name,
|
|
5086
|
+
}
|
|
4889
5087
|
)
|
|
5088
|
+
|
|
5089
|
+
proc = run_cmd(["tmux", "set-window-option", "-t", f"{aggregator_session}:{window_name}", "remain-on-exit", "on"], timeout=10)
|
|
4890
5090
|
if proc.returncode != 0:
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
5091
|
+
return fail("display_session_remain_on_exit_failed", proc)
|
|
5092
|
+
|
|
5093
|
+
for index, (agent_id, agent, linked_session) in enumerate(window_jobs[1:], start=1):
|
|
5094
|
+
proc = run_cmd(
|
|
5095
|
+
[
|
|
5096
|
+
"tmux",
|
|
5097
|
+
"split-window",
|
|
5098
|
+
"-t",
|
|
5099
|
+
f"{aggregator_session}:{window_name}",
|
|
5100
|
+
"-h",
|
|
5101
|
+
"-P",
|
|
5102
|
+
"-F",
|
|
5103
|
+
"#{pane_id}",
|
|
5104
|
+
_ghostty_workspace_pane_command(linked_session),
|
|
5105
|
+
],
|
|
5106
|
+
timeout=10,
|
|
5107
|
+
)
|
|
5108
|
+
if proc.returncode != 0:
|
|
5109
|
+
return fail("display_session_split_failed", proc, linked_session)
|
|
5110
|
+
pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.{index}"
|
|
5111
|
+
title = _ghostty_workspace_pane_title(agent)
|
|
5112
|
+
title_result = _set_ghostty_workspace_pane_title(pane_id, title)
|
|
5113
|
+
if not title_result["ok"]:
|
|
5114
|
+
return fail(title_result["reason"], target=pane_id)
|
|
5115
|
+
panes.append(
|
|
5116
|
+
{
|
|
5117
|
+
"agent_id": agent_id,
|
|
5118
|
+
"pane_id": pane_id,
|
|
5119
|
+
"title": title,
|
|
5120
|
+
"linked_session": linked_session,
|
|
5121
|
+
"window_name": window_name,
|
|
5122
|
+
}
|
|
5123
|
+
)
|
|
4900
5124
|
|
|
4901
|
-
|
|
5125
|
+
proc = run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:{window_name}", "even-horizontal"], timeout=10)
|
|
5126
|
+
if proc.returncode != 0:
|
|
5127
|
+
return fail("display_session_layout_failed", proc)
|
|
5128
|
+
|
|
5129
|
+
proc = run_cmd(["tmux", "set-option", "-t", aggregator_session, "mouse", "on"], timeout=10)
|
|
4902
5130
|
if proc.returncode != 0:
|
|
4903
|
-
|
|
4904
|
-
|
|
5131
|
+
return fail("display_session_mouse_failed", proc)
|
|
5132
|
+
run_cmd(["tmux", "select-window", "-t", f"{aggregator_session}:{_ghostty_workspace_window_name(0)}"], timeout=10)
|
|
4905
5133
|
return {"ok": True, "aggregator_session": aggregator_session, "panes": panes}
|
|
4906
5134
|
|
|
4907
5135
|
|
|
@@ -4963,6 +5191,7 @@ def _open_ghostty_workspace_agent_display(
|
|
|
4963
5191
|
pane_title = _ghostty_workspace_pane_title(agent)
|
|
4964
5192
|
command = _ghostty_workspace_pane_command(linked_session)
|
|
4965
5193
|
pane_id = str(previous_display.get("pane_id") or "")
|
|
5194
|
+
workspace_window = str(previous_display.get("workspace_window") or _ghostty_workspace_window_name(0))
|
|
4966
5195
|
refreshed = False
|
|
4967
5196
|
if pane_id:
|
|
4968
5197
|
proc = run_cmd(["tmux", "respawn-pane", "-k", "-t", pane_id, command], timeout=10)
|
|
@@ -4973,7 +5202,7 @@ def _open_ghostty_workspace_agent_display(
|
|
|
4973
5202
|
"tmux",
|
|
4974
5203
|
"split-window",
|
|
4975
5204
|
"-t",
|
|
4976
|
-
f"{aggregator_session}:
|
|
5205
|
+
f"{aggregator_session}:{workspace_window}",
|
|
4977
5206
|
"-h",
|
|
4978
5207
|
"-P",
|
|
4979
5208
|
"-F",
|
|
@@ -5002,7 +5231,7 @@ def _open_ghostty_workspace_agent_display(
|
|
|
5002
5231
|
reason=title_result["reason"],
|
|
5003
5232
|
note=title_result.get("error") or "pane refresh requires full team restart",
|
|
5004
5233
|
)
|
|
5005
|
-
run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:
|
|
5234
|
+
run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:{workspace_window}", "even-horizontal"], timeout=10)
|
|
5006
5235
|
title = str(previous_display.get("title") or f"team-agent:{session_name}:workspace")
|
|
5007
5236
|
pids = [int(pid) for pid in previous_display.get("pids", []) if str(pid).isdigit()]
|
|
5008
5237
|
display = {
|
|
@@ -5014,6 +5243,7 @@ def _open_ghostty_workspace_agent_display(
|
|
|
5014
5243
|
"linked_session": linked_session,
|
|
5015
5244
|
"aggregator_session": aggregator_session,
|
|
5016
5245
|
"display_session": aggregator_session,
|
|
5246
|
+
"workspace_window": workspace_window,
|
|
5017
5247
|
"pane_id": pane_id,
|
|
5018
5248
|
"pid": pids[0] if pids else None,
|
|
5019
5249
|
"pids": pids,
|