aline-ai 0.6.0__py3-none-any.whl → 0.6.2__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.
- {aline_ai-0.6.0.dist-info → aline_ai-0.6.2.dist-info}/METADATA +1 -1
- {aline_ai-0.6.0.dist-info → aline_ai-0.6.2.dist-info}/RECORD +25 -20
- realign/__init__.py +1 -1
- realign/auth.py +21 -0
- realign/claude_hooks/stop_hook.py +35 -0
- realign/claude_hooks/user_prompt_submit_hook.py +5 -0
- realign/cli.py +76 -34
- realign/commands/auth.py +9 -0
- realign/dashboard/app.py +69 -6
- realign/dashboard/backends/__init__.py +6 -0
- realign/dashboard/backends/iterm2.py +599 -0
- realign/dashboard/backends/kitty.py +372 -0
- realign/dashboard/layout.py +320 -0
- realign/dashboard/screens/create_agent.py +41 -4
- realign/dashboard/terminal_backend.py +110 -0
- realign/dashboard/tmux_manager.py +17 -0
- realign/dashboard/widgets/terminal_panel.py +587 -110
- realign/db/sqlite_db.py +18 -0
- realign/events/session_summarizer.py +17 -2
- realign/watcher_core.py +56 -22
- realign/worker_core.py +2 -0
- {aline_ai-0.6.0.dist-info → aline_ai-0.6.2.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.0.dist-info → aline_ai-0.6.2.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.0.dist-info → aline_ai-0.6.2.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.0.dist-info → aline_ai-0.6.2.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
"""Terminal controls panel
|
|
1
|
+
"""Terminal controls panel with native terminal and tmux support.
|
|
2
2
|
|
|
3
|
-
This panel
|
|
4
|
-
-
|
|
5
|
-
|
|
3
|
+
This panel controls terminal tabs in either:
|
|
4
|
+
1. Native terminals (iTerm2/Kitty) - for better performance with high-frequency updates
|
|
5
|
+
2. tmux - the traditional approach with embedded terminal rendering
|
|
6
|
+
|
|
7
|
+
The mode is determined by the ALINE_TERMINAL_MODE environment variable:
|
|
8
|
+
- "native" or "iterm2" or "kitty": Use native terminal backend
|
|
9
|
+
- "tmux" or unset: Use tmux backend (default)
|
|
6
10
|
"""
|
|
7
11
|
|
|
8
12
|
from __future__ import annotations
|
|
@@ -13,7 +17,7 @@ import re
|
|
|
13
17
|
import shlex
|
|
14
18
|
import traceback
|
|
15
19
|
from pathlib import Path
|
|
16
|
-
from typing import Callable
|
|
20
|
+
from typing import Callable, Union
|
|
17
21
|
|
|
18
22
|
from textual.app import ComposeResult
|
|
19
23
|
from textual.containers import Container, Horizontal, Vertical, VerticalScroll
|
|
@@ -22,6 +26,7 @@ from textual.widgets import Button, Static
|
|
|
22
26
|
from rich.text import Text
|
|
23
27
|
|
|
24
28
|
from .. import tmux_manager
|
|
29
|
+
from ..terminal_backend import TerminalBackend, TerminalInfo
|
|
25
30
|
from ...logging_config import setup_logger
|
|
26
31
|
|
|
27
32
|
logger = setup_logger("realign.dashboard.terminal", "dashboard.log")
|
|
@@ -30,6 +35,19 @@ logger = setup_logger("realign.dashboard.terminal", "dashboard.log")
|
|
|
30
35
|
# Signal directory for permission request notifications
|
|
31
36
|
PERMISSION_SIGNAL_DIR = Path.home() / ".aline" / ".signals" / "permission_request"
|
|
32
37
|
|
|
38
|
+
# Environment variable to control terminal mode
|
|
39
|
+
ENV_TERMINAL_MODE = "ALINE_TERMINAL_MODE"
|
|
40
|
+
|
|
41
|
+
# Terminal mode constants
|
|
42
|
+
MODE_TMUX = "tmux"
|
|
43
|
+
MODE_NATIVE = "native"
|
|
44
|
+
MODE_ITERM2 = "iterm2"
|
|
45
|
+
MODE_KITTY = "kitty"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Type for window data (either tmux InnerWindow or native TerminalInfo)
|
|
49
|
+
WindowData = Union[tmux_manager.InnerWindow, TerminalInfo]
|
|
50
|
+
|
|
33
51
|
|
|
34
52
|
class _SignalFileWatcher:
|
|
35
53
|
"""Watches for new signal files in the permission_request directory.
|
|
@@ -128,8 +146,75 @@ class _SignalFileWatcher:
|
|
|
128
146
|
pass
|
|
129
147
|
|
|
130
148
|
|
|
149
|
+
def _detect_terminal_mode() -> str:
|
|
150
|
+
"""Detect which terminal mode to use.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Terminal mode string (MODE_TMUX, MODE_ITERM2, or MODE_KITTY)
|
|
154
|
+
"""
|
|
155
|
+
mode = os.environ.get(ENV_TERMINAL_MODE, "").strip().lower()
|
|
156
|
+
|
|
157
|
+
if mode in {MODE_ITERM2, "iterm"}:
|
|
158
|
+
return MODE_ITERM2
|
|
159
|
+
if mode == MODE_KITTY:
|
|
160
|
+
return MODE_KITTY
|
|
161
|
+
if mode == MODE_NATIVE:
|
|
162
|
+
# Auto-detect best native terminal
|
|
163
|
+
term_program = os.environ.get("TERM_PROGRAM", "").strip()
|
|
164
|
+
if term_program in {"iTerm.app", "iTerm2"} or term_program.startswith("iTerm"):
|
|
165
|
+
return MODE_ITERM2
|
|
166
|
+
if term_program == "kitty":
|
|
167
|
+
return MODE_KITTY
|
|
168
|
+
# Default to iTerm2 on macOS
|
|
169
|
+
import sys
|
|
170
|
+
if sys.platform == "darwin":
|
|
171
|
+
return MODE_ITERM2
|
|
172
|
+
return MODE_TMUX
|
|
173
|
+
|
|
174
|
+
# Default to tmux
|
|
175
|
+
return MODE_TMUX
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def _get_native_backend(mode: str) -> TerminalBackend | None:
|
|
179
|
+
"""Get the appropriate native terminal backend.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
mode: Terminal mode (MODE_ITERM2 or MODE_KITTY)
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Backend instance if available, None otherwise
|
|
186
|
+
"""
|
|
187
|
+
if mode == MODE_ITERM2:
|
|
188
|
+
try:
|
|
189
|
+
from ..backends.iterm2 import ITermBackend
|
|
190
|
+
|
|
191
|
+
# Check for split pane session ID from environment
|
|
192
|
+
right_pane_session_id = os.environ.get("ALINE_ITERM2_RIGHT_PANE")
|
|
193
|
+
if right_pane_session_id:
|
|
194
|
+
logger.debug(f"Using split pane mode with right pane: {right_pane_session_id}")
|
|
195
|
+
|
|
196
|
+
backend = ITermBackend(right_pane_session_id=right_pane_session_id)
|
|
197
|
+
if await backend.is_available():
|
|
198
|
+
return backend
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.debug(f"iTerm2 backend not available: {e}")
|
|
201
|
+
elif mode == MODE_KITTY:
|
|
202
|
+
try:
|
|
203
|
+
from ..backends.kitty import KittyBackend
|
|
204
|
+
backend = KittyBackend()
|
|
205
|
+
if await backend.is_available():
|
|
206
|
+
return backend
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.debug(f"Kitty backend not available: {e}")
|
|
209
|
+
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
131
213
|
class TerminalPanel(Container, can_focus=True):
|
|
132
|
-
"""Terminal controls panel with permission request notifications.
|
|
214
|
+
"""Terminal controls panel with permission request notifications.
|
|
215
|
+
|
|
216
|
+
Supports both native terminal backends (iTerm2/Kitty) and tmux.
|
|
217
|
+
"""
|
|
133
218
|
|
|
134
219
|
class PermissionRequestDetected(Message):
|
|
135
220
|
"""Posted when a new permission request signal file is detected."""
|
|
@@ -258,42 +343,83 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
258
343
|
|
|
259
344
|
@staticmethod
|
|
260
345
|
def supported() -> bool:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
)
|
|
346
|
+
"""Check if terminal controls are supported.
|
|
347
|
+
|
|
348
|
+
Supports both native terminal mode and tmux mode.
|
|
349
|
+
"""
|
|
350
|
+
mode = _detect_terminal_mode()
|
|
351
|
+
|
|
352
|
+
if mode == MODE_TMUX:
|
|
353
|
+
return (
|
|
354
|
+
tmux_manager.tmux_available()
|
|
355
|
+
and tmux_manager.in_tmux()
|
|
356
|
+
and tmux_manager.managed_env_enabled()
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# For native mode, we check availability asynchronously in refresh_data
|
|
360
|
+
# Here we just return True if native mode is requested
|
|
361
|
+
return mode in {MODE_ITERM2, MODE_KITTY}
|
|
266
362
|
|
|
267
363
|
@staticmethod
|
|
268
364
|
def _support_message() -> str:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
365
|
+
mode = _detect_terminal_mode()
|
|
366
|
+
|
|
367
|
+
if mode == MODE_TMUX:
|
|
368
|
+
if not tmux_manager.tmux_available():
|
|
369
|
+
return "tmux not installed. Run `aline add tmux`, then restart `aline`."
|
|
370
|
+
if not tmux_manager.in_tmux():
|
|
371
|
+
return "Not running inside tmux. Restart with `aline` to enable terminal controls."
|
|
372
|
+
if not tmux_manager.managed_env_enabled():
|
|
373
|
+
return "Not in an Aline-managed tmux session. Start via `aline` to enable terminal controls."
|
|
374
|
+
elif mode == MODE_ITERM2:
|
|
375
|
+
return "iTerm2 Python API not available. Install with: pip install iterm2"
|
|
376
|
+
elif mode == MODE_KITTY:
|
|
377
|
+
return "Kitty remote control not available. Configure listen_on in kitty.conf"
|
|
378
|
+
|
|
275
379
|
return ""
|
|
276
380
|
|
|
277
381
|
@staticmethod
|
|
278
|
-
def _is_claude_window(w:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
382
|
+
def _is_claude_window(w: WindowData) -> bool:
|
|
383
|
+
"""Check if a window is a Claude terminal."""
|
|
384
|
+
if isinstance(w, TerminalInfo):
|
|
385
|
+
# Native terminal: check provider
|
|
386
|
+
return w.provider == "claude"
|
|
387
|
+
|
|
388
|
+
# tmux: check window name and tags
|
|
282
389
|
window_name = (w.window_name or "").strip().lower()
|
|
283
390
|
if re.fullmatch(r"cc(?:-\d+)?", window_name or ""):
|
|
284
391
|
return True
|
|
285
392
|
|
|
286
|
-
# Fallback: treat as Claude only when it looks Aline-managed (terminal_id/context_id),
|
|
287
|
-
# not merely because a hook tagged the window.
|
|
288
393
|
is_claude_tagged = (w.provider == "claude") or (w.session_type == "claude")
|
|
289
394
|
return bool(is_claude_tagged and (w.terminal_id or w.context_id))
|
|
290
395
|
|
|
291
|
-
def __init__(self) -> None:
|
|
396
|
+
def __init__(self, use_native_terminal: bool | None = None) -> None:
|
|
397
|
+
"""Initialize the terminal panel.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
use_native_terminal: If True, use native terminal backend.
|
|
401
|
+
If False, use tmux.
|
|
402
|
+
If None (default), auto-detect from environment.
|
|
403
|
+
"""
|
|
292
404
|
super().__init__()
|
|
293
405
|
self._refresh_lock = asyncio.Lock()
|
|
294
406
|
self._expanded_window_id: str | None = None
|
|
295
407
|
self._signal_watcher: _SignalFileWatcher | None = None
|
|
296
408
|
|
|
409
|
+
# Determine terminal mode
|
|
410
|
+
if use_native_terminal is True:
|
|
411
|
+
self._mode = _detect_terminal_mode()
|
|
412
|
+
if self._mode == MODE_TMUX:
|
|
413
|
+
self._mode = MODE_ITERM2 # Default to iTerm2 if native requested
|
|
414
|
+
elif use_native_terminal is False:
|
|
415
|
+
self._mode = MODE_TMUX
|
|
416
|
+
else:
|
|
417
|
+
self._mode = _detect_terminal_mode()
|
|
418
|
+
|
|
419
|
+
# Native backend (initialized lazily)
|
|
420
|
+
self._native_backend: TerminalBackend | None = None
|
|
421
|
+
self._native_backend_checked = False
|
|
422
|
+
|
|
297
423
|
def compose(self) -> ComposeResult:
|
|
298
424
|
logger.debug("TerminalPanel.compose() started")
|
|
299
425
|
try:
|
|
@@ -318,9 +444,6 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
318
444
|
raise
|
|
319
445
|
|
|
320
446
|
def on_show(self) -> None:
|
|
321
|
-
# Don't `await refresh_data()` directly here: Textual may do an initial layout pass with
|
|
322
|
-
# width=0 while a widget becomes visible, and Rich can raise when wrapping text at width 0.
|
|
323
|
-
# Scheduling after the next refresh avoids a hard crash ("flash exit") when entering the tab.
|
|
324
447
|
self.call_after_refresh(
|
|
325
448
|
lambda: self.run_worker(
|
|
326
449
|
self.refresh_data(),
|
|
@@ -328,11 +451,9 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
328
451
|
exclusive=True,
|
|
329
452
|
)
|
|
330
453
|
)
|
|
331
|
-
# Start watching for permission request signals
|
|
332
454
|
self._start_signal_watcher()
|
|
333
455
|
|
|
334
456
|
def on_hide(self) -> None:
|
|
335
|
-
# Stop watching when panel is hidden
|
|
336
457
|
self._stop_signal_watcher()
|
|
337
458
|
|
|
338
459
|
def _start_signal_watcher(self) -> None:
|
|
@@ -350,7 +471,6 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
350
471
|
|
|
351
472
|
def _on_permission_signal(self) -> None:
|
|
352
473
|
"""Called when a new permission request signal is detected."""
|
|
353
|
-
# Post message to trigger refresh on the main thread
|
|
354
474
|
self.post_message(self.PermissionRequestDetected())
|
|
355
475
|
|
|
356
476
|
def on_terminal_panel_permission_request_detected(
|
|
@@ -363,52 +483,135 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
363
483
|
exclusive=True,
|
|
364
484
|
)
|
|
365
485
|
|
|
486
|
+
async def _ensure_native_backend(self) -> TerminalBackend | None:
|
|
487
|
+
"""Ensure native backend is initialized."""
|
|
488
|
+
if self._native_backend_checked:
|
|
489
|
+
return self._native_backend
|
|
490
|
+
|
|
491
|
+
self._native_backend_checked = True
|
|
492
|
+
|
|
493
|
+
if self._mode in {MODE_ITERM2, MODE_KITTY}:
|
|
494
|
+
self._native_backend = await _get_native_backend(self._mode)
|
|
495
|
+
|
|
496
|
+
return self._native_backend
|
|
497
|
+
|
|
498
|
+
def _is_native_mode(self) -> bool:
|
|
499
|
+
"""Check if we're using native terminal mode."""
|
|
500
|
+
return self._mode in {MODE_ITERM2, MODE_KITTY}
|
|
501
|
+
|
|
366
502
|
async def refresh_data(self) -> None:
|
|
367
503
|
async with self._refresh_lock:
|
|
504
|
+
if self._is_native_mode():
|
|
505
|
+
await self._refresh_native_data()
|
|
506
|
+
else:
|
|
507
|
+
await self._refresh_tmux_data()
|
|
508
|
+
|
|
509
|
+
async def _refresh_native_data(self) -> None:
|
|
510
|
+
"""Refresh data using native terminal backend."""
|
|
511
|
+
backend = await self._ensure_native_backend()
|
|
512
|
+
if not backend:
|
|
513
|
+
# Fall back to showing error message
|
|
368
514
|
try:
|
|
369
|
-
|
|
515
|
+
container = self.query_one("#terminals", Vertical)
|
|
516
|
+
await container.remove_children()
|
|
517
|
+
await container.mount(Static(self._support_message()))
|
|
370
518
|
except Exception:
|
|
371
|
-
|
|
519
|
+
pass
|
|
520
|
+
return
|
|
372
521
|
|
|
373
|
-
|
|
374
|
-
|
|
522
|
+
try:
|
|
523
|
+
windows = await backend.list_tabs()
|
|
524
|
+
except Exception as e:
|
|
525
|
+
logger.error(f"Failed to list native terminals: {e}")
|
|
526
|
+
return
|
|
375
527
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if self._expanded_window_id and self._expanded_window_id != active_window_id:
|
|
382
|
-
self._expanded_window_id = None
|
|
383
|
-
claude_ids = [
|
|
384
|
-
w.session_id for w in windows if self._is_claude_window(w) and w.session_id
|
|
385
|
-
]
|
|
386
|
-
titles = self._fetch_claude_session_titles(claude_ids)
|
|
387
|
-
|
|
388
|
-
context_info_by_context_id: dict[str, tuple[list[str], int, int]] = {}
|
|
389
|
-
all_context_session_ids: set[str] = set()
|
|
390
|
-
for w in windows:
|
|
391
|
-
if not self._is_claude_window(w) or not w.context_id:
|
|
392
|
-
continue
|
|
393
|
-
session_ids, session_count, event_count = self._get_loaded_context_info(
|
|
394
|
-
w.context_id
|
|
395
|
-
)
|
|
396
|
-
if not session_ids and session_count == 0 and event_count == 0:
|
|
397
|
-
continue
|
|
398
|
-
context_info_by_context_id[w.context_id] = (
|
|
399
|
-
session_ids,
|
|
400
|
-
session_count,
|
|
401
|
-
event_count,
|
|
402
|
-
)
|
|
403
|
-
all_context_session_ids.update(session_ids)
|
|
528
|
+
active_window_id = next(
|
|
529
|
+
(w.session_id for w in windows if w.active), None
|
|
530
|
+
)
|
|
531
|
+
if self._expanded_window_id and self._expanded_window_id != active_window_id:
|
|
532
|
+
self._expanded_window_id = None
|
|
404
533
|
|
|
405
|
-
|
|
406
|
-
|
|
534
|
+
# Get Claude session IDs for title lookup
|
|
535
|
+
claude_ids = [
|
|
536
|
+
w.claude_session_id for w in windows
|
|
537
|
+
if self._is_claude_window(w) and w.claude_session_id
|
|
538
|
+
]
|
|
539
|
+
titles = self._fetch_claude_session_titles(claude_ids)
|
|
407
540
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
541
|
+
# Get context info
|
|
542
|
+
context_info_by_context_id: dict[str, tuple[list[str], int, int]] = {}
|
|
543
|
+
all_context_session_ids: set[str] = set()
|
|
544
|
+
for w in windows:
|
|
545
|
+
if not self._is_claude_window(w) or not w.context_id:
|
|
546
|
+
continue
|
|
547
|
+
session_ids, session_count, event_count = self._get_loaded_context_info(
|
|
548
|
+
w.context_id
|
|
549
|
+
)
|
|
550
|
+
if not session_ids and session_count == 0 and event_count == 0:
|
|
551
|
+
continue
|
|
552
|
+
context_info_by_context_id[w.context_id] = (
|
|
553
|
+
session_ids,
|
|
554
|
+
session_count,
|
|
555
|
+
event_count,
|
|
556
|
+
)
|
|
557
|
+
all_context_session_ids.update(session_ids)
|
|
558
|
+
|
|
559
|
+
if all_context_session_ids:
|
|
560
|
+
titles.update(self._fetch_claude_session_titles(sorted(all_context_session_ids)))
|
|
561
|
+
|
|
562
|
+
try:
|
|
563
|
+
await self._render_terminals_native(windows, titles, context_info_by_context_id)
|
|
564
|
+
except Exception:
|
|
565
|
+
return
|
|
566
|
+
|
|
567
|
+
async def _refresh_tmux_data(self) -> None:
|
|
568
|
+
"""Refresh data using tmux backend."""
|
|
569
|
+
try:
|
|
570
|
+
supported = self.supported()
|
|
571
|
+
except Exception:
|
|
572
|
+
return
|
|
573
|
+
|
|
574
|
+
if not supported:
|
|
575
|
+
return
|
|
576
|
+
|
|
577
|
+
try:
|
|
578
|
+
windows = tmux_manager.list_inner_windows()
|
|
579
|
+
except Exception:
|
|
580
|
+
return
|
|
581
|
+
|
|
582
|
+
active_window_id = next((w.window_id for w in windows if w.active), None)
|
|
583
|
+
if self._expanded_window_id and self._expanded_window_id != active_window_id:
|
|
584
|
+
self._expanded_window_id = None
|
|
585
|
+
|
|
586
|
+
claude_ids = [
|
|
587
|
+
w.session_id for w in windows if self._is_claude_window(w) and w.session_id
|
|
588
|
+
]
|
|
589
|
+
titles = self._fetch_claude_session_titles(claude_ids)
|
|
590
|
+
|
|
591
|
+
context_info_by_context_id: dict[str, tuple[list[str], int, int]] = {}
|
|
592
|
+
all_context_session_ids: set[str] = set()
|
|
593
|
+
for w in windows:
|
|
594
|
+
if not self._is_claude_window(w) or not w.context_id:
|
|
595
|
+
continue
|
|
596
|
+
session_ids, session_count, event_count = self._get_loaded_context_info(
|
|
597
|
+
w.context_id
|
|
598
|
+
)
|
|
599
|
+
if not session_ids and session_count == 0 and event_count == 0:
|
|
600
|
+
continue
|
|
601
|
+
context_info_by_context_id[w.context_id] = (
|
|
602
|
+
session_ids,
|
|
603
|
+
session_count,
|
|
604
|
+
event_count,
|
|
605
|
+
)
|
|
606
|
+
all_context_session_ids.update(session_ids)
|
|
607
|
+
|
|
608
|
+
if all_context_session_ids:
|
|
609
|
+
titles.update(self._fetch_claude_session_titles(sorted(all_context_session_ids)))
|
|
610
|
+
|
|
611
|
+
try:
|
|
612
|
+
await self._render_terminals_tmux(windows, titles, context_info_by_context_id)
|
|
613
|
+
except Exception:
|
|
614
|
+
return
|
|
412
615
|
|
|
413
616
|
def _fetch_claude_session_titles(self, session_ids: list[str]) -> dict[str, str]:
|
|
414
617
|
if not session_ids:
|
|
@@ -428,10 +631,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
428
631
|
return {}
|
|
429
632
|
|
|
430
633
|
def _get_loaded_context_info(self, context_id: str) -> tuple[list[str], int, int]:
|
|
431
|
-
"""Best-effort: read ~/.aline/load.json for a context_id, and return its session ids.
|
|
432
|
-
|
|
433
|
-
Also expands any context event ids to their constituent sessions (when DB is available).
|
|
434
|
-
"""
|
|
634
|
+
"""Best-effort: read ~/.aline/load.json for a context_id, and return its session ids."""
|
|
435
635
|
context_id = (context_id or "").strip()
|
|
436
636
|
if not context_id:
|
|
437
637
|
return ([], 0, 0)
|
|
@@ -473,12 +673,103 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
473
673
|
except Exception:
|
|
474
674
|
return ([], 0, 0)
|
|
475
675
|
|
|
476
|
-
async def
|
|
676
|
+
async def _render_terminals_native(
|
|
677
|
+
self,
|
|
678
|
+
windows: list[TerminalInfo],
|
|
679
|
+
titles: dict[str, str],
|
|
680
|
+
context_info_by_context_id: dict[str, tuple[list[str], int, int]],
|
|
681
|
+
) -> None:
|
|
682
|
+
"""Render terminal list for native backend."""
|
|
683
|
+
container = self.query_one("#terminals", Vertical)
|
|
684
|
+
await container.remove_children()
|
|
685
|
+
|
|
686
|
+
if not windows:
|
|
687
|
+
await container.mount(
|
|
688
|
+
Static("No terminals yet. Click 'Create' to open a new agent terminal.")
|
|
689
|
+
)
|
|
690
|
+
return
|
|
691
|
+
|
|
692
|
+
for w in windows:
|
|
693
|
+
safe = self._safe_id_fragment(w.session_id)
|
|
694
|
+
row = Horizontal(classes="terminal-row")
|
|
695
|
+
await container.mount(row)
|
|
696
|
+
|
|
697
|
+
if w.attention:
|
|
698
|
+
await row.mount(Static("●", classes="attention-dot"))
|
|
699
|
+
|
|
700
|
+
switch_classes = "terminal-switch active" if w.active else "terminal-switch"
|
|
701
|
+
loaded_ids: list[str] = []
|
|
702
|
+
raw_sessions = 0
|
|
703
|
+
raw_events = 0
|
|
704
|
+
if self._is_claude_window(w) and w.context_id:
|
|
705
|
+
loaded_ids, raw_sessions, raw_events = context_info_by_context_id.get(
|
|
706
|
+
w.context_id, ([], 0, 0)
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
label = self._window_label_native(w, titles, raw_sessions, raw_events)
|
|
710
|
+
await row.mount(
|
|
711
|
+
Button(
|
|
712
|
+
label,
|
|
713
|
+
id=f"switch-{safe}",
|
|
714
|
+
name=w.session_id,
|
|
715
|
+
classes=switch_classes,
|
|
716
|
+
)
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
can_toggle_ctx = bool(
|
|
720
|
+
self._is_claude_window(w) and w.context_id and (raw_sessions or raw_events)
|
|
721
|
+
)
|
|
722
|
+
expanded = bool(w.active and w.session_id == self._expanded_window_id)
|
|
723
|
+
if w.active and can_toggle_ctx:
|
|
724
|
+
await row.mount(
|
|
725
|
+
Button(
|
|
726
|
+
"▼" if expanded else "▶",
|
|
727
|
+
id=f"toggle-{safe}",
|
|
728
|
+
name=w.session_id,
|
|
729
|
+
variant="default",
|
|
730
|
+
classes="terminal-toggle",
|
|
731
|
+
)
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
await row.mount(
|
|
735
|
+
Button(
|
|
736
|
+
"✕",
|
|
737
|
+
id=f"close-{safe}",
|
|
738
|
+
name=w.session_id,
|
|
739
|
+
variant="error",
|
|
740
|
+
classes="terminal-close",
|
|
741
|
+
)
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
if w.active and self._is_claude_window(w) and w.context_id and expanded:
|
|
745
|
+
ctx = VerticalScroll(id=f"ctx-{safe}", classes="context-sessions")
|
|
746
|
+
await container.mount(ctx)
|
|
747
|
+
if loaded_ids:
|
|
748
|
+
for idx, sid in enumerate(loaded_ids):
|
|
749
|
+
title = titles.get(sid, "").strip() or "(no title)"
|
|
750
|
+
await ctx.mount(
|
|
751
|
+
Button(
|
|
752
|
+
f"{title} ({self._short_id(sid)})",
|
|
753
|
+
id=f"ctxsess-{safe}-{idx}",
|
|
754
|
+
name=sid,
|
|
755
|
+
variant="default",
|
|
756
|
+
classes="context-session",
|
|
757
|
+
)
|
|
758
|
+
)
|
|
759
|
+
else:
|
|
760
|
+
await ctx.mount(
|
|
761
|
+
Static(
|
|
762
|
+
"[dim]Context loaded, but session list isn't available (events not expanded).[/dim]"
|
|
763
|
+
)
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
async def _render_terminals_tmux(
|
|
477
767
|
self,
|
|
478
768
|
windows: list[tmux_manager.InnerWindow],
|
|
479
769
|
titles: dict[str, str],
|
|
480
770
|
context_info_by_context_id: dict[str, tuple[list[str], int, int]],
|
|
481
771
|
) -> None:
|
|
772
|
+
"""Render terminal list for tmux backend."""
|
|
482
773
|
container = self.query_one("#terminals", Vertical)
|
|
483
774
|
await container.remove_children()
|
|
484
775
|
|
|
@@ -503,7 +794,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
503
794
|
loaded_ids, raw_sessions, raw_events = context_info_by_context_id.get(
|
|
504
795
|
w.context_id, ([], 0, 0)
|
|
505
796
|
)
|
|
506
|
-
label = self.
|
|
797
|
+
label = self._window_label_tmux(w, titles, raw_sessions, raw_events)
|
|
507
798
|
await row.mount(
|
|
508
799
|
Button(
|
|
509
800
|
label,
|
|
@@ -528,7 +819,6 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
528
819
|
)
|
|
529
820
|
await row.mount(
|
|
530
821
|
Button(
|
|
531
|
-
# Avoid Rich crash when measured with width=0.
|
|
532
822
|
"✕",
|
|
533
823
|
id=f"close-{safe}",
|
|
534
824
|
name=w.window_id,
|
|
@@ -570,13 +860,48 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
570
860
|
return "ctx 0"
|
|
571
861
|
return "ctx " + " ".join(parts)
|
|
572
862
|
|
|
573
|
-
def
|
|
863
|
+
def _window_label_native(
|
|
864
|
+
self,
|
|
865
|
+
w: TerminalInfo,
|
|
866
|
+
titles: dict[str, str],
|
|
867
|
+
raw_sessions: int = 0,
|
|
868
|
+
raw_events: int = 0,
|
|
869
|
+
) -> str | Text:
|
|
870
|
+
"""Generate label for native terminal window."""
|
|
871
|
+
if not self._is_claude_window(w):
|
|
872
|
+
return Text(w.name, no_wrap=True, overflow="ellipsis")
|
|
873
|
+
|
|
874
|
+
title = titles.get(w.claude_session_id or "", "").strip() if w.claude_session_id else ""
|
|
875
|
+
header = title or ("Claude" if w.claude_session_id else "New Claude")
|
|
876
|
+
|
|
877
|
+
details = Text(no_wrap=True, overflow="ellipsis")
|
|
878
|
+
details.append(header)
|
|
879
|
+
details.append("\n")
|
|
880
|
+
|
|
881
|
+
detail_line = "[Claude]"
|
|
882
|
+
if w.claude_session_id:
|
|
883
|
+
detail_line = f"{detail_line} #{self._short_id(w.claude_session_id)}"
|
|
884
|
+
if w.active:
|
|
885
|
+
loaded_count = raw_sessions + raw_events
|
|
886
|
+
detail_line = f"{detail_line} | loaded context: {loaded_count}"
|
|
887
|
+
else:
|
|
888
|
+
detail_line = (
|
|
889
|
+
f"{detail_line} · {self._format_context_summary(raw_sessions, raw_events)}"
|
|
890
|
+
)
|
|
891
|
+
# Show no-track indicator
|
|
892
|
+
if w.metadata.get("no_track") == "1":
|
|
893
|
+
detail_line = f"{detail_line} [NT]"
|
|
894
|
+
details.append(detail_line, style="dim not bold")
|
|
895
|
+
return details
|
|
896
|
+
|
|
897
|
+
def _window_label_tmux(
|
|
574
898
|
self,
|
|
575
899
|
w: tmux_manager.InnerWindow,
|
|
576
900
|
titles: dict[str, str],
|
|
577
901
|
raw_sessions: int = 0,
|
|
578
902
|
raw_events: int = 0,
|
|
579
903
|
) -> str | Text:
|
|
904
|
+
"""Generate label for tmux window."""
|
|
580
905
|
if not self._is_claude_window(w):
|
|
581
906
|
return Text(w.window_name, no_wrap=True, overflow="ellipsis")
|
|
582
907
|
|
|
@@ -587,7 +912,6 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
587
912
|
details.append(header)
|
|
588
913
|
details.append("\n")
|
|
589
914
|
|
|
590
|
-
# Build detail line with Claude label
|
|
591
915
|
detail_line = "[Claude]"
|
|
592
916
|
if w.session_id:
|
|
593
917
|
detail_line = f"{detail_line} #{self._short_id(w.session_id)}"
|
|
@@ -598,6 +922,9 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
598
922
|
detail_line = (
|
|
599
923
|
f"{detail_line} · {self._format_context_summary(raw_sessions, raw_events)}"
|
|
600
924
|
)
|
|
925
|
+
# Show no-track indicator
|
|
926
|
+
if w.no_track:
|
|
927
|
+
detail_line = f"{detail_line} [NT]"
|
|
601
928
|
details.append(detail_line, style="dim not bold")
|
|
602
929
|
return details
|
|
603
930
|
|
|
@@ -623,24 +950,26 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
623
950
|
"""Wrap a command to run in a specific directory."""
|
|
624
951
|
return f"cd {shlex.quote(directory)} && {command}"
|
|
625
952
|
|
|
626
|
-
def _on_create_agent_result(self, result: tuple[str, str, bool] | None) -> None:
|
|
953
|
+
def _on_create_agent_result(self, result: tuple[str, str, bool, bool] | None) -> None:
|
|
627
954
|
"""Handle the result from CreateAgentScreen modal."""
|
|
628
955
|
if result is None:
|
|
629
956
|
return
|
|
630
957
|
|
|
631
|
-
agent_type, workspace, skip_permissions = result
|
|
958
|
+
agent_type, workspace, skip_permissions, no_track = result
|
|
632
959
|
self.run_worker(
|
|
633
|
-
self._create_agent(
|
|
960
|
+
self._create_agent(
|
|
961
|
+
agent_type, workspace, skip_permissions=skip_permissions, no_track=no_track
|
|
962
|
+
),
|
|
634
963
|
group="terminal-panel-create",
|
|
635
964
|
exclusive=True,
|
|
636
965
|
)
|
|
637
966
|
|
|
638
967
|
async def _create_agent(
|
|
639
|
-
self, agent_type: str, workspace: str, *, skip_permissions: bool = False
|
|
968
|
+
self, agent_type: str, workspace: str, *, skip_permissions: bool = False, no_track: bool = False
|
|
640
969
|
) -> None:
|
|
641
970
|
"""Create a new agent terminal based on the selected type and workspace."""
|
|
642
971
|
if agent_type == "claude":
|
|
643
|
-
await self._create_claude_terminal(workspace, skip_permissions=skip_permissions)
|
|
972
|
+
await self._create_claude_terminal(workspace, skip_permissions=skip_permissions, no_track=no_track)
|
|
644
973
|
elif agent_type == "codex":
|
|
645
974
|
await self._create_codex_terminal(workspace)
|
|
646
975
|
elif agent_type == "opencode":
|
|
@@ -650,9 +979,64 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
650
979
|
await self.refresh_data()
|
|
651
980
|
|
|
652
981
|
async def _create_claude_terminal(
|
|
653
|
-
self, workspace: str, *, skip_permissions: bool = False
|
|
982
|
+
self, workspace: str, *, skip_permissions: bool = False, no_track: bool = False
|
|
654
983
|
) -> None:
|
|
655
984
|
"""Create a new Claude terminal."""
|
|
985
|
+
if self._is_native_mode():
|
|
986
|
+
await self._create_claude_terminal_native(workspace, skip_permissions=skip_permissions, no_track=no_track)
|
|
987
|
+
else:
|
|
988
|
+
await self._create_claude_terminal_tmux(workspace, skip_permissions=skip_permissions, no_track=no_track)
|
|
989
|
+
|
|
990
|
+
async def _create_claude_terminal_native(
|
|
991
|
+
self, workspace: str, *, skip_permissions: bool = False, no_track: bool = False
|
|
992
|
+
) -> None:
|
|
993
|
+
"""Create a new Claude terminal using native backend."""
|
|
994
|
+
backend = await self._ensure_native_backend()
|
|
995
|
+
if not backend:
|
|
996
|
+
self.app.notify(
|
|
997
|
+
"Native terminal backend not available",
|
|
998
|
+
title="Terminal",
|
|
999
|
+
severity="error",
|
|
1000
|
+
)
|
|
1001
|
+
return
|
|
1002
|
+
|
|
1003
|
+
terminal_id = tmux_manager.new_terminal_id()
|
|
1004
|
+
context_id = tmux_manager.new_context_id("cc")
|
|
1005
|
+
|
|
1006
|
+
env = {
|
|
1007
|
+
tmux_manager.ENV_TERMINAL_ID: terminal_id,
|
|
1008
|
+
tmux_manager.ENV_TERMINAL_PROVIDER: "claude",
|
|
1009
|
+
tmux_manager.ENV_CONTEXT_ID: context_id,
|
|
1010
|
+
}
|
|
1011
|
+
if no_track:
|
|
1012
|
+
env["ALINE_NO_TRACK"] = "1"
|
|
1013
|
+
|
|
1014
|
+
# Install hooks
|
|
1015
|
+
self._install_claude_hooks(workspace)
|
|
1016
|
+
|
|
1017
|
+
claude_cmd = "claude"
|
|
1018
|
+
if skip_permissions:
|
|
1019
|
+
claude_cmd = "claude --dangerously-skip-permissions"
|
|
1020
|
+
|
|
1021
|
+
session_id = await backend.create_tab(
|
|
1022
|
+
command=claude_cmd,
|
|
1023
|
+
terminal_id=terminal_id,
|
|
1024
|
+
name="Claude Code",
|
|
1025
|
+
env=env,
|
|
1026
|
+
cwd=workspace,
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
if not session_id:
|
|
1030
|
+
self.app.notify(
|
|
1031
|
+
"Failed to open Claude terminal",
|
|
1032
|
+
title="Terminal",
|
|
1033
|
+
severity="error",
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
async def _create_claude_terminal_tmux(
|
|
1037
|
+
self, workspace: str, *, skip_permissions: bool = False, no_track: bool = False
|
|
1038
|
+
) -> None:
|
|
1039
|
+
"""Create a new Claude terminal using tmux backend."""
|
|
656
1040
|
terminal_id = tmux_manager.new_terminal_id()
|
|
657
1041
|
context_id = tmux_manager.new_context_id("cc")
|
|
658
1042
|
env = {
|
|
@@ -662,7 +1046,30 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
662
1046
|
tmux_manager.ENV_INNER_SESSION: tmux_manager.INNER_SESSION,
|
|
663
1047
|
tmux_manager.ENV_CONTEXT_ID: context_id,
|
|
664
1048
|
}
|
|
1049
|
+
if no_track:
|
|
1050
|
+
env["ALINE_NO_TRACK"] = "1"
|
|
665
1051
|
|
|
1052
|
+
# Install hooks
|
|
1053
|
+
self._install_claude_hooks(workspace)
|
|
1054
|
+
|
|
1055
|
+
claude_cmd = "claude"
|
|
1056
|
+
if skip_permissions:
|
|
1057
|
+
claude_cmd = "claude --dangerously-skip-permissions"
|
|
1058
|
+
command = self._command_in_directory(
|
|
1059
|
+
tmux_manager.zsh_run_and_keep_open(claude_cmd), workspace
|
|
1060
|
+
)
|
|
1061
|
+
created = tmux_manager.create_inner_window(
|
|
1062
|
+
"cc",
|
|
1063
|
+
tmux_manager.shell_command_with_env(command, env),
|
|
1064
|
+
terminal_id=terminal_id,
|
|
1065
|
+
provider="claude",
|
|
1066
|
+
context_id=context_id,
|
|
1067
|
+
)
|
|
1068
|
+
if not created:
|
|
1069
|
+
self.app.notify("Failed to open Claude terminal", title="Terminal", severity="error")
|
|
1070
|
+
|
|
1071
|
+
def _install_claude_hooks(self, workspace: str) -> None:
|
|
1072
|
+
"""Install Claude hooks for a workspace."""
|
|
666
1073
|
try:
|
|
667
1074
|
from ...claude_hooks.stop_hook_installer import (
|
|
668
1075
|
ensure_stop_hook_installed,
|
|
@@ -712,24 +1119,25 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
712
1119
|
except Exception:
|
|
713
1120
|
pass
|
|
714
1121
|
|
|
715
|
-
claude_cmd = "claude"
|
|
716
|
-
if skip_permissions:
|
|
717
|
-
claude_cmd = "claude --dangerously-skip-permissions"
|
|
718
|
-
command = self._command_in_directory(
|
|
719
|
-
tmux_manager.zsh_run_and_keep_open(claude_cmd), workspace
|
|
720
|
-
)
|
|
721
|
-
created = tmux_manager.create_inner_window(
|
|
722
|
-
"cc",
|
|
723
|
-
tmux_manager.shell_command_with_env(command, env),
|
|
724
|
-
terminal_id=terminal_id,
|
|
725
|
-
provider="claude",
|
|
726
|
-
context_id=context_id,
|
|
727
|
-
)
|
|
728
|
-
if not created:
|
|
729
|
-
self.app.notify("Failed to open Claude terminal", title="Terminal", severity="error")
|
|
730
|
-
|
|
731
1122
|
async def _create_codex_terminal(self, workspace: str) -> None:
|
|
732
1123
|
"""Create a new Codex terminal."""
|
|
1124
|
+
if self._is_native_mode():
|
|
1125
|
+
backend = await self._ensure_native_backend()
|
|
1126
|
+
if backend:
|
|
1127
|
+
terminal_id = tmux_manager.new_terminal_id()
|
|
1128
|
+
session_id = await backend.create_tab(
|
|
1129
|
+
command="codex",
|
|
1130
|
+
terminal_id=terminal_id,
|
|
1131
|
+
name="Codex",
|
|
1132
|
+
cwd=workspace,
|
|
1133
|
+
)
|
|
1134
|
+
if not session_id:
|
|
1135
|
+
self.app.notify(
|
|
1136
|
+
"Failed to open Codex terminal", title="Terminal", severity="error"
|
|
1137
|
+
)
|
|
1138
|
+
return
|
|
1139
|
+
|
|
1140
|
+
# Tmux fallback
|
|
733
1141
|
command = self._command_in_directory(
|
|
734
1142
|
tmux_manager.zsh_run_and_keep_open("codex"), workspace
|
|
735
1143
|
)
|
|
@@ -739,6 +1147,23 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
739
1147
|
|
|
740
1148
|
async def _create_opencode_terminal(self, workspace: str) -> None:
|
|
741
1149
|
"""Create a new Opencode terminal."""
|
|
1150
|
+
if self._is_native_mode():
|
|
1151
|
+
backend = await self._ensure_native_backend()
|
|
1152
|
+
if backend:
|
|
1153
|
+
terminal_id = tmux_manager.new_terminal_id()
|
|
1154
|
+
session_id = await backend.create_tab(
|
|
1155
|
+
command="opencode",
|
|
1156
|
+
terminal_id=terminal_id,
|
|
1157
|
+
name="Opencode",
|
|
1158
|
+
cwd=workspace,
|
|
1159
|
+
)
|
|
1160
|
+
if not session_id:
|
|
1161
|
+
self.app.notify(
|
|
1162
|
+
"Failed to open Opencode terminal", title="Terminal", severity="error"
|
|
1163
|
+
)
|
|
1164
|
+
return
|
|
1165
|
+
|
|
1166
|
+
# Tmux fallback
|
|
742
1167
|
command = self._command_in_directory(
|
|
743
1168
|
tmux_manager.zsh_run_and_keep_open("opencode"), workspace
|
|
744
1169
|
)
|
|
@@ -748,6 +1173,23 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
748
1173
|
|
|
749
1174
|
async def _create_zsh_terminal(self, workspace: str) -> None:
|
|
750
1175
|
"""Create a new zsh terminal."""
|
|
1176
|
+
if self._is_native_mode():
|
|
1177
|
+
backend = await self._ensure_native_backend()
|
|
1178
|
+
if backend:
|
|
1179
|
+
terminal_id = tmux_manager.new_terminal_id()
|
|
1180
|
+
session_id = await backend.create_tab(
|
|
1181
|
+
command="zsh -l",
|
|
1182
|
+
terminal_id=terminal_id,
|
|
1183
|
+
name="zsh",
|
|
1184
|
+
cwd=workspace,
|
|
1185
|
+
)
|
|
1186
|
+
if not session_id:
|
|
1187
|
+
self.app.notify(
|
|
1188
|
+
"Failed to open zsh terminal", title="Terminal", severity="error"
|
|
1189
|
+
)
|
|
1190
|
+
return
|
|
1191
|
+
|
|
1192
|
+
# Tmux fallback
|
|
751
1193
|
command = self._command_in_directory("zsh", workspace)
|
|
752
1194
|
created = tmux_manager.create_inner_window("zsh", command)
|
|
753
1195
|
if not created:
|
|
@@ -771,14 +1213,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
771
1213
|
return
|
|
772
1214
|
|
|
773
1215
|
if button_id.startswith("switch-"):
|
|
774
|
-
|
|
775
|
-
if not window_id or not tmux_manager.select_inner_window(window_id):
|
|
776
|
-
self.app.notify("Failed to switch terminal", title="Terminal", severity="error")
|
|
777
|
-
# Clear attention when user clicks on terminal
|
|
778
|
-
if window_id:
|
|
779
|
-
tmux_manager.clear_attention(window_id)
|
|
780
|
-
self._expanded_window_id = None
|
|
781
|
-
await self.refresh_data()
|
|
1216
|
+
await self._handle_switch(event.button.name or "")
|
|
782
1217
|
return
|
|
783
1218
|
|
|
784
1219
|
if button_id.startswith("toggle-"):
|
|
@@ -805,7 +1240,49 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
805
1240
|
return
|
|
806
1241
|
|
|
807
1242
|
if button_id.startswith("close-"):
|
|
808
|
-
|
|
809
|
-
|
|
1243
|
+
await self._handle_close(event.button.name or "")
|
|
1244
|
+
return
|
|
1245
|
+
|
|
1246
|
+
async def _handle_switch(self, window_id: str) -> None:
|
|
1247
|
+
"""Handle switching to a terminal."""
|
|
1248
|
+
if not window_id:
|
|
1249
|
+
return
|
|
1250
|
+
|
|
1251
|
+
if self._is_native_mode():
|
|
1252
|
+
backend = await self._ensure_native_backend()
|
|
1253
|
+
if backend:
|
|
1254
|
+
success = await backend.focus_tab(window_id, steal_focus=True)
|
|
1255
|
+
if not success:
|
|
1256
|
+
self.app.notify(
|
|
1257
|
+
"Failed to switch terminal", title="Terminal", severity="error"
|
|
1258
|
+
)
|
|
1259
|
+
else:
|
|
1260
|
+
if not tmux_manager.select_inner_window(window_id):
|
|
1261
|
+
self.app.notify("Failed to switch terminal", title="Terminal", severity="error")
|
|
1262
|
+
else:
|
|
1263
|
+
# Move cursor focus to the right pane (terminal area)
|
|
1264
|
+
tmux_manager.focus_right_pane()
|
|
1265
|
+
# Clear attention when user clicks on terminal
|
|
1266
|
+
tmux_manager.clear_attention(window_id)
|
|
1267
|
+
|
|
1268
|
+
self._expanded_window_id = None
|
|
1269
|
+
await self.refresh_data()
|
|
1270
|
+
|
|
1271
|
+
async def _handle_close(self, window_id: str) -> None:
|
|
1272
|
+
"""Handle closing a terminal."""
|
|
1273
|
+
if not window_id:
|
|
1274
|
+
return
|
|
1275
|
+
|
|
1276
|
+
if self._is_native_mode():
|
|
1277
|
+
backend = await self._ensure_native_backend()
|
|
1278
|
+
if backend:
|
|
1279
|
+
success = await backend.close_tab(window_id)
|
|
1280
|
+
if not success:
|
|
1281
|
+
self.app.notify(
|
|
1282
|
+
"Failed to close terminal", title="Terminal", severity="error"
|
|
1283
|
+
)
|
|
1284
|
+
else:
|
|
1285
|
+
if not tmux_manager.kill_inner_window(window_id):
|
|
810
1286
|
self.app.notify("Failed to close terminal", title="Terminal", severity="error")
|
|
811
|
-
|
|
1287
|
+
|
|
1288
|
+
await self.refresh_data()
|