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/session.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Shared facts for session listings and attach options."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
|
|
9
|
+
from shikiru.ui.glyphs import GLYPH_ENGINE
|
|
10
|
+
from shikiru.ui.section import BOLD_PRIMARY, number
|
|
11
|
+
from shikiru.ui.tokens import AMBER, SECONDARY_TEXT_COLOR, STYLE_BOLD, SUCCESS_COLOR
|
|
12
|
+
|
|
13
|
+
_SessionEngineState = Literal["up", "down"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def session_identity(session: str) -> Text:
|
|
17
|
+
"""Return a session identity as bold literal text."""
|
|
18
|
+
return Text(session, style=BOLD_PRIMARY, overflow="fold")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def session_engine_value(engine_state: _SessionEngineState) -> Text:
|
|
22
|
+
"""Return the engine glyph and muted or active state word."""
|
|
23
|
+
match engine_state:
|
|
24
|
+
case "up":
|
|
25
|
+
style = f"{STYLE_BOLD} {SUCCESS_COLOR}"
|
|
26
|
+
case "down":
|
|
27
|
+
style = SECONDARY_TEXT_COLOR
|
|
28
|
+
return Text.assemble(
|
|
29
|
+
(GLYPH_ENGINE, style),
|
|
30
|
+
(" ", SECONDARY_TEXT_COLOR),
|
|
31
|
+
(engine_state, style),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def session_engine_fact(engine_state: _SessionEngineState) -> Text:
|
|
36
|
+
"""Return the labeled engine state used in a stacked session row."""
|
|
37
|
+
text = Text("Engine ", style=SECONDARY_TEXT_COLOR)
|
|
38
|
+
text.append_text(session_engine_value(engine_state))
|
|
39
|
+
return text
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def session_count(label: str, count: int) -> Text:
|
|
43
|
+
"""Return one labeled session count."""
|
|
44
|
+
return Text.assemble((f"{label} ", SECONDARY_TEXT_COLOR), number(str(count)))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def escalation_count(count: int) -> Text:
|
|
48
|
+
"""Return an amber nonzero escalation count or a muted zero."""
|
|
49
|
+
style = f"{STYLE_BOLD} {AMBER}" if count else SECONDARY_TEXT_COLOR
|
|
50
|
+
return Text(str(count), style=style)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def session_facts(
|
|
54
|
+
engine_state: _SessionEngineState,
|
|
55
|
+
running: int,
|
|
56
|
+
queued: int,
|
|
57
|
+
escalated: int,
|
|
58
|
+
) -> tuple[Text, ...]:
|
|
59
|
+
"""Return engine and progress facts in their shared display order."""
|
|
60
|
+
return (
|
|
61
|
+
session_engine_fact(engine_state),
|
|
62
|
+
session_count("running", running),
|
|
63
|
+
session_count("queued", queued),
|
|
64
|
+
Text.assemble(("escalated ", SECONDARY_TEXT_COLOR), escalation_count(escalated)),
|
|
65
|
+
)
|
shikiru/ui/size.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Human-readable byte counts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
_UNITS = ("B", "KiB", "MiB", "GiB", "TiB")
|
|
6
|
+
_UNIT_STEP = 1024
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def format_size(size_bytes: int) -> str:
|
|
10
|
+
"""Return `size_bytes` in the largest binary unit that keeps it at or above one."""
|
|
11
|
+
value = float(size_bytes)
|
|
12
|
+
unit = _UNITS[0]
|
|
13
|
+
for unit in _UNITS:
|
|
14
|
+
if value < _UNIT_STEP or unit == _UNITS[-1]:
|
|
15
|
+
break
|
|
16
|
+
value /= _UNIT_STEP
|
|
17
|
+
return f"{value:.0f} {unit}" if unit == _UNITS[0] else f"{value:.1f} {unit}"
|
shikiru/ui/splash.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Escalation splash panels."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
|
|
9
|
+
from shikiru.ui.tokens import ALERT_COLOR, STYLE_BOLD, STYLE_DIM
|
|
10
|
+
|
|
11
|
+
_SPEED_LINE = "\N{BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT}"
|
|
12
|
+
_OPTION_PREFIX = " "
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class SplashPanel:
|
|
17
|
+
"""Render an escalation headline, question and packet options in an alert frame."""
|
|
18
|
+
|
|
19
|
+
headline: str
|
|
20
|
+
question: str
|
|
21
|
+
options: tuple[str, ...]
|
|
22
|
+
pulse_red: bool = True
|
|
23
|
+
|
|
24
|
+
def render(self) -> Text:
|
|
25
|
+
"""Render the panel with numbered options and red speed-line framing."""
|
|
26
|
+
lines = Text()
|
|
27
|
+
prefix = f"{_SPEED_LINE} {_SPEED_LINE} "
|
|
28
|
+
suffix = f" {_SPEED_LINE} {_SPEED_LINE}"
|
|
29
|
+
top = f"{prefix}{self.headline}{suffix}"
|
|
30
|
+
lines.append(f"{top}\n")
|
|
31
|
+
lines.append(self.question)
|
|
32
|
+
for number, option in enumerate(self.options, start=1):
|
|
33
|
+
lines.append(f"\n{_OPTION_PREFIX}{number}. {option}")
|
|
34
|
+
bottom = f"{_SPEED_LINE} {_SPEED_LINE} {_SPEED_LINE}"
|
|
35
|
+
lines.append(f"\n{bottom}")
|
|
36
|
+
border_style = f"{STYLE_BOLD} {ALERT_COLOR}" if self.pulse_red else STYLE_DIM
|
|
37
|
+
lines.stylize(border_style, 0, len(prefix))
|
|
38
|
+
suffix_start = len(prefix) + len(self.headline)
|
|
39
|
+
lines.stylize(border_style, suffix_start, len(top))
|
|
40
|
+
bottom_start = len(lines.plain) - len(bottom)
|
|
41
|
+
lines.stylize(border_style, bottom_start, len(lines.plain))
|
|
42
|
+
return lines
|
shikiru/ui/status.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Shared task-status visuals for operator surfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from shikiru.domain.tasks import Status
|
|
8
|
+
from shikiru.ui.glyphs import (
|
|
9
|
+
GLYPH_BLOCKED,
|
|
10
|
+
GLYPH_DONE,
|
|
11
|
+
GLYPH_ENGINE,
|
|
12
|
+
GLYPH_ESCALATED,
|
|
13
|
+
GLYPH_PENDING,
|
|
14
|
+
)
|
|
15
|
+
from shikiru.ui.tokens import (
|
|
16
|
+
ALERT_COLOR,
|
|
17
|
+
ENGINE_COLOR,
|
|
18
|
+
IDLE_COLOR,
|
|
19
|
+
REWORK_COLOR,
|
|
20
|
+
STYLE_BOLD,
|
|
21
|
+
STYLE_DIM,
|
|
22
|
+
STYLE_REVERSE,
|
|
23
|
+
SUCCESS_COLOR,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class StatusVisual:
|
|
29
|
+
"""The glyph and Rich style representing one task status."""
|
|
30
|
+
|
|
31
|
+
glyph: str
|
|
32
|
+
style: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_STATUS_VISUALS: dict[Status, StatusVisual] = {
|
|
36
|
+
Status.QUEUED: StatusVisual(GLYPH_PENDING, f"{STYLE_DIM} {IDLE_COLOR}"),
|
|
37
|
+
Status.RUNNING: StatusVisual(GLYPH_ENGINE, f"{STYLE_BOLD} {ENGINE_COLOR}"),
|
|
38
|
+
Status.INTEGRATING: StatusVisual(GLYPH_ENGINE, f"{STYLE_BOLD} {ENGINE_COLOR}"),
|
|
39
|
+
Status.ESCALATED: StatusVisual(GLYPH_ESCALATED, f"{STYLE_REVERSE} {ALERT_COLOR}"),
|
|
40
|
+
Status.DONE: StatusVisual(GLYPH_DONE, f"{STYLE_BOLD} {SUCCESS_COLOR}"),
|
|
41
|
+
Status.CANCELLED: StatusVisual(GLYPH_BLOCKED, f"{STYLE_BOLD} {REWORK_COLOR}"),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def status_visual(status: Status) -> StatusVisual:
|
|
46
|
+
"""Return the shared glyph and style for `status`."""
|
|
47
|
+
return _STATUS_VISUALS[status]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""A status strip pinned above a scrolling terminal view, without touching the output below it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import select
|
|
8
|
+
import termios
|
|
9
|
+
import textwrap
|
|
10
|
+
import tty
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from typing import TextIO
|
|
17
|
+
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
|
|
20
|
+
_ESCAPE = "\x1b"
|
|
21
|
+
_CSI = f"{_ESCAPE}["
|
|
22
|
+
_SAVE_CURSOR = f"{_ESCAPE}7"
|
|
23
|
+
_RESTORE_CURSOR = f"{_ESCAPE}8"
|
|
24
|
+
_RESET_REGION = f"{_CSI}r"
|
|
25
|
+
_ERASE_LINE = f"{_CSI}2K"
|
|
26
|
+
_CURSOR_REPORT_REQUEST = f"{_CSI}6n"
|
|
27
|
+
_CURSOR_REPORT = re.compile(rf"{_ESCAPE}\[(\d+);\d+R")
|
|
28
|
+
_CURSOR_REPORT_TIMEOUT_SECONDS = 2.0
|
|
29
|
+
_REPORT_READ_BYTES = 64
|
|
30
|
+
_FIRST_COLUMN = 1
|
|
31
|
+
_FIRST_ROW = 1
|
|
32
|
+
_NO_ROWS: tuple[str, ...] = ()
|
|
33
|
+
_CLEAR_SCROLLBACK = f"{_CSI}3J"
|
|
34
|
+
_CLEAR_SCREEN = f"{_CSI}2J"
|
|
35
|
+
_HOME = f"{_CSI}H"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CursorReportError(RuntimeError):
|
|
39
|
+
"""The terminal did not report where its cursor is."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _go_to(row: int) -> str:
|
|
43
|
+
return f"{_CSI}{row};{_FIRST_COLUMN}H"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class StatusStrip:
|
|
47
|
+
"""Reserves the top rows of a terminal for a status text and keeps the output below it.
|
|
48
|
+
|
|
49
|
+
The strip sits in rows `1..height`; output scrolls in the rows beneath it through a scroll
|
|
50
|
+
region, so it never overwrites or duplicates the strip. Showing or hiding the strip shifts
|
|
51
|
+
the visible output by its height instead of overwriting it; the rows pushed off the top
|
|
52
|
+
stay in the terminal's scrollback. A horizontal resize clears the reflowed terminal buffer;
|
|
53
|
+
the view then restores its output from the event log under the newly sized strip.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, console: Console, keyboard: TextIO, style: str) -> None:
|
|
57
|
+
"""Draw onto `console` in `style`, asking the terminal behind `keyboard` for its cursor."""
|
|
58
|
+
self._console = console
|
|
59
|
+
self._keyboard = keyboard
|
|
60
|
+
self._style = style
|
|
61
|
+
self._rows: tuple[str, ...] = _NO_ROWS
|
|
62
|
+
self._width: int = console.size.width
|
|
63
|
+
self._height: int = console.size.height
|
|
64
|
+
|
|
65
|
+
def sync(self, text: str | None) -> bool:
|
|
66
|
+
"""Draw `text` unless its row count changed; return whether output must be replayed."""
|
|
67
|
+
size = self._console.size
|
|
68
|
+
rows = self._wrap(text, size.width, size.height)
|
|
69
|
+
if rows == self._rows and size.width == self._width and size.height == self._height:
|
|
70
|
+
return False
|
|
71
|
+
if self._rows and len(rows) != len(self._rows):
|
|
72
|
+
return True
|
|
73
|
+
self._width = size.width
|
|
74
|
+
self._height = size.height
|
|
75
|
+
self._apply(rows, size.height)
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def active(self) -> bool:
|
|
80
|
+
"""Return whether a status currently reserves terminal rows."""
|
|
81
|
+
return bool(self._rows)
|
|
82
|
+
|
|
83
|
+
def reset_for_replay(self) -> None:
|
|
84
|
+
"""Clear the screen and scrollback before the view restores its logged output."""
|
|
85
|
+
out = self._console.file
|
|
86
|
+
out.write(_CLEAR_SCREEN + _CLEAR_SCROLLBACK + _RESET_REGION + _HOME)
|
|
87
|
+
out.flush()
|
|
88
|
+
self._rows = _NO_ROWS
|
|
89
|
+
self._width = self._console.size.width
|
|
90
|
+
self._height = self._console.size.height
|
|
91
|
+
|
|
92
|
+
def _wrap(self, text: str | None, width: int, height: int) -> tuple[str, ...]:
|
|
93
|
+
if text is None:
|
|
94
|
+
return _NO_ROWS
|
|
95
|
+
return tuple(textwrap.wrap(text, width) or [""])[: max(height - 1, 1)]
|
|
96
|
+
|
|
97
|
+
def _apply(self, rows: tuple[str, ...], terminal_height: int) -> None:
|
|
98
|
+
previous = len(self._rows)
|
|
99
|
+
row = min(self._cursor_row(), terminal_height)
|
|
100
|
+
out = self._console.file
|
|
101
|
+
out.write(_RESET_REGION)
|
|
102
|
+
if len(rows) > previous:
|
|
103
|
+
grown = len(rows) - previous
|
|
104
|
+
overflow = max(row + grown - terminal_height, 0)
|
|
105
|
+
out.write(_go_to(terminal_height) + "\n" * overflow)
|
|
106
|
+
out.write(_go_to(previous + 1) + f"{_CSI}{grown}L")
|
|
107
|
+
row = min(row + grown, terminal_height)
|
|
108
|
+
elif len(rows) < previous:
|
|
109
|
+
shrunk = previous - len(rows)
|
|
110
|
+
out.write(_go_to(len(rows) + 1) + f"{_CSI}{shrunk}M")
|
|
111
|
+
row = max(row - shrunk, _FIRST_ROW)
|
|
112
|
+
if rows:
|
|
113
|
+
out.write(f"{_CSI}{len(rows) + 1};{terminal_height}r")
|
|
114
|
+
out.write(_go_to(row))
|
|
115
|
+
out.flush()
|
|
116
|
+
self._rows = rows
|
|
117
|
+
self._draw(rows)
|
|
118
|
+
|
|
119
|
+
def _draw(self, rows: tuple[str, ...]) -> None:
|
|
120
|
+
out = self._console.file
|
|
121
|
+
out.write(_SAVE_CURSOR)
|
|
122
|
+
for index, line in enumerate(rows):
|
|
123
|
+
out.write(_go_to(index + 1) + _ERASE_LINE)
|
|
124
|
+
self._console.print(Text(line, style=self._style), end="")
|
|
125
|
+
out.write(_RESTORE_CURSOR)
|
|
126
|
+
out.flush()
|
|
127
|
+
|
|
128
|
+
def _cursor_row(self) -> int:
|
|
129
|
+
descriptor = self._keyboard.fileno()
|
|
130
|
+
saved = termios.tcgetattr(descriptor)
|
|
131
|
+
try:
|
|
132
|
+
tty.setcbreak(descriptor)
|
|
133
|
+
self._console.file.write(_CURSOR_REPORT_REQUEST)
|
|
134
|
+
self._console.file.flush()
|
|
135
|
+
received = ""
|
|
136
|
+
while True:
|
|
137
|
+
ready, _, _ = select.select([descriptor], [], [], _CURSOR_REPORT_TIMEOUT_SECONDS)
|
|
138
|
+
if not ready:
|
|
139
|
+
message = "the terminal did not answer the cursor position request"
|
|
140
|
+
raise CursorReportError(message)
|
|
141
|
+
received += self._read(descriptor)
|
|
142
|
+
report = _CURSOR_REPORT.search(received)
|
|
143
|
+
if report is not None:
|
|
144
|
+
return int(report.group(1))
|
|
145
|
+
finally:
|
|
146
|
+
termios.tcsetattr(descriptor, termios.TCSADRAIN, saved)
|
|
147
|
+
|
|
148
|
+
def _read(self, descriptor: int) -> str:
|
|
149
|
+
return os.read(descriptor, _REPORT_READ_BYTES).decode(errors="ignore")
|
shikiru/ui/stream.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Renders a role's `view.jsonl` rows as terminal lines."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
|
|
9
|
+
from shikiru.agents.contracts import (
|
|
10
|
+
Compacted,
|
|
11
|
+
Message,
|
|
12
|
+
QuotaReported,
|
|
13
|
+
SessionStarted,
|
|
14
|
+
ToolFinished,
|
|
15
|
+
ToolOutcome,
|
|
16
|
+
ToolStarted,
|
|
17
|
+
TurnEnded,
|
|
18
|
+
TurnFailed,
|
|
19
|
+
UsageReported,
|
|
20
|
+
)
|
|
21
|
+
from shikiru.ui.glyphs import GLYPH_DONE, GLYPH_ESCALATED, GLYPH_PENDING, ROLE_GLYPHS
|
|
22
|
+
from shikiru.ui.tokens import ALERT_COLOR, IDLE_COLOR, ROLE_COLORS, STYLE_BOLD, STYLE_DIM
|
|
23
|
+
from shikiru.view_log import VIEW_RUNG_KEY, VIEW_SIGNAL_KEY, VIEW_TURN_KEY
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Callable, Mapping
|
|
27
|
+
|
|
28
|
+
from shikiru.domain.tasks import Role
|
|
29
|
+
|
|
30
|
+
type ViewRow = Mapping[str, Any]
|
|
31
|
+
|
|
32
|
+
_TOOL_GLYPH = "▸"
|
|
33
|
+
_RUNG_SEPARATOR = ":"
|
|
34
|
+
_EFFORT_SEPARATOR = "/"
|
|
35
|
+
_DETAIL_SEPARATOR = " · "
|
|
36
|
+
_OUTCOME_FIELD = "outcome"
|
|
37
|
+
_EXIT_CODE_FIELD = "exit_code"
|
|
38
|
+
_EXIT_LABEL = "exit"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UnknownViewSignalError(ValueError):
|
|
42
|
+
"""A `view.jsonl` row names a signal this renderer does not know."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class StreamLine:
|
|
46
|
+
"""Turns view rows into Rich lines, opening each new turn with its step and rung."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, role: Role) -> None:
|
|
49
|
+
"""Render rows for `role`, whose colour and glyph mark turn boundaries."""
|
|
50
|
+
self._role = role
|
|
51
|
+
self._turn: str | None = None
|
|
52
|
+
self._renderers: dict[str, Callable[[ViewRow], Text | None]] = {
|
|
53
|
+
SessionStarted.__name__: _session_started,
|
|
54
|
+
Message.__name__: _message,
|
|
55
|
+
ToolStarted.__name__: _tool_started,
|
|
56
|
+
ToolFinished.__name__: _tool_finished,
|
|
57
|
+
UsageReported.__name__: _usage,
|
|
58
|
+
QuotaReported.__name__: _silent,
|
|
59
|
+
Compacted.__name__: _compacted,
|
|
60
|
+
TurnEnded.__name__: _turn_ended,
|
|
61
|
+
TurnFailed.__name__: _turn_failed,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def reset(self) -> None:
|
|
65
|
+
"""Reset turn-boundary tracking before replaying a view log from its start."""
|
|
66
|
+
self._turn = None
|
|
67
|
+
|
|
68
|
+
def render(self, row: ViewRow) -> list[Text]:
|
|
69
|
+
"""Return the lines `row` adds to the view, headed by a boundary on a new turn."""
|
|
70
|
+
signal = row[VIEW_SIGNAL_KEY]
|
|
71
|
+
renderer = self._renderers.get(signal)
|
|
72
|
+
if renderer is None:
|
|
73
|
+
message = f"unknown view signal {signal!r}"
|
|
74
|
+
raise UnknownViewSignalError(message)
|
|
75
|
+
lines: list[Text] = []
|
|
76
|
+
if row[VIEW_TURN_KEY] != self._turn:
|
|
77
|
+
self._turn = row[VIEW_TURN_KEY]
|
|
78
|
+
lines.append(self._boundary(row))
|
|
79
|
+
line = renderer(row)
|
|
80
|
+
if line is not None:
|
|
81
|
+
lines.append(line)
|
|
82
|
+
return lines
|
|
83
|
+
|
|
84
|
+
def _boundary(self, row: ViewRow) -> Text:
|
|
85
|
+
rung = row[VIEW_RUNG_KEY]
|
|
86
|
+
label = _RUNG_SEPARATOR.join((rung["agent"], rung["model"]))
|
|
87
|
+
if rung["effort"]:
|
|
88
|
+
label = f"{label}{_EFFORT_SEPARATOR}{rung['effort']}"
|
|
89
|
+
return Text(
|
|
90
|
+
f"{ROLE_GLYPHS[self._role]} {row[VIEW_TURN_KEY]} · {label}",
|
|
91
|
+
style=f"{STYLE_BOLD} {ROLE_COLORS[self._role]}",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _silent(_row: ViewRow) -> None:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _session_started(row: ViewRow) -> Text:
|
|
100
|
+
return Text(f"{GLYPH_PENDING} session {row['session_id']}", style=STYLE_DIM)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _message(row: ViewRow) -> Text:
|
|
104
|
+
return Text(row["text"])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _tool_started(row: ViewRow) -> Text:
|
|
108
|
+
detail = f" {row['detail']}" if row["detail"] else ""
|
|
109
|
+
return Text(f"{_TOOL_GLYPH} {row['name']}{detail}", style=IDLE_COLOR)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _tool_finished(row: ViewRow) -> Text | None:
|
|
113
|
+
outcome = row.get(_OUTCOME_FIELD)
|
|
114
|
+
exit_code = row.get(_EXIT_CODE_FIELD)
|
|
115
|
+
if (
|
|
116
|
+
outcome not in (ToolOutcome.SUCCESS.value, ToolOutcome.FAILURE.value)
|
|
117
|
+
and type(exit_code) is not int
|
|
118
|
+
):
|
|
119
|
+
return None
|
|
120
|
+
details = [row["name"]]
|
|
121
|
+
if outcome in (ToolOutcome.SUCCESS.value, ToolOutcome.FAILURE.value):
|
|
122
|
+
details.append(outcome)
|
|
123
|
+
if type(exit_code) is int:
|
|
124
|
+
details.append(f"{_EXIT_LABEL} {exit_code}")
|
|
125
|
+
style = _tool_outcome_style(outcome)
|
|
126
|
+
return Text(f"{_TOOL_GLYPH} {_DETAIL_SEPARATOR.join(details)}", style=style)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _tool_outcome_style(outcome: object) -> str:
|
|
130
|
+
if outcome == ToolOutcome.FAILURE.value:
|
|
131
|
+
return ALERT_COLOR
|
|
132
|
+
if outcome == ToolOutcome.SUCCESS.value:
|
|
133
|
+
return IDLE_COLOR
|
|
134
|
+
return STYLE_DIM
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _usage(row: ViewRow) -> Text:
|
|
138
|
+
usage = row["usage"]
|
|
139
|
+
tokens = usage["input_tokens"] + usage["output_tokens"]
|
|
140
|
+
return Text(f"{GLYPH_PENDING} {tokens} tokens", style=STYLE_DIM)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _compacted(_row: ViewRow) -> Text:
|
|
144
|
+
return Text(f"{GLYPH_PENDING} context compacted", style=STYLE_DIM)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _turn_ended(_row: ViewRow) -> Text:
|
|
148
|
+
return Text(f"{GLYPH_DONE} turn ended", style=STYLE_DIM)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _turn_failed(row: ViewRow) -> Text:
|
|
152
|
+
return Text(f"{GLYPH_ESCALATED} {row['kind']}: {row['message']}", style=ALERT_COLOR)
|