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.
- shikiru/__init__.py +1 -0
- shikiru/__main__.py +8 -0
- shikiru/agents/__init__.py +3 -0
- shikiru/agents/contracts.py +590 -0
- shikiru/agents/registry.py +87 -0
- shikiru/agents/sdk/__init__.py +17 -0
- shikiru/agents/sdk/reset_time.py +137 -0
- shikiru/agents/sdk/turn.py +335 -0
- shikiru/agents/settings.py +151 -0
- shikiru/agents/testing/__init__.py +26 -0
- shikiru/agents/testing/contract_suite.py +653 -0
- shikiru/agents/testing/resume.py +15 -0
- shikiru/bootstrap.py +1277 -0
- shikiru/cli/__init__.py +1 -0
- shikiru/cli/app.py +770 -0
- shikiru/cli/context.py +63 -0
- shikiru/cli/render.py +1475 -0
- shikiru/cli/report.py +156 -0
- shikiru/cli/session_prompt.py +63 -0
- shikiru/cli/view.py +131 -0
- shikiru/clock.py +41 -0
- shikiru/config/__init__.py +8 -0
- shikiru/config/defaults.toml +131 -0
- shikiru/config/loader.py +178 -0
- shikiru/config/settings.py +392 -0
- shikiru/dashboard/__init__.py +3 -0
- shikiru/dashboard/actions_list.py +799 -0
- shikiru/dashboard/activity.py +150 -0
- shikiru/dashboard/app.py +1765 -0
- shikiru/dashboard/blocker_evidence.py +122 -0
- shikiru/dashboard/blocker_rows.py +84 -0
- shikiru/dashboard/briefing_view.py +871 -0
- shikiru/dashboard/contract.py +1086 -0
- shikiru/dashboard/covered.py +32 -0
- shikiru/dashboard/detail.py +1209 -0
- shikiru/dashboard/detail_fold.py +244 -0
- shikiru/dashboard/detail_hint.py +68 -0
- shikiru/dashboard/details_list_view.py +150 -0
- shikiru/dashboard/diagnostics.py +80 -0
- shikiru/dashboard/diagnostics_sections.py +186 -0
- shikiru/dashboard/evidence_table.py +123 -0
- shikiru/dashboard/extra_details.py +214 -0
- shikiru/dashboard/fault_guidance.py +216 -0
- shikiru/dashboard/fold_bodies.py +276 -0
- shikiru/dashboard/help_keys.py +166 -0
- shikiru/dashboard/help_screen.py +204 -0
- shikiru/dashboard/interaction.py +17 -0
- shikiru/dashboard/ladder_editor.py +544 -0
- shikiru/dashboard/live_settings.py +62 -0
- shikiru/dashboard/motion.py +368 -0
- shikiru/dashboard/orchestration_fields.py +201 -0
- shikiru/dashboard/palette.py +1606 -0
- shikiru/dashboard/palette_app.py +89 -0
- shikiru/dashboard/palette_drafts.py +33 -0
- shikiru/dashboard/resolution.py +573 -0
- shikiru/dashboard/resolution_view.py +302 -0
- shikiru/dashboard/row_list.py +256 -0
- shikiru/dashboard/section_rows.py +173 -0
- shikiru/dashboard/services.py +665 -0
- shikiru/dashboard/session_history.py +402 -0
- shikiru/dashboard/shikiru_init_ascii_art.txt +40 -0
- shikiru/dashboard/task_picker.py +358 -0
- shikiru/dashboard/tick.py +59 -0
- shikiru/dashboard/timeline_filter.py +129 -0
- shikiru/dashboard/timeline_section.py +385 -0
- shikiru/dashboard/usage_body.py +269 -0
- shikiru/dashboard/views.py +2314 -0
- shikiru/dashboard/wizard.py +1389 -0
- shikiru/domain/__init__.py +1 -0
- shikiru/domain/acceptance.py +130 -0
- shikiru/domain/activity.py +28 -0
- shikiru/domain/attempts.py +333 -0
- shikiru/domain/briefing.py +257 -0
- shikiru/domain/commands.py +219 -0
- shikiru/domain/decisions.py +231 -0
- shikiru/domain/docker.py +262 -0
- shikiru/domain/engine_uptime.py +87 -0
- shikiru/domain/escalation.py +194 -0
- shikiru/domain/escalation_evidence.py +186 -0
- shikiru/domain/escalation_panel.py +144 -0
- shikiru/domain/events.py +71 -0
- shikiru/domain/evidence_export.py +89 -0
- shikiru/domain/faults.py +86 -0
- shikiru/domain/feedback.py +180 -0
- shikiru/domain/feedback_rounds.py +172 -0
- shikiru/domain/fencing.py +31 -0
- shikiru/domain/ladder.py +102 -0
- shikiru/domain/ladder_text.py +44 -0
- shikiru/domain/levels.py +44 -0
- shikiru/domain/motion.py +199 -0
- shikiru/domain/packet_evidence.py +377 -0
- shikiru/domain/pushes.py +47 -0
- shikiru/domain/quota_blocks.py +44 -0
- shikiru/domain/quota_waits.py +219 -0
- shikiru/domain/recovery.py +194 -0
- shikiru/domain/reports.py +498 -0
- shikiru/domain/results.py +217 -0
- shikiru/domain/runs.py +85 -0
- shikiru/domain/scheduling.py +231 -0
- shikiru/domain/status_tasks.py +40 -0
- shikiru/domain/steps.py +325 -0
- shikiru/domain/tasks.py +174 -0
- shikiru/domain/timeline.py +208 -0
- shikiru/domain/timeline_phrases.py +271 -0
- shikiru/domain/versions.py +18 -0
- shikiru/domain/workflow.py +372 -0
- shikiru/engine/__init__.py +0 -0
- shikiru/engine/acceptance.py +90 -0
- shikiru/engine/agents.py +61 -0
- shikiru/engine/cosmetic_decorator.py +99 -0
- shikiru/engine/daemon.py +409 -0
- shikiru/engine/dependency.py +37 -0
- shikiru/engine/escalation_filing.py +110 -0
- shikiru/engine/escalations.py +401 -0
- shikiru/engine/events.py +285 -0
- shikiru/engine/evidence.py +81 -0
- shikiru/engine/fault_reporter.py +144 -0
- shikiru/engine/feedback.py +577 -0
- shikiru/engine/feedback_audit.py +80 -0
- shikiru/engine/feedback_intake.py +95 -0
- shikiru/engine/feedback_snapshot.py +148 -0
- shikiru/engine/flow.py +87 -0
- shikiru/engine/handlers/__init__.py +33 -0
- shikiru/engine/handlers/base.py +148 -0
- shikiru/engine/handlers/engine.py +24 -0
- shikiru/engine/handlers/escalations.py +174 -0
- shikiru/engine/handlers/operator.py +81 -0
- shikiru/engine/handlers/tasks.py +237 -0
- shikiru/engine/history.py +128 -0
- shikiru/engine/integration.py +1234 -0
- shikiru/engine/integration_turns.py +337 -0
- shikiru/engine/layout_guard.py +362 -0
- shikiru/engine/lifecycle.py +60 -0
- shikiru/engine/monitor.py +317 -0
- shikiru/engine/notification_delivery.py +124 -0
- shikiru/engine/notification_feed.py +57 -0
- shikiru/engine/notifier.py +91 -0
- shikiru/engine/pull_request_closing.py +117 -0
- shikiru/engine/push_service.py +177 -0
- shikiru/engine/quota_probe_threads.py +79 -0
- shikiru/engine/quota_reconciler.py +210 -0
- shikiru/engine/quota_schedule.py +24 -0
- shikiru/engine/quota_waits.py +102 -0
- shikiru/engine/reconcile.py +91 -0
- shikiru/engine/recovery.py +284 -0
- shikiru/engine/retention.py +102 -0
- shikiru/engine/root_preparation.py +86 -0
- shikiru/engine/scheduler.py +320 -0
- shikiru/engine/session_guard.py +46 -0
- shikiru/engine/sessions.py +95 -0
- shikiru/engine/signalled.py +75 -0
- shikiru/engine/step_runner.py +140 -0
- shikiru/engine/supervisor.py +101 -0
- shikiru/engine/takeover.py +223 -0
- shikiru/engine/task_facts.py +46 -0
- shikiru/engine/tick_guard.py +95 -0
- shikiru/engine/titles.py +297 -0
- shikiru/engine/transitions.py +79 -0
- shikiru/engine/turn_context.py +206 -0
- shikiru/engine/turn_execution.py +30 -0
- shikiru/engine/turn_loop.py +230 -0
- shikiru/engine/turn_processes.py +79 -0
- shikiru/engine/turns.py +457 -0
- shikiru/errors.py +144 -0
- shikiru/helper/__init__.py +3 -0
- shikiru/helper/helper.py +182 -0
- shikiru/helper/models.py +11 -0
- shikiru/mcp/__init__.py +1 -0
- shikiru/mcp/server.py +196 -0
- shikiru/operator/__init__.py +33 -0
- shikiru/operator/attachment.py +24 -0
- shikiru/operator/commands.py +319 -0
- shikiru/operator/detectors.py +374 -0
- shikiru/operator/doctor.py +351 -0
- shikiru/operator/events.py +57 -0
- shikiru/operator/evidence_export.py +498 -0
- shikiru/operator/evidence_writer.py +78 -0
- shikiru/operator/lifecycle.py +294 -0
- shikiru/operator/plugin_checks.py +194 -0
- shikiru/operator/probe.py +287 -0
- shikiru/operator/queries.py +1601 -0
- shikiru/operator/services.py +36 -0
- shikiru/operator/sessions.py +378 -0
- shikiru/operator/strip_views.py +162 -0
- shikiru/operator/timeline.py +416 -0
- shikiru/operator/toml_text.py +62 -0
- shikiru/palette.py +18 -0
- shikiru/paths.py +91 -0
- shikiru/plugins/__init__.py +0 -0
- shikiru/plugins/agents/__init__.py +1 -0
- shikiru/plugins/agents/claude/__init__.py +5 -0
- shikiru/plugins/agents/claude/agent.py +251 -0
- shikiru/plugins/agents/claude/context.py +116 -0
- shikiru/plugins/agents/claude/headless.py +92 -0
- shikiru/plugins/agents/claude/hook.py +57 -0
- shikiru/plugins/agents/claude/model_catalog.py +105 -0
- shikiru/plugins/agents/claude/options.py +62 -0
- shikiru/plugins/agents/claude/turn.py +1018 -0
- shikiru/plugins/agents/codex/__init__.py +5 -0
- shikiru/plugins/agents/codex/agent.py +486 -0
- shikiru/plugins/agents/codex/headless.py +166 -0
- shikiru/plugins/agents/codex/home.py +14 -0
- shikiru/plugins/agents/codex/model_catalog.py +128 -0
- shikiru/plugins/agents/codex/options.py +94 -0
- shikiru/plugins/agents/codex/quota.py +195 -0
- shikiru/plugins/agents/codex/sandbox.py +31 -0
- shikiru/plugins/agents/codex/sandbox_probe.py +97 -0
- shikiru/plugins/agents/codex/turn.py +767 -0
- shikiru/plugins/agents/codex/usage.py +106 -0
- shikiru/plugins/agents/shared_settings.py +62 -0
- shikiru/plugins/widgets/__init__.py +3 -0
- shikiru/plugins/widgets/dialogue.py +144 -0
- shikiru/plugins/widgets/disk.py +123 -0
- shikiru/plugins/widgets/docker.py +225 -0
- shikiru/plugins/widgets/escalations.py +402 -0
- shikiru/plugins/widgets/faults.py +204 -0
- shikiru/plugins/widgets/header.py +321 -0
- shikiru/plugins/widgets/queue.py +1140 -0
- shikiru/plugins/widgets/queue_hint.py +93 -0
- shikiru/plugins/widgets/resources.py +105 -0
- shikiru/plugins/widgets/strips.py +82 -0
- shikiru/plugins/widgets/usage.py +176 -0
- shikiru/private.py +94 -0
- shikiru/process.py +535 -0
- shikiru/prompts/__init__.py +3 -0
- shikiru/prompts/renderer.py +85 -0
- shikiru/prompts/templates/_base.md.j2 +17 -0
- shikiru/prompts/templates/_macros.md.j2 +61 -0
- shikiru/prompts/templates/conflicts.md.j2 +22 -0
- shikiru/prompts/templates/consult.md.j2 +5 -0
- shikiru/prompts/templates/continue.md.j2 +4 -0
- shikiru/prompts/templates/feedback_analysis.md.j2 +29 -0
- shikiru/prompts/templates/feedback_rework.md.j2 +33 -0
- shikiru/prompts/templates/feedback_verify.md.j2 +33 -0
- shikiru/prompts/templates/handoff.md.j2 +23 -0
- shikiru/prompts/templates/implement.md.j2 +13 -0
- shikiru/prompts/templates/plan.md.j2 +30 -0
- shikiru/prompts/templates/rework.md.j2 +13 -0
- shikiru/prompts/templates/triage.md.j2 +20 -0
- shikiru/prompts/templates/verify.md.j2 +29 -0
- shikiru/store/__init__.py +1 -0
- shikiru/store/commands.py +242 -0
- shikiru/store/database.py +297 -0
- shikiru/store/escalations.py +240 -0
- shikiru/store/events.py +213 -0
- shikiru/store/feedback.py +266 -0
- shikiru/store/migrations/001_initial.sql +63 -0
- shikiru/store/migrations/002_headless_turns.sql +31 -0
- shikiru/store/migrations/003_drop_command_handled_at.sql +1 -0
- shikiru/store/migrations/004_repo_base_ref.sql +3 -0
- shikiru/store/migrations/005_complexity_explicit.sql +3 -0
- shikiru/store/migrations/006_events_kind_index.sql +1 -0
- shikiru/store/migrations/007_notification_cursor.sql +1 -0
- shikiru/store/migrations/008_reported_usage.sql +1 -0
- shikiru/store/migrations/009_billing_declarations.sql +2 -0
- shikiru/store/migrations/010_canonical_quota_windows.sql +3 -0
- shikiru/store/migrations/012_drop_task_quota_evidence.sql +2 -0
- shikiru/store/migrations/013_push_targets.sql +7 -0
- shikiru/store/migrations/014_agent_block_time.sql +6 -0
- shikiru/store/migrations/015_repair_orphan_agent_blocks.sql +3 -0
- shikiru/store/migrations/016_feedback_rounds.sql +29 -0
- shikiru/store/migrations/017_feedback_round_comments.sql +5 -0
- shikiru/store/migrations/__init__.py +1 -0
- shikiru/store/notification_cursor.py +29 -0
- shikiru/store/pushes.py +117 -0
- shikiru/store/quotas.py +153 -0
- shikiru/store/reader.py +310 -0
- shikiru/store/retention.py +92 -0
- shikiru/store/runs.py +227 -0
- shikiru/store/tasks.py +949 -0
- shikiru/telemetry/__init__.py +1 -0
- shikiru/telemetry/disk.py +167 -0
- shikiru/telemetry/docker.py +164 -0
- shikiru/telemetry/resources.py +296 -0
- shikiru/tmux/__init__.py +3 -0
- shikiru/tmux/client.py +637 -0
- shikiru/tmux/decorator.py +225 -0
- shikiru/tmux/layout.py +200 -0
- shikiru/tmux/session.py +381 -0
- shikiru/ui/__init__.py +9 -0
- shikiru/ui/age.py +45 -0
- shikiru/ui/bubbles.py +41 -0
- shikiru/ui/compact.py +1209 -0
- shikiru/ui/console.py +49 -0
- shikiru/ui/fold.py +50 -0
- shikiru/ui/gated_static.py +117 -0
- shikiru/ui/gauge.py +40 -0
- shikiru/ui/glyphs.py +109 -0
- shikiru/ui/keys.py +57 -0
- shikiru/ui/live_interval.py +42 -0
- shikiru/ui/popup.py +311 -0
- shikiru/ui/quota_status.py +26 -0
- shikiru/ui/recent_window.py +78 -0
- shikiru/ui/screen.py +57 -0
- shikiru/ui/section.py +193 -0
- shikiru/ui/session.py +65 -0
- shikiru/ui/size.py +17 -0
- shikiru/ui/splash.py +42 -0
- shikiru/ui/status.py +47 -0
- shikiru/ui/status_strip.py +149 -0
- shikiru/ui/stream.py +152 -0
- shikiru/ui/strip.py +332 -0
- shikiru/ui/theme.py +83 -0
- shikiru/ui/tokens.py +51 -0
- shikiru/view_log.py +126 -0
- shikiru/workspace/__init__.py +3 -0
- shikiru/workspace/base_refs.py +160 -0
- shikiru/workspace/caches.py +136 -0
- shikiru/workspace/facts.py +118 -0
- shikiru/workspace/git.py +601 -0
- shikiru/workspace/github.py +744 -0
- shikiru/workspace/messages.py +107 -0
- shikiru/workspace/reports.py +145 -0
- shikiru/workspace/roots.py +180 -0
- shikiru_cli-0.2.0.dist-info/METADATA +428 -0
- shikiru_cli-0.2.0.dist-info/RECORD +319 -0
- shikiru_cli-0.2.0.dist-info/WHEEL +4 -0
- shikiru_cli-0.2.0.dist-info/entry_points.txt +18 -0
- 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)
|