aline-ai 0.5.5__py3-none-any.whl → 0.5.7__py3-none-any.whl

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.
@@ -11,7 +11,6 @@ import asyncio
11
11
  import os
12
12
  import re
13
13
  import shlex
14
- import subprocess
15
14
  from pathlib import Path
16
15
  from typing import Callable
17
16
 
@@ -295,30 +294,15 @@ class TerminalPanel(Container, can_focus=True):
295
294
  controls_enabled = self.supported()
296
295
  with Horizontal(classes="summary"):
297
296
  yield Button(
298
- "+ Claude",
299
- id="new-cc",
297
+ "+ Create",
298
+ id="new-agent",
300
299
  variant="primary",
301
300
  disabled=not controls_enabled,
302
301
  )
303
- yield Button(
304
- "+ Codex",
305
- id="new-codex",
306
- variant="primary",
307
- disabled=not controls_enabled,
308
- )
309
- yield Button(
310
- "+ Opencode",
311
- id="new-opencode",
312
- variant="primary",
313
- disabled=not controls_enabled,
314
- )
315
- yield Button("+ zsh", id="new-zsh", variant="primary", disabled=not controls_enabled)
316
- yield Button("↻", id="refresh")
317
- yield Static("", id="status", classes="status")
318
302
  with Vertical(id="terminals", classes="list"):
319
303
  if controls_enabled:
320
304
  yield Static(
321
- "No terminals yet. Click 'New cc' / 'New codex' to open the right pane."
305
+ "No terminals yet. Click 'Create' to open a new agent terminal."
322
306
  )
323
307
  else:
324
308
  yield Static(self._support_message())
@@ -373,21 +357,15 @@ class TerminalPanel(Container, can_focus=True):
373
357
  async with self._refresh_lock:
374
358
  try:
375
359
  supported = self.supported()
376
- except Exception as e:
377
- self._set_status(f"Terminal support check failed: {e}")
360
+ except Exception:
378
361
  return
379
362
 
380
363
  if not supported:
381
- try:
382
- self._set_status(self._support_message())
383
- except Exception:
384
- self._set_status("Terminal not supported")
385
364
  return
386
365
 
387
366
  try:
388
367
  windows = tmux_manager.list_inner_windows()
389
- except Exception as e:
390
- self._set_status(f"Failed to query tmux windows: {e}")
368
+ except Exception:
391
369
  return
392
370
  active_window_id = next((w.window_id for w in windows if w.active), None)
393
371
  if self._expanded_window_id and self._expanded_window_id != active_window_id:
@@ -419,10 +397,8 @@ class TerminalPanel(Container, can_focus=True):
419
397
 
420
398
  try:
421
399
  await self._render_terminals(windows, titles, context_info_by_context_id)
422
- except Exception as e:
423
- self._set_status(f"Failed to render terminal list: {e}")
400
+ except Exception:
424
401
  return
425
- self._set_status(f"{len(windows)} terminals")
426
402
 
427
403
  def _fetch_claude_session_titles(self, session_ids: list[str]) -> dict[str, str]:
428
404
  if not session_ids:
@@ -487,12 +463,6 @@ class TerminalPanel(Container, can_focus=True):
487
463
  except Exception:
488
464
  return ([], 0, 0)
489
465
 
490
- def _set_status(self, text: str) -> None:
491
- try:
492
- self.query_one("#status", Static).update(text)
493
- except Exception:
494
- pass
495
-
496
466
  async def _render_terminals(
497
467
  self,
498
468
  windows: list[tmux_manager.InnerWindow],
@@ -504,7 +474,7 @@ class TerminalPanel(Container, can_focus=True):
504
474
 
505
475
  if not windows:
506
476
  await container.mount(
507
- Static("No terminals yet. Click 'New cc' / 'New codex' to open the right pane.")
477
+ Static("No terminals yet. Click 'Create' to open a new agent terminal.")
508
478
  )
509
479
  return
510
480
 
@@ -539,9 +509,7 @@ class TerminalPanel(Container, can_focus=True):
539
509
  if w.active and can_toggle_ctx:
540
510
  await row.mount(
541
511
  Button(
542
- # Avoid Rich crash when Textual measures with width=0 (can happen during
543
- # initial layout / tab switching).
544
- "⮟" if expanded else "⮞",
512
+ "▼" if expanded else "▶",
545
513
  id=f"toggle-{safe}",
546
514
  name=w.window_id,
547
515
  variant="default",
@@ -609,9 +577,8 @@ class TerminalPanel(Container, can_focus=True):
609
577
  details.append(header)
610
578
  details.append("\n")
611
579
 
612
- # Include window_name to distinguish terminals with same session_id
613
- window_name = w.window_name or ""
614
- detail_line = f"[{window_name}]" if window_name else "claude"
580
+ # Build detail line with Claude label
581
+ detail_line = "[Claude]"
615
582
  if w.session_id:
616
583
  detail_line = f"{detail_line} #{self._short_id(w.session_id)}"
617
584
  if w.active:
@@ -641,180 +608,149 @@ class TerminalPanel(Container, can_focus=True):
641
608
  return f"w-{safe}"
642
609
  return safe
643
610
 
644
- async def _select_workspace(self, prompt: str = "Select workspace") -> str | None:
645
- """Open macOS folder picker and return selected path, or None if cancelled."""
646
- try:
647
- default_path = os.getcwd()
648
- except Exception:
649
- # Can happen if the original working directory was deleted/moved.
650
- default_path = str(Path.home())
651
- # Use osascript to invoke macOS native folder picker
652
- default_path_escaped = default_path.replace('"', '\\"')
653
- prompt_escaped = prompt.replace('"', '\\"')
654
- script = f"""
655
- set defaultFolder to POSIX file "{default_path_escaped}" as alias
656
- try
657
- set selectedFolder to choose folder with prompt "{prompt_escaped}" default location defaultFolder
658
- return POSIX path of selectedFolder
659
- on error
660
- return ""
661
- end try
662
- """
663
- try:
664
- proc = await asyncio.get_event_loop().run_in_executor(
665
- None,
666
- lambda: subprocess.run(
667
- ["osascript", "-e", script],
668
- capture_output=True,
669
- text=True,
670
- timeout=120,
671
- ),
672
- )
673
- result = (proc.stdout or "").strip()
674
- if result and os.path.isdir(result):
675
- return result
676
- return None
677
- except Exception:
678
- return None
679
-
680
611
  @staticmethod
681
612
  def _command_in_directory(command: str, directory: str) -> str:
682
613
  """Wrap a command to run in a specific directory."""
683
614
  return f"cd {shlex.quote(directory)} && {command}"
684
615
 
685
- async def on_button_pressed(self, event: Button.Pressed) -> None:
686
- button_id = event.button.id or ""
687
-
688
- if button_id == "refresh":
689
- await self.refresh_data()
690
- return
691
-
692
- if not self.supported():
693
- self.app.notify(
694
- self._support_message(),
695
- title="Terminal",
696
- severity="warning",
697
- )
616
+ def _on_create_agent_result(self, result: tuple[str, str] | None) -> None:
617
+ """Handle the result from CreateAgentScreen modal."""
618
+ if result is None:
698
619
  return
699
620
 
700
- if button_id == "new-cc":
701
- workspace = await self._select_workspace("Select workspace for Claude")
702
- if not workspace:
703
- return
704
-
705
- terminal_id = tmux_manager.new_terminal_id()
706
- context_id = tmux_manager.new_context_id("cc")
707
- env = {
708
- tmux_manager.ENV_TERMINAL_ID: terminal_id,
709
- tmux_manager.ENV_TERMINAL_PROVIDER: "claude",
710
- tmux_manager.ENV_INNER_SOCKET: tmux_manager.INNER_SOCKET,
711
- tmux_manager.ENV_INNER_SESSION: tmux_manager.INNER_SESSION,
712
- tmux_manager.ENV_CONTEXT_ID: context_id,
713
- }
714
-
715
- try:
716
- from pathlib import Path
717
-
718
- from ...claude_hooks.stop_hook_installer import (
719
- ensure_stop_hook_installed,
720
- get_settings_path as get_stop_settings_path,
721
- install_stop_hook,
722
- )
723
- from ...claude_hooks.user_prompt_submit_hook_installer import (
724
- ensure_user_prompt_submit_hook_installed,
725
- get_settings_path as get_submit_settings_path,
726
- install_user_prompt_submit_hook,
727
- )
728
- from ...claude_hooks.permission_request_hook_installer import (
729
- ensure_permission_request_hook_installed,
730
- get_settings_path as get_permission_settings_path,
731
- install_permission_request_hook,
732
- )
733
-
734
- ok_global_stop = ensure_stop_hook_installed(quiet=True)
735
- ok_global_submit = ensure_user_prompt_submit_hook_installed(quiet=True)
736
- ok_global_permission = ensure_permission_request_hook_installed(quiet=True)
737
-
738
- project_root = Path(workspace)
739
- ok_project_stop = install_stop_hook(
740
- get_stop_settings_path(project_root), quiet=True
741
- )
742
- ok_project_submit = install_user_prompt_submit_hook(
743
- get_submit_settings_path(project_root), quiet=True
744
- )
745
- ok_project_permission = install_permission_request_hook(
746
- get_permission_settings_path(project_root), quiet=True
747
- )
621
+ agent_type, workspace = result
622
+ self.run_worker(
623
+ self._create_agent(agent_type, workspace),
624
+ group="terminal-panel-create",
625
+ exclusive=True,
626
+ )
748
627
 
749
- all_hooks_ok = (
750
- ok_global_stop
751
- and ok_global_submit
752
- and ok_global_permission
753
- and ok_project_stop
754
- and ok_project_submit
755
- and ok_project_permission
756
- )
757
- if not all_hooks_ok:
758
- self.app.notify(
759
- "Claude hooks not fully installed; session id/title may not update",
760
- title="Terminal",
761
- severity="warning",
762
- )
763
- except Exception:
764
- pass
628
+ async def _create_agent(self, agent_type: str, workspace: str) -> None:
629
+ """Create a new agent terminal based on the selected type and workspace."""
630
+ if agent_type == "claude":
631
+ await self._create_claude_terminal(workspace)
632
+ elif agent_type == "codex":
633
+ await self._create_codex_terminal(workspace)
634
+ elif agent_type == "opencode":
635
+ await self._create_opencode_terminal(workspace)
636
+ elif agent_type == "zsh":
637
+ await self._create_zsh_terminal(workspace)
638
+ await self.refresh_data()
639
+
640
+ async def _create_claude_terminal(self, workspace: str) -> None:
641
+ """Create a new Claude terminal."""
642
+ terminal_id = tmux_manager.new_terminal_id()
643
+ context_id = tmux_manager.new_context_id("cc")
644
+ env = {
645
+ tmux_manager.ENV_TERMINAL_ID: terminal_id,
646
+ tmux_manager.ENV_TERMINAL_PROVIDER: "claude",
647
+ tmux_manager.ENV_INNER_SOCKET: tmux_manager.INNER_SOCKET,
648
+ tmux_manager.ENV_INNER_SESSION: tmux_manager.INNER_SESSION,
649
+ tmux_manager.ENV_CONTEXT_ID: context_id,
650
+ }
765
651
 
766
- command = self._command_in_directory(
767
- tmux_manager.zsh_run_and_keep_open("claude"), workspace
652
+ try:
653
+ from ...claude_hooks.stop_hook_installer import (
654
+ ensure_stop_hook_installed,
655
+ get_settings_path as get_stop_settings_path,
656
+ install_stop_hook,
768
657
  )
769
- created = tmux_manager.create_inner_window(
770
- "cc",
771
- tmux_manager.shell_command_with_env(command, env),
772
- terminal_id=terminal_id,
773
- provider="claude",
774
- context_id=context_id,
658
+ from ...claude_hooks.user_prompt_submit_hook_installer import (
659
+ ensure_user_prompt_submit_hook_installed,
660
+ get_settings_path as get_submit_settings_path,
661
+ install_user_prompt_submit_hook,
662
+ )
663
+ from ...claude_hooks.permission_request_hook_installer import (
664
+ ensure_permission_request_hook_installed,
665
+ get_settings_path as get_permission_settings_path,
666
+ install_permission_request_hook,
775
667
  )
776
- if not created:
777
- self.app.notify("Failed to open cc terminal", title="Terminal", severity="error")
778
- await self.refresh_data()
779
- return
780
668
 
781
- if button_id == "new-codex":
782
- workspace = await self._select_workspace("Select workspace for Codex")
783
- if not workspace:
784
- return
669
+ ok_global_stop = ensure_stop_hook_installed(quiet=True)
670
+ ok_global_submit = ensure_user_prompt_submit_hook_installed(quiet=True)
671
+ ok_global_permission = ensure_permission_request_hook_installed(quiet=True)
785
672
 
786
- command = self._command_in_directory(
787
- tmux_manager.zsh_run_and_keep_open("codex"), workspace
673
+ project_root = Path(workspace)
674
+ ok_project_stop = install_stop_hook(
675
+ get_stop_settings_path(project_root), quiet=True
676
+ )
677
+ ok_project_submit = install_user_prompt_submit_hook(
678
+ get_submit_settings_path(project_root), quiet=True
679
+ )
680
+ ok_project_permission = install_permission_request_hook(
681
+ get_permission_settings_path(project_root), quiet=True
788
682
  )
789
- created = tmux_manager.create_inner_window("codex", command)
790
- if not created:
791
- self.app.notify("Failed to open codex terminal", title="Terminal", severity="error")
792
- await self.refresh_data()
793
- return
794
-
795
- if button_id == "new-opencode":
796
- workspace = await self._select_workspace("Select workspace for Opencode")
797
- if not workspace:
798
- return
799
683
 
800
- command = self._command_in_directory(
801
- tmux_manager.zsh_run_and_keep_open("opencode"), workspace
684
+ all_hooks_ok = (
685
+ ok_global_stop
686
+ and ok_global_submit
687
+ and ok_global_permission
688
+ and ok_project_stop
689
+ and ok_project_submit
690
+ and ok_project_permission
802
691
  )
803
- created = tmux_manager.create_inner_window("opencode", command)
804
- if not created:
692
+ if not all_hooks_ok:
805
693
  self.app.notify(
806
- "Failed to open opencode terminal",
694
+ "Claude hooks not fully installed; session id/title may not update",
807
695
  title="Terminal",
808
- severity="error",
696
+ severity="warning",
809
697
  )
810
- await self.refresh_data()
698
+ except Exception:
699
+ pass
700
+
701
+ command = self._command_in_directory(
702
+ tmux_manager.zsh_run_and_keep_open("claude"), workspace
703
+ )
704
+ created = tmux_manager.create_inner_window(
705
+ "cc",
706
+ tmux_manager.shell_command_with_env(command, env),
707
+ terminal_id=terminal_id,
708
+ provider="claude",
709
+ context_id=context_id,
710
+ )
711
+ if not created:
712
+ self.app.notify("Failed to open Claude terminal", title="Terminal", severity="error")
713
+
714
+ async def _create_codex_terminal(self, workspace: str) -> None:
715
+ """Create a new Codex terminal."""
716
+ command = self._command_in_directory(
717
+ tmux_manager.zsh_run_and_keep_open("codex"), workspace
718
+ )
719
+ created = tmux_manager.create_inner_window("codex", command)
720
+ if not created:
721
+ self.app.notify("Failed to open Codex terminal", title="Terminal", severity="error")
722
+
723
+ async def _create_opencode_terminal(self, workspace: str) -> None:
724
+ """Create a new Opencode terminal."""
725
+ command = self._command_in_directory(
726
+ tmux_manager.zsh_run_and_keep_open("opencode"), workspace
727
+ )
728
+ created = tmux_manager.create_inner_window("opencode", command)
729
+ if not created:
730
+ self.app.notify("Failed to open Opencode terminal", title="Terminal", severity="error")
731
+
732
+ async def _create_zsh_terminal(self, workspace: str) -> None:
733
+ """Create a new zsh terminal."""
734
+ command = self._command_in_directory("zsh", workspace)
735
+ created = tmux_manager.create_inner_window("zsh", command)
736
+ if not created:
737
+ self.app.notify("Failed to open zsh terminal", title="Terminal", severity="error")
738
+
739
+ async def on_button_pressed(self, event: Button.Pressed) -> None:
740
+ button_id = event.button.id or ""
741
+
742
+ if not self.supported():
743
+ self.app.notify(
744
+ self._support_message(),
745
+ title="Terminal",
746
+ severity="warning",
747
+ )
811
748
  return
812
749
 
813
- if button_id == "new-zsh":
814
- created = tmux_manager.create_inner_window("zsh", "zsh")
815
- if not created:
816
- self.app.notify("Failed to open zsh terminal", title="Terminal", severity="error")
817
- await self.refresh_data()
750
+ if button_id == "new-agent":
751
+ from ..screens import CreateAgentScreen
752
+
753
+ self.app.push_screen(CreateAgentScreen(), self._on_create_agent_result)
818
754
  return
819
755
 
820
756
  if button_id.startswith("switch-"):