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
shikiru/ui/strip.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""The task strip renderable."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum, auto
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from rich.cells import cell_len
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from shikiru.domain.tasks import Mode, Role
|
|
13
|
+
from shikiru.ui.glyphs import (
|
|
14
|
+
GLYPH_DONE,
|
|
15
|
+
GLYPH_ESCALATED,
|
|
16
|
+
GLYPH_OPERATOR,
|
|
17
|
+
GLYPH_PAUSED,
|
|
18
|
+
GLYPH_PENDING,
|
|
19
|
+
GLYPH_STALLED,
|
|
20
|
+
PANEL_KANJI,
|
|
21
|
+
ROLE_GLYPHS,
|
|
22
|
+
STAGE_IMPLEMENT,
|
|
23
|
+
STAGE_MERGE,
|
|
24
|
+
STAGE_PLAN,
|
|
25
|
+
STAGE_TRIAGE,
|
|
26
|
+
STAGE_VERIFY,
|
|
27
|
+
)
|
|
28
|
+
from shikiru.ui.tokens import (
|
|
29
|
+
ALERT_COLOR,
|
|
30
|
+
ENGINE_COLOR,
|
|
31
|
+
HOLD_COLOR,
|
|
32
|
+
IDLE_COLOR,
|
|
33
|
+
ROLE_COLORS,
|
|
34
|
+
STYLE_BOLD,
|
|
35
|
+
STYLE_DIM,
|
|
36
|
+
STYLE_REVERSE,
|
|
37
|
+
SUCCESS_COLOR,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from datetime import datetime
|
|
42
|
+
|
|
43
|
+
STRIP_WIDE_MIN_COLUMNS = 60
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PanelState(StrEnum):
|
|
47
|
+
"""One panel's progress."""
|
|
48
|
+
|
|
49
|
+
PENDING = auto()
|
|
50
|
+
ACTIVE = auto()
|
|
51
|
+
DONE = auto()
|
|
52
|
+
ESCALATED = auto()
|
|
53
|
+
STALLED = auto()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class StampKind(StrEnum):
|
|
57
|
+
"""A hold drawn over the active panel."""
|
|
58
|
+
|
|
59
|
+
PAUSED = auto()
|
|
60
|
+
OPERATOR = auto()
|
|
61
|
+
VERDICT = auto()
|
|
62
|
+
MERGED = auto()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_STATE_GLYPHS: dict[PanelState, str] = {
|
|
66
|
+
PanelState.PENDING: GLYPH_PENDING,
|
|
67
|
+
PanelState.DONE: GLYPH_DONE,
|
|
68
|
+
PanelState.ESCALATED: GLYPH_ESCALATED,
|
|
69
|
+
PanelState.STALLED: GLYPH_STALLED,
|
|
70
|
+
}
|
|
71
|
+
_STATE_STYLES: dict[PanelState, str] = {
|
|
72
|
+
PanelState.PENDING: f"{STYLE_DIM} {IDLE_COLOR}",
|
|
73
|
+
PanelState.DONE: f"{STYLE_BOLD} {SUCCESS_COLOR}",
|
|
74
|
+
PanelState.ESCALATED: f"{STYLE_REVERSE} {ALERT_COLOR}",
|
|
75
|
+
PanelState.STALLED: f"{STYLE_BOLD} {HOLD_COLOR}",
|
|
76
|
+
}
|
|
77
|
+
_STAMP_GLYPHS: dict[StampKind, str] = {
|
|
78
|
+
StampKind.PAUSED: GLYPH_PAUSED,
|
|
79
|
+
StampKind.OPERATOR: GLYPH_OPERATOR,
|
|
80
|
+
StampKind.VERDICT: GLYPH_DONE,
|
|
81
|
+
StampKind.MERGED: GLYPH_DONE,
|
|
82
|
+
}
|
|
83
|
+
_STAMP_WORDS: dict[StampKind, str] = {
|
|
84
|
+
StampKind.PAUSED: "PAUSED",
|
|
85
|
+
StampKind.OPERATOR: "OPERATOR",
|
|
86
|
+
StampKind.VERDICT: "VERDICT",
|
|
87
|
+
StampKind.MERGED: "MERGED",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_NARROW_SEPARATOR = " "
|
|
91
|
+
_HEADER_SEPARATOR = " · "
|
|
92
|
+
|
|
93
|
+
_BORDER_TOP_LEFT = "┏"
|
|
94
|
+
_BORDER_TOP_RIGHT = "┓"
|
|
95
|
+
_BORDER_BOTTOM_LEFT = "┗"
|
|
96
|
+
_BORDER_BOTTOM_RIGHT = "┛"
|
|
97
|
+
_BORDER_BOTTOM_JOIN = "┻"
|
|
98
|
+
_BORDER_HORIZONTAL = "━"
|
|
99
|
+
_BORDER_VERTICAL = "┃"
|
|
100
|
+
_TOP_BORDER_PREFIX = f"{_BORDER_TOP_LEFT}{_BORDER_HORIZONTAL} "
|
|
101
|
+
|
|
102
|
+
_MODE_PANEL_LABELS: dict[Mode, tuple[tuple[str, str], ...]] = {
|
|
103
|
+
Mode.IMPLEMENT: (
|
|
104
|
+
("Triage", STAGE_TRIAGE),
|
|
105
|
+
("Implement", STAGE_IMPLEMENT),
|
|
106
|
+
("Verify", STAGE_VERIFY),
|
|
107
|
+
("Merge", STAGE_MERGE),
|
|
108
|
+
),
|
|
109
|
+
Mode.PLAN: (
|
|
110
|
+
("Triage", STAGE_TRIAGE),
|
|
111
|
+
("Plan", STAGE_PLAN),
|
|
112
|
+
("—", ""),
|
|
113
|
+
("Merge", STAGE_MERGE),
|
|
114
|
+
),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def panel_specs_for_mode(mode: Mode) -> tuple[tuple[str, str], ...]:
|
|
119
|
+
"""Return this mode's `(label, stage glyph)` panels."""
|
|
120
|
+
return _MODE_PANEL_LABELS[mode]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class PanelView:
|
|
125
|
+
"""One panel of a task strip.
|
|
126
|
+
|
|
127
|
+
`role` is the acting role, shown as the panel colour while `state` is `ACTIVE`; other
|
|
128
|
+
states carry a fixed colour regardless of role.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
label: str
|
|
132
|
+
state: PanelState
|
|
133
|
+
stage_glyph: str = ""
|
|
134
|
+
detail: str = ""
|
|
135
|
+
role_line: str = ""
|
|
136
|
+
spinner_frame: str = ""
|
|
137
|
+
role: Role | None = None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(frozen=True)
|
|
141
|
+
class StripStamp:
|
|
142
|
+
"""A hold drawn over the strip's active panel."""
|
|
143
|
+
|
|
144
|
+
kind: StampKind
|
|
145
|
+
detail: str = ""
|
|
146
|
+
flash: bool = False
|
|
147
|
+
|
|
148
|
+
def render(self) -> str:
|
|
149
|
+
"""Render as `<glyph> <WORD>`, with `detail` appended when set (e.g. a reset time)."""
|
|
150
|
+
text = f"{_STAMP_GLYPHS[self.kind]} {_STAMP_WORDS[self.kind]}"
|
|
151
|
+
return f"{text} · {self.detail}" if self.detail else text
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True)
|
|
155
|
+
class TaskStripData:
|
|
156
|
+
"""Everything `TaskStrip` needs to render one task, already resolved by its caller."""
|
|
157
|
+
|
|
158
|
+
task_id: str
|
|
159
|
+
title: str
|
|
160
|
+
panels: tuple[PanelView, ...] = ()
|
|
161
|
+
stamp: StripStamp | None = None
|
|
162
|
+
actual_step_started_at: datetime | None = None
|
|
163
|
+
last_signal_at: datetime | None = None
|
|
164
|
+
last_signal: str | None = None
|
|
165
|
+
last_words: str | None = None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _fit(text: str, width: int) -> str:
|
|
169
|
+
if cell_len(text) <= width:
|
|
170
|
+
return text + " " * (width - cell_len(text))
|
|
171
|
+
truncated = Text(text)
|
|
172
|
+
truncated.truncate(width, overflow="crop")
|
|
173
|
+
return truncated.plain
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _panel_style(panel: PanelView) -> str:
|
|
177
|
+
if panel.state is PanelState.ACTIVE:
|
|
178
|
+
color = ROLE_COLORS[panel.role] if panel.role is not None else ENGINE_COLOR
|
|
179
|
+
return f"{STYLE_BOLD} {color}"
|
|
180
|
+
return _STATE_STYLES[panel.state]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _narrow_glyph(panel: PanelView) -> str:
|
|
184
|
+
if panel.state is PanelState.ACTIVE:
|
|
185
|
+
return panel.spinner_frame or GLYPH_PENDING
|
|
186
|
+
return _STATE_GLYPHS[panel.state]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _header_cell(index: int, panel: PanelView) -> str:
|
|
190
|
+
pieces = (PANEL_KANJI[index], panel.stage_glyph, panel.label)
|
|
191
|
+
return " ".join(piece for piece in pieces if piece)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _body_cell(panel: PanelView) -> str:
|
|
195
|
+
if panel.state is PanelState.ACTIVE:
|
|
196
|
+
role_glyph = ROLE_GLYPHS[panel.role] if panel.role is not None else ""
|
|
197
|
+
pieces = (panel.spinner_frame or GLYPH_PENDING, role_glyph, panel.detail)
|
|
198
|
+
else:
|
|
199
|
+
pieces = (_STATE_GLYPHS[panel.state], panel.detail)
|
|
200
|
+
return " ".join(piece for piece in pieces if piece)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _stamp_index(panels: tuple[PanelView, ...]) -> int | None:
|
|
204
|
+
active = _active_index(panels)
|
|
205
|
+
if active is not None:
|
|
206
|
+
return active
|
|
207
|
+
for index in range(len(panels) - 1, -1, -1):
|
|
208
|
+
if panels[index].state is not PanelState.PENDING:
|
|
209
|
+
return index
|
|
210
|
+
return 0 if panels else None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _active_or_last(panels: tuple[PanelView, ...]) -> PanelView:
|
|
214
|
+
for panel in panels:
|
|
215
|
+
if panel.state is PanelState.ACTIVE:
|
|
216
|
+
return panel
|
|
217
|
+
for panel in reversed(panels):
|
|
218
|
+
if panel.state is not PanelState.PENDING:
|
|
219
|
+
return panel
|
|
220
|
+
return panels[0]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _active_index(panels: tuple[PanelView, ...]) -> int | None:
|
|
224
|
+
for index, panel in enumerate(panels):
|
|
225
|
+
if panel.state is PanelState.ACTIVE:
|
|
226
|
+
return index
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _column_widths(width: int, headers: list[str]) -> list[int]:
|
|
231
|
+
count = len(headers)
|
|
232
|
+
available = width - (count + 1)
|
|
233
|
+
minimums = [cell_len(header) for header in headers]
|
|
234
|
+
if sum(minimums) > available:
|
|
235
|
+
base = available // count
|
|
236
|
+
widths = [base] * count
|
|
237
|
+
widths[-1] += available - base * count
|
|
238
|
+
return widths
|
|
239
|
+
widths = list(minimums)
|
|
240
|
+
widths[-1] += available - sum(minimums)
|
|
241
|
+
return widths
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _top_border(header: str, width: int) -> str:
|
|
245
|
+
fixed = cell_len(_TOP_BORDER_PREFIX) + 1 + cell_len(_BORDER_TOP_RIGHT)
|
|
246
|
+
header = _fit(header, max(width - fixed, 0)).rstrip()
|
|
247
|
+
fill = width - cell_len(_TOP_BORDER_PREFIX) - cell_len(header) - 1 - cell_len(_BORDER_TOP_RIGHT)
|
|
248
|
+
return f"{_TOP_BORDER_PREFIX}{header} {_BORDER_HORIZONTAL * max(fill, 0)}{_BORDER_TOP_RIGHT}"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _bottom_border(widths: list[int]) -> str:
|
|
252
|
+
segments = (_BORDER_HORIZONTAL * column for column in widths)
|
|
253
|
+
return f"{_BORDER_BOTTOM_LEFT}{_BORDER_BOTTOM_JOIN.join(segments)}{_BORDER_BOTTOM_RIGHT}"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class TaskStrip:
|
|
257
|
+
"""Renders one task's strip at a given terminal width."""
|
|
258
|
+
|
|
259
|
+
def __init__(self, data: TaskStripData) -> None:
|
|
260
|
+
"""Hold `data`; nothing here mutates it."""
|
|
261
|
+
self._data = data
|
|
262
|
+
|
|
263
|
+
def render(self, width: int) -> Text:
|
|
264
|
+
"""Render the full 4-panel strip at `width >= STRIP_WIDE_MIN_COLUMNS`, else one line."""
|
|
265
|
+
if width >= STRIP_WIDE_MIN_COLUMNS:
|
|
266
|
+
return self._render_wide(width)
|
|
267
|
+
return self._render_narrow(width)
|
|
268
|
+
|
|
269
|
+
def _header_text(self) -> str:
|
|
270
|
+
return f"{self._data.task_id}{_HEADER_SEPARATOR}{self._data.title}"
|
|
271
|
+
|
|
272
|
+
def _render_narrow(self, width: int) -> Text:
|
|
273
|
+
panel = _active_or_last(self._data.panels)
|
|
274
|
+
pieces = [
|
|
275
|
+
self._data.task_id,
|
|
276
|
+
panel.stage_glyph,
|
|
277
|
+
panel.label,
|
|
278
|
+
panel.detail,
|
|
279
|
+
panel.role_line,
|
|
280
|
+
_narrow_glyph(panel),
|
|
281
|
+
]
|
|
282
|
+
line = _NARROW_SEPARATOR.join(piece for piece in pieces if piece)
|
|
283
|
+
if self._data.stamp is not None:
|
|
284
|
+
line = f"{line}{_HEADER_SEPARATOR}{self._data.stamp.render()}"
|
|
285
|
+
text = Text(line)
|
|
286
|
+
if self._data.stamp is not None and self._data.stamp.flash:
|
|
287
|
+
stamp = self._data.stamp.render()
|
|
288
|
+
start = line.rfind(stamp)
|
|
289
|
+
if start >= 0:
|
|
290
|
+
text.stylize(STYLE_REVERSE, start, start + cell_len(stamp))
|
|
291
|
+
if cell_len(line) > width:
|
|
292
|
+
text.truncate(width, overflow="crop")
|
|
293
|
+
return text
|
|
294
|
+
|
|
295
|
+
def _render_wide(self, width: int) -> Text:
|
|
296
|
+
panels = self._data.panels
|
|
297
|
+
headers = [_header_cell(index, panel) for index, panel in enumerate(panels)]
|
|
298
|
+
widths = _column_widths(width, headers)
|
|
299
|
+
|
|
300
|
+
header_cells = [
|
|
301
|
+
_fit(header, column) for header, column in zip(headers, widths, strict=True)
|
|
302
|
+
]
|
|
303
|
+
body_cells = [
|
|
304
|
+
_fit(_body_cell(panel), column) for panel, column in zip(panels, widths, strict=True)
|
|
305
|
+
]
|
|
306
|
+
role_cells = [
|
|
307
|
+
_fit(panel.role_line, column) for panel, column in zip(panels, widths, strict=True)
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
stamp_index = _stamp_index(panels) if self._data.stamp is not None else None
|
|
311
|
+
if stamp_index is not None and self._data.stamp is not None:
|
|
312
|
+
body_cells[stamp_index] = _fit(self._data.stamp.render(), widths[stamp_index])
|
|
313
|
+
role_cells[stamp_index] = _fit("", widths[stamp_index])
|
|
314
|
+
|
|
315
|
+
body = Text(_top_border(self._header_text(), width), style=STYLE_BOLD)
|
|
316
|
+
for row in (header_cells, body_cells, role_cells):
|
|
317
|
+
body.append("\n")
|
|
318
|
+
body.append(_BORDER_VERTICAL)
|
|
319
|
+
for index, cell in enumerate(row):
|
|
320
|
+
style = _panel_style(panels[index]) if row is body_cells else None
|
|
321
|
+
if (
|
|
322
|
+
row is body_cells
|
|
323
|
+
and index == stamp_index
|
|
324
|
+
and self._data.stamp is not None
|
|
325
|
+
and self._data.stamp.flash
|
|
326
|
+
):
|
|
327
|
+
style = f"{style} {STYLE_REVERSE}"
|
|
328
|
+
body.append(cell, style=style)
|
|
329
|
+
body.append(_BORDER_VERTICAL)
|
|
330
|
+
body.append("\n")
|
|
331
|
+
body.append(_bottom_border(widths))
|
|
332
|
+
return body
|
shikiru/ui/theme.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Shared semantic colors for Textual screens and evidence views."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from textual.theme import Theme
|
|
8
|
+
|
|
9
|
+
from shikiru.ui.tokens import (
|
|
10
|
+
ALERT_COLOR,
|
|
11
|
+
AMBER,
|
|
12
|
+
BACKGROUND_COLOR,
|
|
13
|
+
DETAIL_SURFACE_COLOR,
|
|
14
|
+
HOLD_COLOR,
|
|
15
|
+
NEUTRAL_BORDER_COLOR,
|
|
16
|
+
PRIMARY_TEXT_COLOR,
|
|
17
|
+
RAISED_SURFACE_COLOR,
|
|
18
|
+
SCROLLBAR_THUMB_ACTIVE_COLOR,
|
|
19
|
+
SCROLLBAR_THUMB_COLOR,
|
|
20
|
+
SCROLLBAR_THUMB_HOVER_COLOR,
|
|
21
|
+
SCROLLBAR_TRACK_COLOR,
|
|
22
|
+
SECONDARY_TEXT_COLOR,
|
|
23
|
+
SELECTED_SURFACE_COLOR,
|
|
24
|
+
STEEL_BLUE,
|
|
25
|
+
SUCCESS_COLOR,
|
|
26
|
+
SURFACE_COLOR,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from textual.app import App
|
|
31
|
+
|
|
32
|
+
BACKGROUND = BACKGROUND_COLOR
|
|
33
|
+
SURFACE = SURFACE_COLOR
|
|
34
|
+
RAISED_SURFACE = RAISED_SURFACE_COLOR
|
|
35
|
+
NEUTRAL_BORDER = NEUTRAL_BORDER_COLOR
|
|
36
|
+
PRIMARY_TEXT = PRIMARY_TEXT_COLOR
|
|
37
|
+
SECONDARY_TEXT = SECONDARY_TEXT_COLOR
|
|
38
|
+
FOCUS = STEEL_BLUE
|
|
39
|
+
THEME_NAME = "shikiru"
|
|
40
|
+
SCROLLBAR_WIDTH = 1
|
|
41
|
+
|
|
42
|
+
SHIKIRU_THEME = Theme(
|
|
43
|
+
name=THEME_NAME,
|
|
44
|
+
primary=FOCUS,
|
|
45
|
+
secondary=SECONDARY_TEXT,
|
|
46
|
+
warning=HOLD_COLOR,
|
|
47
|
+
error=ALERT_COLOR,
|
|
48
|
+
success=SUCCESS_COLOR,
|
|
49
|
+
accent=FOCUS,
|
|
50
|
+
foreground=PRIMARY_TEXT,
|
|
51
|
+
background=BACKGROUND,
|
|
52
|
+
surface=SURFACE,
|
|
53
|
+
panel=RAISED_SURFACE,
|
|
54
|
+
variables={
|
|
55
|
+
"border": NEUTRAL_BORDER,
|
|
56
|
+
"accent": FOCUS,
|
|
57
|
+
"amber": AMBER,
|
|
58
|
+
"detail-surface": DETAIL_SURFACE_COLOR,
|
|
59
|
+
"dusty-rose": ALERT_COLOR,
|
|
60
|
+
"neutral-border": NEUTRAL_BORDER,
|
|
61
|
+
"focus": FOCUS,
|
|
62
|
+
"muted-text": SECONDARY_TEXT,
|
|
63
|
+
"primary-text": PRIMARY_TEXT,
|
|
64
|
+
"sage": SUCCESS_COLOR,
|
|
65
|
+
"scrollbar": SCROLLBAR_THUMB_COLOR,
|
|
66
|
+
"scrollbar-active": SCROLLBAR_THUMB_ACTIVE_COLOR,
|
|
67
|
+
"scrollbar-background": SCROLLBAR_TRACK_COLOR,
|
|
68
|
+
"scrollbar-background-active": SCROLLBAR_TRACK_COLOR,
|
|
69
|
+
"scrollbar-background-hover": SCROLLBAR_TRACK_COLOR,
|
|
70
|
+
"scrollbar-corner-color": SCROLLBAR_TRACK_COLOR,
|
|
71
|
+
"scrollbar-hover": SCROLLBAR_THUMB_HOVER_COLOR,
|
|
72
|
+
"scrollbar-width": str(SCROLLBAR_WIDTH),
|
|
73
|
+
"secondary-text": SECONDARY_TEXT,
|
|
74
|
+
"selected-surface": SELECTED_SURFACE_COLOR,
|
|
75
|
+
"steel-blue": STEEL_BLUE,
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def install_theme[ReturnT](app: App[ReturnT]) -> None:
|
|
81
|
+
"""Register the shared Shikiru palette on a Textual application."""
|
|
82
|
+
app.register_theme(SHIKIRU_THEME)
|
|
83
|
+
app.theme = THEME_NAME
|
shikiru/ui/tokens.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Colour tokens for the shared visual language."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from shikiru import palette
|
|
6
|
+
from shikiru.domain.levels import UsageLevel
|
|
7
|
+
from shikiru.domain.tasks import Role
|
|
8
|
+
|
|
9
|
+
BACKGROUND_COLOR = palette.BACKGROUND_COLOR
|
|
10
|
+
SURFACE_COLOR = palette.SURFACE_COLOR
|
|
11
|
+
RAISED_SURFACE_COLOR = palette.RAISED_SURFACE_COLOR
|
|
12
|
+
SELECTED_SURFACE_COLOR = palette.SELECTED_SURFACE_COLOR
|
|
13
|
+
DETAIL_SURFACE_COLOR = palette.DETAIL_SURFACE_COLOR
|
|
14
|
+
NEUTRAL_BORDER_COLOR = palette.NEUTRAL_BORDER_COLOR
|
|
15
|
+
PRIMARY_TEXT_COLOR = palette.PRIMARY_TEXT_COLOR
|
|
16
|
+
SECONDARY_TEXT_COLOR = palette.SECONDARY_TEXT_COLOR
|
|
17
|
+
STEEL_BLUE = palette.STEEL_BLUE
|
|
18
|
+
SAGE = palette.SAGE
|
|
19
|
+
AMBER = palette.AMBER
|
|
20
|
+
DUSTY_ROSE = palette.DUSTY_ROSE
|
|
21
|
+
SCROLLBAR_THUMB_COLOR = palette.SCROLLBAR_THUMB_COLOR
|
|
22
|
+
SCROLLBAR_THUMB_HOVER_COLOR = palette.SCROLLBAR_THUMB_HOVER_COLOR
|
|
23
|
+
SCROLLBAR_THUMB_ACTIVE_COLOR = palette.SCROLLBAR_THUMB_ACTIVE_COLOR
|
|
24
|
+
SCROLLBAR_TRACK_COLOR = palette.SCROLLBAR_TRACK_COLOR
|
|
25
|
+
|
|
26
|
+
ROLE_COLORS: dict[Role, str] = {
|
|
27
|
+
Role.LEADER: STEEL_BLUE,
|
|
28
|
+
Role.IMPLEMENTER: STEEL_BLUE,
|
|
29
|
+
Role.HELPER: STEEL_BLUE,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
OPERATOR_COLOR = palette.STEEL_BLUE
|
|
33
|
+
ENGINE_COLOR = palette.SECONDARY_TEXT_COLOR
|
|
34
|
+
SUCCESS_COLOR = palette.SAGE
|
|
35
|
+
REWORK_COLOR = palette.AMBER
|
|
36
|
+
HOLD_COLOR = palette.AMBER
|
|
37
|
+
ALERT_COLOR = palette.DUSTY_ROSE
|
|
38
|
+
STOPPED_DURING = "stopped during"
|
|
39
|
+
IDLE_COLOR = palette.SECONDARY_TEXT_COLOR
|
|
40
|
+
|
|
41
|
+
STYLE_DIM = "dim"
|
|
42
|
+
STYLE_BOLD = "bold"
|
|
43
|
+
STYLE_REVERSE = "reverse"
|
|
44
|
+
"""Attributes paired with every panel colour: under `NO_COLOR`, Rich keeps the
|
|
45
|
+
attribute and drops the colour, giving the required monochrome bold/dim/reverse."""
|
|
46
|
+
|
|
47
|
+
USAGE_LEVEL_COLORS: dict[UsageLevel, str] = {
|
|
48
|
+
UsageLevel.OK: SAGE,
|
|
49
|
+
UsageLevel.WARN: AMBER,
|
|
50
|
+
UsageLevel.ALERT: DUSTY_ROSE,
|
|
51
|
+
}
|
shikiru/view_log.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""The role view log: its path, row keys and the pane command tailing it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from shikiru.agents.contracts import (
|
|
11
|
+
Message,
|
|
12
|
+
SessionStarted,
|
|
13
|
+
ToolFinished,
|
|
14
|
+
ToolStarted,
|
|
15
|
+
TurnEnded,
|
|
16
|
+
TurnFailed,
|
|
17
|
+
role_dir,
|
|
18
|
+
shikiru_argv,
|
|
19
|
+
)
|
|
20
|
+
from shikiru.domain.quota_waits import QuotaAction
|
|
21
|
+
from shikiru.private import TEXT_ENCODING, make_private_dir, write_private_text
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
VIEW_LOG_NAME = "view.jsonl"
|
|
27
|
+
VIEW_TIME_KEY = "ts"
|
|
28
|
+
VIEW_TURN_KEY = "turn"
|
|
29
|
+
VIEW_RUNG_KEY = "rung"
|
|
30
|
+
VIEW_SIGNAL_KEY = "signal"
|
|
31
|
+
VIEW_MESSAGE_SIGNAL = Message.__name__
|
|
32
|
+
VIEW_MESSAGE_TEXT_KEY = "text"
|
|
33
|
+
VIEW_TURN_ENDED_TEXT_KEY = "last_message"
|
|
34
|
+
VIEW_SESSION_STARTED_SIGNAL = SessionStarted.__name__
|
|
35
|
+
VIEW_TOOL_STARTED_SIGNAL = ToolStarted.__name__
|
|
36
|
+
VIEW_TOOL_FINISHED_SIGNAL = ToolFinished.__name__
|
|
37
|
+
VIEW_TURN_ENDED_SIGNAL = TurnEnded.__name__
|
|
38
|
+
VIEW_TURN_FAILED_SIGNAL = TurnFailed.__name__
|
|
39
|
+
QUOTA_STATUS_NAME = "quota-status.json"
|
|
40
|
+
QUOTA_STATUS_ACTION_KEY = "action"
|
|
41
|
+
QUOTA_STATUS_AT_KEY = "at"
|
|
42
|
+
TURN_DIR_SEPARATOR = "-"
|
|
43
|
+
TURN_DIR_ITERATION_MARKER = "-i"
|
|
44
|
+
CLI_VIEW_COMMAND = "view"
|
|
45
|
+
CLI_PROJECT_FLAG = "--project"
|
|
46
|
+
PARTIAL_SUFFIX = ".partial"
|
|
47
|
+
_TAIL_BLOCK_BYTES = 65536
|
|
48
|
+
_LINE_BREAK = b"\n"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def ordered_turn_dirs(turns_root: Path) -> list[Path]:
|
|
52
|
+
"""Return the turn directories of `turns_root` ordered by turn number, however many digits."""
|
|
53
|
+
return sorted(
|
|
54
|
+
(entry for entry in turns_root.iterdir() if entry.is_dir()),
|
|
55
|
+
key=lambda entry: int(entry.name.partition(TURN_DIR_SEPARATOR)[0]),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def turn_iteration(turn_name: str) -> int | None:
|
|
60
|
+
"""Return the task iteration a turn directory name (`NN-<step>-i<n>`) was started in."""
|
|
61
|
+
_, marker, iteration = turn_name.rpartition(TURN_DIR_ITERATION_MARKER)
|
|
62
|
+
return int(iteration) if marker and iteration.isdecimal() else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def view_argv(project_root: Path, task_id: str, role: str) -> tuple[str, ...]:
|
|
66
|
+
"""Return the argv of `role`'s read-only view pane for `task_id`."""
|
|
67
|
+
return shikiru_argv(CLI_PROJECT_FLAG, str(project_root), CLI_VIEW_COMMAND, task_id, role)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def view_log_path(run_dir: Path, role: str) -> Path:
|
|
71
|
+
"""Return `role`'s live view log, one JSON row per agent signal."""
|
|
72
|
+
return role_dir(run_dir, role) / VIEW_LOG_NAME
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def quota_status_path(run_dir: Path, role: str) -> Path:
|
|
76
|
+
"""Return the file holding `role`'s current quota status, present only during a quota wait."""
|
|
77
|
+
return role_dir(run_dir, role) / QUOTA_STATUS_NAME
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def write_quota_status(path: Path, action: QuotaAction | None, at: datetime | None) -> None:
|
|
81
|
+
"""Publish a quota wait for `path`'s view pane, replacing any earlier one in a single step."""
|
|
82
|
+
payload = {
|
|
83
|
+
QUOTA_STATUS_ACTION_KEY: action.value if action is not None else None,
|
|
84
|
+
QUOTA_STATUS_AT_KEY: at.isoformat() if at is not None else None,
|
|
85
|
+
}
|
|
86
|
+
make_private_dir(path.parent)
|
|
87
|
+
partial = path.with_name(f"{path.name}{PARTIAL_SUFFIX}")
|
|
88
|
+
write_private_text(partial, json.dumps(payload))
|
|
89
|
+
partial.replace(path)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def clear_quota_status(path: Path) -> None:
|
|
93
|
+
"""Remove the quota status of `path`'s view pane; nothing happens when none is published."""
|
|
94
|
+
path.unlink(missing_ok=True)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def read_quota_status(path: Path) -> tuple[QuotaAction | None, datetime | None] | None:
|
|
98
|
+
"""Return the published quota status as its action and time, or none outside a quota wait."""
|
|
99
|
+
try:
|
|
100
|
+
text = path.read_text(encoding=TEXT_ENCODING)
|
|
101
|
+
except FileNotFoundError:
|
|
102
|
+
return None
|
|
103
|
+
payload = json.loads(text)
|
|
104
|
+
action = payload[QUOTA_STATUS_ACTION_KEY]
|
|
105
|
+
at = payload[QUOTA_STATUS_AT_KEY]
|
|
106
|
+
return (
|
|
107
|
+
QuotaAction(action) if action is not None else None,
|
|
108
|
+
datetime.fromisoformat(at) if at is not None else None,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def read_tail_lines(path: Path, count: int) -> list[str]:
|
|
113
|
+
"""Return the last `count` lines of `path`, reading no more of the file than they need."""
|
|
114
|
+
blocks: list[bytes] = []
|
|
115
|
+
line_breaks = 0
|
|
116
|
+
with path.open("rb") as handle:
|
|
117
|
+
position = handle.seek(0, os.SEEK_END)
|
|
118
|
+
while position > 0 and line_breaks <= count:
|
|
119
|
+
size = min(_TAIL_BLOCK_BYTES, position)
|
|
120
|
+
position -= size
|
|
121
|
+
handle.seek(position)
|
|
122
|
+
block = handle.read(size)
|
|
123
|
+
blocks.append(block)
|
|
124
|
+
line_breaks += block.count(_LINE_BREAK)
|
|
125
|
+
text = b"".join(reversed(blocks)).decode(TEXT_ENCODING, errors="replace")
|
|
126
|
+
return text.splitlines()[-count:]
|