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,122 @@
|
|
|
1
|
+
"""The evidence disclosure of a task's Blockers: quota readings, notes and raw events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from rich.console import Group
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from shikiru.clock import to_local
|
|
11
|
+
from shikiru.domain.levels import displayed_pct
|
|
12
|
+
from shikiru.domain.quota_waits import (
|
|
13
|
+
QUOTA_NOT_REPORTED,
|
|
14
|
+
provider_reset_text,
|
|
15
|
+
quota_window_label,
|
|
16
|
+
)
|
|
17
|
+
from shikiru.ui.age import AGE_NOW, format_short_age
|
|
18
|
+
from shikiru.ui.section import grey
|
|
19
|
+
from shikiru.ui.tokens import SECONDARY_TEXT_COLOR
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from collections.abc import Sequence
|
|
23
|
+
from datetime import datetime, timedelta
|
|
24
|
+
|
|
25
|
+
from rich.console import RenderableType
|
|
26
|
+
from rich.text import Text
|
|
27
|
+
|
|
28
|
+
from shikiru.dashboard.contract import BlockerEvidenceView, QuotaEvidenceView, QuotaReadingView
|
|
29
|
+
|
|
30
|
+
_OBSERVED_PREFIX = "observed"
|
|
31
|
+
_AGO_SUFFIX = "ago"
|
|
32
|
+
_JUST_NOW = "just now"
|
|
33
|
+
_DIFFERENT_TIMES = "at different times"
|
|
34
|
+
_RANGE_SEPARATOR = " to "
|
|
35
|
+
_WINDOW_HEADER = "Window"
|
|
36
|
+
_USED_HEADER = "Used"
|
|
37
|
+
_RESET_HEADER = "Provider reset"
|
|
38
|
+
_NO_READING = "No provider quota reading is recorded."
|
|
39
|
+
_NO_CURRENT_READING = "No current provider quota reading is available."
|
|
40
|
+
_COLUMN_GAP = 2
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def evidence_headline(evidence: BlockerEvidenceView, now: datetime) -> str | None:
|
|
44
|
+
"""Return how old the observations are: one age, or the span of differing ages."""
|
|
45
|
+
ages = sorted(
|
|
46
|
+
[now - reading.observed_at for quota in evidence.quotas for reading in quota.readings]
|
|
47
|
+
+ [now - observed for observed in evidence.observed_at]
|
|
48
|
+
)
|
|
49
|
+
if not ages:
|
|
50
|
+
return None
|
|
51
|
+
newest, oldest = ages[0], ages[-1]
|
|
52
|
+
newest_age = _observed_age(newest)
|
|
53
|
+
oldest_age = _observed_age(oldest)
|
|
54
|
+
if newest == oldest:
|
|
55
|
+
return f"{_OBSERVED_PREFIX} {newest_age}"
|
|
56
|
+
if newest_age == oldest_age:
|
|
57
|
+
return f"{_OBSERVED_PREFIX} {_DIFFERENT_TIMES} ({oldest_age})"
|
|
58
|
+
return f"{_OBSERVED_PREFIX} {newest_age}{_RANGE_SEPARATOR}{oldest_age}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def evidence_body(evidence: BlockerEvidenceView, now: datetime) -> RenderableType:
|
|
62
|
+
"""Return the normal evidence: each provider's readings and reset, then the notes.
|
|
63
|
+
|
|
64
|
+
A provider label heads each group only when more than one provider is blocked.
|
|
65
|
+
"""
|
|
66
|
+
labelled = len(evidence.quotas) > 1
|
|
67
|
+
parts: list[RenderableType] = []
|
|
68
|
+
for quota in evidence.quotas:
|
|
69
|
+
if labelled:
|
|
70
|
+
parts.append(grey(quota.agent))
|
|
71
|
+
parts.append(_quota_block(quota, to_local(now)))
|
|
72
|
+
parts.extend(grey(note) for note in evidence.notes)
|
|
73
|
+
return Group(*parts)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def raw_evidence_text(evidence: BlockerEvidenceView) -> Text:
|
|
77
|
+
"""Return the raw events in grey: exact timestamps, event references and diagnostics."""
|
|
78
|
+
return grey("\n".join(evidence.raw))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _quota_block(quota: QuotaEvidenceView, now: datetime) -> RenderableType:
|
|
82
|
+
if quota.readings:
|
|
83
|
+
parts: list[RenderableType] = [_readings_table(quota.readings, now)]
|
|
84
|
+
elif quota.has_recorded_readings:
|
|
85
|
+
parts = [grey(_NO_CURRENT_READING)]
|
|
86
|
+
else:
|
|
87
|
+
parts = [grey(_NO_READING)]
|
|
88
|
+
if quota.retry_note is not None:
|
|
89
|
+
parts.append(grey(quota.retry_note))
|
|
90
|
+
return Group(*parts)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _readings_table(readings: Sequence[QuotaReadingView], now: datetime) -> Table:
|
|
94
|
+
table = Table(
|
|
95
|
+
box=None,
|
|
96
|
+
show_edge=False,
|
|
97
|
+
pad_edge=False,
|
|
98
|
+
padding=(0, _COLUMN_GAP, 0, 0),
|
|
99
|
+
header_style=SECONDARY_TEXT_COLOR,
|
|
100
|
+
style=SECONDARY_TEXT_COLOR,
|
|
101
|
+
)
|
|
102
|
+
table.add_column(_WINDOW_HEADER)
|
|
103
|
+
table.add_column(_USED_HEADER, justify="right")
|
|
104
|
+
table.add_column(_RESET_HEADER)
|
|
105
|
+
for reading in readings:
|
|
106
|
+
table.add_row(
|
|
107
|
+
quota_window_label(reading.window).capitalize(),
|
|
108
|
+
f"{displayed_pct(reading.used_pct)}%",
|
|
109
|
+
_reset_text(reading.resets_at, now),
|
|
110
|
+
)
|
|
111
|
+
return table
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _reset_text(resets_at: datetime | None, now: datetime) -> str:
|
|
115
|
+
if resets_at is None:
|
|
116
|
+
return QUOTA_NOT_REPORTED
|
|
117
|
+
return provider_reset_text(to_local(resets_at), now)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _observed_age(age: timedelta) -> str:
|
|
121
|
+
short = format_short_age(age)
|
|
122
|
+
return _JUST_NOW if short == AGE_NOW else f"{short} {_AGO_SUFFIX}"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""The Blockers rows of the briefing's Problems group: each cause, next step and occupants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from rich.cells import cell_len
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
from shikiru.clock import to_local
|
|
11
|
+
from shikiru.domain.quota_waits import quota_clock_text
|
|
12
|
+
from shikiru.domain.scheduling import WaitReason
|
|
13
|
+
from shikiru.domain.tasks import Status
|
|
14
|
+
from shikiru.ui.glyphs import GLYPH_WARNING
|
|
15
|
+
from shikiru.ui.section import Row, grey
|
|
16
|
+
from shikiru.ui.tokens import ALERT_COLOR, AMBER, SECONDARY_TEXT_COLOR, STYLE_BOLD
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
|
|
21
|
+
from shikiru.dashboard.contract import TaskBlockerView, TaskDetailView, TaskReferenceView
|
|
22
|
+
|
|
23
|
+
BLOCKERS_LABEL = "Blockers"
|
|
24
|
+
_BOLD_AMBER = f"{STYLE_BOLD} {AMBER}"
|
|
25
|
+
_BOLD_ALERT = f"{STYLE_BOLD} {ALERT_COLOR}"
|
|
26
|
+
_COLUMN_GAP = " "
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def blocker_rows(view: TaskDetailView, now: datetime) -> list[Row]:
|
|
30
|
+
"""Return the Blockers rows: each blocker's warning headline, then its grey detail lines.
|
|
31
|
+
|
|
32
|
+
The label sits on the first row only, and every line lines up under the value column. A quota
|
|
33
|
+
wait's clock is worded for `now`, so the countdown moves with the clock. A blocker waiting on a
|
|
34
|
+
cancelled task turns the alert colour.
|
|
35
|
+
"""
|
|
36
|
+
rows: list[Row] = []
|
|
37
|
+
for index, blocker in enumerate(view.blockers):
|
|
38
|
+
alert = _waits_on_cancelled(view, blocker)
|
|
39
|
+
label = grey(BLOCKERS_LABEL) if index == 0 else Text()
|
|
40
|
+
rows.append((label, (_headline(blocker, now, alert),)))
|
|
41
|
+
rows.extend((Text(), (grey(line),)) for line in blocker.details)
|
|
42
|
+
rows.extend((Text(), (line,)) for line in _occupant_lines(blocker.related_tasks))
|
|
43
|
+
return rows
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _headline(blocker: TaskBlockerView, now: datetime, alert: bool) -> Text:
|
|
47
|
+
cause = blocker.headline
|
|
48
|
+
if blocker.quota_wait is not None:
|
|
49
|
+
quota = blocker.quota_wait
|
|
50
|
+
description, remaining = quota_clock_text(quota.agent, to_local(quota.until), to_local(now))
|
|
51
|
+
cause = f"{description} (in {remaining})"
|
|
52
|
+
return Text.assemble(
|
|
53
|
+
(GLYPH_WARNING, _BOLD_ALERT if alert else _BOLD_AMBER),
|
|
54
|
+
(f" {cause}", ALERT_COLOR if alert else ""),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _occupant_lines(tasks: tuple[TaskReferenceView, ...]) -> list[Text]:
|
|
59
|
+
id_width = max((cell_len(task.task_id) for task in tasks), default=0)
|
|
60
|
+
title_width = max((cell_len(task.title) for task in tasks), default=0)
|
|
61
|
+
return [
|
|
62
|
+
Text(
|
|
63
|
+
f"{_pad_cells(task.task_id, id_width)}{_COLUMN_GAP}"
|
|
64
|
+
f"{_pad_cells(task.title, title_width)}{_COLUMN_GAP}{task.status}",
|
|
65
|
+
style=ALERT_COLOR if task.status == Status.CANCELLED.value else SECONDARY_TEXT_COLOR,
|
|
66
|
+
)
|
|
67
|
+
for task in tasks
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _pad_cells(text: str, width: int) -> str:
|
|
72
|
+
return f"{text}{' ' * (width - cell_len(text))}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _waits_on_cancelled(view: TaskDetailView, blocker: TaskBlockerView) -> bool:
|
|
76
|
+
if blocker.kind == WaitReason.DEPENDENCY.value:
|
|
77
|
+
if view.relationships is None:
|
|
78
|
+
return False
|
|
79
|
+
return _any_cancelled(view.relationships.blocking_dependencies)
|
|
80
|
+
return _any_cancelled(blocker.related_tasks)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _any_cancelled(tasks: tuple[TaskReferenceView, ...]) -> bool:
|
|
84
|
+
return any(task.status == Status.CANCELLED.value for task in tasks)
|