@@ -40,7 +40,7 @@ from session_utils import (
4040)
4141from pane_registry import upsert_registry , load_registry_by_project_id
4242from project_id import compute_ccb_project_id
43- from providers import CASK_CLIENT_SPEC , GASK_CLIENT_SPEC , OASK_CLIENT_SPEC , LASK_CLIENT_SPEC , DASK_CLIENT_SPEC
43+ from providers import CASK_CLIENT_SPEC , GASK_CLIENT_SPEC , OASK_CLIENT_SPEC , LASK_CLIENT_SPEC , DASK_CLIENT_SPEC , HASK_CLIENT_SPEC , BASK_CLIENT_SPEC , QASK_CLIENT_SPEC
4444from process_lock import ProviderLock
4545from askd_rpc import shutdown_daemon , read_state
4646from askd_runtime import state_file_path
@@ -608,7 +608,7 @@ class AILauncher:
608608 """Managed env + explicit caller marker for the pane/provider process."""
609609 env = self ._managed_env_overrides ()
610610 prov = (provider or "" ).strip ().lower ()
611- if prov in {"claude" , "codex" , "gemini" , "opencode" , "droid" , "email" , "manual" }:
611+ if prov in {"claude" , "codex" , "gemini" , "opencode" , "droid" , "copilot" , "codebuddy" , "qwen" , " email" , "manual" }:
612612 env ["CCB_CALLER" ] = prov
613613 return env
614614
@@ -631,7 +631,7 @@ class AILauncher:
631631 if not cfg .is_dir ():
632632 return
633633
634- for name in (".codex-session" , ".gemini-session" , ".opencode-session" , ".claude-session" , ".droid-session" ):
634+ for name in (".codex-session" , ".gemini-session" , ".opencode-session" , ".claude-session" , ".droid-session" , ".copilot-session" , ".codebuddy-session" , ".qwen-session" ):
635635 legacy = self .project_root / name
636636 if not legacy .exists ():
637637 continue
@@ -760,7 +760,7 @@ class AILauncher:
760760 def _maybe_start_unified_askd (self , * , quiet : bool = False ) -> None :
761761 """Start unified askd daemon (provider-agnostic)."""
762762 # Try to start for any enabled provider that uses askd (including claude)
763- for provider in ["codex" , "gemini" , "opencode" , "droid" , "claude" ]:
763+ for provider in ["codex" , "gemini" , "opencode" , "droid" , "claude" , "copilot" , "codebuddy" , "qwen" ]:
764764 if provider in [p .lower () for p in self .providers ]:
765765 # Try to start and check if successful
766766 self ._maybe_start_provider_daemon (provider , quiet = quiet )
@@ -799,6 +799,9 @@ class AILauncher:
799799 "opencode" : OASK_CLIENT_SPEC ,
800800 "claude" : LASK_CLIENT_SPEC ,
801801 "droid" : DASK_CLIENT_SPEC ,
802+ "copilot" : HASK_CLIENT_SPEC ,
803+ "codebuddy" : BASK_CLIENT_SPEC ,
804+ "qwen" : QASK_CLIENT_SPEC ,
802805 }
803806 spec = specs .get (provider )
804807 if not spec :
@@ -1227,6 +1230,8 @@ class AILauncher:
12271230 return self ._start_opencode_tmux (parent_pane = parent_pane , direction = direction )
12281231 elif provider == "droid" :
12291232 return self ._start_droid_tmux (parent_pane = parent_pane , direction = direction )
1233+ elif provider in ("copilot" , "codebuddy" , "qwen" ):
1234+ return self ._start_generic_tmux (provider , parent_pane = parent_pane , direction = direction )
12301235 else :
12311236 print (f"❌ { t ('unknown_provider' , provider = provider )} " )
12321237 return None
@@ -1280,6 +1285,8 @@ class AILauncher:
12801285 self ._write_opencode_session (runtime , None , pane_id = pane_id , pane_title_marker = pane_title_marker , start_cmd = start_cmd )
12811286 elif provider == "droid" :
12821287 self ._write_droid_session (runtime , None , pane_id = pane_id , pane_title_marker = pane_title_marker , start_cmd = start_cmd )
1288+ elif provider in ("copilot" , "codebuddy" , "qwen" ):
1289+ self ._write_generic_session (provider , runtime , None , pane_id = pane_id , pane_title_marker = pane_title_marker , start_cmd = start_cmd )
12831290 else :
12841291 print (f"❌ { t ('unknown_provider' , provider = provider )} " )
12851292 return None
@@ -1851,16 +1858,19 @@ class AILauncher:
18511858 def _warmup_provider (self , provider : str , timeout : float = 8.0 ) -> bool :
18521859 if provider == "gemini" :
18531860 return True
1854- if provider == "codex" :
1855- ping_script = self .script_dir / "bin" / "cping"
1856- elif provider == "gemini" :
1857- ping_script = self .script_dir / "bin" / "gping"
1858- elif provider == "opencode" :
1859- ping_script = self .script_dir / "bin" / "oping"
1860- elif provider == "droid" :
1861- ping_script = self .script_dir / "bin" / "dping"
1862- else :
1861+ ping_map = {
1862+ "codex" : "cping" ,
1863+ "gemini" : "gping" ,
1864+ "opencode" : "oping" ,
1865+ "droid" : "dping" ,
1866+ "copilot" : "hping" ,
1867+ "codebuddy" : "bping" ,
1868+ "qwen" : "qping" ,
1869+ }
1870+ ping_name = ping_map .get (provider )
1871+ if not ping_name :
18631872 return False
1873+ ping_script = self .script_dir / "bin" / ping_name
18641874
18651875 if not ping_script .exists ():
18661876 return False
@@ -1904,6 +1914,12 @@ class AILauncher:
19041914 return self ._build_opencode_start_cmd ()
19051915 elif provider == "droid" :
19061916 return self ._build_droid_start_cmd ()
1917+ elif provider == "copilot" :
1918+ return self ._build_generic_start_cmd (provider , "gh copilot" , "COPILOT_START_CMD" )
1919+ elif provider == "codebuddy" :
1920+ return self ._build_generic_start_cmd (provider , "codebuddy" , "CODEBUDDY_START_CMD" )
1921+ elif provider == "qwen" :
1922+ return self ._build_generic_start_cmd (provider , "qwen" , "QWEN_START_CMD" )
19071923 return ""
19081924
19091925 def _opencode_resume_allowed (self ) -> bool :
@@ -2347,6 +2363,113 @@ class AILauncher:
23472363 print (f"✅ { t ('started_backend' , provider = 'Droid' , terminal = 'tmux pane' , pane_id = pane_id )} " )
23482364 return pane_id
23492365
2366+ def _start_generic_tmux (
2367+ self ,
2368+ provider : str ,
2369+ * ,
2370+ parent_pane : str | None = None ,
2371+ direction : str | None = None ,
2372+ ) -> str | None :
2373+ runtime = self .runtime_dir / provider
2374+ runtime .mkdir (parents = True , exist_ok = True )
2375+
2376+ env_overrides = self ._provider_env_overrides (provider )
2377+ start_cmd = self ._build_env_prefix (env_overrides ) + _build_export_path_cmd (self .script_dir / "bin" ) + self ._get_start_cmd (provider )
2378+ pane_title_marker = f"CCB-{ provider .capitalize ()} "
2379+
2380+ backend = TmuxBackend ()
2381+
2382+ use_direction = (direction or ("right" if not self .tmux_panes else "bottom" )).strip () or "right"
2383+ use_parent = parent_pane
2384+ if not use_parent :
2385+ try :
2386+ use_parent = backend .get_current_pane_id ()
2387+ except Exception :
2388+ use_parent = None
2389+ if not use_parent and use_direction == "bottom" :
2390+ try :
2391+ use_parent = next (reversed (self .tmux_panes .values ()))
2392+ except StopIteration :
2393+ use_parent = None
2394+
2395+ try :
2396+ if use_parent and str (use_parent ).startswith ("%" ) and not backend .pane_exists (str (use_parent )):
2397+ use_parent = backend .get_current_pane_id ()
2398+ except Exception :
2399+ use_parent = None
2400+
2401+ pane_id = backend .create_pane ("" , str (Path .cwd ()), direction = use_direction , percent = 50 , parent_pane = use_parent )
2402+ backend .respawn_pane (pane_id , cmd = start_cmd , cwd = str (Path .cwd ()), remain_on_exit = True )
2403+ backend .set_pane_title (pane_id , pane_title_marker )
2404+ backend .set_pane_user_option (pane_id , "@ccb_agent" , provider .capitalize ())
2405+
2406+ self .tmux_panes [provider ] = pane_id
2407+
2408+ self ._write_generic_session (
2409+ provider ,
2410+ runtime ,
2411+ None ,
2412+ pane_id = pane_id ,
2413+ pane_title_marker = pane_title_marker ,
2414+ start_cmd = start_cmd ,
2415+ )
2416+
2417+ print (f"✅ { t ('started_backend' , provider = provider .capitalize (), terminal = 'tmux pane' , pane_id = pane_id )} " )
2418+ return pane_id
2419+
2420+ def _build_generic_start_cmd (self , provider : str , default_cmd : str , env_var : str ) -> str :
2421+ cmd = (os .environ .get (env_var ) or default_cmd ).strip () or default_cmd
2422+ return cmd
2423+
2424+ def _write_generic_session (self , provider , runtime , tmux_session , pane_id = None , pane_title_marker = None , start_cmd = None ):
2425+ session_file = self ._project_session_file (f".{ provider } -session" )
2426+
2427+ writable , reason , fix = check_session_writable (session_file )
2428+ if not writable :
2429+ print (f"❌ Cannot write { session_file .name } : { reason } " , file = sys .stderr )
2430+ print (f"💡 Fix: { fix } " , file = sys .stderr )
2431+ return False
2432+
2433+ data = {
2434+ "session_id" : self .session_id ,
2435+ "ccb_session_id" : self .session_id ,
2436+ "ccb_project_id" : compute_ccb_project_id (self .project_root ),
2437+ "runtime_dir" : str (runtime ),
2438+ "terminal" : self .terminal_type ,
2439+ "tmux_session" : tmux_session ,
2440+ "pane_id" : pane_id ,
2441+ "pane_title_marker" : pane_title_marker ,
2442+ "work_dir" : str (self .project_root ),
2443+ "work_dir_norm" : _normalize_path_for_match (str (self .project_root )),
2444+ "start_dir" : str (self .invocation_dir ),
2445+ "active" : True ,
2446+ "started_at" : time .strftime ("%Y-%m-%d %H:%M:%S" ),
2447+ "start_cmd" : str (start_cmd ) if start_cmd else None ,
2448+ }
2449+
2450+ ok , err = safe_write_session (session_file , json .dumps (data , ensure_ascii = False , indent = 2 ))
2451+ if not ok :
2452+ print (err , file = sys .stderr )
2453+ return False
2454+ try :
2455+ upsert_registry ({
2456+ "ccb_session_id" : self .session_id ,
2457+ "ccb_project_id" : compute_ccb_project_id (self .project_root ),
2458+ "work_dir" : str (self .project_root ),
2459+ "terminal" : self .terminal_type ,
2460+ "providers" : {
2461+ provider : {
2462+ "pane_id" : pane_id ,
2463+ "pane_title_marker" : pane_title_marker ,
2464+ "session_file" : str (session_file ),
2465+ }
2466+ },
2467+ })
2468+ except Exception :
2469+ pass
2470+ self ._maybe_start_provider_daemon (provider )
2471+ return True
2472+
23502473 def _start_cmd_pane (
23512474 self ,
23522475 * ,
@@ -3068,6 +3191,19 @@ class AILauncher:
30683191 else :
30693192 env ["DROID_TMUX_SESSION" ] = pane_id
30703193
3194+ for extra in ("copilot" , "codebuddy" , "qwen" ):
3195+ if extra in self .providers :
3196+ runtime = self .runtime_dir / extra
3197+ prefix = extra .upper ()
3198+ env [f"{ prefix } _SESSION_ID" ] = self .session_id
3199+ env [f"{ prefix } _RUNTIME_DIR" ] = str (runtime )
3200+ env [f"{ prefix } _TERMINAL" ] = self .terminal_type or ""
3201+ pane_id = self ._provider_pane_id (extra )
3202+ if self .terminal_type == "wezterm" :
3203+ env [f"{ prefix } _WEZTERM_PANE" ] = pane_id
3204+ else :
3205+ env [f"{ prefix } _TMUX_SESSION" ] = pane_id
3206+
30713207 return env
30723208
30733209 def _build_claude_env (self ) -> dict :
@@ -4847,7 +4983,7 @@ def main():
48474983 subparsers = parser .add_subparsers (dest = "command" , help = "Subcommands" )
48484984
48494985 kill_parser = subparsers .add_parser ("kill" , help = "Terminate session or clean up zombies" )
4850- kill_parser .add_argument ("providers" , nargs = "*" , default = [], help = "Backends to terminate (codex/gemini/opencode/claude/droid)" )
4986+ kill_parser .add_argument ("providers" , nargs = "*" , default = [], help = "Backends to terminate (codex/gemini/opencode/claude/droid/copilot/codebuddy/qwen )" )
48514987 kill_parser .add_argument ("-f" , "--force" , action = "store_true" , help = "Clean up all zombie tmux sessions globally" )
48524988 kill_parser .add_argument ("-y" , "--yes" , action = "store_true" , help = "Skip confirmation prompt (with -f)" )
48534989
@@ -4885,7 +5021,7 @@ def main():
48855021 start_parser .add_argument (
48865022 "providers" ,
48875023 nargs = "*" ,
4888- help = "Backends to start (space or comma separated): codex, gemini, opencode, claude, droid (add cmd for a shell pane)" ,
5024+ help = "Backends to start (space or comma separated): codex, gemini, opencode, claude, droid, copilot, codebuddy, qwen (add cmd for a shell pane)" ,
48895025 )
48905026 start_parser .add_argument ("-r" , "--resume" , "--restore" , action = "store_true" , help = "Resume context" )
48915027 start_parser .add_argument ("-a" , "--auto" , action = "store_true" , help = "Full auto permission mode" )
0 commit comments