portacode 0.3.12.dev7__tar.gz → 0.3.12.dev9__tar.gz

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.
Files changed (43) hide show
  1. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/PKG-INFO +1 -1
  2. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/_version.py +2 -2
  3. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +79 -11
  4. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/session.py +116 -20
  5. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/terminal.py +3 -2
  6. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode.egg-info/PKG-INFO +1 -1
  7. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/.claude/agents/communication-manager.md +0 -0
  8. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/.claude/settings.local.json +0 -0
  9. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/.gitignore +0 -0
  10. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/.gitmodules +0 -0
  11. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/LICENSE +0 -0
  12. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/MANIFEST.in +0 -0
  13. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/Makefile +0 -0
  14. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/README.md +0 -0
  15. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/backup.sh +0 -0
  16. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/docker-compose.yaml +0 -0
  17. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/README.md +0 -0
  18. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/__init__.py +0 -0
  19. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/__main__.py +0 -0
  20. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/cli.py +0 -0
  21. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/README.md +0 -0
  22. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/__init__.py +0 -0
  23. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/client.py +0 -0
  24. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/README.md +0 -0
  25. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/__init__.py +0 -0
  26. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/base.py +0 -0
  27. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/file_handlers.py +0 -0
  28. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/registry.py +0 -0
  29. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/system_handlers.py +0 -0
  30. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/terminal_handlers.py +0 -0
  31. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/multiplex.py +0 -0
  32. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/data.py +0 -0
  33. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/keypair.py +0 -0
  34. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/service.py +0 -0
  35. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode.egg-info/SOURCES.txt +0 -0
  36. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode.egg-info/dependency_links.txt +0 -0
  37. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode.egg-info/entry_points.txt +0 -0
  38. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode.egg-info/requires.txt +0 -0
  39. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode.egg-info/top_level.txt +0 -0
  40. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/pyproject.toml +0 -0
  41. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/restore.sh +0 -0
  42. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/setup.cfg +0 -0
  43. {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portacode
3
- Version: 0.3.12.dev7
3
+ Version: 0.3.12.dev9
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.3.12.dev7'
21
- __version_tuple__ = version_tuple = (0, 3, 12, 'dev7')
20
+ __version__ = version = '0.3.12.dev9'
21
+ __version_tuple__ = version_tuple = (0, 3, 12, 'dev9')
@@ -26,6 +26,7 @@ This document outlines the WebSocket communication protocol between the Portacod
26
26
  - [`error`](#error)
27
27
  - [Terminal Events](#terminal-events)
28
28
  - [`terminal_started`](#terminal_started)
29
+ - [`terminal_data`](#terminal_data)
29
30
  - [`terminal_exit`](#terminal_exit)
30
31
  - [`terminal_send_ack`](#terminal_send_ack)
31
32
  - [`terminal_stopped`](#terminal_stopped)
@@ -79,14 +80,14 @@ Actions are messages sent from the server to the device, placed within the `payl
79
80
  "payload": {
80
81
  "arg1": "value1",
81
82
  "...": "..."
82
- },
83
- "reply_channel": "<channel_name>"
83
+ }
84
84
  }
85
85
  ```
86
86
 
87
87
  * `command` (string, mandatory): The name of the action to be executed (e.g., `terminal_start`).
88
88
  * `payload` (object, mandatory): An object containing the specific arguments for the action.
89
- * `reply_channel` (string, optional): **DEPRECATED** - A channel name for backward compatibility. Modern implementations should use the `client_sessions` mechanism instead.
89
+
90
+ **Note**: Actions do not require targeting information - responses are automatically routed using the client session management system.
90
91
 
91
92
  ### `terminal_start`
92
93
 
@@ -248,13 +249,22 @@ Events are messages sent from the device to the server, placed within the `paylo
248
249
  {
249
250
  "event": "<event_name>",
250
251
  // Event-specific fields...
251
- "reply_channel": "<channel_name>"
252
+ "device_id": 123,
253
+ "project_id": "<project_uuid>",
254
+ "client_sessions": ["channel.abc123", "channel.def456"]
252
255
  }
253
256
  ```
254
257
 
258
+ **Standard Fields (automatically added by the system):**
259
+
255
260
  * `event` (string, mandatory): The name of the event being sent (e.g., `terminal_started`).
256
- * <a name="reply_channel"></a>`reply_channel` (string, optional): **DEPRECATED** - For backward compatibility only. Modern events include `client_sessions` array for targeting.
257
- * `client_sessions` (array, optional): Array of client session channel names that should receive this event. This is the modern way to target specific connected clients.
261
+ * `device_id` (integer, mandatory): The ID of the authenticated device that generated this event. **Added by the server based on the authenticated connection for security** - devices cannot self-identify.
262
+ * `project_id` (string, optional): The project UUID associated with this event, used for project-scoped filtering. Sent by the device.
263
+ * `client_sessions` (array, optional): Array of client session channel names that should receive this event. **Added by the device's terminal manager** based on interested client sessions. When present, the event is sent only to these specific sessions. When absent, the event is broadcast to all sessions for the device owner.
264
+
265
+ **Event-Specific Fields:**
266
+
267
+ Each event type includes additional fields specific to its purpose, documented in the individual event sections below.
258
268
 
259
269
  ### <a name="error"></a>`error`
260
270
 
@@ -274,6 +284,41 @@ Confirms that a new terminal session has been successfully started. Triggered by
274
284
  * `channel` (string, mandatory): The channel name for terminal I/O.
275
285
  * `project_id` (string, optional): The project ID associated with the terminal.
276
286
 
287
+ ### <a name="terminal_data"></a>`terminal_data`
288
+
289
+ Streams real-time terminal output data from a running terminal session to connected clients. This event is automatically generated whenever the terminal process produces output (stdout/stderr). Generated by [`TerminalSession`](./session.py) through the terminal manager's session-aware messaging system.
290
+
291
+ **Event-Specific Fields:**
292
+
293
+ * `channel` (string, mandatory): The terminal UUID identifying which terminal session produced this output. This matches the `terminal_id` from the corresponding `terminal_started` event.
294
+ * `data` (string, mandatory): The raw terminal output data. **See detailed description below.**
295
+
296
+ **The `data` Field - Detailed Specification:**
297
+
298
+ The `data` field contains the exact bytes output by the terminal process, decoded as a UTF-8 string with error handling:
299
+
300
+ * **Encoding**: UTF-8 with `errors="ignore"` - invalid UTF-8 sequences are silently dropped
301
+ * **Content**: Raw terminal output including:
302
+ - Regular command output (stdout)
303
+ - Error messages (stderr) - combined with stdout in PTY mode
304
+ - ANSI escape sequences for colors, cursor positioning, screen clearing, etc.
305
+ - Control characters (newlines, tabs, backspace, etc.)
306
+ - Shell prompts and interactive application output
307
+ * **Buffering**: Data is read in 1024-byte chunks from the terminal process and sent immediately (no line buffering)
308
+ * **Binary Safety**: Binary data is handled via UTF-8 decoding with error tolerance
309
+ * **Size**: Individual chunks are typically ≤1024 characters, but can be smaller for real-time responsiveness
310
+
311
+ **Examples of `data` content:**
312
+ ```
313
+ "Hello, World!\n" // Simple command output
314
+ "\u001b[32mSuccess\u001b[0m\n" // ANSI colored text
315
+ "user@host:~/project$ " // Shell prompt
316
+ "\u001b[2J\u001b[H" // Clear screen escape sequence
317
+ "Progress: [████████████████████] 100%\r" // Progress bar with carriage return
318
+ ```
319
+
320
+ **Security Note**: The `device_id` field is automatically injected by the server based on the authenticated connection - the device cannot and should not specify its own ID. The `project_id` and `client_sessions` fields are added by the device's terminal manager for proper routing and filtering.
321
+
277
322
  ### <a name="terminal_exit"></a>`terminal_exit`
278
323
 
279
324
  Notifies the server that a terminal session has terminated. This can be due to the process ending or the session being stopped. Handled by [`terminal_start`](./terminal_handlers.py).
@@ -402,11 +447,33 @@ This event carries no additional fields.
402
447
 
403
448
  ### Terminal Data
404
449
 
405
- ### <a name="terminal_data"></a>Terminal I/O Data
450
+ ### Terminal I/O Data Formats
451
+
452
+ Terminal I/O data can be sent in two formats depending on the implementation:
453
+
454
+ #### Modern Format (Recommended)
455
+
456
+ Terminal data is sent as a proper [`terminal_data`](#terminal_data) event on the control channel (channel 0) with client session targeting support:
457
+
458
+ ```json
459
+ {
460
+ "channel": 0,
461
+ "payload": {
462
+ "event": "terminal_data",
463
+ "channel": "<terminal_uuid>",
464
+ "data": "<terminal_output_string>",
465
+ "device_id": 123,
466
+ "project_id": "<project_uuid>",
467
+ "client_sessions": ["channel.abc123", "channel.def456"]
468
+ }
469
+ }
470
+ ```
471
+
472
+ This format follows the standard event structure with automatic system field injection (device_id, project_id, client_sessions) for proper routing and security.
406
473
 
407
- Terminal input/output data is sent directly on terminal channels (not on the control channel). Each terminal session has its own dedicated channel identified by the terminal's UUID.
474
+ #### Legacy Format (Deprecated)
408
475
 
409
- **Terminal Data Format:**
476
+ Terminal data sent directly on terminal channels (not on the control channel):
410
477
 
411
478
  ```json
412
479
  {
@@ -416,8 +483,9 @@ Terminal input/output data is sent directly on terminal channels (not on the con
416
483
  ```
417
484
 
418
485
  * Terminal output is sent as raw string data in the payload
419
- * Input to terminals is sent the same way but in the opposite direction
420
- * No event wrapper is used for terminal I/O data
486
+ * Input to terminals is sent the same way but in the opposite direction
487
+ * No event wrapper or client targeting is used
488
+ * This format broadcasts to all sessions for the device owner
421
489
 
422
490
  ### Server-Side Events
423
491
 
@@ -4,6 +4,7 @@ import asyncio
4
4
  import logging
5
5
  import os
6
6
  import sys
7
+ import time
7
8
  import uuid
8
9
  from asyncio.subprocess import Process
9
10
  from pathlib import Path
@@ -13,6 +14,10 @@ from collections import deque
13
14
  if TYPE_CHECKING:
14
15
  from ..multiplex import Channel
15
16
 
17
+ # Terminal data rate limiting configuration
18
+ TERMINAL_DATA_RATE_LIMIT_MS = 200 # Minimum time between terminal_data events (milliseconds)
19
+ TERMINAL_DATA_MAX_WAIT_MS = 1000 # Maximum time to wait before sending accumulated data (milliseconds)
20
+
16
21
  logger = logging.getLogger(__name__)
17
22
 
18
23
  _IS_WINDOWS = sys.platform.startswith("win")
@@ -36,13 +41,19 @@ def _build_child_env() -> Dict[str, str]:
36
41
  class TerminalSession:
37
42
  """Represents a local shell subprocess bound to a mux channel."""
38
43
 
39
- def __init__(self, session_id: str, proc: Process, channel: "Channel", project_id: Optional[str] = None):
44
+ def __init__(self, session_id: str, proc: Process, channel: "Channel", project_id: Optional[str] = None, terminal_manager: Optional["TerminalManager"] = None):
40
45
  self.id = session_id
41
46
  self.proc = proc
42
47
  self.channel = channel
43
48
  self.project_id = project_id
49
+ self.terminal_manager = terminal_manager
44
50
  self._reader_task: Optional[asyncio.Task[None]] = None
45
51
  self._buffer: deque[str] = deque(maxlen=400)
52
+
53
+ # Rate limiting for terminal_data events
54
+ self._last_send_time: float = 0
55
+ self._pending_data: str = ""
56
+ self._debounce_task: Optional[asyncio.Task[None]] = None
46
57
 
47
58
  async def start_io_forwarding(self) -> None:
48
59
  """Spawn background task that copies stdout/stderr to the channel."""
@@ -56,13 +67,9 @@ class TerminalSession:
56
67
  break
57
68
  text = data.decode(errors="ignore")
58
69
  logging.getLogger("portacode.terminal").debug(f"[MUX] Terminal {self.id} output: {text!r}")
59
- self._buffer.append(text)
60
- try:
61
- await self.channel.send(text)
62
- except Exception as exc:
63
- logger.warning("Failed to forward terminal output: %s", exc)
64
- await asyncio.sleep(0.5)
65
- continue
70
+
71
+ # Use rate-limited sending instead of immediate sending
72
+ await self._handle_terminal_data(text)
66
73
  finally:
67
74
  if self.proc and self.proc.returncode is None:
68
75
  pass # Keep alive across reconnects
@@ -118,6 +125,20 @@ class TerminalSession:
118
125
  except asyncio.CancelledError:
119
126
  pass
120
127
 
128
+ # Cancel and flush any pending terminal data
129
+ if self._debounce_task and not self._debounce_task.done():
130
+ logger.info("session.stop: Cancelling debounce task for session %s", self.id)
131
+ self._debounce_task.cancel()
132
+ try:
133
+ await self._debounce_task
134
+ except asyncio.CancelledError:
135
+ pass
136
+
137
+ # Send any remaining pending data
138
+ if self._pending_data:
139
+ logger.info("session.stop: Flushing pending terminal data for session %s", self.id)
140
+ await self._flush_pending_data()
141
+
121
142
  # Wait for process to exit
122
143
  if self.proc.returncode is None:
123
144
  logger.info("session.stop: Waiting for process to exit for session %s", self.id)
@@ -132,6 +153,70 @@ class TerminalSession:
132
153
  logger.exception("session.stop: Error stopping session %s: %s", self.id, exc)
133
154
  raise
134
155
 
156
+ async def _send_terminal_data_now(self, data: str) -> None:
157
+ """Send terminal data immediately and update last send time."""
158
+ self._last_send_time = time.time()
159
+
160
+ # Add to buffer for snapshots
161
+ self._buffer.append(data)
162
+
163
+ try:
164
+ # Send terminal data via control channel with client session targeting
165
+ if self.terminal_manager:
166
+ await self.terminal_manager._send_session_aware({
167
+ "event": "terminal_data",
168
+ "channel": self.id,
169
+ "data": data,
170
+ "project_id": self.project_id
171
+ }, project_id=self.project_id)
172
+ else:
173
+ # Fallback to raw channel for backward compatibility
174
+ await self.channel.send(data)
175
+ except Exception as exc:
176
+ logger.warning("Failed to forward terminal output: %s", exc)
177
+
178
+ async def _flush_pending_data(self) -> None:
179
+ """Send accumulated pending data and reset pending buffer."""
180
+ if self._pending_data:
181
+ data_to_send = self._pending_data
182
+ self._pending_data = ""
183
+ await self._send_terminal_data_now(data_to_send)
184
+
185
+ # Clear the debounce task
186
+ self._debounce_task = None
187
+
188
+ async def _handle_terminal_data(self, data: str) -> None:
189
+ """Handle new terminal data with rate limiting and debouncing."""
190
+ current_time = time.time()
191
+ time_since_last_send = (current_time - self._last_send_time) * 1000 # Convert to milliseconds
192
+
193
+ # Add new data to pending buffer
194
+ self._pending_data += data
195
+
196
+ # Cancel existing debounce task if any
197
+ if self._debounce_task and not self._debounce_task.done():
198
+ self._debounce_task.cancel()
199
+
200
+ if time_since_last_send >= TERMINAL_DATA_RATE_LIMIT_MS:
201
+ # Enough time has passed, send immediately
202
+ await self._flush_pending_data()
203
+ else:
204
+ # Too soon, set up debounce timer
205
+ async def _debounce_timer():
206
+ try:
207
+ # Wait for either the rate limit period or max wait time
208
+ wait_time = min(
209
+ (TERMINAL_DATA_RATE_LIMIT_MS - time_since_last_send) / 1000,
210
+ TERMINAL_DATA_MAX_WAIT_MS / 1000
211
+ )
212
+ await asyncio.sleep(wait_time)
213
+ await self._flush_pending_data()
214
+ except asyncio.CancelledError:
215
+ # Timer was cancelled, another data event came in
216
+ pass
217
+
218
+ self._debounce_task = asyncio.create_task(_debounce_timer())
219
+
135
220
  def snapshot_buffer(self) -> str:
136
221
  """Return concatenated last buffer contents suitable for UI."""
137
222
  return "".join(self._buffer)
@@ -147,7 +232,7 @@ class TerminalSession:
147
232
  class WindowsTerminalSession(TerminalSession):
148
233
  """Terminal session backed by a Windows ConPTY."""
149
234
 
150
- def __init__(self, session_id: str, pty, channel: "Channel", project_id: Optional[str] = None):
235
+ def __init__(self, session_id: str, pty, channel: "Channel", project_id: Optional[str] = None, terminal_manager: Optional["TerminalManager"] = None):
151
236
  # Create a proxy for the PTY process
152
237
  class _WinPTYProxy:
153
238
  def __init__(self, pty):
@@ -165,7 +250,7 @@ class WindowsTerminalSession(TerminalSession):
165
250
  loop = asyncio.get_running_loop()
166
251
  await loop.run_in_executor(None, self._pty.wait)
167
252
 
168
- super().__init__(session_id, _WinPTYProxy(pty), channel, project_id)
253
+ super().__init__(session_id, _WinPTYProxy(pty), channel, project_id, terminal_manager)
169
254
  self._pty = pty
170
255
 
171
256
  async def start_io_forwarding(self) -> None:
@@ -186,13 +271,9 @@ class WindowsTerminalSession(TerminalSession):
186
271
  else:
187
272
  text = data
188
273
  logging.getLogger("portacode.terminal").debug(f"[MUX] Terminal {self.id} output: {text!r}")
189
- self._buffer.append(text)
190
- try:
191
- await self.channel.send(text)
192
- except Exception as exc:
193
- logger.warning("Failed to forward terminal output: %s", exc)
194
- await asyncio.sleep(0.5)
195
- continue
274
+
275
+ # Use rate-limited sending instead of immediate sending
276
+ await self._handle_terminal_data(text)
196
277
  finally:
197
278
  if self._pty and self._pty.isalive():
198
279
  self._pty.kill()
@@ -237,6 +318,20 @@ class WindowsTerminalSession(TerminalSession):
237
318
  except asyncio.CancelledError:
238
319
  pass
239
320
 
321
+ # Cancel and flush any pending terminal data
322
+ if self._debounce_task and not self._debounce_task.done():
323
+ logger.info("session.stop: Cancelling debounce task for Windows session %s", self.id)
324
+ self._debounce_task.cancel()
325
+ try:
326
+ await self._debounce_task
327
+ except asyncio.CancelledError:
328
+ pass
329
+
330
+ # Send any remaining pending data
331
+ if self._pending_data:
332
+ logger.info("session.stop: Flushing pending terminal data for Windows session %s", self.id)
333
+ await self._flush_pending_data()
334
+
240
335
  logger.info("session.stop: Successfully stopped Windows session %s", self.id)
241
336
 
242
337
  except Exception as exc:
@@ -247,8 +342,9 @@ class WindowsTerminalSession(TerminalSession):
247
342
  class SessionManager:
248
343
  """Manages terminal sessions."""
249
344
 
250
- def __init__(self, mux):
345
+ def __init__(self, mux, terminal_manager=None):
251
346
  self.mux = mux
347
+ self.terminal_manager = terminal_manager
252
348
  self._sessions: Dict[str, TerminalSession] = {}
253
349
 
254
350
  def _allocate_channel_id(self) -> str:
@@ -287,7 +383,7 @@ class SessionManager:
287
383
  raise RuntimeError("pywinpty not installed on client")
288
384
 
289
385
  pty_proc = PtyProcess.spawn(shell, cwd=cwd or None, env=_build_child_env())
290
- session = WindowsTerminalSession(term_id, pty_proc, channel, project_id)
386
+ session = WindowsTerminalSession(term_id, pty_proc, channel, project_id, self.terminal_manager)
291
387
  else:
292
388
  # Unix: try real PTY for proper TTY semantics
293
389
  try:
@@ -320,7 +416,7 @@ class SessionManager:
320
416
  cwd=cwd,
321
417
  env=_build_child_env(),
322
418
  )
323
- session = TerminalSession(term_id, proc, channel, project_id)
419
+ session = TerminalSession(term_id, proc, channel, project_id, self.terminal_manager)
324
420
 
325
421
  self._sessions[term_id] = session
326
422
  await session.start_io_forwarding()
@@ -167,11 +167,12 @@ class TerminalManager:
167
167
 
168
168
  # Only create new session manager on initial setup, preserve existing one on reconnection
169
169
  if is_initial or self._session_manager is None:
170
- self._session_manager = SessionManager(mux)
170
+ self._session_manager = SessionManager(mux, terminal_manager=self)
171
171
  logger.info("Created new SessionManager")
172
172
  else:
173
- # Update existing session manager's mux reference
173
+ # Update existing session manager's mux and terminal_manager references
174
174
  self._session_manager.mux = mux
175
+ self._session_manager.terminal_manager = self
175
176
  logger.info("Preserved existing SessionManager with %d sessions", len(self._session_manager._sessions))
176
177
 
177
178
  # Create context for handlers
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portacode
3
- Version: 0.3.12.dev7
3
+ Version: 0.3.12.dev9
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
File without changes