cortexshift 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 (100) hide show
  1. cortexshift/__init__.py +10 -0
  2. cortexshift/__main__.py +6 -0
  3. cortexshift/adapters/__init__.py +22 -0
  4. cortexshift/adapters/command_runner.py +116 -0
  5. cortexshift/adapters/discovery.py +55 -0
  6. cortexshift/adapters/git/__init__.py +10 -0
  7. cortexshift/adapters/git/inspector.py +321 -0
  8. cortexshift/adapters/git/parser.py +140 -0
  9. cortexshift/adapters/headless_runner.py +92 -0
  10. cortexshift/adapters/process_runner.py +56 -0
  11. cortexshift/adapters/providers/__init__.py +4 -0
  12. cortexshift/adapters/providers/antigravity.py +530 -0
  13. cortexshift/adapters/providers/claude.py +375 -0
  14. cortexshift/adapters/providers/codex.py +434 -0
  15. cortexshift/adapters/sqlite/__init__.py +10 -0
  16. cortexshift/adapters/sqlite/migrations.py +268 -0
  17. cortexshift/adapters/sqlite/store.py +914 -0
  18. cortexshift/adapters/workspace_lease.py +123 -0
  19. cortexshift/application/__init__.py +42 -0
  20. cortexshift/application/checkpoint_builder.py +218 -0
  21. cortexshift/application/checkpoint_service.py +273 -0
  22. cortexshift/application/doctor.py +80 -0
  23. cortexshift/application/handoff_builder.py +281 -0
  24. cortexshift/application/handoff_renderer.py +430 -0
  25. cortexshift/application/handoff_service.py +66 -0
  26. cortexshift/application/init_service.py +86 -0
  27. cortexshift/application/locator.py +48 -0
  28. cortexshift/application/native_session.py +65 -0
  29. cortexshift/application/recovery_service.py +235 -0
  30. cortexshift/application/repository_service.py +146 -0
  31. cortexshift/application/resume_service.py +124 -0
  32. cortexshift/application/run_service.py +270 -0
  33. cortexshift/application/session_launcher.py +183 -0
  34. cortexshift/application/session_service.py +63 -0
  35. cortexshift/application/source_session.py +62 -0
  36. cortexshift/application/status_service.py +73 -0
  37. cortexshift/application/switch_service.py +671 -0
  38. cortexshift/application/task_service.py +201 -0
  39. cortexshift/application/task_workspace.py +152 -0
  40. cortexshift/cli/__init__.py +5 -0
  41. cortexshift/cli/app.py +2477 -0
  42. cortexshift/domain/__init__.py +153 -0
  43. cortexshift/domain/checkpoint.py +174 -0
  44. cortexshift/domain/doctor.py +68 -0
  45. cortexshift/domain/errors.py +277 -0
  46. cortexshift/domain/git.py +102 -0
  47. cortexshift/domain/handoff.py +241 -0
  48. cortexshift/domain/identifiers.py +27 -0
  49. cortexshift/domain/launch.py +58 -0
  50. cortexshift/domain/mcp_binding.py +81 -0
  51. cortexshift/domain/native_session.py +19 -0
  52. cortexshift/domain/project.py +37 -0
  53. cortexshift/domain/provider.py +67 -0
  54. cortexshift/domain/session.py +92 -0
  55. cortexshift/domain/status.py +40 -0
  56. cortexshift/domain/task.py +191 -0
  57. cortexshift/mcp/__init__.py +38 -0
  58. cortexshift/mcp/context.py +165 -0
  59. cortexshift/mcp/facade.py +513 -0
  60. cortexshift/mcp/models.py +178 -0
  61. cortexshift/mcp/resources.py +45 -0
  62. cortexshift/mcp/server.py +52 -0
  63. cortexshift/mcp/tools.py +176 -0
  64. cortexshift/ports/__init__.py +39 -0
  65. cortexshift/ports/checkpoint_store.py +45 -0
  66. cortexshift/ports/command_runner.py +56 -0
  67. cortexshift/ports/discovery.py +41 -0
  68. cortexshift/ports/handoff_delivery.py +91 -0
  69. cortexshift/ports/handoff_store.py +43 -0
  70. cortexshift/ports/headless_runner.py +58 -0
  71. cortexshift/ports/native_session.py +20 -0
  72. cortexshift/ports/process_runner.py +31 -0
  73. cortexshift/ports/provider.py +152 -0
  74. cortexshift/ports/repository.py +44 -0
  75. cortexshift/ports/session_store.py +27 -0
  76. cortexshift/ports/state_store.py +55 -0
  77. cortexshift/ports/workspace_lease.py +39 -0
  78. cortexshift/tui/__init__.py +24 -0
  79. cortexshift/tui/actions.py +58 -0
  80. cortexshift/tui/app.py +1051 -0
  81. cortexshift/tui/coordinator.py +173 -0
  82. cortexshift/tui/cortexshift.tcss +258 -0
  83. cortexshift/tui/facade.py +614 -0
  84. cortexshift/tui/modals.py +594 -0
  85. cortexshift/tui/models.py +503 -0
  86. cortexshift/tui/screens/__init__.py +81 -0
  87. cortexshift/tui/screens/checkpoints.py +188 -0
  88. cortexshift/tui/screens/handoffs.py +180 -0
  89. cortexshift/tui/screens/help.py +117 -0
  90. cortexshift/tui/screens/overview.py +200 -0
  91. cortexshift/tui/screens/providers.py +169 -0
  92. cortexshift/tui/screens/repository.py +143 -0
  93. cortexshift/tui/screens/sessions.py +146 -0
  94. cortexshift/tui/screens/task.py +174 -0
  95. cortexshift/tui/widgets.py +209 -0
  96. cortexshift-0.1.0.dist-info/METADATA +202 -0
  97. cortexshift-0.1.0.dist-info/RECORD +100 -0
  98. cortexshift-0.1.0.dist-info/WHEEL +4 -0
  99. cortexshift-0.1.0.dist-info/entry_points.txt +2 -0
  100. cortexshift-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,173 @@
1
+ """Runs the dashboard, then hands the terminal to a native provider.
2
+
3
+ The ordering enforced here is an architectural guarantee, not a convenience:
4
+
5
+ CortexShiftApp.run() → returns TuiExitRequest
6
+ (Textual has torn down and restored the terminal)
7
+ → RunService / ResumeService / SwitchService
8
+ → native provider owns the terminal
9
+
10
+ No provider process is ever started while the Textual application is alive, and no
11
+ provider TUI is embedded, captured, or multiplexed. `launch_provider` is only ever
12
+ reached after `run_dashboard` has returned.
13
+ """
14
+
15
+ import sys
16
+ from collections.abc import Callable
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+
20
+ from cortexshift.application.resume_service import ResumeService
21
+ from cortexshift.application.run_service import RunService
22
+ from cortexshift.application.switch_service import SwitchService
23
+ from cortexshift.domain.errors import ProjectNotInitializedError, TerminalRequiredError
24
+ from cortexshift.domain.session import Session
25
+ from cortexshift.tui.actions import TuiExitAction, TuiExitRequest
26
+ from cortexshift.tui.app import CortexShiftApp, build_app
27
+
28
+ TTY_REQUIRED_MESSAGE = (
29
+ "The CortexShift dashboard requires an interactive terminal (TTY).\n\n"
30
+ "Both standard input and standard output must be attached to a terminal."
31
+ )
32
+
33
+
34
+ def _silent(message: str) -> None:
35
+ """Default announcement sink: the dashboard says nothing on its own."""
36
+
37
+
38
+ def terminal_is_interactive() -> bool:
39
+ """Whether both stdin and stdout are attached to a terminal."""
40
+ try:
41
+ return sys.stdin.isatty() and sys.stdout.isatty()
42
+ except Exception:
43
+ return False
44
+
45
+
46
+ @dataclass
47
+ class TuiSession:
48
+ """Result of one dashboard invocation and any provider launch that followed."""
49
+
50
+ request: TuiExitRequest | None = None
51
+ session: Session | None = None
52
+ launched: bool = False
53
+ events: list[str] = field(default_factory=list)
54
+
55
+ @property
56
+ def exit_code(self) -> int:
57
+ """Process exit code derived from the launched provider session, if any."""
58
+ if self.session is None:
59
+ return 0
60
+ from cortexshift.domain.session import SessionStatus
61
+
62
+ if self.session.status in (SessionStatus.COMPLETED, SessionStatus.INTERRUPTED):
63
+ return 0
64
+ return self.session.exit_code or 1
65
+
66
+
67
+ class TuiCoordinator:
68
+ """Sequences dashboard lifetime and native provider launch."""
69
+
70
+ def __init__(
71
+ self,
72
+ *,
73
+ run_service: RunService | None = None,
74
+ resume_service: ResumeService | None = None,
75
+ switch_service: SwitchService | None = None,
76
+ app_factory: Callable[[], CortexShiftApp] | None = None,
77
+ dashboard_runner: Callable[[], TuiExitRequest | None] | None = None,
78
+ is_tty: Callable[[], bool] = terminal_is_interactive,
79
+ announce: Callable[[str], None] | None = None,
80
+ ) -> None:
81
+ self._run_service = run_service
82
+ self._resume_service = resume_service
83
+ self._switch_service = switch_service
84
+ self._app_factory = app_factory
85
+ self._dashboard_runner = dashboard_runner
86
+ self._is_tty = is_tty
87
+ self._announce = announce or _silent
88
+
89
+ # ------------------------------------------------------------------
90
+
91
+ def start(self, project_root: Path | str | None = None) -> TuiSession:
92
+ """Run the dashboard and, if requested, launch a provider afterwards.
93
+
94
+ Raises:
95
+ ProjectNotInitializedError: If no initialized project is found.
96
+ TerminalRequiredError: If the process has no interactive terminal.
97
+ """
98
+ result = TuiSession()
99
+
100
+ request = self._run_dashboard(project_root)
101
+ result.events.append("dashboard_finished")
102
+ result.request = request
103
+
104
+ if request is None:
105
+ return result
106
+
107
+ self._announce(f"{request.description} — CortexShift released the terminal.")
108
+ result.events.append("provider_launch_started")
109
+ result.launched = True
110
+ result.session = self.launch_provider(request)
111
+ result.events.append("provider_launch_finished")
112
+ return result
113
+
114
+ def _run_dashboard(self, project_root: Path | str | None) -> TuiExitRequest | None:
115
+ """Run the Textual application to completion and return its exit request.
116
+
117
+ The project is resolved before the terminal is checked: someone standing in the
118
+ wrong directory should be told that, not told about their terminal.
119
+ """
120
+ if self._dashboard_runner is not None:
121
+ return self._dashboard_runner()
122
+
123
+ resolved = resolve_project_root(project_root)
124
+
125
+ if not self._is_tty():
126
+ raise TerminalRequiredError(TTY_REQUIRED_MESSAGE)
127
+
128
+ app = self._app_factory() if self._app_factory else build_app(resolved)
129
+ return app.run()
130
+
131
+ # ------------------------------------------------------------------
132
+
133
+ def launch_provider(self, request: TuiExitRequest) -> Session | None:
134
+ """Invoke the existing provider service for a dashboard exit request.
135
+
136
+ Only ever called after the Textual application has finished. The provider owns
137
+ the terminal from here; CortexShift does not wrap, capture, or emulate it.
138
+ """
139
+ if request.action is TuiExitAction.RUN:
140
+ service = self._run_service or RunService()
141
+ return service.run(provider_name=request.provider)
142
+
143
+ if request.action is TuiExitAction.RESUME:
144
+ service_resume = self._resume_service or ResumeService()
145
+ outcome = service_resume.resume(
146
+ provider_name=request.provider,
147
+ session_id=request.selected_session_id,
148
+ )
149
+ return outcome if isinstance(outcome, Session) else None
150
+
151
+ service_switch = self._switch_service or SwitchService()
152
+ switch_result = service_switch.switch(
153
+ target_provider_name=request.provider,
154
+ new_session=request.force_new_session,
155
+ resume_session_id=request.selected_session_id,
156
+ note=request.note,
157
+ )
158
+ return switch_result.target_session
159
+
160
+
161
+ def resolve_project_root(start_dir: Path | str | None = None) -> Path:
162
+ """Resolve the initialized project the dashboard should bind to.
163
+
164
+ Raises:
165
+ ProjectNotInitializedError: If no initialized project root is found.
166
+ """
167
+ from cortexshift.application.locator import ProjectLocator
168
+
169
+ start_path = Path(start_dir) if start_dir is not None else None
170
+ project_root = ProjectLocator.find_project_root(start_path)
171
+ if project_root is None:
172
+ raise ProjectNotInitializedError()
173
+ return project_root
@@ -0,0 +1,258 @@
1
+ /* CortexShift control center theme.
2
+ *
3
+ * A restrained engineering palette: dark ground, one accent, subtle borders, and
4
+ * status emphasis reserved for state that actually needs attention. Meaning is never
5
+ * carried by colour alone — glyphs accompany every status marker.
6
+ */
7
+
8
+ Screen {
9
+ background: $surface;
10
+ color: $foreground;
11
+ }
12
+
13
+ /* ---------- Shell layout ---------- */
14
+
15
+ #shell {
16
+ height: 1fr;
17
+ }
18
+
19
+ #sidebar {
20
+ width: 20;
21
+ min-width: 20;
22
+ background: $panel;
23
+ border-right: solid $primary-darken-2;
24
+ padding: 1 1 0 1;
25
+ }
26
+
27
+ #sidebar-title {
28
+ color: $accent;
29
+ text-style: bold;
30
+ padding: 0 0 1 1;
31
+ }
32
+
33
+ #nav {
34
+ height: auto;
35
+ border: none;
36
+ background: $panel;
37
+ }
38
+
39
+ #nav > .option-list--option {
40
+ padding: 0 0;
41
+ }
42
+
43
+ #nav:focus > .option-list--option-highlighted {
44
+ background: $primary;
45
+ color: $text;
46
+ text-style: bold;
47
+ }
48
+
49
+ #content {
50
+ padding: 1 2;
51
+ height: 1fr;
52
+ }
53
+
54
+ #status-line {
55
+ height: 1;
56
+ padding: 0 2;
57
+ background: $panel;
58
+ color: $text-muted;
59
+ }
60
+
61
+ /* Compact layout for terminals narrower than 90 columns: the sidebar folds away and
62
+ the section name moves into the status line. Number keys keep working. */
63
+
64
+ .compact #sidebar {
65
+ display: none;
66
+ }
67
+
68
+ .compact #content {
69
+ padding: 1 1;
70
+ }
71
+
72
+ /* ---------- Sections ---------- */
73
+
74
+ .section {
75
+ height: 1fr;
76
+ overflow-y: auto;
77
+ }
78
+
79
+ .section-title {
80
+ text-style: bold;
81
+ color: $accent;
82
+ padding: 0 0 1 0;
83
+ }
84
+
85
+ .panel {
86
+ height: auto;
87
+ border-top: solid $primary-darken-2;
88
+ border-title-color: $accent;
89
+ border-title-style: bold;
90
+ border-subtitle-color: $text-muted;
91
+ border-subtitle-style: italic;
92
+ padding: 0 0 1 0;
93
+ margin: 1 0 0 0;
94
+ }
95
+
96
+ .panel-title {
97
+ color: $text-muted;
98
+ text-style: bold;
99
+ padding: 1 0 0 0;
100
+ }
101
+
102
+ .field-list {
103
+ height: auto;
104
+ padding: 0 0 0 1;
105
+ }
106
+
107
+ .bullets {
108
+ height: auto;
109
+ padding: 0 0 0 0;
110
+ }
111
+
112
+ .columns {
113
+ height: auto;
114
+ }
115
+
116
+ .column {
117
+ width: 1fr;
118
+ height: auto;
119
+ padding: 0 2 0 0;
120
+ }
121
+
122
+ .empty-state {
123
+ padding: 2 2;
124
+ color: $text-muted;
125
+ }
126
+
127
+ .notice {
128
+ padding: 0 0 1 0;
129
+ color: $warning;
130
+ }
131
+
132
+ /* ---------- Tables ---------- */
133
+
134
+ DataTable {
135
+ height: auto;
136
+ max-height: 20;
137
+ border: round $primary-darken-2;
138
+ }
139
+
140
+ DataTable > .datatable--header {
141
+ text-style: bold;
142
+ background: $panel;
143
+ color: $text-muted;
144
+ }
145
+
146
+ DataTable > .datatable--cursor {
147
+ background: $primary;
148
+ color: $text;
149
+ }
150
+
151
+ .detail {
152
+ height: auto;
153
+ border-top: solid $primary-darken-2;
154
+ padding: 1 0 0 0;
155
+ }
156
+
157
+ /* ---------- Status emphasis ---------- */
158
+
159
+ .state-ok {
160
+ color: $success;
161
+ }
162
+
163
+ .state-warn {
164
+ color: $warning;
165
+ }
166
+
167
+ .state-fail {
168
+ color: $error;
169
+ }
170
+
171
+ .state-active {
172
+ color: $accent;
173
+ }
174
+
175
+ .state-muted {
176
+ color: $text-muted;
177
+ }
178
+
179
+ /* ---------- Modals ---------- */
180
+
181
+ ModalScreen {
182
+ align: center middle;
183
+ background: $surface 60%;
184
+ }
185
+
186
+ .modal {
187
+ width: 72;
188
+ max-width: 90%;
189
+ height: auto;
190
+ max-height: 90%;
191
+ padding: 1 2;
192
+ background: $panel;
193
+ border: thick $primary;
194
+ }
195
+
196
+ .modal-title {
197
+ text-style: bold;
198
+ color: $accent;
199
+ padding: 0 0 1 0;
200
+ }
201
+
202
+ .modal-help {
203
+ color: $text-muted;
204
+ padding: 0 0 1 0;
205
+ }
206
+
207
+ .modal-error {
208
+ color: $error;
209
+ height: auto;
210
+ padding: 0 0 1 0;
211
+ }
212
+
213
+ .modal-body {
214
+ height: auto;
215
+ max-height: 24;
216
+ overflow-y: auto;
217
+ }
218
+
219
+ .modal-buttons {
220
+ height: auto;
221
+ align-horizontal: right;
222
+ padding: 1 0 0 0;
223
+ }
224
+
225
+ .modal-buttons Button {
226
+ margin: 0 0 0 2;
227
+ min-width: 12;
228
+ }
229
+
230
+ .field-label {
231
+ color: $text-muted;
232
+ padding: 1 0 0 0;
233
+ }
234
+
235
+ Input {
236
+ border: tall $primary-darken-2;
237
+ }
238
+
239
+ Input:focus {
240
+ border: tall $accent;
241
+ }
242
+
243
+ /* ---------- Too-small terminal guard ---------- */
244
+
245
+ #too-small {
246
+ display: none;
247
+ height: 1fr;
248
+ padding: 1 2;
249
+ color: $warning;
250
+ }
251
+
252
+ .too-small #shell {
253
+ display: none;
254
+ }
255
+
256
+ .too-small #too-small {
257
+ display: block;
258
+ }