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,503 @@
1
+ """Immutable presentation models for the CortexShift terminal control center.
2
+
3
+ These are read models: bounded, flattened projections assembled by `TuiFacade` from
4
+ canonical domain entities so screen code never has to understand persistence adapters,
5
+ Git inspection results, or provider probe internals. They are never persisted, never
6
+ serialized to a wire protocol, and never substitute for canonical domain objects.
7
+
8
+ Every model carries an explicit authority label (`DataAuthority`) so the dashboard can
9
+ state honestly whether the reader is looking at live truth, an immutable historical
10
+ observation, an unverified agent report, or a last-known value that may have gone stale.
11
+ """
12
+
13
+ from collections.abc import Sequence
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime
16
+ from enum import StrEnum
17
+ from pathlib import Path
18
+
19
+ from cortexshift.domain.checkpoint import CheckpointRecord
20
+ from cortexshift.domain.git import RepositoryInspectionStatus
21
+ from cortexshift.domain.handoff import HandoffRecord
22
+ from cortexshift.domain.task import Task
23
+
24
+
25
+ class DataAuthority(StrEnum):
26
+ """How much authority a rendered value carries in the truth hierarchy.
27
+
28
+ CortexShift never presents a historical observation as current reality, and never
29
+ presents an agent's report as a verified result.
30
+ """
31
+
32
+ LIVE = "live"
33
+ HISTORICAL = "historical"
34
+ REPORTED = "reported"
35
+ LAST_KNOWN = "last_known"
36
+ UNKNOWN = "unknown"
37
+
38
+
39
+ AUTHORITY_LABELS: dict[DataAuthority, str] = {
40
+ DataAuthority.LIVE: "Live",
41
+ DataAuthority.HISTORICAL: "Historical observation",
42
+ DataAuthority.REPORTED: "Reported / unverified",
43
+ DataAuthority.LAST_KNOWN: "Last-known",
44
+ DataAuthority.UNKNOWN: "Unknown",
45
+ }
46
+
47
+
48
+ class WorkspaceActivity(StrEnum):
49
+ """Observed state of the exclusive project workspace lease.
50
+
51
+ Derived strictly from a non-blocking OS advisory lock probe. The presence of the
52
+ lock file on disk is never treated as evidence of an active lease.
53
+ """
54
+
55
+ FREE = "free"
56
+ BUSY = "busy"
57
+ UNKNOWN = "unknown"
58
+
59
+
60
+ @dataclass(frozen=True, slots=True)
61
+ class TuiProjectModel:
62
+ """Identity of the CortexShift project the dashboard is bound to."""
63
+
64
+ project_id: str
65
+ name: str
66
+ root: Path
67
+ state_file: str
68
+ schema_version: int
69
+ cortexshift_version: str
70
+
71
+ @property
72
+ def short_id(self) -> str:
73
+ """Abbreviated project identifier for dense table and header rendering."""
74
+ return abbreviate_id(self.project_id)
75
+
76
+
77
+ @dataclass(frozen=True, slots=True)
78
+ class TuiTaskProgress:
79
+ """Structured completion counts for the active Task.
80
+
81
+ A percentage is only meaningful when structured items exist. CortexShift never
82
+ infers progress from Git state or from a provider process exit status.
83
+ """
84
+
85
+ completed_count: int = 0
86
+ remaining_count: int = 0
87
+ issue_count: int = 0
88
+
89
+ @property
90
+ def total(self) -> int:
91
+ """Number of structured progress items forming the denominator."""
92
+ return self.completed_count + self.remaining_count
93
+
94
+ @property
95
+ def has_denominator(self) -> bool:
96
+ """Whether a meaningful completion ratio can be computed at all."""
97
+ return self.total > 0
98
+
99
+ @property
100
+ def fraction(self) -> float | None:
101
+ """Completion ratio in [0, 1], or None when no structured progress exists."""
102
+ if not self.has_denominator:
103
+ return None
104
+ return self.completed_count / self.total
105
+
106
+ @property
107
+ def percent(self) -> int | None:
108
+ """Completion percentage, or None when no structured progress exists."""
109
+ fraction = self.fraction
110
+ if fraction is None:
111
+ return None
112
+ return int(round(fraction * 100))
113
+
114
+
115
+ @dataclass(frozen=True, slots=True)
116
+ class TuiTaskModel:
117
+ """Complete canonical Task state for the Task screen."""
118
+
119
+ id: str
120
+ title: str
121
+ status: str
122
+ objective: str
123
+ requirements: tuple[str, ...] = ()
124
+ constraints: tuple[str, ...] = ()
125
+ completed: tuple[str, ...] = ()
126
+ current_work: str | None = None
127
+ remaining: tuple[str, ...] = ()
128
+ known_issues: tuple[str, ...] = ()
129
+ created_at: datetime | None = None
130
+ updated_at: datetime | None = None
131
+
132
+ @property
133
+ def short_id(self) -> str:
134
+ """Abbreviated task identifier for dense rendering."""
135
+ return abbreviate_id(self.id)
136
+
137
+ @property
138
+ def progress(self) -> TuiTaskProgress:
139
+ """Structured progress counts derived from canonical task lists."""
140
+ return TuiTaskProgress(
141
+ completed_count=len(self.completed),
142
+ remaining_count=len(self.remaining),
143
+ issue_count=len(self.known_issues),
144
+ )
145
+
146
+ @classmethod
147
+ def from_task(cls, task: Task) -> "TuiTaskModel":
148
+ """Project a canonical Task into its read model."""
149
+ return cls(
150
+ id=task.id,
151
+ title=task.title,
152
+ status=task.status.value,
153
+ objective=task.objective,
154
+ requirements=tuple(task.requirements),
155
+ constraints=tuple(task.constraints),
156
+ completed=tuple(task.completed),
157
+ current_work=task.current_work,
158
+ remaining=tuple(task.remaining),
159
+ known_issues=tuple(task.known_issues),
160
+ created_at=task.created_at,
161
+ updated_at=task.updated_at,
162
+ )
163
+
164
+
165
+ @dataclass(frozen=True, slots=True)
166
+ class TuiTaskRow:
167
+ """One row of the Task table."""
168
+
169
+ id: str
170
+ title: str
171
+ status: str
172
+ is_active: bool
173
+ is_terminal: bool
174
+ completed_count: int
175
+ remaining_count: int
176
+ updated_at: datetime | None = None
177
+
178
+ @property
179
+ def short_id(self) -> str:
180
+ """Abbreviated task identifier for dense rendering."""
181
+ return abbreviate_id(self.id)
182
+
183
+
184
+ @dataclass(frozen=True, slots=True)
185
+ class TuiRepositoryModel:
186
+ """Result of a live, strictly read-only Git working tree inspection."""
187
+
188
+ status: RepositoryInspectionStatus
189
+ project_root: Path
190
+ git_available: bool
191
+ git_version: str | None = None
192
+ git_root: str | None = None
193
+ branch: str | None = None
194
+ head_sha: str | None = None
195
+ detached_head: bool = False
196
+ dirty: bool = False
197
+ staged_files: tuple[str, ...] = ()
198
+ modified_files: tuple[str, ...] = ()
199
+ untracked_files: tuple[str, ...] = ()
200
+ conflicted_files: tuple[str, ...] = ()
201
+ working_tree_diff_summary: str | None = None
202
+ staged_diff_summary: str | None = None
203
+ diagnostic: str | None = None
204
+ observed_at: datetime | None = None
205
+
206
+ @property
207
+ def ready(self) -> bool:
208
+ """Whether the repository was inspected successfully."""
209
+ return self.status == RepositoryInspectionStatus.READY
210
+
211
+ @property
212
+ def changed_file_count(self) -> int:
213
+ """Total number of distinct changed paths reported by the inspection."""
214
+ return len(
215
+ {
216
+ *self.staged_files,
217
+ *self.modified_files,
218
+ *self.untracked_files,
219
+ *self.conflicted_files,
220
+ }
221
+ )
222
+
223
+ @property
224
+ def short_head(self) -> str:
225
+ """Abbreviated HEAD commit, or an em dash when no commit exists."""
226
+ if not self.head_sha:
227
+ return "—"
228
+ return self.head_sha[:8]
229
+
230
+
231
+ @dataclass(frozen=True, slots=True)
232
+ class TuiSessionRow:
233
+ """One CortexShift orchestration Session.
234
+
235
+ Holds orchestration metadata only. Provider transcripts, prompts, reasoning, and
236
+ terminal history are never read and never displayed.
237
+ """
238
+
239
+ id: str
240
+ task_id: str
241
+ provider_id: str
242
+ status: str
243
+ native_resumable: bool
244
+ native_session_id: str | None = None
245
+ resumed_from_session_id: str | None = None
246
+ exit_reason: str | None = None
247
+ exit_code: int | None = None
248
+ started_at: datetime | None = None
249
+ ended_at: datetime | None = None
250
+ reconciled_at: datetime | None = None
251
+
252
+ @property
253
+ def short_id(self) -> str:
254
+ """Abbreviated session identifier for dense rendering."""
255
+ return abbreviate_id(self.id)
256
+
257
+ @property
258
+ def unfinalized(self) -> bool:
259
+ """Whether this record was never finalized and may warrant recovery."""
260
+ return self.status in ("initializing", "running")
261
+
262
+ @property
263
+ def authority(self) -> DataAuthority:
264
+ """An unfinalized row is last-known state, not a live running process."""
265
+ return DataAuthority.LAST_KNOWN if self.unfinalized else DataAuthority.HISTORICAL
266
+
267
+
268
+ @dataclass(frozen=True, slots=True)
269
+ class TuiCheckpointRow:
270
+ """One immutable Checkpoint observation."""
271
+
272
+ id: str
273
+ kind: str
274
+ session_id: str | None
275
+ created_at: datetime
276
+ git_summary: str
277
+ test_reported: bool
278
+ record: CheckpointRecord
279
+
280
+ @property
281
+ def short_id(self) -> str:
282
+ """Abbreviated checkpoint identifier for dense rendering."""
283
+ return abbreviate_id(self.id)
284
+
285
+
286
+ @dataclass(frozen=True, slots=True)
287
+ class TuiHandoffRow:
288
+ """One canonical cross-provider Handoff record."""
289
+
290
+ id: str
291
+ source_provider_id: str
292
+ target_provider_id: str
293
+ status: str
294
+ checkpoint_id: str | None
295
+ created_at: datetime
296
+ record: HandoffRecord
297
+
298
+ @property
299
+ def short_id(self) -> str:
300
+ """Abbreviated handoff identifier for dense rendering."""
301
+ return abbreviate_id(self.id)
302
+
303
+
304
+ @dataclass(frozen=True, slots=True)
305
+ class TuiProviderStatus:
306
+ """Passive discovery result for one provider CLI.
307
+
308
+ Account identifiers, credential paths, and token material are never collected.
309
+ """
310
+
311
+ provider_id: str
312
+ display_name: str
313
+ executable: str
314
+ installed: bool
315
+ version: str | None = None
316
+ authentication: str = "unknown"
317
+ supports_native_resume: bool = False
318
+ supports_exact_resume: bool = False
319
+ mcp_integration: str = "unknown"
320
+ diagnostics: tuple[str, ...] = ()
321
+
322
+ @property
323
+ def available(self) -> bool:
324
+ """Whether the provider CLI resolved on PATH."""
325
+ return self.installed
326
+
327
+
328
+ @dataclass(frozen=True, slots=True)
329
+ class TuiMcpStatus:
330
+ """CortexShift MCP server and provider integration status.
331
+
332
+ Rendering this never starts an MCP server.
333
+ """
334
+
335
+ sdk_available: bool
336
+ sdk_version: str
337
+ transport: str = "stdio (local only)"
338
+ claude_integration: str = "automatic per launch (--mcp-config)"
339
+ codex_integration: str = "automatic per launch (-c overrides)"
340
+ antigravity_configured: bool = False
341
+ antigravity_config_path: str | None = None
342
+ read_tools: tuple[str, ...] = ()
343
+ write_tools: tuple[str, ...] = ()
344
+ resources: tuple[str, ...] = ()
345
+
346
+
347
+ @dataclass(frozen=True, slots=True)
348
+ class TuiActivityModel:
349
+ """Most recent orchestration activity recorded for the project."""
350
+
351
+ latest_session: TuiSessionRow | None = None
352
+ latest_checkpoint: TuiCheckpointRow | None = None
353
+ latest_handoff: TuiHandoffRow | None = None
354
+ unfinalized_session_count: int = 0
355
+
356
+ @property
357
+ def recovery_may_be_required(self) -> bool:
358
+ """Whether unfinalized session records exist that recovery could reconcile."""
359
+ return self.unfinalized_session_count > 0
360
+
361
+
362
+ @dataclass(frozen=True, slots=True)
363
+ class TuiStateSnapshot:
364
+ """All SQLite-backed read models assembled in one pass.
365
+
366
+ Deliberately excludes live Git inspection and provider discovery so the lightweight
367
+ refresh timer never spawns a subprocess.
368
+ """
369
+
370
+ project: TuiProjectModel
371
+ active_task: TuiTaskModel | None = None
372
+ tasks: tuple[TuiTaskRow, ...] = ()
373
+ sessions: tuple[TuiSessionRow, ...] = ()
374
+ checkpoints: tuple[TuiCheckpointRow, ...] = ()
375
+ handoffs: tuple[TuiHandoffRow, ...] = ()
376
+ activity: TuiActivityModel = field(default_factory=TuiActivityModel)
377
+ loaded_at: datetime | None = None
378
+
379
+
380
+ @dataclass(frozen=True, slots=True)
381
+ class TuiRecoveryPreview:
382
+ """Non-mutating preview of what a recovery run would reconcile."""
383
+
384
+ task_id: str
385
+ task_title: str
386
+ stale_session_ids: tuple[str, ...] = ()
387
+ dirty: bool = False
388
+ files_touched: tuple[str, ...] = ()
389
+ repository_status: str = "unknown"
390
+
391
+ @property
392
+ def stale_count(self) -> int:
393
+ """Number of unfinalized sessions that would be reconciled."""
394
+ return len(self.stale_session_ids)
395
+
396
+
397
+ @dataclass(frozen=True, slots=True)
398
+ class TuiSwitchPreview:
399
+ """Bounded preview of a provider switch, produced without any model call."""
400
+
401
+ target_provider_id: str
402
+ target_provider_name: str
403
+ source_provider_id: str
404
+ source_session_id: str
405
+ task_id: str
406
+ task_title: str
407
+ target_native_mode: str
408
+ selected_prior_target_session_id: str | None
409
+ git_status: str
410
+ git_branch: str | None
411
+ git_dirty: bool
412
+ delivery_strategy: str
413
+ bootstrap_model_turn_required: bool
414
+ checkpoint_enrichment: str
415
+ context_characters: int
416
+ context_max_characters: int
417
+ context_truncated: bool
418
+
419
+
420
+ @dataclass(frozen=True, slots=True)
421
+ class TuiHandoffPreview:
422
+ """Bounded canonical handoff context rendered without persisting anything."""
423
+
424
+ target_provider_id: str
425
+ target_provider_name: str
426
+ delivery_strategy: str
427
+ bootstrap_model_turn_required: bool
428
+ rendered_context: str
429
+ context_characters: int
430
+ context_max_characters: int
431
+ context_truncated: bool
432
+
433
+
434
+ def abbreviate_id(value: str, keep: int = 8) -> str:
435
+ """Abbreviate a canonical identifier for dense table rendering.
436
+
437
+ Only ever used for display. Service calls always receive the full canonical ID, and
438
+ detail panels always expose it in full.
439
+ """
440
+ if not value:
441
+ return "—"
442
+ prefix, separator, suffix = value.partition("_")
443
+ if not separator:
444
+ return value if len(value) <= keep else f"{value[:keep]}…"
445
+ if len(suffix) <= keep:
446
+ return value
447
+ return f"{prefix}_{suffix[:keep]}…"
448
+
449
+
450
+ def format_relative(moment: datetime | None, *, now: datetime | None = None) -> str:
451
+ """Render a UTC timestamp as a compact relative age.
452
+
453
+ Canonical UTC datetimes are never mutated; this is a display projection only. Exact
454
+ timestamps remain available in detail views.
455
+ """
456
+ if moment is None:
457
+ return "—"
458
+
459
+ from cortexshift.domain.identifiers import utc_now
460
+
461
+ reference = now or utc_now()
462
+ anchored = moment.replace(tzinfo=reference.tzinfo) if moment.tzinfo is None else moment
463
+ seconds = (reference - anchored).total_seconds()
464
+ if seconds < 0:
465
+ return "just now"
466
+ if seconds < 60:
467
+ return "just now"
468
+ minutes = int(seconds // 60)
469
+ if minutes < 60:
470
+ return f"{minutes}m ago"
471
+ hours = minutes // 60
472
+ if hours < 24:
473
+ return f"{hours}h ago"
474
+ days = hours // 24
475
+ if days < 30:
476
+ return f"{days}d ago"
477
+ months = days // 30
478
+ if months < 12:
479
+ return f"{months}mo ago"
480
+ return f"{days // 365}y ago"
481
+
482
+
483
+ def format_timestamp(moment: datetime | None) -> str:
484
+ """Render an exact UTC timestamp for copy-friendly detail panels."""
485
+ if moment is None:
486
+ return "—"
487
+ return moment.isoformat()
488
+
489
+
490
+ def truncate(text: str | None, limit: int) -> str:
491
+ """Bound a free-text value for dense rendering without altering canonical state."""
492
+ if not text:
493
+ return "—"
494
+ collapsed = " ".join(text.split())
495
+ if len(collapsed) <= limit:
496
+ return collapsed
497
+ return f"{collapsed[: max(1, limit - 1)]}…"
498
+
499
+
500
+ def bounded(items: Sequence[str], limit: int) -> tuple[list[str], int]:
501
+ """Split a sequence into a displayable head and a count of omitted entries."""
502
+ head = list(items[:limit])
503
+ return head, max(0, len(items) - limit)
@@ -0,0 +1,81 @@
1
+ """Primary dashboard sections.
2
+
3
+ Each section is a self-contained view over presentation models supplied by the app. A
4
+ section never reaches into persistence, never runs Git, and never constructs provider
5
+ arguments; it renders what `TuiFacade` produced and raises intent back to the app.
6
+ """
7
+
8
+ from enum import StrEnum
9
+
10
+ from textual.containers import Vertical
11
+
12
+ from cortexshift.tui.models import (
13
+ TuiMcpStatus,
14
+ TuiProviderStatus,
15
+ TuiRepositoryModel,
16
+ TuiStateSnapshot,
17
+ WorkspaceActivity,
18
+ )
19
+
20
+
21
+ class TuiSection(StrEnum):
22
+ """The primary navigable sections of the control center."""
23
+
24
+ OVERVIEW = "overview"
25
+ TASK = "task"
26
+ REPOSITORY = "repository"
27
+ SESSIONS = "sessions"
28
+ CHECKPOINTS = "checkpoints"
29
+ HANDOFFS = "handoffs"
30
+ PROVIDERS = "providers"
31
+
32
+ @property
33
+ def label(self) -> str:
34
+ """Display name shown in the sidebar and status line."""
35
+ return self.value.capitalize()
36
+
37
+ @property
38
+ def shortcut(self) -> str:
39
+ """The number key that jumps directly to this section."""
40
+ return str(SECTION_ORDER.index(self) + 1)
41
+
42
+
43
+ SECTION_ORDER: tuple[TuiSection, ...] = (
44
+ TuiSection.OVERVIEW,
45
+ TuiSection.TASK,
46
+ TuiSection.REPOSITORY,
47
+ TuiSection.SESSIONS,
48
+ TuiSection.CHECKPOINTS,
49
+ TuiSection.HANDOFFS,
50
+ TuiSection.PROVIDERS,
51
+ )
52
+
53
+
54
+ class SectionView(Vertical):
55
+ """Base class for a primary section of the dashboard."""
56
+
57
+ DEFAULT_CLASSES = "section"
58
+
59
+ section: TuiSection
60
+
61
+ def update_state(self, snapshot: TuiStateSnapshot) -> None:
62
+ """Render newly loaded persisted state."""
63
+
64
+ def update_repository(self, repository: TuiRepositoryModel | None) -> None:
65
+ """Render the result of a live repository inspection."""
66
+
67
+ def update_providers(
68
+ self,
69
+ providers: tuple[TuiProviderStatus, ...],
70
+ mcp: TuiMcpStatus | None,
71
+ ) -> None:
72
+ """Render provider discovery and MCP integration status."""
73
+
74
+ def update_activity(self, activity: WorkspaceActivity) -> None:
75
+ """Render the observed workspace lease state."""
76
+
77
+ def on_section_shown(self) -> None:
78
+ """Called when this section becomes the visible section."""
79
+
80
+
81
+ __all__ = ["SECTION_ORDER", "SectionView", "TuiSection"]