shikiru-cli 0.2.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 (319) hide show
  1. shikiru/__init__.py +1 -0
  2. shikiru/__main__.py +8 -0
  3. shikiru/agents/__init__.py +3 -0
  4. shikiru/agents/contracts.py +590 -0
  5. shikiru/agents/registry.py +87 -0
  6. shikiru/agents/sdk/__init__.py +17 -0
  7. shikiru/agents/sdk/reset_time.py +137 -0
  8. shikiru/agents/sdk/turn.py +335 -0
  9. shikiru/agents/settings.py +151 -0
  10. shikiru/agents/testing/__init__.py +26 -0
  11. shikiru/agents/testing/contract_suite.py +653 -0
  12. shikiru/agents/testing/resume.py +15 -0
  13. shikiru/bootstrap.py +1277 -0
  14. shikiru/cli/__init__.py +1 -0
  15. shikiru/cli/app.py +770 -0
  16. shikiru/cli/context.py +63 -0
  17. shikiru/cli/render.py +1475 -0
  18. shikiru/cli/report.py +156 -0
  19. shikiru/cli/session_prompt.py +63 -0
  20. shikiru/cli/view.py +131 -0
  21. shikiru/clock.py +41 -0
  22. shikiru/config/__init__.py +8 -0
  23. shikiru/config/defaults.toml +131 -0
  24. shikiru/config/loader.py +178 -0
  25. shikiru/config/settings.py +392 -0
  26. shikiru/dashboard/__init__.py +3 -0
  27. shikiru/dashboard/actions_list.py +799 -0
  28. shikiru/dashboard/activity.py +150 -0
  29. shikiru/dashboard/app.py +1765 -0
  30. shikiru/dashboard/blocker_evidence.py +122 -0
  31. shikiru/dashboard/blocker_rows.py +84 -0
  32. shikiru/dashboard/briefing_view.py +871 -0
  33. shikiru/dashboard/contract.py +1086 -0
  34. shikiru/dashboard/covered.py +32 -0
  35. shikiru/dashboard/detail.py +1209 -0
  36. shikiru/dashboard/detail_fold.py +244 -0
  37. shikiru/dashboard/detail_hint.py +68 -0
  38. shikiru/dashboard/details_list_view.py +150 -0
  39. shikiru/dashboard/diagnostics.py +80 -0
  40. shikiru/dashboard/diagnostics_sections.py +186 -0
  41. shikiru/dashboard/evidence_table.py +123 -0
  42. shikiru/dashboard/extra_details.py +214 -0
  43. shikiru/dashboard/fault_guidance.py +216 -0
  44. shikiru/dashboard/fold_bodies.py +276 -0
  45. shikiru/dashboard/help_keys.py +166 -0
  46. shikiru/dashboard/help_screen.py +204 -0
  47. shikiru/dashboard/interaction.py +17 -0
  48. shikiru/dashboard/ladder_editor.py +544 -0
  49. shikiru/dashboard/live_settings.py +62 -0
  50. shikiru/dashboard/motion.py +368 -0
  51. shikiru/dashboard/orchestration_fields.py +201 -0
  52. shikiru/dashboard/palette.py +1606 -0
  53. shikiru/dashboard/palette_app.py +89 -0
  54. shikiru/dashboard/palette_drafts.py +33 -0
  55. shikiru/dashboard/resolution.py +573 -0
  56. shikiru/dashboard/resolution_view.py +302 -0
  57. shikiru/dashboard/row_list.py +256 -0
  58. shikiru/dashboard/section_rows.py +173 -0
  59. shikiru/dashboard/services.py +665 -0
  60. shikiru/dashboard/session_history.py +402 -0
  61. shikiru/dashboard/shikiru_init_ascii_art.txt +40 -0
  62. shikiru/dashboard/task_picker.py +358 -0
  63. shikiru/dashboard/tick.py +59 -0
  64. shikiru/dashboard/timeline_filter.py +129 -0
  65. shikiru/dashboard/timeline_section.py +385 -0
  66. shikiru/dashboard/usage_body.py +269 -0
  67. shikiru/dashboard/views.py +2314 -0
  68. shikiru/dashboard/wizard.py +1389 -0
  69. shikiru/domain/__init__.py +1 -0
  70. shikiru/domain/acceptance.py +130 -0
  71. shikiru/domain/activity.py +28 -0
  72. shikiru/domain/attempts.py +333 -0
  73. shikiru/domain/briefing.py +257 -0
  74. shikiru/domain/commands.py +219 -0
  75. shikiru/domain/decisions.py +231 -0
  76. shikiru/domain/docker.py +262 -0
  77. shikiru/domain/engine_uptime.py +87 -0
  78. shikiru/domain/escalation.py +194 -0
  79. shikiru/domain/escalation_evidence.py +186 -0
  80. shikiru/domain/escalation_panel.py +144 -0
  81. shikiru/domain/events.py +71 -0
  82. shikiru/domain/evidence_export.py +89 -0
  83. shikiru/domain/faults.py +86 -0
  84. shikiru/domain/feedback.py +180 -0
  85. shikiru/domain/feedback_rounds.py +172 -0
  86. shikiru/domain/fencing.py +31 -0
  87. shikiru/domain/ladder.py +102 -0
  88. shikiru/domain/ladder_text.py +44 -0
  89. shikiru/domain/levels.py +44 -0
  90. shikiru/domain/motion.py +199 -0
  91. shikiru/domain/packet_evidence.py +377 -0
  92. shikiru/domain/pushes.py +47 -0
  93. shikiru/domain/quota_blocks.py +44 -0
  94. shikiru/domain/quota_waits.py +219 -0
  95. shikiru/domain/recovery.py +194 -0
  96. shikiru/domain/reports.py +498 -0
  97. shikiru/domain/results.py +217 -0
  98. shikiru/domain/runs.py +85 -0
  99. shikiru/domain/scheduling.py +231 -0
  100. shikiru/domain/status_tasks.py +40 -0
  101. shikiru/domain/steps.py +325 -0
  102. shikiru/domain/tasks.py +174 -0
  103. shikiru/domain/timeline.py +208 -0
  104. shikiru/domain/timeline_phrases.py +271 -0
  105. shikiru/domain/versions.py +18 -0
  106. shikiru/domain/workflow.py +372 -0
  107. shikiru/engine/__init__.py +0 -0
  108. shikiru/engine/acceptance.py +90 -0
  109. shikiru/engine/agents.py +61 -0
  110. shikiru/engine/cosmetic_decorator.py +99 -0
  111. shikiru/engine/daemon.py +409 -0
  112. shikiru/engine/dependency.py +37 -0
  113. shikiru/engine/escalation_filing.py +110 -0
  114. shikiru/engine/escalations.py +401 -0
  115. shikiru/engine/events.py +285 -0
  116. shikiru/engine/evidence.py +81 -0
  117. shikiru/engine/fault_reporter.py +144 -0
  118. shikiru/engine/feedback.py +577 -0
  119. shikiru/engine/feedback_audit.py +80 -0
  120. shikiru/engine/feedback_intake.py +95 -0
  121. shikiru/engine/feedback_snapshot.py +148 -0
  122. shikiru/engine/flow.py +87 -0
  123. shikiru/engine/handlers/__init__.py +33 -0
  124. shikiru/engine/handlers/base.py +148 -0
  125. shikiru/engine/handlers/engine.py +24 -0
  126. shikiru/engine/handlers/escalations.py +174 -0
  127. shikiru/engine/handlers/operator.py +81 -0
  128. shikiru/engine/handlers/tasks.py +237 -0
  129. shikiru/engine/history.py +128 -0
  130. shikiru/engine/integration.py +1234 -0
  131. shikiru/engine/integration_turns.py +337 -0
  132. shikiru/engine/layout_guard.py +362 -0
  133. shikiru/engine/lifecycle.py +60 -0
  134. shikiru/engine/monitor.py +317 -0
  135. shikiru/engine/notification_delivery.py +124 -0
  136. shikiru/engine/notification_feed.py +57 -0
  137. shikiru/engine/notifier.py +91 -0
  138. shikiru/engine/pull_request_closing.py +117 -0
  139. shikiru/engine/push_service.py +177 -0
  140. shikiru/engine/quota_probe_threads.py +79 -0
  141. shikiru/engine/quota_reconciler.py +210 -0
  142. shikiru/engine/quota_schedule.py +24 -0
  143. shikiru/engine/quota_waits.py +102 -0
  144. shikiru/engine/reconcile.py +91 -0
  145. shikiru/engine/recovery.py +284 -0
  146. shikiru/engine/retention.py +102 -0
  147. shikiru/engine/root_preparation.py +86 -0
  148. shikiru/engine/scheduler.py +320 -0
  149. shikiru/engine/session_guard.py +46 -0
  150. shikiru/engine/sessions.py +95 -0
  151. shikiru/engine/signalled.py +75 -0
  152. shikiru/engine/step_runner.py +140 -0
  153. shikiru/engine/supervisor.py +101 -0
  154. shikiru/engine/takeover.py +223 -0
  155. shikiru/engine/task_facts.py +46 -0
  156. shikiru/engine/tick_guard.py +95 -0
  157. shikiru/engine/titles.py +297 -0
  158. shikiru/engine/transitions.py +79 -0
  159. shikiru/engine/turn_context.py +206 -0
  160. shikiru/engine/turn_execution.py +30 -0
  161. shikiru/engine/turn_loop.py +230 -0
  162. shikiru/engine/turn_processes.py +79 -0
  163. shikiru/engine/turns.py +457 -0
  164. shikiru/errors.py +144 -0
  165. shikiru/helper/__init__.py +3 -0
  166. shikiru/helper/helper.py +182 -0
  167. shikiru/helper/models.py +11 -0
  168. shikiru/mcp/__init__.py +1 -0
  169. shikiru/mcp/server.py +196 -0
  170. shikiru/operator/__init__.py +33 -0
  171. shikiru/operator/attachment.py +24 -0
  172. shikiru/operator/commands.py +319 -0
  173. shikiru/operator/detectors.py +374 -0
  174. shikiru/operator/doctor.py +351 -0
  175. shikiru/operator/events.py +57 -0
  176. shikiru/operator/evidence_export.py +498 -0
  177. shikiru/operator/evidence_writer.py +78 -0
  178. shikiru/operator/lifecycle.py +294 -0
  179. shikiru/operator/plugin_checks.py +194 -0
  180. shikiru/operator/probe.py +287 -0
  181. shikiru/operator/queries.py +1601 -0
  182. shikiru/operator/services.py +36 -0
  183. shikiru/operator/sessions.py +378 -0
  184. shikiru/operator/strip_views.py +162 -0
  185. shikiru/operator/timeline.py +416 -0
  186. shikiru/operator/toml_text.py +62 -0
  187. shikiru/palette.py +18 -0
  188. shikiru/paths.py +91 -0
  189. shikiru/plugins/__init__.py +0 -0
  190. shikiru/plugins/agents/__init__.py +1 -0
  191. shikiru/plugins/agents/claude/__init__.py +5 -0
  192. shikiru/plugins/agents/claude/agent.py +251 -0
  193. shikiru/plugins/agents/claude/context.py +116 -0
  194. shikiru/plugins/agents/claude/headless.py +92 -0
  195. shikiru/plugins/agents/claude/hook.py +57 -0
  196. shikiru/plugins/agents/claude/model_catalog.py +105 -0
  197. shikiru/plugins/agents/claude/options.py +62 -0
  198. shikiru/plugins/agents/claude/turn.py +1018 -0
  199. shikiru/plugins/agents/codex/__init__.py +5 -0
  200. shikiru/plugins/agents/codex/agent.py +486 -0
  201. shikiru/plugins/agents/codex/headless.py +166 -0
  202. shikiru/plugins/agents/codex/home.py +14 -0
  203. shikiru/plugins/agents/codex/model_catalog.py +128 -0
  204. shikiru/plugins/agents/codex/options.py +94 -0
  205. shikiru/plugins/agents/codex/quota.py +195 -0
  206. shikiru/plugins/agents/codex/sandbox.py +31 -0
  207. shikiru/plugins/agents/codex/sandbox_probe.py +97 -0
  208. shikiru/plugins/agents/codex/turn.py +767 -0
  209. shikiru/plugins/agents/codex/usage.py +106 -0
  210. shikiru/plugins/agents/shared_settings.py +62 -0
  211. shikiru/plugins/widgets/__init__.py +3 -0
  212. shikiru/plugins/widgets/dialogue.py +144 -0
  213. shikiru/plugins/widgets/disk.py +123 -0
  214. shikiru/plugins/widgets/docker.py +225 -0
  215. shikiru/plugins/widgets/escalations.py +402 -0
  216. shikiru/plugins/widgets/faults.py +204 -0
  217. shikiru/plugins/widgets/header.py +321 -0
  218. shikiru/plugins/widgets/queue.py +1140 -0
  219. shikiru/plugins/widgets/queue_hint.py +93 -0
  220. shikiru/plugins/widgets/resources.py +105 -0
  221. shikiru/plugins/widgets/strips.py +82 -0
  222. shikiru/plugins/widgets/usage.py +176 -0
  223. shikiru/private.py +94 -0
  224. shikiru/process.py +535 -0
  225. shikiru/prompts/__init__.py +3 -0
  226. shikiru/prompts/renderer.py +85 -0
  227. shikiru/prompts/templates/_base.md.j2 +17 -0
  228. shikiru/prompts/templates/_macros.md.j2 +61 -0
  229. shikiru/prompts/templates/conflicts.md.j2 +22 -0
  230. shikiru/prompts/templates/consult.md.j2 +5 -0
  231. shikiru/prompts/templates/continue.md.j2 +4 -0
  232. shikiru/prompts/templates/feedback_analysis.md.j2 +29 -0
  233. shikiru/prompts/templates/feedback_rework.md.j2 +33 -0
  234. shikiru/prompts/templates/feedback_verify.md.j2 +33 -0
  235. shikiru/prompts/templates/handoff.md.j2 +23 -0
  236. shikiru/prompts/templates/implement.md.j2 +13 -0
  237. shikiru/prompts/templates/plan.md.j2 +30 -0
  238. shikiru/prompts/templates/rework.md.j2 +13 -0
  239. shikiru/prompts/templates/triage.md.j2 +20 -0
  240. shikiru/prompts/templates/verify.md.j2 +29 -0
  241. shikiru/store/__init__.py +1 -0
  242. shikiru/store/commands.py +242 -0
  243. shikiru/store/database.py +297 -0
  244. shikiru/store/escalations.py +240 -0
  245. shikiru/store/events.py +213 -0
  246. shikiru/store/feedback.py +266 -0
  247. shikiru/store/migrations/001_initial.sql +63 -0
  248. shikiru/store/migrations/002_headless_turns.sql +31 -0
  249. shikiru/store/migrations/003_drop_command_handled_at.sql +1 -0
  250. shikiru/store/migrations/004_repo_base_ref.sql +3 -0
  251. shikiru/store/migrations/005_complexity_explicit.sql +3 -0
  252. shikiru/store/migrations/006_events_kind_index.sql +1 -0
  253. shikiru/store/migrations/007_notification_cursor.sql +1 -0
  254. shikiru/store/migrations/008_reported_usage.sql +1 -0
  255. shikiru/store/migrations/009_billing_declarations.sql +2 -0
  256. shikiru/store/migrations/010_canonical_quota_windows.sql +3 -0
  257. shikiru/store/migrations/012_drop_task_quota_evidence.sql +2 -0
  258. shikiru/store/migrations/013_push_targets.sql +7 -0
  259. shikiru/store/migrations/014_agent_block_time.sql +6 -0
  260. shikiru/store/migrations/015_repair_orphan_agent_blocks.sql +3 -0
  261. shikiru/store/migrations/016_feedback_rounds.sql +29 -0
  262. shikiru/store/migrations/017_feedback_round_comments.sql +5 -0
  263. shikiru/store/migrations/__init__.py +1 -0
  264. shikiru/store/notification_cursor.py +29 -0
  265. shikiru/store/pushes.py +117 -0
  266. shikiru/store/quotas.py +153 -0
  267. shikiru/store/reader.py +310 -0
  268. shikiru/store/retention.py +92 -0
  269. shikiru/store/runs.py +227 -0
  270. shikiru/store/tasks.py +949 -0
  271. shikiru/telemetry/__init__.py +1 -0
  272. shikiru/telemetry/disk.py +167 -0
  273. shikiru/telemetry/docker.py +164 -0
  274. shikiru/telemetry/resources.py +296 -0
  275. shikiru/tmux/__init__.py +3 -0
  276. shikiru/tmux/client.py +637 -0
  277. shikiru/tmux/decorator.py +225 -0
  278. shikiru/tmux/layout.py +200 -0
  279. shikiru/tmux/session.py +381 -0
  280. shikiru/ui/__init__.py +9 -0
  281. shikiru/ui/age.py +45 -0
  282. shikiru/ui/bubbles.py +41 -0
  283. shikiru/ui/compact.py +1209 -0
  284. shikiru/ui/console.py +49 -0
  285. shikiru/ui/fold.py +50 -0
  286. shikiru/ui/gated_static.py +117 -0
  287. shikiru/ui/gauge.py +40 -0
  288. shikiru/ui/glyphs.py +109 -0
  289. shikiru/ui/keys.py +57 -0
  290. shikiru/ui/live_interval.py +42 -0
  291. shikiru/ui/popup.py +311 -0
  292. shikiru/ui/quota_status.py +26 -0
  293. shikiru/ui/recent_window.py +78 -0
  294. shikiru/ui/screen.py +57 -0
  295. shikiru/ui/section.py +193 -0
  296. shikiru/ui/session.py +65 -0
  297. shikiru/ui/size.py +17 -0
  298. shikiru/ui/splash.py +42 -0
  299. shikiru/ui/status.py +47 -0
  300. shikiru/ui/status_strip.py +149 -0
  301. shikiru/ui/stream.py +152 -0
  302. shikiru/ui/strip.py +332 -0
  303. shikiru/ui/theme.py +83 -0
  304. shikiru/ui/tokens.py +51 -0
  305. shikiru/view_log.py +126 -0
  306. shikiru/workspace/__init__.py +3 -0
  307. shikiru/workspace/base_refs.py +160 -0
  308. shikiru/workspace/caches.py +136 -0
  309. shikiru/workspace/facts.py +118 -0
  310. shikiru/workspace/git.py +601 -0
  311. shikiru/workspace/github.py +744 -0
  312. shikiru/workspace/messages.py +107 -0
  313. shikiru/workspace/reports.py +145 -0
  314. shikiru/workspace/roots.py +180 -0
  315. shikiru_cli-0.2.0.dist-info/METADATA +428 -0
  316. shikiru_cli-0.2.0.dist-info/RECORD +319 -0
  317. shikiru_cli-0.2.0.dist-info/WHEEL +4 -0
  318. shikiru_cli-0.2.0.dist-info/entry_points.txt +18 -0
  319. shikiru_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,321 @@
1
+ """The `header` widget: project, engine state, slots and repo."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, replace
6
+ from typing import TYPE_CHECKING
7
+
8
+ from rich.cells import cell_len
9
+ from rich.text import Text
10
+
11
+ from shikiru.dashboard.contract import DiagnosticSampleState, PublicationState, WidgetPlacement
12
+ from shikiru.ui.age import format_duration
13
+ from shikiru.ui.gated_static import GatedStatic
14
+ from shikiru.ui.screen import widget_is_attached, widget_is_current
15
+ from shikiru.ui.tokens import (
16
+ ALERT_COLOR,
17
+ AMBER,
18
+ PRIMARY_TEXT_COLOR,
19
+ SAGE,
20
+ SECONDARY_TEXT_COLOR,
21
+ STEEL_BLUE,
22
+ STYLE_BOLD,
23
+ SUCCESS_COLOR,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from collections.abc import Callable
28
+
29
+ from rich.console import Console, ConsoleOptions, RenderResult
30
+ from textual import events
31
+ from textual.widget import Widget
32
+
33
+ from shikiru.dashboard.contract import (
34
+ HeaderView,
35
+ PublicationView,
36
+ WidgetContext,
37
+ WorkspaceSummaryView,
38
+ )
39
+
40
+
41
+ MIN_HEADER_GAP = 2
42
+ RUNTIME_SEPARATOR_WIDTH = 3
43
+ ENGINE_RUNNING_MARKER = "●"
44
+ ENGINE_STOPPED_MARKER = "○"
45
+ MERGE_INTEGRATION = "merge"
46
+ SQUASH_INTEGRATION = "squash"
47
+ PULL_REQUEST_INTEGRATION = "pr"
48
+ IN_SYNC_MARKER = "✓"
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class _HeaderContent:
53
+ view: HeaderView
54
+ summary: WorkspaceSummaryView
55
+ include_alerts: bool
56
+
57
+ @property
58
+ def render_key(self) -> object:
59
+ return (
60
+ replace(self.view, engine_uptime_seconds=None, previous_engine_run_seconds=None),
61
+ self.summary,
62
+ self.include_alerts,
63
+ _runtime_label(self.view),
64
+ )
65
+
66
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
67
+ del console
68
+ yield _home_header_text(
69
+ self.view,
70
+ self.summary,
71
+ include_alerts=self.include_alerts,
72
+ width=options.max_width,
73
+ )
74
+
75
+
76
+ def _home_header_text(
77
+ view: HeaderView,
78
+ summary: WorkspaceSummaryView,
79
+ *,
80
+ include_alerts: bool,
81
+ width: int,
82
+ ) -> Text:
83
+ identity = f"SHIKIRU {view.project}"
84
+ if view.repo.root is not None:
85
+ identity = f"{identity} ({view.repo.root})"
86
+ identity_width = cell_len(identity)
87
+ engine_label = (
88
+ f"{ENGINE_RUNNING_MARKER} Engine running"
89
+ if view.engine_up
90
+ else f"{ENGINE_STOPPED_MARKER} Engine stopped"
91
+ )
92
+ runtime_label = _runtime_label(view)
93
+ runtime_width = cell_len(engine_label) + cell_len(runtime_label) + RUNTIME_SEPARATOR_WIDTH
94
+ checkout = _checkout_row(view)
95
+ compact_header = identity_width + runtime_width + MIN_HEADER_GAP > width
96
+ identity_text = Text.assemble(
97
+ ("SHIKIRU", f"{STYLE_BOLD} {STEEL_BLUE}"),
98
+ (f" {view.project}", PRIMARY_TEXT_COLOR),
99
+ )
100
+ if view.repo.root is not None:
101
+ identity_text.append(f" ({view.repo.root})", style=SECONDARY_TEXT_COLOR)
102
+ rendered = _fold_text(identity_text, width)
103
+ if compact_header:
104
+ rendered.append("\n")
105
+ rendered.append_text(_runtime_text(engine_label, runtime_label, view.engine_up))
106
+ rendered.append("\n")
107
+ rendered.append_text(checkout)
108
+ else:
109
+ gap = " " * max(MIN_HEADER_GAP, width - identity_width - runtime_width)
110
+ rendered.append(gap)
111
+ rendered.append_text(_runtime_text(engine_label, runtime_label, view.engine_up))
112
+ rendered.append("\n")
113
+ rendered.append_text(checkout)
114
+ rendered.append("\n")
115
+ if view.repo.state is DiagnosticSampleState.STALE:
116
+ age = view.repo.sampled_ago or "age unavailable"
117
+ rendered.append(f"Checkout stale · sampled {age}", style=SECONDARY_TEXT_COLOR)
118
+ rendered.append("\n")
119
+ if view.repo.error is not None:
120
+ rendered.append(f"Checkout read failed · {view.repo.error}", style=ALERT_COLOR)
121
+ rendered.append("\n")
122
+ elif view.repo.state is DiagnosticSampleState.FAILED:
123
+ rendered.append("Checkout unavailable", style=SECONDARY_TEXT_COLOR)
124
+ if view.repo.error is not None:
125
+ rendered.append(f" · {view.repo.error}", style=ALERT_COLOR)
126
+ rendered.append("\n")
127
+ rendered.append("\n")
128
+ rendered.append_text(
129
+ Text.assemble(
130
+ ("Slots ", SECONDARY_TEXT_COLOR),
131
+ f"{view.occupied_slots}/{view.maximum_slots} · ",
132
+ ("Running ", SECONDARY_TEXT_COLOR),
133
+ f"{summary.running} · ",
134
+ ("Waiting ", SECONDARY_TEXT_COLOR),
135
+ f"{summary.waiting}",
136
+ )
137
+ )
138
+ rendered.append("\n")
139
+ if include_alerts:
140
+ for alert in view.alerts:
141
+ rendered.append(alert, style=ALERT_COLOR)
142
+ rendered.append("\n")
143
+ return rendered
144
+
145
+
146
+ def _fold_text(value: Text, width: int) -> Text:
147
+ if width <= 0:
148
+ return value
149
+ offsets: list[int] = []
150
+ current_width = 0
151
+ for index, character in enumerate(value.plain):
152
+ character_width = cell_len(character)
153
+ if current_width and current_width + character_width > width:
154
+ offsets.append(index)
155
+ current_width = 0
156
+ current_width += character_width
157
+ rendered = Text()
158
+ for index, line in enumerate(value.divide(offsets)):
159
+ if index:
160
+ rendered.append("\n")
161
+ rendered.append_text(line)
162
+ return rendered
163
+
164
+
165
+ def _runtime_text(engine: str, runtime: str, running: bool) -> Text:
166
+ rendered = Text(engine, style=SAGE if running else ALERT_COLOR)
167
+ rendered.append(f" · {runtime}", style=SECONDARY_TEXT_COLOR)
168
+ return rendered
169
+
170
+
171
+ def _runtime_label(view: HeaderView) -> str:
172
+ if view.engine_up:
173
+ if view.engine_uptime_seconds is None:
174
+ return "Uptime unavailable"
175
+ return f"Uptime {format_duration(view.engine_uptime_seconds)}"
176
+ if view.previous_engine_run_seconds is None:
177
+ return "Last unavailable"
178
+ return f"Last {format_duration(view.previous_engine_run_seconds)}"
179
+
180
+
181
+ def _checkout_text(view: HeaderView) -> Text:
182
+ repo = view.repo
183
+ if repo.state is DiagnosticSampleState.FAILED or repo.branch is None:
184
+ rendered = Text("branch unavailable", style=SECONDARY_TEXT_COLOR)
185
+ elif repo.dirty == 0:
186
+ state = " (previously clean)" if repo.state is DiagnosticSampleState.STALE else " (clean)"
187
+ rendered = Text.assemble((repo.branch, PRIMARY_TEXT_COLOR), (state, PRIMARY_TEXT_COLOR))
188
+ elif repo.dirty is None or repo.added_lines is None or repo.deleted_lines is None:
189
+ rendered = Text(f"{repo.branch} (line counts unavailable)", style=AMBER)
190
+ else:
191
+ rendered = Text.assemble(
192
+ (f"{repo.branch} (", AMBER),
193
+ (f"+{repo.added_lines}", SUCCESS_COLOR),
194
+ (" ", AMBER),
195
+ (f"-{repo.deleted_lines}", ALERT_COLOR),
196
+ (")", AMBER),
197
+ )
198
+ rendered.append(f" · {_integration_label(view.integration_mode)}", style=SECONDARY_TEXT_COLOR)
199
+ if (
200
+ repo.state is not DiagnosticSampleState.FAILED
201
+ and repo.branch is not None
202
+ and repo.dirty != 0
203
+ ):
204
+ if repo.binary_entries is None:
205
+ rendered.append(" · binary count unavailable", style=SECONDARY_TEXT_COLOR)
206
+ elif repo.binary_entries:
207
+ rendered.append(f" · {repo.binary_entries} binary uncounted", style=AMBER)
208
+ if repo.untracked_paths is None:
209
+ rendered.append(" · untracked count unavailable", style=SECONDARY_TEXT_COLOR)
210
+ elif repo.untracked_paths:
211
+ rendered.append(
212
+ f" · {repo.untracked_paths} untracked paths excluded from line totals",
213
+ style=AMBER,
214
+ )
215
+ if (
216
+ repo.dirty is not None
217
+ and repo.dirty > 0
218
+ and repo.added_lines == 0
219
+ and repo.deleted_lines == 0
220
+ and not repo.binary_entries
221
+ and not repo.untracked_paths
222
+ ):
223
+ rendered.append(f" · {repo.dirty} changed paths", style=AMBER)
224
+ return rendered
225
+
226
+
227
+ def _checkout_row(view: HeaderView) -> Text:
228
+ rendered = _checkout_text(view)
229
+ rendered.append_text(
230
+ _publication_text(
231
+ view.publication,
232
+ stale=view.repo.state is DiagnosticSampleState.STALE,
233
+ )
234
+ )
235
+ return rendered
236
+
237
+
238
+ def _publication_text(publication: PublicationView, *, stale: bool) -> Text:
239
+ rendered = Text(" · ", style=SECONDARY_TEXT_COLOR)
240
+ if publication.state is PublicationState.IN_SYNC:
241
+ rendered.append(IN_SYNC_MARKER, style=SUCCESS_COLOR)
242
+ rendered.append(f" in sync with {publication.remote}", style=PRIMARY_TEXT_COLOR)
243
+ elif publication.state is PublicationState.AWAITING_PUSH:
244
+ rendered.append(str(publication.unpushed), style=f"{STYLE_BOLD} {PRIMARY_TEXT_COLOR}")
245
+ rendered.append(f" behind {publication.remote}", style=PRIMARY_TEXT_COLOR)
246
+ elif publication.state is PublicationState.NOT_IN_SYNC:
247
+ rendered.append(f"not in sync with {publication.remote}", style=AMBER)
248
+ else:
249
+ rendered.append(f"{publication.remote} unavailable", style=SECONDARY_TEXT_COLOR)
250
+ if stale and publication.state is not PublicationState.UNAVAILABLE:
251
+ rendered.append(" (previously)", style=SECONDARY_TEXT_COLOR)
252
+ if publication.push_time is not None:
253
+ rendered.append(f" · push at {publication.push_time}", style=SECONDARY_TEXT_COLOR)
254
+ return rendered
255
+
256
+
257
+ def _integration_label(value: str) -> str:
258
+ if value == MERGE_INTEGRATION:
259
+ return "Local merge"
260
+ if value == SQUASH_INTEGRATION:
261
+ return "Local squash and merge"
262
+ if value == PULL_REQUEST_INTEGRATION:
263
+ return "Pull request"
264
+ return value
265
+
266
+
267
+ class HeaderPanel(GatedStatic):
268
+ """Shows project, engine, task counts and checkout identity."""
269
+
270
+ def __init__(self, context: WidgetContext) -> None:
271
+ """Hold `context`; nothing here does I/O until `on_mount`."""
272
+ super().__init__(id="header")
273
+ self._widget_context = context
274
+ self._remove_motion_callback: Callable[[], None] | None = None
275
+
276
+ async def on_mount(self) -> None:
277
+ """Render the current view, then refresh on every state change."""
278
+ await self._refresh()
279
+ self.run_worker(self._widget_context.on_change(self._refresh_if_attached), exclusive=True)
280
+ self._remove_motion_callback = self._widget_context.on_motion(self._refresh_if_active)
281
+
282
+ def on_unmount(self) -> None:
283
+ if self._remove_motion_callback is not None:
284
+ self._remove_motion_callback()
285
+ self._remove_motion_callback = None
286
+
287
+ async def _refresh(self) -> None:
288
+ view = await self._widget_context.views.header()
289
+ if not widget_is_attached(self):
290
+ return
291
+ summary = self._widget_context.views.workspace_summary()
292
+ include_alerts = (
293
+ self._widget_context.placement is not WidgetPlacement.IDENTITY
294
+ or not self._widget_context.home_attention_present
295
+ )
296
+ content = _HeaderContent(view, summary, include_alerts)
297
+ self.update_if_changed(content, key=content.render_key)
298
+
299
+ async def on_resize(self, event: events.Resize) -> None:
300
+ del event
301
+ if widget_is_current(self):
302
+ await self._refresh()
303
+
304
+ async def _refresh_if_active(self) -> None:
305
+ if widget_is_current(self):
306
+ await self._refresh()
307
+
308
+ async def _refresh_if_attached(self) -> None:
309
+ if widget_is_attached(self):
310
+ await self._refresh()
311
+
312
+
313
+ class HeaderWidget:
314
+ """The `header` dashboard widget plugin."""
315
+
316
+ name = "header"
317
+ placement = WidgetPlacement.IDENTITY
318
+
319
+ def create(self, context: WidgetContext) -> Widget:
320
+ """Build the `HeaderPanel` for `context`."""
321
+ return HeaderPanel(context)