@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 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. Teammates appear in separate windows. The lead reports progress, raises decisions when needed, and shuts everything down when you say so.
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
- 完事。队员窗口出现,lead 推进工作、需要决策时停下来问你,你说"关掉"就关掉。
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -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: ghostty_window
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
- Omitting `display_backend` defaults to `ghostty_window`; set `display_backend: none` only for headless/CI runs.
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.
@@ -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
- return {
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
- raise RuntimeError(f"tmux kill-session failed: {proc.stderr.strip()}")
1770
- event_log.write("shutdown.kill_session", session=session_name, keep_logs=keep_logs, captured=captured)
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
- panes: list[dict[str, Any]] = []
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
- return title_result
4868
- panes.append({"agent_id": first_agent_id, "pane_id": first_pane_id, "title": first_title, "linked_session": first_linked_session})
4869
-
4870
- proc = run_cmd(["tmux", "set-window-option", "-t", f"{aggregator_session}:{window_name}", "remain-on-exit", "on"], timeout=10)
4871
- if proc.returncode != 0:
4872
- run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
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
- for index, (agent_id, agent, linked_session) in enumerate(linked_jobs[1:], start=1):
4876
- proc = run_cmd(
4877
- [
4878
- "tmux",
4879
- "split-window",
4880
- "-t",
4881
- f"{aggregator_session}:{window_name}",
4882
- "-h",
4883
- "-P",
4884
- "-F",
4885
- "#{pane_id}",
4886
- _ghostty_workspace_pane_command(linked_session),
4887
- ],
4888
- timeout=10,
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
- run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
4892
- return {"ok": False, "reason": "display_session_split_failed", "error": proc.stderr.strip(), "target": linked_session}
4893
- pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.{index}"
4894
- title = _ghostty_workspace_pane_title(agent)
4895
- title_result = _set_ghostty_workspace_pane_title(pane_id, title)
4896
- if not title_result["ok"]:
4897
- run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
4898
- return title_result
4899
- panes.append({"agent_id": agent_id, "pane_id": pane_id, "title": title, "linked_session": linked_session})
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
- proc = run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:{window_name}", "even-horizontal"], timeout=10)
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
- run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
4904
- return {"ok": False, "reason": "display_session_layout_failed", "error": proc.stderr.strip()}
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}:overview",
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}:overview", "even-horizontal"], timeout=10)
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,