glaip-sdk 0.7.0__py3-none-any.whl → 0.7.1__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.
@@ -170,7 +170,9 @@ class AccountsController:
170
170
  callbacks = AccountsTUICallbacks(switch_account=_switch_in_textual)
171
171
  active = next((row["name"] for row in rows if row.get("active")), None)
172
172
  try:
173
- run_accounts_textual(rows, active_account=active, env_lock=env_lock, callbacks=callbacks)
173
+ # Inject TUI context for theme support
174
+ tui_ctx = getattr(self.session, "tui_ctx", None)
175
+ run_accounts_textual(rows, active_account=active, env_lock=env_lock, callbacks=callbacks, ctx=tui_ctx)
174
176
  except Exception as exc: # pragma: no cover - defensive around Textual failures
175
177
  self.console.print(f"[{WARNING_STYLE}]Accounts browser exited unexpectedly: {exc}[/]")
176
178
 
@@ -6,6 +6,7 @@ Authors:
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import asyncio
9
10
  import importlib
10
11
  import os
11
12
  import shlex
@@ -51,6 +52,7 @@ from glaip_sdk.cli.slash.prompt import (
51
52
  to_formatted_text,
52
53
  )
53
54
  from glaip_sdk.cli.slash.remote_runs_controller import RemoteRunsController
55
+ from glaip_sdk.cli.slash.tui.context import TUIContext
54
56
  from glaip_sdk.cli.transcript import (
55
57
  export_cached_transcript,
56
58
  load_history_snapshot,
@@ -186,6 +188,7 @@ class SlashSession:
186
188
  self._update_notifier = maybe_notify_update
187
189
  self._home_hint_shown = False
188
190
  self._agent_transcript_ready: dict[str, str] = {}
191
+ self.tui_ctx: TUIContext | None = None
189
192
 
190
193
  # ------------------------------------------------------------------
191
194
  # Session orchestration
@@ -215,6 +218,22 @@ class SlashSession:
215
218
 
216
219
  def run(self, initial_commands: Iterable[str] | None = None) -> None:
217
220
  """Start the command palette session loop."""
221
+ # Initialize TUI context asynchronously
222
+ try:
223
+ self.tui_ctx = asyncio.run(TUIContext.create())
224
+ except RuntimeError:
225
+ try:
226
+ loop = asyncio.get_event_loop()
227
+ except RuntimeError:
228
+ self.tui_ctx = None
229
+ else:
230
+ if loop.is_running():
231
+ self.tui_ctx = None
232
+ else:
233
+ self.tui_ctx = loop.run_until_complete(TUIContext.create())
234
+ except Exception:
235
+ self.tui_ctx = None
236
+
218
237
  ctx_obj = self.ctx.obj if isinstance(self.ctx.obj, dict) else None
219
238
  previous_session = None
220
239
  if ctx_obj is not None:
@@ -1,6 +1,14 @@
1
1
  """Textual UI helpers for slash commands."""
2
2
 
3
+ from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter, ClipboardResult
3
4
  from glaip_sdk.cli.slash.tui.context import TUIContext
5
+ from glaip_sdk.cli.slash.tui.keybind_registry import (
6
+ Keybind,
7
+ KeybindRegistry,
8
+ format_key_sequence,
9
+ parse_key_sequence,
10
+ )
11
+ from glaip_sdk.cli.slash.tui.toast import ToastBus, ToastVariant
4
12
  from glaip_sdk.cli.slash.tui.remote_runs_app import (
5
13
  RemoteRunsTextualApp,
6
14
  RemoteRunsTUICallbacks,
@@ -10,9 +18,17 @@ from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities, detect_termin
10
18
 
11
19
  __all__ = [
12
20
  "TUIContext",
21
+ "ToastBus",
22
+ "ToastVariant",
13
23
  "TerminalCapabilities",
14
24
  "detect_terminal_background",
15
25
  "RemoteRunsTextualApp",
16
26
  "RemoteRunsTUICallbacks",
17
27
  "run_remote_runs_textual",
28
+ "KeybindRegistry",
29
+ "Keybind",
30
+ "parse_key_sequence",
31
+ "format_key_sequence",
32
+ "ClipboardAdapter",
33
+ "ClipboardResult",
18
34
  ]
@@ -11,7 +11,7 @@
11
11
 
12
12
  #env-lock {
13
13
  padding: 0 1 0 1;
14
- color: yellow;
14
+ color: $warning;
15
15
  height: 1;
16
16
  }
17
17
 
@@ -45,6 +45,7 @@
45
45
  padding: 0 1 0 1;
46
46
  margin: 0 0 0 0;
47
47
  height: 1fr;
48
+ border: tall $primary;
48
49
  }
49
50
 
50
51
  #status-bar {
@@ -58,11 +59,12 @@
58
59
  }
59
60
 
60
61
  #status {
61
- padding: 0 1 0 1;
62
- margin: 0;
63
- color: cyan;
62
+ height: 3;
63
+ padding: 0 1;
64
+ color: $secondary;
64
65
  }
65
66
 
67
+
66
68
  .form-label {
67
69
  padding: 0 1 0 1;
68
70
  }
@@ -73,7 +75,7 @@
73
75
 
74
76
  #form-status, #confirm-status {
75
77
  padding: 0 1;
76
- color: yellow;
78
+ color: $warning;
77
79
  }
78
80
 
79
81
  #form-test {
@@ -23,7 +23,10 @@ from glaip_sdk.cli.slash.accounts_shared import (
23
23
  env_credentials_present,
24
24
  )
25
25
  from glaip_sdk.cli.slash.tui.background_tasks import BackgroundTaskMixin
26
+ from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter
27
+ from glaip_sdk.cli.slash.tui.context import TUIContext
26
28
  from glaip_sdk.cli.slash.tui.loading import hide_loading_indicator, show_loading_indicator
29
+ from glaip_sdk.cli.slash.tui.theme.catalog import _BUILTIN_THEMES
27
30
  from glaip_sdk.cli.validators import validate_api_key
28
31
  from glaip_sdk.utils.validation import validate_url
29
32
 
@@ -51,6 +54,13 @@ except Exception: # pragma: no cover - optional dependency
51
54
  LoadingIndicator = None # type: ignore[assignment]
52
55
  ModalScreen = None # type: ignore[assignment]
53
56
  Static = None # type: ignore[assignment]
57
+ Theme = None # type: ignore[assignment]
58
+
59
+ if App is not None:
60
+ try: # pragma: no cover - optional dependency
61
+ from textual.theme import Theme
62
+ except Exception: # pragma: no cover - optional dependency
63
+ Theme = None # type: ignore[assignment]
54
64
 
55
65
  TEXTUAL_SUPPORTED = App is not None and DataTable is not None
56
66
 
@@ -201,11 +211,12 @@ def run_accounts_textual(
201
211
  active_account: str | None,
202
212
  env_lock: bool,
203
213
  callbacks: AccountsTUICallbacks,
214
+ ctx: TUIContext | None = None,
204
215
  ) -> None:
205
216
  """Launch the Textual accounts browser if dependencies are available."""
206
217
  if not TEXTUAL_SUPPORTED:
207
218
  return
208
- app = AccountsTextualApp(rows, active_account, env_lock, callbacks)
219
+ app = AccountsTextualApp(rows, active_account, env_lock, callbacks, ctx=ctx)
209
220
  app.run()
210
221
 
211
222
 
@@ -379,16 +390,18 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
379
390
 
380
391
  CSS_PATH = CSS_FILE_NAME
381
392
  BINDINGS = [
382
- Binding("enter", "switch_row", "Switch", show=True),
383
- Binding("return", "switch_row", "Switch", show=False),
384
- Binding("/", "focus_filter", "Filter", show=True),
385
- Binding("a", "add_account", "Add", show=True),
386
- Binding("e", "edit_account", "Edit", show=True),
387
- Binding("d", "delete_account", "Delete", show=True),
393
+ Binding("enter", "switch_row", "Switch", show=True) if Binding else None,
394
+ Binding("return", "switch_row", "Switch", show=False) if Binding else None,
395
+ Binding("/", "focus_filter", "Filter", show=True) if Binding else None,
396
+ Binding("a", "add_account", "Add", show=True) if Binding else None,
397
+ Binding("e", "edit_account", "Edit", show=True) if Binding else None,
398
+ Binding("d", "delete_account", "Delete", show=True) if Binding else None,
399
+ Binding("c", "copy_account", "Copy", show=True) if Binding else None,
388
400
  # Esc clears filter when focused/non-empty; otherwise exits
389
- Binding("escape", "clear_or_exit", "Close", priority=True),
390
- Binding("q", "app_exit", "Close", priority=True),
401
+ Binding("escape", "clear_or_exit", "Close", priority=True) if Binding else None,
402
+ Binding("q", "app_exit", "Close", priority=True) if Binding else None,
391
403
  ]
404
+ BINDINGS = [b for b in BINDINGS if b is not None]
392
405
 
393
406
  def __init__(
394
407
  self,
@@ -396,6 +409,7 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
396
409
  active_account: str | None,
397
410
  env_lock: bool,
398
411
  callbacks: AccountsTUICallbacks,
412
+ ctx: TUIContext | None = None,
399
413
  ) -> None:
400
414
  """Initialize the Textual accounts app.
401
415
 
@@ -404,6 +418,7 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
404
418
  active_account: Name of the currently active account.
405
419
  env_lock: Whether environment credentials are locking account switching.
406
420
  callbacks: Callbacks for account switching operations.
421
+ ctx: Shared TUI context.
407
422
  """
408
423
  super().__init__()
409
424
  self._store = get_account_store()
@@ -411,6 +426,7 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
411
426
  self._active_account = active_account
412
427
  self._env_lock = env_lock
413
428
  self._callbacks = callbacks
429
+ self._ctx = ctx
414
430
  self._filter_text: str = ""
415
431
  self._is_switching = False
416
432
 
@@ -449,6 +465,7 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
449
465
 
450
466
  def on_mount(self) -> None:
451
467
  """Configure table columns and load rows."""
468
+ self._apply_theme()
452
469
  table = self.query_one(ACCOUNTS_TABLE_ID, DataTable)
453
470
  table.add_column("Name", width=20)
454
471
  table.add_column("API URL", width=40)
@@ -752,6 +769,26 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
752
769
  return
753
770
  self.push_screen(ConfirmDeleteModal(name), self._on_delete_result)
754
771
 
772
+ def action_copy_account(self) -> None:
773
+ """Copy selected account name and URL to clipboard."""
774
+ name = self._get_selected_name()
775
+ if not name:
776
+ self._set_status("Select an account to copy.", "yellow")
777
+ return
778
+
779
+ account = self._store.get_account(name)
780
+ if not account:
781
+ return
782
+
783
+ text = f"Account: {name}\nURL: {account.get('api_url', '')}"
784
+ adapter = ClipboardAdapter()
785
+ result = adapter.copy(text)
786
+
787
+ if result.success:
788
+ self._set_status(f"Copied '{name}' to clipboard.", "green")
789
+ else:
790
+ self._set_status(f"Copy failed: {result.message}", "red")
791
+
755
792
  def _check_env_lock_hotkey(self) -> bool:
756
793
  """Prevent mutations when env credentials are present."""
757
794
  if not self._is_env_locked():
@@ -874,3 +911,23 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
874
911
  filter_input = self.query_one(FILTER_INPUT_ID, Input)
875
912
  clear_btn = self.query_one("#filter-clear", Button)
876
913
  clear_btn.display = bool(filter_input.value or self._filter_text)
914
+
915
+ def _apply_theme(self) -> None:
916
+ """Register built-in themes and set the active one from context."""
917
+ if not self._ctx or not self._ctx.theme or Theme is None:
918
+ return
919
+
920
+ for name, tokens in _BUILTIN_THEMES.items():
921
+ self.register_theme(
922
+ Theme(
923
+ name=name,
924
+ primary=tokens.primary,
925
+ secondary=tokens.secondary,
926
+ accent=tokens.accent,
927
+ warning=tokens.warning,
928
+ error=tokens.error,
929
+ success=tokens.success,
930
+ )
931
+ )
932
+
933
+ self.theme = self._ctx.theme.theme_name
@@ -0,0 +1,147 @@
1
+ """Clipboard adapter for TUI copy actions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import os
7
+ import platform
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+ from typing import Any
14
+ from collections.abc import Callable
15
+
16
+ from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities, detect_osc52_support
17
+
18
+
19
+ class ClipboardMethod(str, Enum):
20
+ """Supported clipboard backends."""
21
+
22
+ OSC52 = "osc52"
23
+ PBCOPY = "pbcopy"
24
+ XCLIP = "xclip"
25
+ XSEL = "xsel"
26
+ WL_COPY = "wl-copy"
27
+ CLIP = "clip"
28
+ NONE = "none"
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class ClipboardResult:
33
+ """Result of a clipboard operation."""
34
+
35
+ success: bool
36
+ method: ClipboardMethod
37
+ message: str
38
+
39
+
40
+ _SUBPROCESS_COMMANDS: dict[ClipboardMethod, list[str]] = {
41
+ ClipboardMethod.PBCOPY: ["pbcopy"],
42
+ ClipboardMethod.XCLIP: ["xclip", "-selection", "clipboard"],
43
+ ClipboardMethod.XSEL: ["xsel", "--clipboard", "--input"],
44
+ ClipboardMethod.WL_COPY: ["wl-copy"],
45
+ ClipboardMethod.CLIP: ["clip"],
46
+ }
47
+
48
+
49
+ class ClipboardAdapter:
50
+ """Cross-platform clipboard access with OSC 52 fallback."""
51
+
52
+ def __init__(
53
+ self,
54
+ *,
55
+ terminal: TerminalCapabilities | None = None,
56
+ method: ClipboardMethod | None = None,
57
+ ) -> None:
58
+ """Initialize the adapter."""
59
+ self._terminal = terminal
60
+ self._method = method or self._detect_method()
61
+
62
+ @property
63
+ def method(self) -> ClipboardMethod:
64
+ """Return the detected clipboard backend."""
65
+ return self._method
66
+
67
+ def copy(self, text: str, *, writer: Callable[[str], Any] | None = None) -> ClipboardResult:
68
+ """Copy text to clipboard using the best available method.
69
+
70
+ Args:
71
+ text: Text to copy.
72
+ writer: Optional function to write OSC 52 sequence (e.g., self.app.console.write).
73
+ Defaults to sys.stdout.write if not provided.
74
+ """
75
+ if self._method == ClipboardMethod.OSC52:
76
+ return self._copy_osc52(text, writer=writer)
77
+
78
+ command = _SUBPROCESS_COMMANDS.get(self._method)
79
+ if command is None:
80
+ return self._copy_osc52(text, writer=writer)
81
+
82
+ result = self._copy_subprocess(command, text)
83
+ if not result.success:
84
+ return self._copy_osc52(text, writer=writer)
85
+
86
+ return result
87
+
88
+ def _detect_method(self) -> ClipboardMethod:
89
+ if self._terminal.osc52 if self._terminal else detect_osc52_support():
90
+ return ClipboardMethod.OSC52
91
+
92
+ system = platform.system()
93
+ if system == "Darwin":
94
+ return self._detect_darwin_method()
95
+ if system == "Linux":
96
+ return self._detect_linux_method()
97
+ if system == "Windows":
98
+ return self._detect_windows_method()
99
+ return ClipboardMethod.NONE
100
+
101
+ def _detect_darwin_method(self) -> ClipboardMethod:
102
+ return ClipboardMethod.PBCOPY if shutil.which("pbcopy") else ClipboardMethod.NONE
103
+
104
+ def _detect_linux_method(self) -> ClipboardMethod:
105
+ if not os.getenv("DISPLAY") and not os.getenv("WAYLAND_DISPLAY"):
106
+ return ClipboardMethod.NONE
107
+
108
+ for cmd, method in (
109
+ ("xclip", ClipboardMethod.XCLIP),
110
+ ("xsel", ClipboardMethod.XSEL),
111
+ ("wl-copy", ClipboardMethod.WL_COPY),
112
+ ):
113
+ if shutil.which(cmd):
114
+ return method
115
+ return ClipboardMethod.NONE
116
+
117
+ def _detect_windows_method(self) -> ClipboardMethod:
118
+ return ClipboardMethod.CLIP if shutil.which("clip") else ClipboardMethod.NONE
119
+
120
+ def _copy_osc52(self, text: str, *, writer: Callable[[str], Any] | None = None) -> ClipboardResult:
121
+ encoded = base64.b64encode(text.encode("utf-8")).decode("ascii")
122
+ sequence = f"\x1b]52;c;{encoded}\x07"
123
+ try:
124
+ if writer:
125
+ writer(sequence)
126
+ else:
127
+ sys.stdout.write(sequence)
128
+ sys.stdout.flush()
129
+ except Exception as exc:
130
+ return ClipboardResult(False, ClipboardMethod.OSC52, str(exc))
131
+
132
+ return ClipboardResult(True, ClipboardMethod.OSC52, "Copied to clipboard")
133
+
134
+ def _copy_subprocess(self, cmd: list[str], text: str) -> ClipboardResult:
135
+ try:
136
+ completed = subprocess.run(
137
+ cmd,
138
+ input=text.encode("utf-8"),
139
+ check=False,
140
+ )
141
+ except OSError as exc:
142
+ return ClipboardResult(False, self._method, str(exc))
143
+
144
+ if completed.returncode == 0:
145
+ return ClipboardResult(True, self._method, "Copied to clipboard")
146
+
147
+ return ClipboardResult(False, self._method, f"Command failed: {completed.returncode}")
@@ -10,9 +10,15 @@ Authors:
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import os
13
14
  from dataclasses import dataclass
15
+ from typing import TYPE_CHECKING
14
16
 
15
17
  from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities
18
+ from glaip_sdk.cli.slash.tui.theme import ThemeManager
19
+
20
+ if TYPE_CHECKING:
21
+ from glaip_sdk.cli.slash.tui.toast import ToastBus
16
22
 
17
23
 
18
24
  @dataclass
@@ -32,8 +38,8 @@ class TUIContext:
32
38
 
33
39
  terminal: TerminalCapabilities
34
40
  keybinds: object | None = None
35
- theme: object | None = None
36
- toasts: object | None = None
41
+ theme: ThemeManager | None = None
42
+ toasts: ToastBus | None = None
37
43
  clipboard: object | None = None
38
44
 
39
45
  @classmethod
@@ -48,4 +54,6 @@ class TUIContext:
48
54
  TUIContext instance with terminal capabilities detected.
49
55
  """
50
56
  terminal = await TerminalCapabilities.detect()
51
- return cls(terminal=terminal)
57
+ theme_name = os.getenv("AIP_TUI_THEME") or None
58
+ theme = ThemeManager(terminal, theme=theme_name)
59
+ return cls(terminal=terminal, theme=theme)
@@ -0,0 +1,235 @@
1
+ """Keybinding registry helpers for TUI applications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from dataclasses import dataclass
7
+
8
+ DEFAULT_LEADER = "ctrl+x"
9
+ _LEADER_TOKEN = "<leader>"
10
+
11
+ _MODIFIER_ORDER = ("ctrl", "alt", "shift", "meta")
12
+ _MODIFIER_SYNONYMS = {
13
+ "control": "ctrl",
14
+ "ctl": "ctrl",
15
+ "cmd": "meta",
16
+ "command": "meta",
17
+ "option": "alt",
18
+ "return": "enter",
19
+ }
20
+
21
+ _KEY_SYNONYMS = {
22
+ "esc": "escape",
23
+ }
24
+
25
+ _KEY_DISPLAY = {
26
+ "escape": "Esc",
27
+ "enter": "Enter",
28
+ "space": "Space",
29
+ "tab": "Tab",
30
+ "backspace": "Backspace",
31
+ }
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class Keybind:
36
+ """A registered keybinding."""
37
+
38
+ action: str
39
+ sequence: tuple[str, ...]
40
+ description: str
41
+ category: str | None = None
42
+
43
+ def __repr__(self) -> str:
44
+ """Return a readable representation of the keybind."""
45
+ return (
46
+ f"Keybind(action={self.action!r}, sequence={self.sequence}, "
47
+ f"description={self.description!r}, category={self.category!r})"
48
+ )
49
+
50
+
51
+ class KeybindRegistry:
52
+ """Central registry of keybindings and associated metadata."""
53
+
54
+ def __init__(self, *, leader: str = DEFAULT_LEADER) -> None:
55
+ """Initialize the registry."""
56
+ normalized = _normalize_chord(leader)
57
+ self._leader = normalized or DEFAULT_LEADER
58
+ self._keybinds: dict[str, Keybind] = {}
59
+
60
+ @property
61
+ def leader(self) -> str:
62
+ """Return the normalized leader chord."""
63
+ return self._leader
64
+
65
+ def register(
66
+ self,
67
+ *,
68
+ action: str,
69
+ key: str,
70
+ description: str,
71
+ category: str | None = None,
72
+ ) -> Keybind:
73
+ """Register a keybinding for an action."""
74
+ if action in self._keybinds:
75
+ raise ValueError(f"Action already registered: {action}")
76
+
77
+ sequence = parse_key_sequence(key)
78
+ keybind = Keybind(action=action, sequence=sequence, description=description, category=category)
79
+ self._keybinds[action] = keybind
80
+ return keybind
81
+
82
+ def get(self, action: str) -> Keybind | None:
83
+ """Return keybind for action, if present."""
84
+ return self._keybinds.get(action)
85
+
86
+ def actions(self) -> list[str]:
87
+ """Return sorted list of registered actions."""
88
+ return sorted(self._keybinds)
89
+
90
+ def matches(self, action: str, sequence: str | Iterable[str]) -> bool:
91
+ """Return True if the provided sequence matches the action's keybind."""
92
+ keybind = self._keybinds.get(action)
93
+ if keybind is None:
94
+ return False
95
+
96
+ candidate = _coerce_sequence(sequence)
97
+ return candidate == keybind.sequence
98
+
99
+ def format(self, action: str) -> str:
100
+ """Return a human-readable sequence for an action."""
101
+ keybind = self._keybinds.get(action)
102
+ if keybind is None:
103
+ return ""
104
+
105
+ return format_key_sequence(keybind.sequence, leader=self._leader)
106
+
107
+
108
+ def parse_key_sequence(key: str) -> tuple[str, ...]:
109
+ """Parse a key sequence string into normalized tokens."""
110
+ tokens = [token for token in key.strip().split() if token]
111
+ normalized: list[str] = []
112
+
113
+ for token in tokens:
114
+ if token.lower() == _LEADER_TOKEN:
115
+ normalized.append(_LEADER_TOKEN)
116
+ continue
117
+
118
+ chord = _normalize_chord(token)
119
+ if chord:
120
+ normalized.append(chord)
121
+
122
+ return tuple(normalized)
123
+
124
+
125
+ def format_key_sequence(sequence: tuple[str, ...], *, leader: str = DEFAULT_LEADER) -> str:
126
+ """Format a normalized sequence into a display string."""
127
+ rendered: list[str] = []
128
+
129
+ for token in sequence:
130
+ if token == _LEADER_TOKEN:
131
+ rendered.append(_format_token(leader))
132
+ continue
133
+ rendered.append(_format_token(token))
134
+
135
+ return " ".join(rendered)
136
+
137
+
138
+ def _coerce_sequence(sequence: str | Iterable[str]) -> tuple[str, ...]:
139
+ if isinstance(sequence, str):
140
+ return parse_key_sequence(sequence)
141
+
142
+ tokens: list[str] = []
143
+ for token in sequence:
144
+ if not token:
145
+ continue
146
+ if token.lower() == _LEADER_TOKEN:
147
+ tokens.append(_LEADER_TOKEN)
148
+ continue
149
+ chord = _normalize_chord(token)
150
+ if chord:
151
+ tokens.append(chord)
152
+
153
+ return tuple(tokens)
154
+
155
+
156
+ def _normalize_chord(chord: str) -> str:
157
+ """Normalize a key chord string to canonical form.
158
+
159
+ Normalization rules:
160
+ - Converts separators: both '-' and '+' are normalized to '+'
161
+ - Handles synonyms: 'control'/'ctl' -> 'ctrl', 'cmd'/'command' -> 'meta', 'option' -> 'alt'
162
+ - Deduplicates modifiers: 'ctrl+ctrl+l' -> 'ctrl+l'
163
+ - Orders modifiers: ctrl < alt < shift < meta (unknown modifiers sort last)
164
+ - Case-insensitive: 'Ctrl+L' == 'ctrl+l' == 'CTRL-L'
165
+
166
+ Args:
167
+ chord: Key chord string (e.g., "Ctrl+L", "ctrl-l", "CTRL+CTRL+L")
168
+
169
+ Returns:
170
+ Normalized chord string (e.g., "ctrl+l") or empty string if invalid.
171
+ """
172
+ parts = [part for part in chord.replace("-", "+").split("+") if part.strip()]
173
+ if not parts:
174
+ return ""
175
+
176
+ normalized_parts = [_normalize_key_part(part) for part in parts]
177
+ if len(normalized_parts) == 1:
178
+ return normalized_parts[0]
179
+
180
+ modifiers, key = normalized_parts[:-1], normalized_parts[-1]
181
+
182
+ seen: set[str] = set()
183
+ unique_mods: list[str] = []
184
+ for mod in modifiers:
185
+ if mod in seen:
186
+ continue
187
+ seen.add(mod)
188
+ unique_mods.append(mod)
189
+
190
+ unique_mods.sort(key=_modifier_sort_key)
191
+ return "+".join([*unique_mods, key])
192
+
193
+
194
+ def _normalize_key_part(part: str) -> str:
195
+ token = part.strip().lower()
196
+ token = _MODIFIER_SYNONYMS.get(token, token)
197
+ return _KEY_SYNONYMS.get(token, token)
198
+
199
+
200
+ def _modifier_sort_key(modifier: str) -> int:
201
+ try:
202
+ return _MODIFIER_ORDER.index(modifier)
203
+ except ValueError:
204
+ return len(_MODIFIER_ORDER)
205
+
206
+
207
+ def _format_token(token: str) -> str:
208
+ if "+" in token:
209
+ return _format_chord(token)
210
+
211
+ return _KEY_DISPLAY.get(token, token)
212
+
213
+
214
+ def _format_chord(chord: str) -> str:
215
+ parts = chord.split("+")
216
+ modifiers, key = parts[:-1], parts[-1]
217
+
218
+ rendered_mods: list[str] = []
219
+ for mod in modifiers:
220
+ if mod == "ctrl":
221
+ rendered_mods.append("Ctrl")
222
+ elif mod == "alt":
223
+ rendered_mods.append("Alt")
224
+ elif mod == "shift":
225
+ rendered_mods.append("Shift")
226
+ elif mod == "meta":
227
+ rendered_mods.append("Meta")
228
+ else:
229
+ rendered_mods.append(mod.title())
230
+
231
+ rendered_key = _KEY_DISPLAY.get(key, key)
232
+ if len(rendered_key) == 1 and rendered_key.isalpha():
233
+ rendered_key = rendered_key.upper()
234
+
235
+ return "+".join([*rendered_mods, rendered_key])
@@ -89,7 +89,7 @@ class TerminalCapabilities:
89
89
 
90
90
  # Basic capability detection
91
91
  ansi = tty_available and term not in ("dumb", "")
92
- osc52 = _detect_osc52_support()
92
+ osc52 = detect_osc52_support()
93
93
  mouse = tty_available and term not in ("dumb", "")
94
94
  truecolor = colorterm in ("truecolor", "24bit")
95
95
 
@@ -366,7 +366,7 @@ def _check_terminal_in_env(env_value: str, terminals: list[str]) -> bool:
366
366
  return any(terminal in env_value for terminal in terminals)
367
367
 
368
368
 
369
- def _detect_osc52_support() -> bool:
369
+ def detect_osc52_support() -> bool:
370
370
  """Check if terminal likely supports OSC 52 (clipboard).
371
371
 
372
372
  Returns:
@@ -0,0 +1,15 @@
1
+ """Theme system primitives for Textual TUIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from glaip_sdk.cli.slash.tui.theme.catalog import get_builtin_theme, list_builtin_themes
6
+ from glaip_sdk.cli.slash.tui.theme.manager import ThemeManager, ThemeMode
7
+ from glaip_sdk.cli.slash.tui.theme.tokens import ThemeTokens
8
+
9
+ __all__ = [
10
+ "ThemeManager",
11
+ "ThemeMode",
12
+ "ThemeTokens",
13
+ "get_builtin_theme",
14
+ "list_builtin_themes",
15
+ ]
@@ -0,0 +1,79 @@
1
+ """Built-in theme catalog for TUI applications.
2
+
3
+ This module implements Phase 2 of the TUI Theme System spec, providing a foundational
4
+ set of 12 color tokens (primary, secondary, accent, background, background_panel, text,
5
+ text_muted, success, warning, error, info). Additional tokens (e.g., diff.added,
6
+ syntax.*, backgroundElevated, textDim) will be added in future phases per the spec's
7
+ "100+ color tokens" requirement.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from glaip_sdk.cli.slash.tui.theme.tokens import ThemeModeLiteral, ThemeTokens
13
+
14
+ _BUILTIN_THEMES: dict[str, ThemeTokens] = {
15
+ "gl-dark": ThemeTokens(
16
+ name="gl-dark",
17
+ mode="dark",
18
+ primary="#6EA8FE",
19
+ secondary="#ADB5BD",
20
+ accent="#C77DFF",
21
+ background="#0B0F19",
22
+ background_panel="#111827",
23
+ text="#E5E7EB",
24
+ text_muted="#9CA3AF",
25
+ success="#34D399",
26
+ warning="#FBBF24",
27
+ error="#F87171",
28
+ info="#60A5FA",
29
+ ),
30
+ "gl-light": ThemeTokens(
31
+ name="gl-light",
32
+ mode="light",
33
+ primary="#1D4ED8",
34
+ secondary="#4B5563",
35
+ accent="#7C3AED",
36
+ background="#FFFFFF",
37
+ background_panel="#F3F4F6",
38
+ text="#111827",
39
+ text_muted="#4B5563",
40
+ success="#059669",
41
+ warning="#B45309",
42
+ error="#B91C1C",
43
+ info="#1D4ED8",
44
+ ),
45
+ "gl-high-contrast": ThemeTokens(
46
+ name="gl-high-contrast",
47
+ mode="dark",
48
+ # High-contrast theme uses uniform colors (#FFFFFF on #000000) to maximize
49
+ # contrast for accessibility. Semantic distinctions (success/warning/error)
50
+ # are intentionally uniform to prioritize maximum readability over color
51
+ # coding, per accessibility best practices for high-contrast modes.
52
+ primary="#FFFFFF",
53
+ secondary="#FFFFFF",
54
+ accent="#FFFFFF",
55
+ background="#000000",
56
+ background_panel="#000000",
57
+ text="#FFFFFF",
58
+ text_muted="#FFFFFF",
59
+ success="#FFFFFF",
60
+ warning="#FFFFFF",
61
+ error="#FFFFFF",
62
+ info="#FFFFFF",
63
+ ),
64
+ }
65
+
66
+
67
+ def get_builtin_theme(name: str) -> ThemeTokens | None:
68
+ """Return a built-in theme by name."""
69
+ return _BUILTIN_THEMES.get(name)
70
+
71
+
72
+ def list_builtin_themes() -> list[str]:
73
+ """List available built-in theme names."""
74
+ return sorted(_BUILTIN_THEMES)
75
+
76
+
77
+ def default_theme_name_for_mode(mode: ThemeModeLiteral) -> str:
78
+ """Return the default theme name for the given light/dark mode."""
79
+ return "gl-light" if mode == "light" else "gl-dark"
@@ -0,0 +1,86 @@
1
+ """Theme manager for TUI applications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from enum import Enum
7
+ from typing import Literal
8
+
9
+ from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities
10
+ from glaip_sdk.cli.slash.tui.theme.catalog import default_theme_name_for_mode, get_builtin_theme
11
+ from glaip_sdk.cli.slash.tui.theme.tokens import ThemeTokens
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ThemeMode(str, Enum):
17
+ """User-selectable theme mode."""
18
+
19
+ AUTO = "auto"
20
+ LIGHT = "light"
21
+ DARK = "dark"
22
+
23
+
24
+ class ThemeManager:
25
+ """Resolve active theme tokens from terminal state and user preferences."""
26
+
27
+ def __init__(
28
+ self,
29
+ terminal: TerminalCapabilities,
30
+ *,
31
+ mode: ThemeMode | str = ThemeMode.AUTO,
32
+ theme: str | None = None,
33
+ ) -> None:
34
+ """Initialize the theme manager."""
35
+ self._terminal = terminal
36
+ self._mode = self._coerce_mode(mode)
37
+ self._theme = theme
38
+
39
+ @property
40
+ def mode(self) -> ThemeMode:
41
+ """Return configured mode (auto/light/dark)."""
42
+ return self._mode
43
+
44
+ @property
45
+ def effective_mode(self) -> Literal["light", "dark"]:
46
+ """Return resolved light/dark mode."""
47
+ if self._mode == ThemeMode.AUTO:
48
+ return self._terminal.background_mode
49
+ return "light" if self._mode == ThemeMode.LIGHT else "dark"
50
+
51
+ @property
52
+ def theme_name(self) -> str:
53
+ """Return resolved theme name."""
54
+ return self._theme or default_theme_name_for_mode(self.effective_mode)
55
+
56
+ @property
57
+ def tokens(self) -> ThemeTokens:
58
+ """Return tokens for the resolved theme."""
59
+ chosen = get_builtin_theme(self.theme_name)
60
+ if chosen is not None:
61
+ return chosen
62
+
63
+ fallback_name = default_theme_name_for_mode(self.effective_mode)
64
+ fallback = get_builtin_theme(fallback_name)
65
+ if fallback is None:
66
+ raise RuntimeError(f"Missing default theme: {fallback_name}")
67
+
68
+ return fallback
69
+
70
+ def set_mode(self, mode: ThemeMode | str) -> None:
71
+ """Set auto/light/dark mode."""
72
+ self._mode = self._coerce_mode(mode)
73
+
74
+ def set_theme(self, theme: str | None) -> None:
75
+ """Set explicit theme name (or None to use the default)."""
76
+ self._theme = theme
77
+
78
+ def _coerce_mode(self, mode: ThemeMode | str) -> ThemeMode:
79
+ """Coerce a mode value to ThemeMode enum, defaulting to AUTO on invalid input."""
80
+ if isinstance(mode, ThemeMode):
81
+ return mode
82
+ try:
83
+ return ThemeMode(mode)
84
+ except ValueError:
85
+ logger.warning(f"Invalid theme mode '{mode}', defaulting to AUTO")
86
+ return ThemeMode.AUTO
@@ -0,0 +1,55 @@
1
+ """Theme token definitions for TUI applications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Literal
7
+
8
+ ThemeModeLiteral = Literal["light", "dark"]
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class ThemeTokens:
13
+ """Color token set for a built-in theme."""
14
+
15
+ name: str
16
+ mode: ThemeModeLiteral
17
+
18
+ primary: str
19
+ secondary: str
20
+ accent: str
21
+
22
+ background: str
23
+ background_panel: str
24
+
25
+ text: str
26
+ text_muted: str
27
+
28
+ success: str
29
+ warning: str
30
+ error: str
31
+ info: str
32
+
33
+ def as_dict(self) -> dict[str, str]:
34
+ """Return color tokens as a plain dictionary.
35
+
36
+ Returns only color tokens (primary, secondary, accent, background, etc.),
37
+ excluding metadata fields (name, mode). This is intentional for use cases
38
+ like Textual TCSS mapping where only color values are needed.
39
+
40
+ Returns:
41
+ Dictionary mapping color token names to hex color strings.
42
+ """
43
+ return {
44
+ "primary": self.primary,
45
+ "secondary": self.secondary,
46
+ "accent": self.accent,
47
+ "background": self.background,
48
+ "background_panel": self.background_panel,
49
+ "text": self.text,
50
+ "text_muted": self.text_muted,
51
+ "success": self.success,
52
+ "warning": self.warning,
53
+ "error": self.error,
54
+ "info": self.info,
55
+ }
@@ -0,0 +1,123 @@
1
+ """Toast notification helpers for Textual TUIs.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from dataclasses import dataclass
11
+ from enum import Enum
12
+
13
+
14
+ class ToastVariant(str, Enum):
15
+ """Toast message variant."""
16
+
17
+ INFO = "info"
18
+ SUCCESS = "success"
19
+ WARNING = "warning"
20
+ ERROR = "error"
21
+
22
+
23
+ DEFAULT_TOAST_DURATIONS_SECONDS: dict[ToastVariant, float] = {
24
+ ToastVariant.SUCCESS: 2.0,
25
+ ToastVariant.INFO: 3.0,
26
+ ToastVariant.WARNING: 3.0,
27
+ ToastVariant.ERROR: 5.0,
28
+ }
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class ToastState:
33
+ """Immutable toast payload."""
34
+
35
+ message: str
36
+ variant: ToastVariant
37
+ duration_seconds: float
38
+
39
+
40
+ class ToastBus:
41
+ """Single-toast state holder with auto-dismiss."""
42
+
43
+ def __init__(self) -> None:
44
+ """Initialize the bus."""
45
+ self._state: ToastState | None = None
46
+ self._dismiss_task: asyncio.Task[None] | None = None
47
+
48
+ @property
49
+ def state(self) -> ToastState | None:
50
+ """Return the current toast state."""
51
+ return self._state
52
+
53
+ def show(
54
+ self,
55
+ message: str,
56
+ variant: ToastVariant | str = ToastVariant.INFO,
57
+ *,
58
+ duration_seconds: float | None = None,
59
+ ) -> None:
60
+ """Set toast state and schedule auto-dismiss."""
61
+ resolved_variant = self._coerce_variant(variant)
62
+ resolved_duration = (
63
+ DEFAULT_TOAST_DURATIONS_SECONDS[resolved_variant] if duration_seconds is None else float(duration_seconds)
64
+ )
65
+
66
+ self._state = ToastState(
67
+ message=message,
68
+ variant=resolved_variant,
69
+ duration_seconds=resolved_duration,
70
+ )
71
+
72
+ self._cancel_dismiss_task()
73
+
74
+ try:
75
+ loop = asyncio.get_running_loop()
76
+ except RuntimeError:
77
+ raise RuntimeError(
78
+ "Cannot schedule toast auto-dismiss: no running event loop. "
79
+ "ToastBus.show() must be called from within an async context."
80
+ ) from None
81
+
82
+ self._dismiss_task = loop.create_task(self._auto_dismiss(resolved_duration))
83
+
84
+ def clear(self) -> None:
85
+ """Clear the current toast."""
86
+ self._cancel_dismiss_task()
87
+ self._state = None
88
+
89
+ def copy_success(self, label: str | None = None) -> None:
90
+ """Show clipboard success toast."""
91
+ message = "Copied to clipboard" if not label else f"Copied {label} to clipboard"
92
+ self.show(message=message, variant=ToastVariant.SUCCESS)
93
+
94
+ def copy_failed(self) -> None:
95
+ """Show clipboard failure toast."""
96
+ self.show(
97
+ message="Clipboard unavailable. Text printed below",
98
+ variant=ToastVariant.WARNING,
99
+ )
100
+
101
+ def _coerce_variant(self, variant: ToastVariant | str) -> ToastVariant:
102
+ if isinstance(variant, ToastVariant):
103
+ return variant
104
+ try:
105
+ return ToastVariant(variant)
106
+ except ValueError:
107
+ return ToastVariant.INFO
108
+
109
+ def _cancel_dismiss_task(self) -> None:
110
+ if self._dismiss_task is None:
111
+ return
112
+ if not self._dismiss_task.done():
113
+ self._dismiss_task.cancel()
114
+ self._dismiss_task = None
115
+
116
+ async def _auto_dismiss(self, duration_seconds: float) -> None:
117
+ try:
118
+ await asyncio.sleep(duration_seconds)
119
+ except asyncio.CancelledError:
120
+ return
121
+
122
+ self._state = None
123
+ self._dismiss_task = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glaip-sdk
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Python SDK and CLI for GL AIP (GDP Labs AI Agent Package) - Build, run, and manage AI agents
5
5
  Author-email: Raymond Christopher <raymond.christopher@gdplabs.id>
6
6
  License: MIT
@@ -71,20 +71,27 @@ glaip_sdk/cli/core/rendering.py,sha256=QgbYzTcKH8wa7-BdR3UgiS3KBx1QYZjDcV2Hyy5ox
71
71
  glaip_sdk/cli/parsers/__init__.py,sha256=NzLrSH6GOdNoewXtKNpB6GwrauA8rb_IGYV6cz5Hn3o,113
72
72
  glaip_sdk/cli/parsers/json_input.py,sha256=kxoxeIlgfsaH2jhe6apZAgSxAtwlpSINLTMRsZZYboQ,5630
73
73
  glaip_sdk/cli/slash/__init__.py,sha256=J9TPL2UcNTkW8eifG6nRmAEGHhyEgdYMYk4cHaaObC0,386
74
- glaip_sdk/cli/slash/accounts_controller.py,sha256=-7v_4nTAVCqXySbOLtTfMpUpsqCzDTWmZYkBU880AzI,24803
74
+ glaip_sdk/cli/slash/accounts_controller.py,sha256=SceJlc2F2ZdlSDkuWO3Js3akL89bVtQLyGM_oA-F2qI,24928
75
75
  glaip_sdk/cli/slash/accounts_shared.py,sha256=Mq5HxlI0YsVEQ0KKISWvyBZhzOFFWCzwRbhF5xwvUbM,2626
76
76
  glaip_sdk/cli/slash/agent_session.py,sha256=tuVOme-NbEyr6rwJvsBEKZYWQmsaRf4piJeRvIGu0ns,11384
77
77
  glaip_sdk/cli/slash/prompt.py,sha256=q4f1c2zr7ZMUeO6AgOBF2Nz4qgMOXrVPt6WzPRQMbAM,8501
78
78
  glaip_sdk/cli/slash/remote_runs_controller.py,sha256=a5X5rYgb9l6dHhvTewRUCj-hAo7mKRnuM_MwGvxs8jI,21363
79
- glaip_sdk/cli/slash/session.py,sha256=f6yetP4ih_x7MZCbv4sVQfqHH7JJYgxLY0Q6VHHWTes,64423
80
- glaip_sdk/cli/slash/tui/__init__.py,sha256=78O54cB5v0nt6vlC1hoXr8YDmST2RjZkq8PUeYratcE,518
81
- glaip_sdk/cli/slash/tui/accounts.tcss,sha256=xuQjQ0tBM08K1DUv6lI5Sfu1zgZzQxg60c9-RlEWB4s,1160
82
- glaip_sdk/cli/slash/tui/accounts_app.py,sha256=QDaOpVStS6Z51tfXcS8GRRjTrVfMO26-guHepqysU9k,33715
79
+ glaip_sdk/cli/slash/session.py,sha256=Zn2hXND_Tfameh_PI8g4VIMd7GPWxwhtPNMN9p6cF7M,65081
80
+ glaip_sdk/cli/slash/tui/__init__.py,sha256=oBUzeoslYwPKVlhqhgg4I7480b77vQNc9ec0NgdTC1s,977
81
+ glaip_sdk/cli/slash/tui/accounts.tcss,sha256=BCjIuTetmVjydv6DCliY38Cze2LUEu7IY44sL5nIuLU,1194
82
+ glaip_sdk/cli/slash/tui/accounts_app.py,sha256=6ihnAnzKD49eeXYW3dYWUAdUEyoXNFwoEoi3kS3WtXM,35999
83
83
  glaip_sdk/cli/slash/tui/background_tasks.py,sha256=SAe1mV2vXB3mJcSGhelU950vf8Lifjhws9iomyIVFKw,2422
84
- glaip_sdk/cli/slash/tui/context.py,sha256=oEw4P0ym77uPI0cbHiBam4xpSL2TT0OPvqpBo8gLR30,1835
84
+ glaip_sdk/cli/slash/tui/clipboard.py,sha256=HL_RWIdONyRmDtTYuDzxJTS_mRcLxuR37Ac9Ug5nh40,4730
85
+ glaip_sdk/cli/slash/tui/context.py,sha256=03mo2kgvpyUcNBYz7G2Uyu7X3FJlSUzVoP5Rt9MCZZY,2141
86
+ glaip_sdk/cli/slash/tui/keybind_registry.py,sha256=_rK05BxTxNudYc4iJ9gDxpgeUkjDAq8rarIT-9A-jyM,6739
85
87
  glaip_sdk/cli/slash/tui/loading.py,sha256=nW5pv_Tnl9FUOPR3Qf2O5gt1AGHSo3b5-Uofg34F6AE,1909
86
88
  glaip_sdk/cli/slash/tui/remote_runs_app.py,sha256=RCrI-c5ilKV6Iy1lz2Aok9xo2Ou02vqcXACMXTdodnE,24716
87
- glaip_sdk/cli/slash/tui/terminal.py,sha256=SnMOuA6uGeQKA3sGEEM3yTm8j_1LzpYmqsJKyGEKWcg,12110
89
+ glaip_sdk/cli/slash/tui/terminal.py,sha256=iC31XChTL34gXY6vXdSIX3HmD36tuA9EYTPZ2Sn4uOI,12108
90
+ glaip_sdk/cli/slash/tui/toast.py,sha256=LP_myZwgnrdowrRxGK24lMlx7iZt7iOwFhrbc4NW0DY,3493
91
+ glaip_sdk/cli/slash/tui/theme/__init__.py,sha256=rtM2ik83YNCRcI1qh_Sf3rnxco2OvCNNT3NbHY6cLvw,432
92
+ glaip_sdk/cli/slash/tui/theme/catalog.py,sha256=G52eU3h8YI9D8XUALVg1KVZ4Lq65VnZdgPS3F_P7XLE,2544
93
+ glaip_sdk/cli/slash/tui/theme/manager.py,sha256=X600J_WIBM1CHgsQeMFGFPuaVAFCINFcBXFWmeD4B5Q,2707
94
+ glaip_sdk/cli/slash/tui/theme/tokens.py,sha256=ympMRny_d-gHtmnPR-lmNZ-C9SGBy2q-MH81l0L1h-Y,1423
88
95
  glaip_sdk/cli/transcript/__init__.py,sha256=yiYHyNtebMCu3BXu56Xm5RBC2tDc865q8UGPnoe6QRs,920
89
96
  glaip_sdk/cli/transcript/cache.py,sha256=Wi1uln6HP1U6F-MRTrfnxi9bn6XJTxwWXhREIRPoMqQ,17439
90
97
  glaip_sdk/cli/transcript/capture.py,sha256=t8j_62cC6rhb51oCluZd17N04vcXqyjkhPRcRd3ZcmM,10291
@@ -193,8 +200,8 @@ glaip_sdk/utils/rendering/steps/format.py,sha256=Chnq7OBaj8XMeBntSBxrX5zSmrYeGcO
193
200
  glaip_sdk/utils/rendering/steps/manager.py,sha256=BiBmTeQMQhjRMykgICXsXNYh1hGsss-fH9BIGVMWFi0,13194
194
201
  glaip_sdk/utils/rendering/viewer/__init__.py,sha256=XrxmE2cMAozqrzo1jtDFm8HqNtvDcYi2mAhXLXn5CjI,457
195
202
  glaip_sdk/utils/rendering/viewer/presenter.py,sha256=mlLMTjnyeyPVtsyrAbz1BJu9lFGQSlS-voZ-_Cuugv0,5725
196
- glaip_sdk-0.7.0.dist-info/METADATA,sha256=o7SZ0YWA7OLjK7DsXMcecJJZ0TZFtEjglxJNmdajLpk,8365
197
- glaip_sdk-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
198
- glaip_sdk-0.7.0.dist-info/entry_points.txt,sha256=65vNPUggyYnVGhuw7RhNJ8Fp2jygTcX0yxJBcBY3iLU,48
199
- glaip_sdk-0.7.0.dist-info/top_level.txt,sha256=td7yXttiYX2s94-4wFhv-5KdT0rSZ-pnJRSire341hw,10
200
- glaip_sdk-0.7.0.dist-info/RECORD,,
203
+ glaip_sdk-0.7.1.dist-info/METADATA,sha256=Pb8KaBV3ypCfgVvXTZd8mWSP_MgdaqFFdUTEmgPal5I,8365
204
+ glaip_sdk-0.7.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
205
+ glaip_sdk-0.7.1.dist-info/entry_points.txt,sha256=65vNPUggyYnVGhuw7RhNJ8Fp2jygTcX0yxJBcBY3iLU,48
206
+ glaip_sdk-0.7.1.dist-info/top_level.txt,sha256=td7yXttiYX2s94-4wFhv-5KdT0rSZ-pnJRSire341hw,10
207
+ glaip_sdk-0.7.1.dist-info/RECORD,,