mycode-coding-agent 0.1.0__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.
Files changed (121) hide show
  1. mycode/__init__.py +0 -0
  2. mycode/adapters/__init__.py +21 -0
  3. mycode/adapters/jsonl.py +692 -0
  4. mycode/agent/__init__.py +25 -0
  5. mycode/agent/events.py +111 -0
  6. mycode/agent/outcome.py +103 -0
  7. mycode/agent/progress.py +373 -0
  8. mycode/agent/runner.py +1481 -0
  9. mycode/application/__init__.py +38 -0
  10. mycode/application/agent_session.py +367 -0
  11. mycode/application/events.py +59 -0
  12. mycode/application/runtime.py +211 -0
  13. mycode/application/sessions.py +180 -0
  14. mycode/cli.py +840 -0
  15. mycode/config.py +355 -0
  16. mycode/context/__init__.py +1 -0
  17. mycode/context/artifacts.py +672 -0
  18. mycode/context/budget.py +752 -0
  19. mycode/context/builder.py +112 -0
  20. mycode/context/compact.py +795 -0
  21. mycode/context/tool_result_format.py +199 -0
  22. mycode/context/tool_result_retention.py +261 -0
  23. mycode/conversation.py +78 -0
  24. mycode/error_handling.py +481 -0
  25. mycode/event_format.py +147 -0
  26. mycode/instructions.py +285 -0
  27. mycode/llm.py +771 -0
  28. mycode/mcp/__init__.py +41 -0
  29. mycode/mcp/client.py +44 -0
  30. mycode/mcp/config.py +207 -0
  31. mycode/mcp/errors.py +302 -0
  32. mycode/mcp/manager.py +339 -0
  33. mycode/mcp/models.py +20 -0
  34. mycode/mcp/result_adapter.py +58 -0
  35. mycode/mcp/tool_adapter.py +145 -0
  36. mycode/mcp/trust.py +313 -0
  37. mycode/memory.py +570 -0
  38. mycode/memory_context.py +245 -0
  39. mycode/messages.py +63 -0
  40. mycode/observability.py +28 -0
  41. mycode/permissions.py +262 -0
  42. mycode/persistence/__init__.py +1 -0
  43. mycode/persistence/filesystem.py +291 -0
  44. mycode/persistence/project_storage.py +208 -0
  45. mycode/persistence/session_lock.py +138 -0
  46. mycode/persistence/session_store.py +503 -0
  47. mycode/presentation/__init__.py +1 -0
  48. mycode/presentation/cli/__init__.py +14 -0
  49. mycode/presentation/cli/confirmer.py +116 -0
  50. mycode/presentation/cli/mcp_trust.py +61 -0
  51. mycode/presentation/cli/presenter.py +320 -0
  52. mycode/presentation/cli/session_menu.py +146 -0
  53. mycode/presentation/cli/subagent_observer.py +124 -0
  54. mycode/presentation/command_format.py +90 -0
  55. mycode/presentation/commands.py +95 -0
  56. mycode/presentation/tui/__init__.py +6 -0
  57. mycode/presentation/tui/app.py +1351 -0
  58. mycode/presentation/tui/interactions.py +253 -0
  59. mycode/presentation/tui/presenter.py +266 -0
  60. mycode/presentation/tui/screens.py +305 -0
  61. mycode/presentation/tui/widgets.py +214 -0
  62. mycode/project.py +22 -0
  63. mycode/prompts.py +181 -0
  64. mycode/reasoning.py +40 -0
  65. mycode/session.py +86 -0
  66. mycode/skills/__init__.py +27 -0
  67. mycode/skills/builtin/database-recovery/SKILL.md +138 -0
  68. mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
  69. mycode/skills/registry.py +295 -0
  70. mycode/skills/state.py +68 -0
  71. mycode/subagents/__init__.py +1 -0
  72. mycode/subagents/audit.py +212 -0
  73. mycode/subagents/concurrency.py +124 -0
  74. mycode/subagents/contracts.py +421 -0
  75. mycode/subagents/delegate.py +80 -0
  76. mycode/subagents/delegation.py +128 -0
  77. mycode/subagents/lifecycle.py +86 -0
  78. mycode/subagents/limits.py +7 -0
  79. mycode/subagents/observability.py +150 -0
  80. mycode/subagents/persistence.py +152 -0
  81. mycode/subagents/profiles.py +184 -0
  82. mycode/subagents/prompts.py +67 -0
  83. mycode/subagents/results.py +178 -0
  84. mycode/subagents/runtime.py +528 -0
  85. mycode/subagents/snapshots.py +211 -0
  86. mycode/subagents/tool_batch.py +260 -0
  87. mycode/tools/__init__.py +81 -0
  88. mycode/tools/base.py +222 -0
  89. mycode/tools/bounds.py +14 -0
  90. mycode/tools/command_executor.py +167 -0
  91. mycode/tools/command_output.py +166 -0
  92. mycode/tools/command_risk.py +596 -0
  93. mycode/tools/defaults.py +59 -0
  94. mycode/tools/edit_file.py +524 -0
  95. mycode/tools/file_mutation.py +30 -0
  96. mycode/tools/glob.py +247 -0
  97. mycode/tools/grep.py +324 -0
  98. mycode/tools/ignore.py +122 -0
  99. mycode/tools/inspect_changes.py +269 -0
  100. mycode/tools/load_skill.py +92 -0
  101. mycode/tools/memory.py +264 -0
  102. mycode/tools/path_permissions.py +78 -0
  103. mycode/tools/patterns.py +48 -0
  104. mycode/tools/permission_metadata.py +27 -0
  105. mycode/tools/process_tree.py +166 -0
  106. mycode/tools/read_file.py +242 -0
  107. mycode/tools/read_skill_resource.py +93 -0
  108. mycode/tools/registry.py +279 -0
  109. mycode/tools/run_command.py +237 -0
  110. mycode/tools/run_skill_script.py +206 -0
  111. mycode/tools/run_validation.py +107 -0
  112. mycode/tools/submit_result.py +93 -0
  113. mycode/tools/text.py +15 -0
  114. mycode/tools/validation_command.py +377 -0
  115. mycode/tools/workspace.py +33 -0
  116. mycode/tools/write_file.py +169 -0
  117. mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
  118. mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
  119. mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  120. mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  121. mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1351 @@
1
+ """Textual application orchestration for MyCode's presentation layer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from threading import Event, Lock
7
+ from uuid import uuid4
8
+
9
+ from textual import on
10
+ from textual.app import App
11
+ from textual.message import Message
12
+ from textual.widgets import Input, OptionList
13
+
14
+ from mycode.agent.outcome import AgentRunOutcome
15
+ from mycode.application.agent_session import (
16
+ AgentApplicationSession,
17
+ CompactResult,
18
+ ContextStatus,
19
+ start_agent_application_session,
20
+ )
21
+ from mycode.application.events import RuntimeEvent
22
+ from mycode.application.sessions import (
23
+ SessionStartRequest,
24
+ list_project_sessions,
25
+ )
26
+ from mycode.config import LLMConfig, load_llm_config
27
+ from mycode.error_handling import error_summary
28
+ from mycode.mcp import MCPConfig, MCPConfigError, load_mcp_config_layers
29
+ from mycode.mcp.trust import resolve_project_mcp_trust
30
+ from mycode.permissions import ConfirmationResult
31
+ from mycode.persistence.session_store import (
32
+ SessionInUseError,
33
+ SessionRecord,
34
+ SessionStore,
35
+ )
36
+ from mycode.presentation.commands import (
37
+ CommandParseError,
38
+ ParsedCommand,
39
+ parse_slash_command,
40
+ )
41
+ from mycode.presentation.command_format import (
42
+ format_command_help,
43
+ format_compact_result,
44
+ format_context_status,
45
+ format_session_list,
46
+ )
47
+ from mycode.presentation.tui.interactions import (
48
+ MCPTrustRequestMessage,
49
+ MCPTrustWarningMessage,
50
+ PermissionRequestMessage,
51
+ PermissionResponseHandle,
52
+ SubAgentResultMessage,
53
+ SubAgentStateMessage,
54
+ TuiConfirmer,
55
+ TuiMCPTrustConfirmer,
56
+ TuiSubAgentObserver,
57
+ )
58
+ from mycode.presentation.tui.presenter import TuiPresenter
59
+ from mycode.presentation.tui.screens import (
60
+ LoadingScreen,
61
+ MCPTrustScreen,
62
+ MainScreen,
63
+ PermissionScreen,
64
+ WelcomeScreen,
65
+ )
66
+ from mycode.presentation.tui.widgets import (
67
+ SPLASH_LOGO,
68
+ ConversationView,
69
+ HeaderBar,
70
+ StatusBar,
71
+ )
72
+ from mycode.project import ProjectIdentity
73
+ from mycode.tools.workspace import Workspace
74
+
75
+
76
+ HistoryItem = tuple[str, str]
77
+
78
+
79
+ class WelcomeMetadataMessage(Message):
80
+ def __init__(
81
+ self,
82
+ *,
83
+ generation: int,
84
+ llm_config: LLMConfig | None,
85
+ sessions: tuple[SessionRecord, ...],
86
+ error: str = "",
87
+ ) -> None:
88
+ self.generation = generation
89
+ self.llm_config = llm_config
90
+ self.sessions = sessions
91
+ self.error = error
92
+ super().__init__()
93
+
94
+
95
+ class StartupProgressMessage(Message):
96
+ def __init__(self, value: str) -> None:
97
+ self.value = value
98
+ super().__init__()
99
+
100
+
101
+ class StartupEventMessage(Message):
102
+ def __init__(self, event: RuntimeEvent) -> None:
103
+ self.event = event
104
+ super().__init__()
105
+
106
+
107
+ class StartupSucceededMessage(Message):
108
+ def __init__(
109
+ self,
110
+ application_session: AgentApplicationSession,
111
+ history: tuple[HistoryItem, ...],
112
+ *,
113
+ replace_current: bool = False,
114
+ ) -> None:
115
+ self.application_session = application_session
116
+ self.history = history
117
+ self.replace_current = replace_current
118
+ super().__init__()
119
+
120
+
121
+ class StartupFailedMessage(Message):
122
+ def __init__(
123
+ self,
124
+ error: BaseException,
125
+ *,
126
+ session_in_use: bool = False,
127
+ replace_current: bool = False,
128
+ ) -> None:
129
+ self.error = error
130
+ self.session_in_use = session_in_use
131
+ self.replace_current = replace_current
132
+ super().__init__()
133
+
134
+
135
+ class SessionListMessage(Message):
136
+ def __init__(
137
+ self,
138
+ sessions: tuple[SessionRecord, ...],
139
+ *,
140
+ error: str = "",
141
+ ) -> None:
142
+ self.sessions = sessions
143
+ self.error = error
144
+ super().__init__()
145
+
146
+
147
+ class ContextStatusMessage(Message):
148
+ def __init__(
149
+ self,
150
+ status: ContextStatus | None = None,
151
+ *,
152
+ error: str = "",
153
+ ) -> None:
154
+ self.status = status
155
+ self.error = error
156
+ super().__init__()
157
+
158
+
159
+ class CompactResultMessage(Message):
160
+ def __init__(
161
+ self,
162
+ result: CompactResult | None = None,
163
+ *,
164
+ error: str = "",
165
+ ) -> None:
166
+ self.result = result
167
+ self.error = error
168
+ super().__init__()
169
+
170
+
171
+ class TurnRuntimeEventMessage(Message):
172
+ def __init__(self, event: RuntimeEvent) -> None:
173
+ self.event = event
174
+ super().__init__()
175
+
176
+
177
+ class TurnCompletedMessage(Message):
178
+ def __init__(self, turn_id: str, outcome: AgentRunOutcome) -> None:
179
+ self.turn_id = turn_id
180
+ self.outcome = outcome
181
+ super().__init__()
182
+
183
+
184
+ class TurnFailedMessage(Message):
185
+ def __init__(
186
+ self,
187
+ turn_id: str,
188
+ error: BaseException,
189
+ interrupt_error: BaseException | None = None,
190
+ ) -> None:
191
+ self.turn_id = turn_id
192
+ self.error = error
193
+ self.interrupt_error = interrupt_error
194
+ super().__init__()
195
+
196
+
197
+ class MyCodeTuiApp(App[None]):
198
+ """Welcome, session startup, and the Textual Agent presentation shell."""
199
+
200
+ TITLE = "MyCode"
201
+ CSS = """
202
+ Screen {
203
+ background: $surface;
204
+ }
205
+
206
+ #welcome-content, #loading-content {
207
+ align: center middle;
208
+ height: 100%;
209
+ width: 100%;
210
+ }
211
+
212
+ #welcome-logo, #loading-logo {
213
+ color: $text;
214
+ text-align: center;
215
+ width: auto;
216
+ }
217
+
218
+ #welcome-meta, #loading-meta, #loading-session {
219
+ color: $text-muted;
220
+ margin-top: 1;
221
+ text-align: center;
222
+ width: auto;
223
+ }
224
+
225
+ #welcome-error {
226
+ color: $error;
227
+ margin-top: 1;
228
+ width: 60%;
229
+ }
230
+
231
+ #welcome-notice, #welcome-help, #loading-status {
232
+ color: $text-muted;
233
+ margin-top: 1;
234
+ text-align: center;
235
+ width: auto;
236
+ }
237
+
238
+ #session-options {
239
+ height: auto;
240
+ max-height: 12;
241
+ margin-top: 1;
242
+ min-width: 60;
243
+ width: 60%;
244
+ }
245
+
246
+ #loading-indicator {
247
+ margin-top: 1;
248
+ width: 5;
249
+ }
250
+
251
+ #mcp-trust-dialog {
252
+ align: center middle;
253
+ background: $panel;
254
+ border: round $primary;
255
+ height: auto;
256
+ max-height: 80%;
257
+ padding: 1 2;
258
+ width: 70%;
259
+ }
260
+
261
+ #mcp-trust-details {
262
+ height: auto;
263
+ max-height: 1fr;
264
+ overflow-y: auto;
265
+ }
266
+
267
+ #mcp-trust-actions {
268
+ align: center middle;
269
+ height: 3;
270
+ layout: horizontal;
271
+ margin-top: 1;
272
+ }
273
+
274
+ #mcp-trust-actions Button {
275
+ margin: 0 1;
276
+ }
277
+
278
+ #permission-dialog {
279
+ align: center middle;
280
+ background: $panel;
281
+ border: round $primary;
282
+ height: auto;
283
+ max-height: 80%;
284
+ padding: 1 2;
285
+ width: 70%;
286
+ }
287
+
288
+ #permission-details {
289
+ height: auto;
290
+ max-height: 1fr;
291
+ overflow-y: auto;
292
+ }
293
+
294
+ #permission-actions {
295
+ align: center middle;
296
+ height: auto;
297
+ layout: horizontal;
298
+ margin-top: 1;
299
+ }
300
+
301
+ #permission-actions Button {
302
+ margin: 0 1;
303
+ }
304
+
305
+ #header {
306
+ background: $panel;
307
+ color: $text;
308
+ height: 3;
309
+ padding: 1 2;
310
+ }
311
+
312
+ #conversation {
313
+ border: round $primary-darken-2;
314
+ height: 1fr;
315
+ margin: 1 2;
316
+ padding: 1 2;
317
+ }
318
+
319
+ #status {
320
+ background: $panel;
321
+ color: $text-muted;
322
+ height: 1;
323
+ padding: 0 2;
324
+ }
325
+
326
+ #prompt {
327
+ height: 3;
328
+ margin: 0 2 1 2;
329
+ }
330
+ """
331
+ BINDINGS = [("ctrl+c", "quit", "Quit")]
332
+
333
+ def __init__(
334
+ self,
335
+ *,
336
+ workspace_path: Path | None = None,
337
+ session_store: SessionStore | None = None,
338
+ llm_config: LLMConfig | None = None,
339
+ mcp_config: MCPConfig | None = None,
340
+ trust_file: str | Path | None = None,
341
+ # Kept as a compatibility keyword for callers of the 14.6.1 shell.
342
+ splash_duration: float | None = None,
343
+ **kwargs,
344
+ ) -> None:
345
+ super().__init__(**kwargs)
346
+ del splash_duration
347
+ workspace_root = Path.cwd() if workspace_path is None else workspace_path
348
+ self.workspace = Workspace(workspace_root)
349
+ self.project = ProjectIdentity.from_workspace(self.workspace.root)
350
+ self.session_store = session_store or SessionStore()
351
+ self._llm_config = llm_config
352
+ self._mcp_config_override = mcp_config
353
+ self._trust_file = trust_file
354
+ self._trust_confirmer = TuiMCPTrustConfirmer(self.post_message)
355
+ self._permission_confirmer = TuiConfirmer(self.post_message)
356
+ self._subagent_observer = TuiSubAgentObserver(self.post_message)
357
+
358
+ self.presenter: TuiPresenter | None = None
359
+ self._queued_events: list[RuntimeEvent] = []
360
+ self._session_records: tuple[SessionRecord, ...] = ()
361
+ self._metadata_ready = False
362
+ self._metadata_generation = 0
363
+ self._startup_active = False
364
+ self._shutdown_requested = Event()
365
+ self._session_lock = Lock()
366
+ self._application_session: AgentApplicationSession | None = None
367
+ self._pending_application_sessions: list[AgentApplicationSession] = []
368
+ self._turn_session_owned_by_worker = False
369
+ self._active_turn_id: str | None = None
370
+ self._command_worker_active = False
371
+ self._session_unusable = False
372
+ self._session_switch_active = False
373
+ self._session_switch_events: list[RuntimeEvent] = []
374
+ self._command_session_owned_by_worker: AgentApplicationSession | None = None
375
+ self._status_bar: StatusBar | None = None
376
+ self._permission_queue: list[PermissionResponseHandle] = []
377
+ self._active_permission_handle: PermissionResponseHandle | None = None
378
+ self._status_before_permission: str | None = None
379
+
380
+ @property
381
+ def workspace_label(self) -> str:
382
+ return self.workspace.root.name or str(self.workspace.root)
383
+
384
+ @property
385
+ def model_label(self) -> str:
386
+ return "—" if self._llm_config is None else self._llm_config.model
387
+
388
+ def on_mount(self) -> None:
389
+ self.push_screen(
390
+ WelcomeScreen(
391
+ workspace=self.workspace_label,
392
+ model=self.model_label,
393
+ enabled=False,
394
+ )
395
+ )
396
+ self._start_welcome_metadata_load(name="welcome-metadata")
397
+
398
+ def _start_welcome_metadata_load(self, *, name: str) -> None:
399
+ self._metadata_generation += 1
400
+ generation = self._metadata_generation
401
+ self.run_worker(
402
+ lambda: self._load_welcome_metadata(generation),
403
+ name=name,
404
+ group="startup-refresh",
405
+ thread=True,
406
+ exit_on_error=False,
407
+ )
408
+
409
+ def _load_welcome_metadata(self, generation: int) -> None:
410
+ config = self._llm_config
411
+ error = ""
412
+ try:
413
+ if config is None:
414
+ config = load_llm_config(workspace_root=self.workspace.root)
415
+ except Exception as caught: # noqa: BLE001 - UI boundary reports a summary
416
+ error = error_summary(caught)
417
+
418
+ try:
419
+ sessions = tuple(list_project_sessions(self.session_store, self.project))
420
+ except Exception as caught: # noqa: BLE001 - UI boundary reports a summary
421
+ sessions = ()
422
+ error = error or error_summary(caught)
423
+
424
+ self.post_message(
425
+ WelcomeMetadataMessage(
426
+ generation=generation,
427
+ llm_config=config,
428
+ sessions=sessions,
429
+ error=error,
430
+ )
431
+ )
432
+
433
+ @on(WelcomeMetadataMessage)
434
+ def _on_welcome_metadata(self, message: WelcomeMetadataMessage) -> None:
435
+ if message.generation != self._metadata_generation:
436
+ return
437
+ if message.llm_config is not None:
438
+ self._llm_config = message.llm_config
439
+ self._session_records = message.sessions
440
+ self._metadata_ready = True
441
+ if self._startup_active or not isinstance(self.screen, WelcomeScreen):
442
+ return
443
+ notice = ""
444
+ error = message.error
445
+ notice = self.screen.notice
446
+ error = self.screen.error or error
447
+ self._show_welcome(notice=notice, error=error)
448
+
449
+ @on(OptionList.OptionSelected)
450
+ def _on_session_selected(self, event: OptionList.OptionSelected) -> None:
451
+ if self._startup_active:
452
+ return
453
+ if not isinstance(self.screen, WelcomeScreen):
454
+ return
455
+ request = WelcomeScreen.request_from_option(event.option.id)
456
+ if request is None or self._llm_config is None:
457
+ self._show_welcome(error="LLM configuration is not available yet.")
458
+ return
459
+ self._begin_startup(request)
460
+
461
+ def _begin_startup(self, request: SessionStartRequest) -> None:
462
+ # Invalidate any refresh result that was started before this startup.
463
+ self._metadata_generation += 1
464
+ self._startup_active = True
465
+ self._session_switch_active = False
466
+ self._session_switch_events = []
467
+ self.switch_screen(
468
+ LoadingScreen(
469
+ workspace=self.workspace_label,
470
+ model=self.model_label,
471
+ request_label=_request_label(request),
472
+ )
473
+ )
474
+ self.run_worker(
475
+ lambda: self._startup_worker(request),
476
+ name="session-startup",
477
+ group="startup",
478
+ exclusive=True,
479
+ thread=True,
480
+ exit_on_error=False,
481
+ )
482
+
483
+ def _begin_session_switch(self, request: SessionStartRequest) -> None:
484
+ if self._startup_active or self._active_turn_id is not None:
485
+ return
486
+ if self._llm_config is None:
487
+ self._show_command_notice(
488
+ "Session commands are unavailable until runtime startup completes.",
489
+ level="warning",
490
+ )
491
+ return
492
+ self._metadata_generation += 1
493
+ self._startup_active = True
494
+ self._session_switch_active = True
495
+ self._session_switch_events = []
496
+ self._set_prompt_enabled(False)
497
+ self._set_status("Opening session…")
498
+ self.run_worker(
499
+ lambda: self._startup_worker(request, replace_current=True),
500
+ name="session-switch",
501
+ group="startup",
502
+ exclusive=True,
503
+ thread=True,
504
+ exit_on_error=False,
505
+ )
506
+
507
+ def _startup_worker(
508
+ self,
509
+ request: SessionStartRequest,
510
+ *,
511
+ replace_current: bool = False,
512
+ ) -> None:
513
+ application_session: AgentApplicationSession | None = None
514
+ try:
515
+ if self._shutdown_requested.is_set():
516
+ return
517
+ self._post_progress("Loading project configuration...")
518
+ config = self._llm_config
519
+ if config is None:
520
+ config = load_llm_config(workspace_root=self.workspace.root)
521
+ self._llm_config = config
522
+
523
+ self._post_progress("Checking MCP trust...")
524
+ if self._mcp_config_override is None:
525
+ try:
526
+ loaded_mcp = load_mcp_config_layers(
527
+ workspace_root=self.workspace.root
528
+ )
529
+ trust_resolution = resolve_project_mcp_trust(
530
+ loaded_mcp,
531
+ self.project,
532
+ confirmer=self._trust_confirmer,
533
+ trust_file=self._trust_file,
534
+ )
535
+ effective_mcp_config = trust_resolution.config
536
+ except MCPConfigError as caught:
537
+ self._post_progress(f"MCP config unavailable: {error_summary(caught)}")
538
+ effective_mcp_config = MCPConfig()
539
+ else:
540
+ effective_mcp_config = self._mcp_config_override
541
+
542
+ self._post_progress("Connecting MCP...")
543
+ self._post_progress("Starting runtime...")
544
+ if self._shutdown_requested.is_set():
545
+ return
546
+ application_session = start_agent_application_session(
547
+ self.session_store,
548
+ self.project,
549
+ request=request,
550
+ mcp_config=effective_mcp_config,
551
+ confirmer=self._permission_confirmer,
552
+ external_observer=self._subagent_observer,
553
+ llm_config=config,
554
+ )
555
+ if not self._register_pending_application_session(application_session):
556
+ return
557
+ history = _visible_history(
558
+ application_session.active_project_session.load_history()
559
+ )
560
+
561
+ for event in application_session.startup_events():
562
+ self.post_message(StartupEventMessage(event))
563
+ self.post_message(
564
+ StartupSucceededMessage(
565
+ application_session,
566
+ history,
567
+ replace_current=replace_current,
568
+ )
569
+ )
570
+ except SessionInUseError as caught:
571
+ if application_session is not None:
572
+ self._cleanup_startup_session(application_session)
573
+ self.post_message(
574
+ StartupFailedMessage(
575
+ caught,
576
+ session_in_use=True,
577
+ replace_current=replace_current,
578
+ )
579
+ )
580
+ except Exception as caught: # noqa: BLE001 - worker boundary returns to Welcome
581
+ if application_session is not None:
582
+ self._cleanup_startup_session(application_session)
583
+ self.post_message(
584
+ StartupFailedMessage(caught, replace_current=replace_current)
585
+ )
586
+
587
+ def _register_pending_application_session(
588
+ self,
589
+ application_session: AgentApplicationSession,
590
+ ) -> bool:
591
+ with self._session_lock:
592
+ if self._shutdown_requested.is_set():
593
+ should_close = True
594
+ else:
595
+ self._pending_application_sessions.append(application_session)
596
+ should_close = False
597
+ if should_close:
598
+ application_session.close()
599
+ return False
600
+ return True
601
+
602
+ def _cleanup_startup_session(
603
+ self,
604
+ application_session: AgentApplicationSession,
605
+ ) -> None:
606
+ with self._session_lock:
607
+ try:
608
+ self._pending_application_sessions.remove(application_session)
609
+ except ValueError:
610
+ claimed = False
611
+ else:
612
+ claimed = True
613
+ if claimed:
614
+ application_session.close()
615
+
616
+ def _post_progress(self, value: str) -> None:
617
+ self.post_message(StartupProgressMessage(value))
618
+
619
+ @on(StartupProgressMessage)
620
+ def _on_startup_progress(self, message: StartupProgressMessage) -> None:
621
+ if isinstance(self.screen, LoadingScreen):
622
+ self.screen.set_progress(message.value)
623
+ elif self._session_switch_active and isinstance(self.screen, MainScreen):
624
+ self._set_status(message.value)
625
+
626
+ @on(MCPTrustRequestMessage)
627
+ def _on_mcp_trust_request(self, message: MCPTrustRequestMessage) -> None:
628
+ if self._shutdown_requested.is_set():
629
+ message.handle.resolve(False)
630
+ return
631
+ self.push_screen(
632
+ MCPTrustScreen(message.handle.request),
633
+ lambda approved: message.handle.resolve(bool(approved)),
634
+ )
635
+
636
+ @on(MCPTrustWarningMessage)
637
+ def _on_mcp_trust_warning(self, message: MCPTrustWarningMessage) -> None:
638
+ self._post_progress(f"Warning: {message.warning.message}")
639
+
640
+ @on(PermissionRequestMessage)
641
+ def _on_permission_request(self, message: PermissionRequestMessage) -> None:
642
+ if self._shutdown_requested.is_set() or self._session_unusable:
643
+ message.handle.resolve(
644
+ ConfirmationResult.rejected(
645
+ message="Permission confirmation unavailable."
646
+ )
647
+ )
648
+ return
649
+ self._permission_queue.append(message.handle)
650
+ self._maybe_show_next_permission()
651
+
652
+ def _maybe_show_next_permission(self) -> None:
653
+ if self._active_permission_handle is not None:
654
+ return
655
+ if self._shutdown_requested.is_set() or self._session_unusable:
656
+ self._drain_permission_queue_rejected()
657
+ return
658
+ if not self._permission_queue:
659
+ self._restore_status_after_permission()
660
+ return
661
+ handle = self._permission_queue.pop(0)
662
+ self._active_permission_handle = handle
663
+ if self._status_before_permission is None:
664
+ self._status_before_permission = self._current_status_text()
665
+ self._set_status("Waiting for permission…")
666
+ self.push_screen(
667
+ PermissionScreen(handle.request),
668
+ self._resolve_active_permission,
669
+ )
670
+
671
+ def _resolve_active_permission(self, result: ConfirmationResult) -> None:
672
+ handle = self._active_permission_handle
673
+ self._active_permission_handle = None
674
+ if handle is not None:
675
+ handle.resolve(result)
676
+ self._maybe_show_next_permission()
677
+
678
+ def _drain_permission_queue_rejected(self) -> None:
679
+ queue = self._permission_queue
680
+ self._permission_queue = []
681
+ active = self._active_permission_handle
682
+ self._active_permission_handle = None
683
+ for handle in (*queue, active):
684
+ if handle is None:
685
+ continue
686
+ handle.resolve(
687
+ ConfirmationResult.rejected(
688
+ message="Permission confirmation unavailable."
689
+ )
690
+ )
691
+ self._restore_status_after_permission()
692
+
693
+ def _restore_status_after_permission(self) -> None:
694
+ previous = self._status_before_permission
695
+ self._status_before_permission = None
696
+ if previous is not None:
697
+ self._set_status(previous)
698
+
699
+ def _current_status_text(self) -> str:
700
+ try:
701
+ if isinstance(self.screen, MainScreen):
702
+ return str(self.screen.query_one(StatusBar).render())
703
+ if self._status_bar is not None:
704
+ return str(self._status_bar.render())
705
+ except Exception: # noqa: BLE001 - snapshot is best-effort
706
+ pass
707
+ return "Ready"
708
+
709
+ def _set_status(self, value: str) -> None:
710
+ try:
711
+ if isinstance(self.screen, MainScreen):
712
+ self.screen.query_one(StatusBar).set_status(value)
713
+ return
714
+ if self._status_bar is not None:
715
+ self._status_bar.set_status(value)
716
+ except Exception: # noqa: BLE001 - status update is best-effort
717
+ pass
718
+
719
+ @on(StartupEventMessage)
720
+ def _on_startup_event(self, message: StartupEventMessage) -> None:
721
+ if self._session_switch_active:
722
+ self._session_switch_events.append(message.event)
723
+ return
724
+ self.present_event(message.event)
725
+
726
+ @on(SessionListMessage)
727
+ def _on_session_list(self, message: SessionListMessage) -> None:
728
+ if self._shutdown_requested.is_set() or not isinstance(self.screen, MainScreen):
729
+ return
730
+ self._command_worker_active = False
731
+ if message.error:
732
+ self._show_command_notice(
733
+ f"/sessions failed: {message.error}",
734
+ level="error",
735
+ )
736
+ self._set_prompt_enabled(True)
737
+ self._set_status("Ready")
738
+ self._focus_prompt()
739
+ return
740
+
741
+ self._session_records = message.sessions
742
+ self._show_sessions(message.sessions)
743
+ self._set_prompt_enabled(True)
744
+ self._set_status("Ready")
745
+ self._focus_prompt()
746
+
747
+ @on(ContextStatusMessage)
748
+ def _on_context_status(self, message: ContextStatusMessage) -> None:
749
+ if self._shutdown_requested.is_set() or not isinstance(self.screen, MainScreen):
750
+ return
751
+ self._command_worker_active = False
752
+ if message.error or message.status is None:
753
+ self._show_command_notice(
754
+ f"/context failed: {message.error or 'unavailable'}",
755
+ level="error",
756
+ )
757
+ else:
758
+ self._show_context_status(message.status)
759
+ self._set_prompt_enabled(True)
760
+ self._set_status("Ready")
761
+ self._focus_prompt()
762
+
763
+ @on(CompactResultMessage)
764
+ def _on_compact_result(self, message: CompactResultMessage) -> None:
765
+ if self._shutdown_requested.is_set() or not isinstance(self.screen, MainScreen):
766
+ return
767
+ self._command_worker_active = False
768
+ if message.error or message.result is None:
769
+ self._show_command_notice(
770
+ f"/compact failed: {message.error or 'unavailable'}",
771
+ level="error",
772
+ )
773
+ else:
774
+ result = message.result
775
+ level = "info"
776
+ if result.status == "skipped":
777
+ level = "warning"
778
+ elif result.status == "failed":
779
+ level = "error"
780
+ self._show_command_notice(
781
+ format_compact_result(result),
782
+ level=level,
783
+ )
784
+ self._set_prompt_enabled(True)
785
+ self._set_status("Ready")
786
+ self._focus_prompt()
787
+
788
+ @on(StartupSucceededMessage)
789
+ def _on_startup_succeeded(self, message: StartupSucceededMessage) -> None:
790
+ old_session: AgentApplicationSession | None = None
791
+ switch_events: tuple[RuntimeEvent, ...] = ()
792
+ with self._session_lock:
793
+ try:
794
+ self._pending_application_sessions.remove(message.application_session)
795
+ except ValueError:
796
+ claimed = False
797
+ else:
798
+ claimed = True
799
+ if not claimed:
800
+ should_close = False
801
+ elif self._shutdown_requested.is_set():
802
+ should_close = True
803
+ elif message.replace_current:
804
+ old_session = self._application_session
805
+ self._application_session = message.application_session
806
+ self._turn_session_owned_by_worker = False
807
+ self._session_unusable = False
808
+ self._session_switch_active = False
809
+ self._startup_active = False
810
+ switch_events = tuple(self._session_switch_events)
811
+ self._session_switch_events = []
812
+ should_close = False
813
+ else:
814
+ self._application_session = message.application_session
815
+ self._turn_session_owned_by_worker = False
816
+ should_close = False
817
+ if not claimed:
818
+ return
819
+ if should_close:
820
+ message.application_session.close()
821
+ return
822
+
823
+ if message.replace_current:
824
+ if old_session is not None:
825
+ try:
826
+ old_session.close()
827
+ except Exception as caught: # noqa: BLE001 - preserve new session
828
+ close_warning = error_summary(caught)
829
+ else:
830
+ close_warning = ""
831
+ else:
832
+ close_warning = ""
833
+ self._queued_events.extend(switch_events)
834
+ self.switch_screen(
835
+ MainScreen(
836
+ workspace=self.workspace_label,
837
+ model=self.model_label,
838
+ history=message.history,
839
+ )
840
+ )
841
+ if close_warning:
842
+ self._show_command_notice(
843
+ f"Old session cleanup warning: {close_warning}",
844
+ level="warning",
845
+ )
846
+ return
847
+
848
+ self._startup_active = False
849
+ self.switch_screen(
850
+ MainScreen(
851
+ workspace=self.workspace_label,
852
+ model=self.model_label,
853
+ history=message.history,
854
+ )
855
+ )
856
+
857
+ @on(StartupFailedMessage)
858
+ def _on_startup_failed(self, message: StartupFailedMessage) -> None:
859
+ if message.replace_current:
860
+ self._startup_active = False
861
+ self._session_switch_active = False
862
+ self._session_switch_events = []
863
+ if message.session_in_use:
864
+ detail = "selected session is currently in use"
865
+ else:
866
+ detail = error_summary(message.error)
867
+ self._show_command_notice(
868
+ f"Session switch failed: {detail}",
869
+ level="warning" if message.session_in_use else "error",
870
+ )
871
+ self._set_prompt_enabled(True)
872
+ self._set_status("Ready")
873
+ self._focus_prompt()
874
+ self._start_welcome_metadata_load(name="refresh-session-list")
875
+ return
876
+
877
+ self._startup_active = False
878
+ if message.session_in_use:
879
+ notice = "Selected session is currently in use. Session list refreshed."
880
+ error = ""
881
+ else:
882
+ notice = "Startup failed. Choose a session to try again."
883
+ error = f"{type(message.error).__name__}: {error_summary(message.error)}"
884
+ self._show_welcome(notice=notice, error=error)
885
+ self._start_welcome_metadata_load(name="refresh-session-list")
886
+
887
+ @on(TurnRuntimeEventMessage)
888
+ def _on_turn_runtime_event(self, message: TurnRuntimeEventMessage) -> None:
889
+ if message.event.turn_id != self._active_turn_id:
890
+ return
891
+ self.present_event(message.event)
892
+
893
+ @on(TurnCompletedMessage)
894
+ def _on_turn_completed(self, message: TurnCompletedMessage) -> None:
895
+ if message.turn_id != self._active_turn_id:
896
+ return
897
+ self._active_turn_id = None
898
+ self._set_prompt_enabled(True)
899
+ self._set_status("Ready")
900
+ self._refresh_session_title()
901
+ self._focus_prompt()
902
+
903
+ @on(SubAgentStateMessage)
904
+ def _on_subagent_state(self, message: SubAgentStateMessage) -> None:
905
+ if self._shutdown_requested.is_set() or self.presenter is None:
906
+ return
907
+ self.presenter.present_subagent_transition(message.transition)
908
+
909
+ @on(SubAgentResultMessage)
910
+ def _on_subagent_result(self, message: SubAgentResultMessage) -> None:
911
+ if self._shutdown_requested.is_set() or self.presenter is None:
912
+ return
913
+ self.presenter.present_subagent_result(message.execution)
914
+
915
+ def _refresh_session_title(self) -> None:
916
+ session = self._application_session
917
+ if session is None or self.presenter is None:
918
+ return
919
+ record = getattr(session.active_project_session, "record", None)
920
+ title = getattr(record, "title", None)
921
+ if not title:
922
+ return
923
+ self.presenter.header.set_session(str(title))
924
+
925
+ @on(TurnFailedMessage)
926
+ def _on_turn_failed(self, message: TurnFailedMessage) -> None:
927
+ if message.turn_id != self._active_turn_id:
928
+ return
929
+ self._active_turn_id = None
930
+ self._session_unusable = True
931
+ self._set_prompt_enabled(False)
932
+ if self.presenter is not None:
933
+ self.presenter.conversation.add_notice(
934
+ f"✗ fatal agent error: {error_summary(message.error)}",
935
+ level="error",
936
+ )
937
+ if message.interrupt_error is not None:
938
+ self.presenter.conversation.add_notice(
939
+ "✗ session interrupt failed: "
940
+ f"{error_summary(message.interrupt_error)}",
941
+ level="error",
942
+ )
943
+ self._set_status("Fatal error")
944
+
945
+ def _show_welcome(self, *, notice: str = "", error: str = "") -> None:
946
+ self.switch_screen(
947
+ WelcomeScreen(
948
+ workspace=self.workspace_label,
949
+ model=self.model_label,
950
+ sessions=self._session_records,
951
+ enabled=self._metadata_ready,
952
+ notice=notice,
953
+ error=error,
954
+ )
955
+ )
956
+
957
+ def _handle_command(self, command: ParsedCommand) -> None:
958
+ if command.name == "help":
959
+ self._show_command_help()
960
+ return
961
+ if command.name == "sessions":
962
+ self._begin_session_list()
963
+ return
964
+ if command.name == "new":
965
+ self._begin_session_switch(SessionStartRequest(mode="new"))
966
+ return
967
+ if command.name == "resume":
968
+ target_session_id = command.args[0]
969
+ if target_session_id == self._current_session_id():
970
+ self._show_command_notice(
971
+ "already using current session",
972
+ level="info",
973
+ )
974
+ return
975
+ self._begin_session_switch(
976
+ SessionStartRequest(mode="resume", session_id=target_session_id)
977
+ )
978
+ return
979
+ if command.name == "exit":
980
+ self.action_quit()
981
+ return
982
+ if command.name == "context":
983
+ self._begin_context_status()
984
+ return
985
+ if command.name == "compact":
986
+ self._begin_compact()
987
+ return
988
+ self._show_command_notice(
989
+ f"/{command.name} is not available yet.",
990
+ level="info",
991
+ )
992
+
993
+ def _show_command_help(self) -> None:
994
+ for line in format_command_help():
995
+ self._show_command_notice(line)
996
+
997
+ def _begin_session_list(self) -> None:
998
+ if self._startup_active or self._command_worker_active:
999
+ return
1000
+ self._command_worker_active = True
1001
+ self._set_prompt_enabled(False)
1002
+ self._set_status("Loading sessions…")
1003
+ self.run_worker(
1004
+ self._session_list_worker,
1005
+ name="session-list",
1006
+ group="session-command",
1007
+ exclusive=True,
1008
+ thread=True,
1009
+ exit_on_error=False,
1010
+ )
1011
+
1012
+ def _begin_context_status(self) -> None:
1013
+ if self._startup_active or self._command_worker_active:
1014
+ return
1015
+ self._command_worker_active = True
1016
+ self._set_prompt_enabled(False)
1017
+ self._set_status("Loading context…")
1018
+ self.run_worker(
1019
+ self._context_status_worker,
1020
+ name="context-status",
1021
+ group="session-command",
1022
+ exclusive=True,
1023
+ thread=True,
1024
+ exit_on_error=False,
1025
+ )
1026
+
1027
+ def _context_status_worker(self) -> None:
1028
+ application_session = self._claim_command_session_for_worker()
1029
+ if application_session is None:
1030
+ self.post_message(ContextStatusMessage(error="runtime unavailable"))
1031
+ return
1032
+ try:
1033
+ message = ContextStatusMessage(application_session.get_context_status())
1034
+ except Exception as caught: # noqa: BLE001 - UI boundary reports a summary
1035
+ message = ContextStatusMessage(error=error_summary(caught))
1036
+ if self._release_command_session_after_worker(application_session):
1037
+ application_session.close()
1038
+ return
1039
+ self.post_message(message)
1040
+
1041
+ def _begin_compact(self) -> None:
1042
+ if self._startup_active or self._command_worker_active:
1043
+ return
1044
+ self._command_worker_active = True
1045
+ self._set_prompt_enabled(False)
1046
+ self._set_status("Compacting…")
1047
+ self.run_worker(
1048
+ self._compact_worker,
1049
+ name="compact-context",
1050
+ group="session-command",
1051
+ exclusive=True,
1052
+ thread=True,
1053
+ exit_on_error=False,
1054
+ )
1055
+
1056
+ def _compact_worker(self) -> None:
1057
+ application_session = self._claim_command_session_for_worker()
1058
+ if application_session is None:
1059
+ self.post_message(CompactResultMessage(error="runtime unavailable"))
1060
+ return
1061
+ try:
1062
+ message = CompactResultMessage(application_session.compact_context())
1063
+ except Exception as caught: # noqa: BLE001 - UI boundary reports a summary
1064
+ message = CompactResultMessage(error=error_summary(caught))
1065
+ if self._release_command_session_after_worker(application_session):
1066
+ application_session.close()
1067
+ return
1068
+ self.post_message(message)
1069
+
1070
+ def _session_list_worker(self) -> None:
1071
+ try:
1072
+ sessions = tuple(list_project_sessions(self.session_store, self.project))
1073
+ message = SessionListMessage(sessions)
1074
+ except Exception as caught: # noqa: BLE001 - UI boundary reports a summary
1075
+ message = SessionListMessage((), error=error_summary(caught))
1076
+ self.post_message(message)
1077
+
1078
+ def _show_sessions(self, sessions: tuple[SessionRecord, ...]) -> None:
1079
+ current_session_id = self._current_session_id()
1080
+ for line in format_session_list(
1081
+ sessions,
1082
+ current_session_id=current_session_id or "",
1083
+ ):
1084
+ self._show_command_notice(line)
1085
+
1086
+ def _show_context_status(self, status: ContextStatus) -> None:
1087
+ for line in format_context_status(status):
1088
+ self._show_command_notice(line)
1089
+
1090
+ def _show_command_notice(self, content: str, *, level: str = "info") -> None:
1091
+ if self.presenter is not None:
1092
+ self.presenter.conversation.add_notice(
1093
+ f"command> {content}",
1094
+ level=level,
1095
+ )
1096
+
1097
+ def _current_session_id(self) -> str | None:
1098
+ with self._session_lock:
1099
+ session = self._application_session
1100
+ if session is None:
1101
+ return None
1102
+ record = getattr(session.active_project_session, "record", None)
1103
+ session_id = getattr(record, "id", None)
1104
+ return session_id if isinstance(session_id, str) else None
1105
+
1106
+ def _activate_main_screen(self, screen: MainScreen) -> None:
1107
+ conversation = screen.query_one(ConversationView)
1108
+ self._status_bar = screen.query_one(StatusBar)
1109
+ self.presenter = TuiPresenter(
1110
+ conversation=conversation,
1111
+ header=screen.query_one(HeaderBar),
1112
+ status=screen.query_one(StatusBar),
1113
+ )
1114
+ for role, content in screen.history:
1115
+ conversation.add_history_message(role, content)
1116
+ queued_events = self._queued_events
1117
+ self._queued_events = []
1118
+ for runtime_event in queued_events:
1119
+ self.presenter.present(runtime_event)
1120
+
1121
+ def present_event(self, event: RuntimeEvent) -> None:
1122
+ if self.presenter is None:
1123
+ self._queued_events.append(event)
1124
+ return
1125
+ self.presenter.present(event)
1126
+
1127
+ def submit_user_message(self, content: str) -> None:
1128
+ if not content.strip() or self.presenter is None or self._session_unusable:
1129
+ return
1130
+ try:
1131
+ command = parse_slash_command(content)
1132
+ except CommandParseError as error:
1133
+ self._show_command_notice(str(error), level="error")
1134
+ return
1135
+ if command is not None:
1136
+ if self._active_turn_id is not None:
1137
+ if command.name == "exit":
1138
+ self.action_quit()
1139
+ else:
1140
+ self._show_command_notice(
1141
+ "Agent turn is running; wait for completion before using commands.",
1142
+ level="warning",
1143
+ )
1144
+ return
1145
+ if command.name != "exit" and self._startup_active:
1146
+ self._show_command_notice(
1147
+ "Session startup is running; wait for it to finish.",
1148
+ level="warning",
1149
+ )
1150
+ return
1151
+ if command.name != "exit" and self._command_worker_active:
1152
+ self._show_command_notice(
1153
+ "Command is running; wait for it to finish.",
1154
+ level="warning",
1155
+ )
1156
+ return
1157
+ self._handle_command(command)
1158
+ return
1159
+ if (
1160
+ self._active_turn_id is not None
1161
+ or self._startup_active
1162
+ or self._command_worker_active
1163
+ ):
1164
+ return
1165
+ self.presenter.show_user_message(content)
1166
+ if self._application_session is None:
1167
+ return
1168
+ turn_id = uuid4().hex
1169
+ self._active_turn_id = turn_id
1170
+ self._set_prompt_enabled(False)
1171
+ self._set_status("Thinking…")
1172
+ self.run_worker(
1173
+ lambda: self._turn_worker(content, turn_id),
1174
+ name=f"agent-turn-{turn_id[:8]}",
1175
+ group="agent-turn",
1176
+ exclusive=True,
1177
+ thread=True,
1178
+ exit_on_error=False,
1179
+ )
1180
+
1181
+ def _turn_worker(self, content: str, turn_id: str) -> None:
1182
+ application_session = self._claim_turn_session_for_worker()
1183
+ if application_session is None:
1184
+ return
1185
+
1186
+ def handle_event(event: RuntimeEvent) -> None:
1187
+ self.post_message(TurnRuntimeEventMessage(event))
1188
+
1189
+ try:
1190
+ outcome = application_session.run_turn(
1191
+ content,
1192
+ turn_id=turn_id,
1193
+ event_handler=handle_event,
1194
+ )
1195
+ except Exception as caught: # noqa: BLE001 - worker boundary reports fatal turns
1196
+ interrupt_error: BaseException | None = None
1197
+ try:
1198
+ application_session.interrupt()
1199
+ except BaseException as interrupt_caught: # noqa: BLE001 - preserve UI recovery
1200
+ interrupt_error = interrupt_caught
1201
+ if self._release_turn_session_after_turn(application_session):
1202
+ application_session.close()
1203
+ return
1204
+ self.post_message(TurnFailedMessage(turn_id, caught, interrupt_error))
1205
+ return
1206
+ if self._release_turn_session_after_turn(application_session):
1207
+ application_session.close()
1208
+ return
1209
+ self.post_message(TurnCompletedMessage(turn_id, outcome))
1210
+
1211
+ def _claim_command_session_for_worker(
1212
+ self,
1213
+ ) -> AgentApplicationSession | None:
1214
+ with self._session_lock:
1215
+ if (
1216
+ self._shutdown_requested.is_set()
1217
+ or self._application_session is None
1218
+ or self._turn_session_owned_by_worker
1219
+ or self._command_session_owned_by_worker is not None
1220
+ ):
1221
+ return None
1222
+ self._command_session_owned_by_worker = self._application_session
1223
+ return self._application_session
1224
+
1225
+ def _release_command_session_after_worker(
1226
+ self,
1227
+ application_session: AgentApplicationSession,
1228
+ ) -> bool:
1229
+ """Release command ownership, returning whether the worker must close."""
1230
+ with self._session_lock:
1231
+ if self._command_session_owned_by_worker is not application_session:
1232
+ return False
1233
+ self._command_session_owned_by_worker = None
1234
+ if self._shutdown_requested.is_set():
1235
+ if self._application_session is application_session:
1236
+ self._application_session = None
1237
+ return True
1238
+ return False
1239
+
1240
+ def _claim_turn_session_for_worker(
1241
+ self,
1242
+ ) -> AgentApplicationSession | None:
1243
+ with self._session_lock:
1244
+ if (
1245
+ self._shutdown_requested.is_set()
1246
+ or self._application_session is None
1247
+ or self._turn_session_owned_by_worker
1248
+ ):
1249
+ return None
1250
+ self._turn_session_owned_by_worker = True
1251
+ return self._application_session
1252
+
1253
+ def _release_turn_session_after_turn(
1254
+ self,
1255
+ application_session: AgentApplicationSession,
1256
+ ) -> bool:
1257
+ """Release a worker-owned session, or keep cleanup in the worker.
1258
+
1259
+ Returns whether the worker must close the session. The lock makes the
1260
+ release-to-UI and shutdown decision one atomic ownership transition.
1261
+ """
1262
+ with self._session_lock:
1263
+ if (
1264
+ self._application_session is not application_session
1265
+ or not self._turn_session_owned_by_worker
1266
+ ):
1267
+ return False
1268
+ if self._shutdown_requested.is_set():
1269
+ self._application_session = None
1270
+ self._turn_session_owned_by_worker = False
1271
+ return True
1272
+ self._turn_session_owned_by_worker = False
1273
+ return False
1274
+
1275
+ def _set_prompt_enabled(self, enabled: bool) -> None:
1276
+ if not isinstance(self.screen, MainScreen):
1277
+ return
1278
+ self.screen.query_one(Input).disabled = not enabled
1279
+
1280
+ def _focus_prompt(self) -> None:
1281
+ if self._session_unusable or not isinstance(self.screen, MainScreen):
1282
+ return
1283
+ self.set_focus(self.screen.query_one(Input))
1284
+
1285
+ def action_quit(self) -> None:
1286
+ if self._active_turn_id is not None:
1287
+ if self.presenter is not None:
1288
+ self.presenter.conversation.add_notice(
1289
+ "⚠ Agent turn is running; wait for completion before quitting.",
1290
+ level="warning",
1291
+ )
1292
+ self._set_status("Running…")
1293
+ return
1294
+ if self._command_worker_active:
1295
+ if self.presenter is not None:
1296
+ self.presenter.conversation.add_notice(
1297
+ "⚠ Command is running; wait for it to finish before quitting.",
1298
+ level="warning",
1299
+ )
1300
+ self._set_status("Running…")
1301
+ return
1302
+ self.exit()
1303
+
1304
+ def on_unmount(self) -> None:
1305
+ self._shutdown_requested.set()
1306
+ self._trust_confirmer.reject_all()
1307
+ self._permission_confirmer.reject_all()
1308
+ self._subagent_observer.close()
1309
+ self._drain_permission_queue_rejected()
1310
+ with self._session_lock:
1311
+ sessions = list(self._pending_application_sessions)
1312
+ self._pending_application_sessions.clear()
1313
+ if (
1314
+ self._application_session is not None
1315
+ and not self._turn_session_owned_by_worker
1316
+ and self._command_session_owned_by_worker is None
1317
+ ):
1318
+ sessions.append(self._application_session)
1319
+ self._application_session = None
1320
+ for application_session in sessions:
1321
+ application_session.close()
1322
+
1323
+
1324
+ def _request_label(request: SessionStartRequest) -> str:
1325
+ if request.mode == "resume":
1326
+ identifier = request.session_id or ""
1327
+ return f"resume {identifier[:8]}"
1328
+ return request.mode
1329
+
1330
+
1331
+ def _visible_history(conversation) -> tuple[HistoryItem, ...]:
1332
+ return tuple(
1333
+ (message.role, message.content)
1334
+ for message in conversation.get_messages()
1335
+ if message.role in {"user", "assistant"}
1336
+ )
1337
+
1338
+
1339
+ def run_tui() -> None:
1340
+ MyCodeTuiApp().run()
1341
+
1342
+
1343
+ __all__ = [
1344
+ "ConversationView",
1345
+ "HeaderBar",
1346
+ "MainScreen",
1347
+ "MyCodeTuiApp",
1348
+ "SPLASH_LOGO",
1349
+ "StatusBar",
1350
+ "run_tui",
1351
+ ]