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,186 @@
|
|
|
1
|
+
"""The permanent Resources and Engine sections of the Project diagnostics popup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from rich.cells import cell_len
|
|
9
|
+
from rich.console import Group, RenderableType
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from shikiru.dashboard.contract import DiagnosticSampleState
|
|
13
|
+
from shikiru.ui.age import format_duration
|
|
14
|
+
from shikiru.ui.compact import BYTES_PER_GIB, RESOURCE_STATE_UNKNOWN, format_cpu_pct, format_gib
|
|
15
|
+
from shikiru.ui.section import (
|
|
16
|
+
FAILED_WORD,
|
|
17
|
+
GROUP_SEPARATOR,
|
|
18
|
+
UNAVAILABLE_WORD,
|
|
19
|
+
Row,
|
|
20
|
+
Section,
|
|
21
|
+
grey,
|
|
22
|
+
labelled_rows,
|
|
23
|
+
number,
|
|
24
|
+
row_name,
|
|
25
|
+
state_line,
|
|
26
|
+
)
|
|
27
|
+
from shikiru.ui.tokens import ALERT_COLOR, AMBER, SAGE, SECONDARY_TEXT_COLOR
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
|
31
|
+
|
|
32
|
+
from shikiru.dashboard.contract import EngineStatusView, ResourceView
|
|
33
|
+
|
|
34
|
+
RESOURCES_TITLE = "Resources"
|
|
35
|
+
ENGINE_TITLE = "Engine"
|
|
36
|
+
LABEL_HOST = "Host"
|
|
37
|
+
LABEL_SHIKIRU = "Shikiru"
|
|
38
|
+
LABEL_AGENTS = "Agents"
|
|
39
|
+
LABEL_REFRESH = "Refresh"
|
|
40
|
+
LABEL_STATUS = "Status"
|
|
41
|
+
LABEL_FAULTS = "Faults"
|
|
42
|
+
CPU_WORD = "CPU"
|
|
43
|
+
RAM_WORD = "RAM"
|
|
44
|
+
RSS_WORD = "RSS"
|
|
45
|
+
GIB_UNIT = "GiB"
|
|
46
|
+
RAM_DIVIDER = " / "
|
|
47
|
+
NO_SAMPLE_RECORDED = "no sample recorded"
|
|
48
|
+
PROCESS_TREE_EMPTY = "process tree is empty"
|
|
49
|
+
CPU_RSS_UNAVAILABLE = "CPU/RSS unavailable"
|
|
50
|
+
RAM_UNAVAILABLE = "RAM used/total unavailable"
|
|
51
|
+
STATUS_RUNNING = "Running"
|
|
52
|
+
STATUS_NOT_RUNNING = "Not running"
|
|
53
|
+
UPTIME_PREFIX = "up"
|
|
54
|
+
UPTIME_UNAVAILABLE = "uptime unavailable"
|
|
55
|
+
FAULTS_NONE = "None open"
|
|
56
|
+
FAULTS_OPEN = "open"
|
|
57
|
+
_OBSERVED_STATES = frozenset({DiagnosticSampleState.AVAILABLE, DiagnosticSampleState.EMPTY})
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _measure(word: str, value: Text) -> Text:
|
|
61
|
+
return Text.assemble(grey(f"{word} "), value)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _host_groups(sample: ResourceView) -> tuple[Text, ...]:
|
|
65
|
+
cpu = _measure(CPU_WORD, number(format_cpu_pct(sample.host_cpu_pct)))
|
|
66
|
+
used = sample.host_ram_used_bytes
|
|
67
|
+
total = sample.host_ram_total_bytes
|
|
68
|
+
if used is None or total is None:
|
|
69
|
+
return (cpu, grey(RAM_UNAVAILABLE))
|
|
70
|
+
memory = Text.assemble(
|
|
71
|
+
number(format_gib(used / BYTES_PER_GIB)),
|
|
72
|
+
grey(RAM_DIVIDER),
|
|
73
|
+
number(f"{format_gib(total / BYTES_PER_GIB)} {GIB_UNIT}"),
|
|
74
|
+
)
|
|
75
|
+
return (cpu, _measure(RAM_WORD, memory))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _process_groups(
|
|
79
|
+
cpu_pct: float | None, rss_gb: float | None, *, tree_empty: bool | None
|
|
80
|
+
) -> tuple[Text, ...]:
|
|
81
|
+
if cpu_pct is None or rss_gb is None:
|
|
82
|
+
return (grey(PROCESS_TREE_EMPTY if tree_empty else CPU_RSS_UNAVAILABLE),)
|
|
83
|
+
return (
|
|
84
|
+
_measure(CPU_WORD, number(format_cpu_pct(cpu_pct))),
|
|
85
|
+
_measure(RSS_WORD, number(f"{format_gib(rss_gb)} {GIB_UNIT}")),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _failure(error: str) -> Text:
|
|
90
|
+
return Text(f"{FAILED_WORD}{GROUP_SEPARATOR}{error}", style=AMBER)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class _ResourceRows:
|
|
95
|
+
sample: ResourceView
|
|
96
|
+
refresh_error: str | None
|
|
97
|
+
|
|
98
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
99
|
+
del console
|
|
100
|
+
sample = self.sample
|
|
101
|
+
rows: list[Row] = [
|
|
102
|
+
(row_name(LABEL_HOST), _host_groups(sample)),
|
|
103
|
+
(
|
|
104
|
+
row_name(LABEL_SHIKIRU),
|
|
105
|
+
_process_groups(
|
|
106
|
+
sample.shikiru_cpu_pct,
|
|
107
|
+
sample.shikiru_rss_gb,
|
|
108
|
+
tree_empty=sample.shikiru_process_tree_empty,
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
(
|
|
112
|
+
row_name(LABEL_AGENTS),
|
|
113
|
+
_process_groups(
|
|
114
|
+
sample.agents_cpu_pct,
|
|
115
|
+
sample.agents_rss_gb,
|
|
116
|
+
tree_empty=sample.agents_process_tree_empty,
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
]
|
|
120
|
+
if self.refresh_error is not None:
|
|
121
|
+
rows.append((grey(LABEL_REFRESH), (_failure(self.refresh_error),)))
|
|
122
|
+
label_width = max(cell_len(label.plain) for label, _ in rows)
|
|
123
|
+
yield labelled_rows(rows, options.max_width, label_width)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def resources_section(sample: ResourceView | None) -> Section:
|
|
127
|
+
"""Render `sample` as the unruled first Resources section in every state, with no age hint."""
|
|
128
|
+
if sample is None:
|
|
129
|
+
return Section(
|
|
130
|
+
RESOURCES_TITLE,
|
|
131
|
+
state_line(UNAVAILABLE_WORD, SECONDARY_TEXT_COLOR, NO_SAMPLE_RECORDED),
|
|
132
|
+
ruled=False,
|
|
133
|
+
)
|
|
134
|
+
if sample.state is DiagnosticSampleState.FAILED:
|
|
135
|
+
return Section(
|
|
136
|
+
RESOURCES_TITLE, state_line(FAILED_WORD, ALERT_COLOR, sample.error), ruled=False
|
|
137
|
+
)
|
|
138
|
+
if sample.state is DiagnosticSampleState.UNAVAILABLE:
|
|
139
|
+
return Section(
|
|
140
|
+
RESOURCES_TITLE,
|
|
141
|
+
state_line(UNAVAILABLE_WORD, SECONDARY_TEXT_COLOR, sample.error),
|
|
142
|
+
ruled=False,
|
|
143
|
+
)
|
|
144
|
+
stale = sample.state is DiagnosticSampleState.STALE
|
|
145
|
+
rows = _ResourceRows(sample, sample.error if stale else None)
|
|
146
|
+
body: RenderableType = rows
|
|
147
|
+
if not stale and sample.state not in _OBSERVED_STATES:
|
|
148
|
+
body = Group(grey(RESOURCE_STATE_UNKNOWN), rows)
|
|
149
|
+
return Section(RESOURCES_TITLE, body, ruled=False)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _status_groups(status: EngineStatusView) -> tuple[Text, ...]:
|
|
153
|
+
if not status.up:
|
|
154
|
+
return (Text(STATUS_NOT_RUNNING, style=ALERT_COLOR),)
|
|
155
|
+
uptime = (
|
|
156
|
+
UPTIME_UNAVAILABLE
|
|
157
|
+
if status.uptime_seconds is None
|
|
158
|
+
else f"{UPTIME_PREFIX} {format_duration(status.uptime_seconds)}"
|
|
159
|
+
)
|
|
160
|
+
return (Text(STATUS_RUNNING, style=SAGE), grey(uptime))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _faults_groups(fault_count: int) -> tuple[Text, ...]:
|
|
164
|
+
if not fault_count:
|
|
165
|
+
return (grey(FAULTS_NONE),)
|
|
166
|
+
return (Text(f"{fault_count} {FAULTS_OPEN}", style=AMBER),)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass(frozen=True)
|
|
170
|
+
class _EngineRows:
|
|
171
|
+
status: EngineStatusView
|
|
172
|
+
fault_count: int
|
|
173
|
+
|
|
174
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
175
|
+
del console
|
|
176
|
+
rows: list[Row] = [
|
|
177
|
+
(grey(LABEL_STATUS), _status_groups(self.status)),
|
|
178
|
+
(grey(LABEL_FAULTS), _faults_groups(self.fault_count)),
|
|
179
|
+
]
|
|
180
|
+
label_width = max(cell_len(label.plain) for label, _ in rows)
|
|
181
|
+
yield labelled_rows(rows, options.max_width, label_width)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def engine_section(status: EngineStatusView, fault_count: int) -> Section:
|
|
185
|
+
"""Render engine liveness and the number of open faults as the Engine section."""
|
|
186
|
+
return Section(ENGINE_TITLE, _EngineRows(status, fault_count))
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""A selectable table of evidence entries with a shaded, structured details block under it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from rich.cells import cell_len
|
|
9
|
+
from rich.console import Group
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
from textual import on
|
|
12
|
+
from textual.containers import Vertical, VerticalScroll
|
|
13
|
+
from textual.widgets import Static
|
|
14
|
+
|
|
15
|
+
from shikiru.dashboard.row_list import RowCells, RowList
|
|
16
|
+
from shikiru.dashboard.section_rows import TONE_COLORS
|
|
17
|
+
from shikiru.ui.section import BOLD_STEEL, STACKED_BELOW_WIDTH, Row, grey, labelled_rows
|
|
18
|
+
from shikiru.ui.tokens import PRIMARY_TEXT_COLOR, STEEL_BLUE
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
|
|
23
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
|
24
|
+
from textual.app import ComposeResult
|
|
25
|
+
|
|
26
|
+
from shikiru.domain.packet_evidence import DetailRow, EvidenceEntry, Part
|
|
27
|
+
|
|
28
|
+
MAX_LABEL_WIDTH = 19
|
|
29
|
+
DETAIL_STACKED_BELOW_WIDTH = 50
|
|
30
|
+
_KIND_GAP = " "
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def part_text(part: Part) -> Text:
|
|
34
|
+
"""Return `part` in its tone colour, or the accent colour."""
|
|
35
|
+
style = STEEL_BLUE if part.accent else TONE_COLORS[part.tone.value] or PRIMARY_TEXT_COLOR
|
|
36
|
+
return Text(part.text, style=style)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def detail_row(row: DetailRow) -> Row:
|
|
40
|
+
"""Return a details row as a grey label and its toned value."""
|
|
41
|
+
return grey(row.label), (Text("").join(part_text(part) for part in row.parts),)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class LabelledRows:
|
|
46
|
+
"""Rows as a capped-width label column and wrapped values; unlabelled rows as plain lines."""
|
|
47
|
+
|
|
48
|
+
rows: list[Row]
|
|
49
|
+
stack_below: int = STACKED_BELOW_WIDTH
|
|
50
|
+
|
|
51
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
52
|
+
del console
|
|
53
|
+
label_width = min(
|
|
54
|
+
max((cell_len(label.plain) for label, _ in self.rows), default=0), MAX_LABEL_WIDTH
|
|
55
|
+
)
|
|
56
|
+
if label_width:
|
|
57
|
+
yield labelled_rows(self.rows, options.max_width, label_width, self.stack_below)
|
|
58
|
+
else:
|
|
59
|
+
yield Group(*(values[0] for _, values in self.rows))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _PageScrollingRowList(RowList):
|
|
63
|
+
def action_page(self, older: int) -> None:
|
|
64
|
+
scroller = self.query_ancestor(VerticalScroll)
|
|
65
|
+
if older:
|
|
66
|
+
scroller.scroll_page_down(animate=False)
|
|
67
|
+
else:
|
|
68
|
+
scroller.scroll_page_up(animate=False)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class EvidenceTable(Vertical):
|
|
72
|
+
"""A titled one-line-per-entry table; the selected entry's details show beneath it."""
|
|
73
|
+
|
|
74
|
+
DEFAULT_CSS = """
|
|
75
|
+
EvidenceTable { height: auto; }
|
|
76
|
+
EvidenceTable RowList { max-height: 8; overflow-y: auto; scrollbar-size-vertical: 1; }
|
|
77
|
+
EvidenceTable .evidence-detail {
|
|
78
|
+
height: auto; padding: 0 1; margin: 1 0 0 0;
|
|
79
|
+
color: $primary-text; background: $detail-surface;
|
|
80
|
+
}
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, title: str, entries: Sequence[EvidenceEntry], *, table_id: str) -> None:
|
|
84
|
+
"""Show `title` over one row per entry in `entries`, which must not be empty."""
|
|
85
|
+
super().__init__()
|
|
86
|
+
self._title = title
|
|
87
|
+
self._entries = tuple(entries)
|
|
88
|
+
self._table_id = table_id
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def table(self) -> RowList:
|
|
92
|
+
"""The focusable row list."""
|
|
93
|
+
return self.query_one(f"#{self._table_id}", RowList)
|
|
94
|
+
|
|
95
|
+
def compose(self) -> ComposeResult:
|
|
96
|
+
yield Static(Text(self._title, style=BOLD_STEEL))
|
|
97
|
+
yield _PageScrollingRowList(widget_id=self._table_id)
|
|
98
|
+
yield Static(classes="evidence-detail")
|
|
99
|
+
|
|
100
|
+
def on_mount(self) -> None:
|
|
101
|
+
kind_width = max(cell_len(entry.kind) for entry in self._entries)
|
|
102
|
+
self.table.set_rows(
|
|
103
|
+
[self._cells(entry, kind_width) for entry in self._entries], selected=0, empty=""
|
|
104
|
+
)
|
|
105
|
+
self._show(0)
|
|
106
|
+
|
|
107
|
+
@on(RowList.Moved)
|
|
108
|
+
def _moved(self, message: RowList.Moved) -> None:
|
|
109
|
+
message.stop()
|
|
110
|
+
self._show(message.index)
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _cells(entry: EvidenceEntry, kind_width: int) -> RowCells:
|
|
114
|
+
text = Text(entry.kind.ljust(kind_width), style=STEEL_BLUE)
|
|
115
|
+
text.append(_KIND_GAP)
|
|
116
|
+
text.append_text(part_text(entry.outcome))
|
|
117
|
+
return RowCells(entry.lead, text)
|
|
118
|
+
|
|
119
|
+
def _show(self, index: int) -> None:
|
|
120
|
+
rows = [detail_row(row) for row in self._entries[index].details]
|
|
121
|
+
self.query_one(".evidence-detail", Static).update(
|
|
122
|
+
LabelledRows(rows, DETAIL_STACKED_BELOW_WIDTH)
|
|
123
|
+
)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""The Extra details popup: the escalation packet's evidence as one scrolling page."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
7
|
+
|
|
8
|
+
from rich.console import Group, RenderableType
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
11
|
+
from textual.screen import ModalScreen
|
|
12
|
+
from textual.widgets import Button, Label, Rule, Static
|
|
13
|
+
|
|
14
|
+
from shikiru.dashboard.evidence_table import EvidenceTable, LabelledRows
|
|
15
|
+
from shikiru.dashboard.resolution_view import ResolutionLayout, layout_for
|
|
16
|
+
from shikiru.dashboard.row_list import RowList
|
|
17
|
+
from shikiru.dashboard.section_rows import TONE_COLORS, glyph_prefix
|
|
18
|
+
from shikiru.domain.escalation_evidence import EscalationPacket, parse_escalation_packet
|
|
19
|
+
from shikiru.domain.packet_evidence import parse_reports, parse_signals
|
|
20
|
+
from shikiru.domain.timeline_phrases import NO_GLYPH, kind_phrase
|
|
21
|
+
from shikiru.ui.popup import (
|
|
22
|
+
POPUP_BODY_CLASS,
|
|
23
|
+
POPUP_CLOSE_CLASS,
|
|
24
|
+
POPUP_CSS,
|
|
25
|
+
POPUP_FRAME_CLASS,
|
|
26
|
+
POPUP_HEADER_CLASS,
|
|
27
|
+
POPUP_RULE_CLASS,
|
|
28
|
+
POPUP_TITLE_CLASS,
|
|
29
|
+
PopupFrame,
|
|
30
|
+
popup_close_label,
|
|
31
|
+
prepare_popup,
|
|
32
|
+
)
|
|
33
|
+
from shikiru.ui.section import BOLD_STEEL, GROUP_SEPARATOR, Row, Section, grey
|
|
34
|
+
from shikiru.ui.tokens import AMBER, PRIMARY_TEXT_COLOR
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from collections.abc import Callable
|
|
38
|
+
|
|
39
|
+
from textual import events
|
|
40
|
+
from textual.app import ComposeResult
|
|
41
|
+
from textual.widget import Widget
|
|
42
|
+
|
|
43
|
+
EXTRA_DETAILS_CLOSE_BUTTON = "extra-details-close"
|
|
44
|
+
_TITLE = "Extra details"
|
|
45
|
+
_ENTRY_PREFIX = "- "
|
|
46
|
+
_NONE_RECORDED = "(none)"
|
|
47
|
+
_TIME_FORMAT = "%m-%d %H:%M"
|
|
48
|
+
_KIND_SEPARATOR = ": "
|
|
49
|
+
_RESUME_SEPARATOR = " \N{EM DASH} "
|
|
50
|
+
_SESSION_MARKER = " session `"
|
|
51
|
+
_SESSION_ID_CLOSE = "`"
|
|
52
|
+
_ERROR_PREFIX = "Evidence error: "
|
|
53
|
+
_QUESTION_TITLE = "Question"
|
|
54
|
+
_TIMELINE = "Timeline"
|
|
55
|
+
_REPORTS = "Reports"
|
|
56
|
+
_RECENT_SIGNALS = "Recent turn signals"
|
|
57
|
+
_SESSIONS = "Sessions"
|
|
58
|
+
_SIGNALS_TITLE = "Recent signals"
|
|
59
|
+
_REPORTS_TABLE = "extra-details-reports"
|
|
60
|
+
_SIGNALS_TABLE = "extra-details-signals"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _value(text: str) -> tuple[Text, ...]:
|
|
64
|
+
return (Text(text, style=PRIMARY_TEXT_COLOR),)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _plain_row(line: str) -> Row:
|
|
68
|
+
return Text(), _value(line)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _timeline_row(line: str) -> Row:
|
|
72
|
+
entry = line.removeprefix(_ENTRY_PREFIX)
|
|
73
|
+
stamp, _, message = entry.partition(" ")
|
|
74
|
+
try:
|
|
75
|
+
label = datetime.fromisoformat(stamp).strftime(_TIME_FORMAT)
|
|
76
|
+
except ValueError:
|
|
77
|
+
return _plain_row(entry)
|
|
78
|
+
kind, separator, detail = message.partition(_KIND_SEPARATOR)
|
|
79
|
+
phrase = kind_phrase(kind)
|
|
80
|
+
if phrase is None:
|
|
81
|
+
return grey(label), (Text(glyph_prefix(NO_GLYPH) + message, style=PRIMARY_TEXT_COLOR),)
|
|
82
|
+
text = glyph_prefix(phrase.glyph) + (phrase.text + separator + detail)
|
|
83
|
+
return grey(label), (Text(text, style=TONE_COLORS[phrase.tone.value] or PRIMARY_TEXT_COLOR),)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _session_row(line: str) -> Row:
|
|
87
|
+
entry = line.removeprefix(_ENTRY_PREFIX).partition(_RESUME_SEPARATOR)[0]
|
|
88
|
+
role, _, rest = entry.partition(_KIND_SEPARATOR)
|
|
89
|
+
agent, marker, session = rest.partition(_SESSION_MARKER)
|
|
90
|
+
if not marker:
|
|
91
|
+
return _plain_row(entry)
|
|
92
|
+
return grey(role), (*_value(agent), grey(session.removesuffix(_SESSION_ID_CLOSE)))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _line_rows(content: str, make: Callable[[str], Row]) -> list[Row]:
|
|
96
|
+
return [make(line) for line in content.splitlines() if line.strip()]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
_ROW_SECTIONS: dict[str, tuple[str, Callable[[str], Row]]] = {
|
|
100
|
+
_TIMELINE: (_TIMELINE, _timeline_row),
|
|
101
|
+
_SESSIONS: (_SESSIONS, _session_row),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _titled(title: str, body: RenderableType) -> Section:
|
|
106
|
+
return Section(title, body, ruled=False, title_style=BOLD_STEEL)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _static(title: str, rows: list[Row]) -> Static:
|
|
110
|
+
return Static(_titled(title, LabelledRows(rows)))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _page_widgets(packet: EscalationPacket) -> list[Widget]:
|
|
114
|
+
request = packet.agent_request
|
|
115
|
+
question = request.question if request is not None else packet.question
|
|
116
|
+
widgets: list[Widget] = []
|
|
117
|
+
if question:
|
|
118
|
+
widgets.append(Static(_titled(_QUESTION_TITLE, Text(question, style=PRIMARY_TEXT_COLOR))))
|
|
119
|
+
for label, content in packet.evidence_sections:
|
|
120
|
+
if not content or content == _NONE_RECORDED:
|
|
121
|
+
continue
|
|
122
|
+
if label in _ROW_SECTIONS:
|
|
123
|
+
title, make = _ROW_SECTIONS[label]
|
|
124
|
+
widgets.append(_static(title, _line_rows(content, make)))
|
|
125
|
+
elif label == _REPORTS and (reports := parse_reports(content)):
|
|
126
|
+
widgets.append(EvidenceTable(_REPORTS, reports, table_id=_REPORTS_TABLE))
|
|
127
|
+
elif label == _RECENT_SIGNALS and (signals := parse_signals(content)):
|
|
128
|
+
widgets.append(EvidenceTable(_SIGNALS_TITLE, signals, table_id=_SIGNALS_TABLE))
|
|
129
|
+
return widgets
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _error_widget(packet: EscalationPacket, packet_text: str) -> Static:
|
|
133
|
+
error = Text(_ERROR_PREFIX + GROUP_SEPARATOR.join(packet.errors), style=AMBER)
|
|
134
|
+
return Static(Group(error, Text(""), Text(packet_text, style=PRIMARY_TEXT_COLOR)))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ExtraDetailsScreen(ModalScreen[None]):
|
|
138
|
+
"""Show the evidence of one escalation until the Operator closes it with Esc."""
|
|
139
|
+
|
|
140
|
+
BINDINGS: ClassVar[list[tuple[str, str, str]]] = [("escape", "close", "Close")]
|
|
141
|
+
DEFAULT_CSS = (
|
|
142
|
+
POPUP_CSS
|
|
143
|
+
+ """
|
|
144
|
+
#extra-details-frame.compact { margin: 0 1; padding: 0 1; }
|
|
145
|
+
#extra-details-title { text-wrap: nowrap; text-overflow: ellipsis; }
|
|
146
|
+
#extra-details-scroll { max-height: 1fr; min-height: 0; }
|
|
147
|
+
#extra-details-body { height: auto; }
|
|
148
|
+
#extra-details-body > * { margin-top: 1; }
|
|
149
|
+
#extra-details-body > :first-child { margin-top: 0; }
|
|
150
|
+
"""
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def __init__(self, task_id: str, packet_text: str) -> None:
|
|
154
|
+
"""Hold the task id for the title and the packet text to lay out."""
|
|
155
|
+
super().__init__()
|
|
156
|
+
prepare_popup(self)
|
|
157
|
+
self._task_id = task_id
|
|
158
|
+
self._packet_text = packet_text
|
|
159
|
+
|
|
160
|
+
def compose(self) -> ComposeResult:
|
|
161
|
+
"""Lay out the popup frame with its header, rule and scrolling body."""
|
|
162
|
+
with PopupFrame(widget_id="extra-details-frame", classes=POPUP_FRAME_CLASS):
|
|
163
|
+
with Horizontal(classes=POPUP_HEADER_CLASS):
|
|
164
|
+
yield Label(
|
|
165
|
+
GROUP_SEPARATOR.join((_TITLE, self._task_id)),
|
|
166
|
+
id="extra-details-title",
|
|
167
|
+
classes=POPUP_TITLE_CLASS,
|
|
168
|
+
)
|
|
169
|
+
yield Button(
|
|
170
|
+
popup_close_label(),
|
|
171
|
+
id=EXTRA_DETAILS_CLOSE_BUTTON,
|
|
172
|
+
classes=POPUP_CLOSE_CLASS,
|
|
173
|
+
)
|
|
174
|
+
yield Rule(classes=POPUP_RULE_CLASS)
|
|
175
|
+
with (
|
|
176
|
+
VerticalScroll(id="extra-details-scroll", classes=POPUP_BODY_CLASS),
|
|
177
|
+
Vertical(id="extra-details-body"),
|
|
178
|
+
):
|
|
179
|
+
yield from self._page()
|
|
180
|
+
|
|
181
|
+
def _page(self) -> list[Widget]:
|
|
182
|
+
packet = parse_escalation_packet(self._packet_text)
|
|
183
|
+
if packet.errors:
|
|
184
|
+
return [_error_widget(packet, self._packet_text)]
|
|
185
|
+
return _page_widgets(packet)
|
|
186
|
+
|
|
187
|
+
def on_mount(self) -> None:
|
|
188
|
+
self._fit()
|
|
189
|
+
tables = self.query(RowList)
|
|
190
|
+
first = tables.first() if tables else self.query_one("#extra-details-scroll")
|
|
191
|
+
self.set_focus(first, scroll_visible=False)
|
|
192
|
+
self.call_after_refresh(self.query_one("#extra-details-frame", PopupFrame).content_ready)
|
|
193
|
+
|
|
194
|
+
def on_resize(self, event: events.Resize) -> None:
|
|
195
|
+
del event
|
|
196
|
+
if self.is_mounted:
|
|
197
|
+
self._fit()
|
|
198
|
+
self.call_after_refresh(
|
|
199
|
+
self.query_one("#extra-details-frame", PopupFrame).update_preferred_size
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
203
|
+
if event.button.id == EXTRA_DETAILS_CLOSE_BUTTON:
|
|
204
|
+
self.dismiss()
|
|
205
|
+
|
|
206
|
+
def action_close(self) -> None:
|
|
207
|
+
"""Close the popup and return to the Escalations screen."""
|
|
208
|
+
self.dismiss()
|
|
209
|
+
|
|
210
|
+
def _fit(self) -> None:
|
|
211
|
+
layout = layout_for(self.size.width, self.size.height)
|
|
212
|
+
self.query_one("#extra-details-frame", PopupFrame).set_class(
|
|
213
|
+
layout is not ResolutionLayout.WIDE, "compact"
|
|
214
|
+
)
|