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,1389 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import resources
|
|
4
|
+
from typing import TYPE_CHECKING, ClassVar, cast
|
|
5
|
+
|
|
6
|
+
from rich.console import Group
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from textual import on
|
|
9
|
+
from textual.app import App, ComposeResult
|
|
10
|
+
from textual.binding import Binding
|
|
11
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
12
|
+
from textual.message import Message
|
|
13
|
+
from textual.widgets import Button, Input, Label, Rule, Select, Static
|
|
14
|
+
|
|
15
|
+
from shikiru.agents.settings import AgentSetting, AgentSettingError, SettingKind
|
|
16
|
+
from shikiru.config.settings import (
|
|
17
|
+
ANY_COMMENT_FEEDBACK,
|
|
18
|
+
CHANGES_REQUESTED_FEEDBACK,
|
|
19
|
+
MERGE_INTEGRATION,
|
|
20
|
+
PULL_REQUEST_INTEGRATION,
|
|
21
|
+
SQUASH_INTEGRATION,
|
|
22
|
+
)
|
|
23
|
+
from shikiru.dashboard.ladder_editor import LADDER_ACTION_CSS, LadderConfiguration, LadderEditor
|
|
24
|
+
from shikiru.dashboard.orchestration_fields import (
|
|
25
|
+
MAX_CPU,
|
|
26
|
+
MAX_ITERATIONS,
|
|
27
|
+
MAX_PARALLEL,
|
|
28
|
+
MAX_QUOTA_WAIT,
|
|
29
|
+
MIN_FREE_RAM,
|
|
30
|
+
ORCHESTRATION_FIELDS,
|
|
31
|
+
PUSH_INTERVAL,
|
|
32
|
+
STALL_TIMEOUT,
|
|
33
|
+
TOOL_TIMEOUT,
|
|
34
|
+
OrchestrationFieldError,
|
|
35
|
+
)
|
|
36
|
+
from shikiru.dashboard.row_list import RowList
|
|
37
|
+
from shikiru.domain.tasks import Role
|
|
38
|
+
from shikiru.errors import ConfigError, ShikiruError
|
|
39
|
+
from shikiru.operator.detectors import (
|
|
40
|
+
DetectedProject,
|
|
41
|
+
InitWizardResult,
|
|
42
|
+
OrchestrationChoices,
|
|
43
|
+
ProjectDetectors,
|
|
44
|
+
)
|
|
45
|
+
from shikiru.paths import project_config_path
|
|
46
|
+
from shikiru.ui.keys import key_hint
|
|
47
|
+
from shikiru.ui.popup import (
|
|
48
|
+
POPUP_BODY_CLASS,
|
|
49
|
+
POPUP_COMPACT_CONTROL_CSS,
|
|
50
|
+
POPUP_CSS,
|
|
51
|
+
POPUP_FRAME_CLASS,
|
|
52
|
+
POPUP_HEADER_CLASS,
|
|
53
|
+
POPUP_RULE_CLASS,
|
|
54
|
+
POPUP_TITLE_CLASS,
|
|
55
|
+
PopupFrame,
|
|
56
|
+
)
|
|
57
|
+
from shikiru.ui.theme import install_theme
|
|
58
|
+
from shikiru.ui.tokens import (
|
|
59
|
+
ALERT_COLOR,
|
|
60
|
+
AMBER,
|
|
61
|
+
PRIMARY_TEXT_COLOR,
|
|
62
|
+
SECONDARY_TEXT_COLOR,
|
|
63
|
+
STEEL_BLUE,
|
|
64
|
+
STYLE_BOLD,
|
|
65
|
+
SUCCESS_COLOR,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if TYPE_CHECKING:
|
|
69
|
+
from collections.abc import Callable
|
|
70
|
+
from pathlib import Path
|
|
71
|
+
|
|
72
|
+
from rich.console import RenderableType
|
|
73
|
+
from textual import events
|
|
74
|
+
from textual.binding import BindingType
|
|
75
|
+
from textual.timer import Timer
|
|
76
|
+
|
|
77
|
+
from shikiru.agents.contracts import ModelCatalog
|
|
78
|
+
from shikiru.agents.settings import AgentSettings
|
|
79
|
+
from shikiru.config.settings import OrchestrationConfig
|
|
80
|
+
from shikiru.dashboard.orchestration_fields import OrchestrationField
|
|
81
|
+
from shikiru.operator.doctor import Doctor, DoctorReport
|
|
82
|
+
from shikiru.operator.plugin_checks import AgentCapabilityResult
|
|
83
|
+
from shikiru.workspace.git import LineChangeStats
|
|
84
|
+
|
|
85
|
+
STEP_REPOSITORY = "Repository"
|
|
86
|
+
STEP_AGENTS = "Agents and ladders"
|
|
87
|
+
STEP_TRACKERS = "Trackers"
|
|
88
|
+
STEP_CACHES = "Caches and CoW"
|
|
89
|
+
STEP_ORCHESTRATION = "Orchestration"
|
|
90
|
+
STEP_AGENT_SETTINGS = "Agents"
|
|
91
|
+
STEP_INTEGRATION = "Integration mode"
|
|
92
|
+
STEP_FINISH = "Finish"
|
|
93
|
+
TRACKERS_INPUT_ID = "trackers"
|
|
94
|
+
CACHES_INPUT_ID = "caches"
|
|
95
|
+
TRACKERS_LIST_ID = "tracker-candidates"
|
|
96
|
+
CACHES_LIST_ID = "cache-candidates"
|
|
97
|
+
INTEGRATION_SELECT_ID = "integration"
|
|
98
|
+
AGENT_SETTING_ID_PREFIX = "agent-setting"
|
|
99
|
+
UNSET_CHOICE_LABEL = "unset"
|
|
100
|
+
REPOSITORY_CHECKOUT_ID = "wizard-checkout"
|
|
101
|
+
WIZARD_STEPS = (
|
|
102
|
+
STEP_REPOSITORY,
|
|
103
|
+
STEP_AGENTS,
|
|
104
|
+
STEP_TRACKERS,
|
|
105
|
+
STEP_CACHES,
|
|
106
|
+
STEP_ORCHESTRATION,
|
|
107
|
+
STEP_AGENT_SETTINGS,
|
|
108
|
+
STEP_INTEGRATION,
|
|
109
|
+
STEP_FINISH,
|
|
110
|
+
)
|
|
111
|
+
REPOSITORY_STEP_INDEX = WIZARD_STEPS.index(STEP_REPOSITORY)
|
|
112
|
+
AGENTS_STEP_INDEX = WIZARD_STEPS.index(STEP_AGENTS)
|
|
113
|
+
TRACKERS_STEP_INDEX = WIZARD_STEPS.index(STEP_TRACKERS)
|
|
114
|
+
COW_STEP_INDEX = WIZARD_STEPS.index(STEP_CACHES)
|
|
115
|
+
ORCHESTRATION_STEP_INDEX = WIZARD_STEPS.index(STEP_ORCHESTRATION)
|
|
116
|
+
AGENT_SETTINGS_STEP_INDEX = WIZARD_STEPS.index(STEP_AGENT_SETTINGS)
|
|
117
|
+
INTEGRATION_STEP_INDEX = WIZARD_STEPS.index(STEP_INTEGRATION)
|
|
118
|
+
FINISH_STEP_INDEX = WIZARD_STEPS.index(STEP_FINISH)
|
|
119
|
+
BUTTON_FOCUS_IDS = {REPOSITORY_STEP_INDEX: "#wizard-next", FINISH_STEP_INDEX: "#wizard-finish"}
|
|
120
|
+
INTEGRATION_OPTIONS = (
|
|
121
|
+
("Local merge", MERGE_INTEGRATION),
|
|
122
|
+
("Local squash and merge", SQUASH_INTEGRATION),
|
|
123
|
+
("Pull request", PULL_REQUEST_INTEGRATION),
|
|
124
|
+
)
|
|
125
|
+
INTEGRATION_LABELS = {value: label for label, value in INTEGRATION_OPTIONS}
|
|
126
|
+
INTEGRATION_UNSELECTED = "needs a choice"
|
|
127
|
+
PR_FEEDBACK_ROW_ID = "pr-feedback-row"
|
|
128
|
+
PR_FEEDBACK_SELECT_ID = "pr-feedback"
|
|
129
|
+
PR_FEEDBACK_OPTIONS = (
|
|
130
|
+
("Changes requested only", CHANGES_REQUESTED_FEEDBACK),
|
|
131
|
+
("Any review or discussion comment", ANY_COMMENT_FEEDBACK),
|
|
132
|
+
)
|
|
133
|
+
WIZARD_TITLE = "SHIKIRU"
|
|
134
|
+
LEGACY_CATALOG_DETAIL = "agent does not expose an authoritative model catalog"
|
|
135
|
+
WIZARD_FRAME_MAX_WIDTH = 96
|
|
136
|
+
WIZARD_FRAME_HEIGHT = 36
|
|
137
|
+
WIZARD_HINT_NARROW_WIDTH = 72
|
|
138
|
+
WIZARD_ART_WIDTH = 120
|
|
139
|
+
WIZARD_ART_HEIGHT = 40
|
|
140
|
+
WIZARD_COMPACT_HEIGHT = 12
|
|
141
|
+
WIZARD_COMPACT_WIDTH = 48
|
|
142
|
+
WIZARD_ART_NAME = "shikiru_init_ascii_art.txt"
|
|
143
|
+
ART_REVEAL_SECONDS = 5
|
|
144
|
+
FIELD_EDIT_HINT = "Tab / Enter edit"
|
|
145
|
+
NARROW_AGENT_HINT = "Tab / Enter select"
|
|
146
|
+
NARROW_CANDIDATE_HINT = "Enter add"
|
|
147
|
+
WIZARD_HINTS = (
|
|
148
|
+
"",
|
|
149
|
+
"↑↓ select scope · Enter edit",
|
|
150
|
+
"↑↓ select · Enter add",
|
|
151
|
+
"↑↓ select · Enter add",
|
|
152
|
+
FIELD_EDIT_HINT,
|
|
153
|
+
FIELD_EDIT_HINT,
|
|
154
|
+
"Tab / Enter choose",
|
|
155
|
+
"Enter run Doctor",
|
|
156
|
+
)
|
|
157
|
+
SCROLL_HINT = "PgUp/PgDn scroll"
|
|
158
|
+
EXIT_HINT = "Enter Exit"
|
|
159
|
+
PATH_SEPARATOR = ","
|
|
160
|
+
NO_CANDIDATES = "No detected candidates."
|
|
161
|
+
DETAIL_LABEL_WIDTH = 15
|
|
162
|
+
MINUS_SIGN = "\N{MINUS SIGN}"
|
|
163
|
+
|
|
164
|
+
WIZARD_LAYER_CSS = f"""
|
|
165
|
+
#wizard-art {{
|
|
166
|
+
layer: art; position: absolute; offset: 0 0;
|
|
167
|
+
width: {WIZARD_ART_WIDTH}; height: {WIZARD_ART_HEIGHT}; overflow: hidden;
|
|
168
|
+
}}
|
|
169
|
+
#wizard-frame {{
|
|
170
|
+
layer: panel; width: auto; min-width: 0; max-width: {WIZARD_FRAME_MAX_WIDTH};
|
|
171
|
+
height: {WIZARD_FRAME_HEIGHT}; min-height: {WIZARD_FRAME_HEIGHT};
|
|
172
|
+
max-height: 100%; margin: 0; padding: 1 2;
|
|
173
|
+
background: $surface; color: $primary-text;
|
|
174
|
+
}}
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _wizard_art() -> Text:
|
|
179
|
+
contents = (
|
|
180
|
+
resources.files("shikiru.dashboard").joinpath(WIZARD_ART_NAME).read_text(encoding="utf-8")
|
|
181
|
+
)
|
|
182
|
+
art = Text.from_ansi(contents)
|
|
183
|
+
art.no_wrap = True
|
|
184
|
+
return art
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _step_counter(step: int) -> str:
|
|
188
|
+
return f"{step + 1} / {len(WIZARD_STEPS)}"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _step_title(step: int) -> Text:
|
|
192
|
+
title = Text()
|
|
193
|
+
title.append(WIZARD_TITLE, style=f"{STYLE_BOLD} {STEEL_BLUE}")
|
|
194
|
+
title.append(" · ", style=PRIMARY_TEXT_COLOR)
|
|
195
|
+
title.append(WIZARD_STEPS[step], style=f"{STYLE_BOLD} {PRIMARY_TEXT_COLOR}")
|
|
196
|
+
return title
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class WizardDetailRow(Horizontal):
|
|
200
|
+
"""Render read-only facts with a fixed label and wrapping value column."""
|
|
201
|
+
|
|
202
|
+
def __init__(
|
|
203
|
+
self,
|
|
204
|
+
label: str,
|
|
205
|
+
value: str | Text,
|
|
206
|
+
value_style: str = PRIMARY_TEXT_COLOR,
|
|
207
|
+
*,
|
|
208
|
+
widget_id: str | None = None,
|
|
209
|
+
) -> None:
|
|
210
|
+
super().__init__(id=widget_id, classes="wizard-detail-row")
|
|
211
|
+
self._label = label
|
|
212
|
+
self._value = value if isinstance(value, Text) else Text(value, style=value_style)
|
|
213
|
+
|
|
214
|
+
def compose(self) -> ComposeResult:
|
|
215
|
+
yield Static(
|
|
216
|
+
Text(self._label, style=SECONDARY_TEXT_COLOR),
|
|
217
|
+
classes="wizard-detail-label",
|
|
218
|
+
markup=False,
|
|
219
|
+
)
|
|
220
|
+
yield Static(self._value, classes="wizard-detail-value", markup=False)
|
|
221
|
+
|
|
222
|
+
def update_value(self, value: str | Text) -> None:
|
|
223
|
+
"""Replace the complete read-only value while keeping its label column."""
|
|
224
|
+
self._value = value if isinstance(value, Text) else Text(value, style=PRIMARY_TEXT_COLOR)
|
|
225
|
+
self.query_one(".wizard-detail-value", Static).update(self._value)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _detail_row(
|
|
229
|
+
label: str,
|
|
230
|
+
value: str | Text,
|
|
231
|
+
value_style: str = PRIMARY_TEXT_COLOR,
|
|
232
|
+
*,
|
|
233
|
+
widget_id: str | None = None,
|
|
234
|
+
) -> WizardDetailRow:
|
|
235
|
+
if isinstance(value, str):
|
|
236
|
+
value = Text(value, style=value_style)
|
|
237
|
+
return WizardDetailRow(label, value, widget_id=widget_id)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _section_heading(value: str) -> Text:
|
|
241
|
+
return Text(value, style=f"{STYLE_BOLD} {STEEL_BLUE}")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _agent_setting_id(agent: int, key: int) -> str:
|
|
245
|
+
return f"{AGENT_SETTING_ID_PREFIX}-{agent}-{key}"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _candidate_add_id(field_id: str, index: int) -> str:
|
|
249
|
+
return f"{field_id}-candidate-add-{index}"
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _repository_checkout(
|
|
253
|
+
base: str,
|
|
254
|
+
*,
|
|
255
|
+
clean: bool,
|
|
256
|
+
changes: LineChangeStats | None = None,
|
|
257
|
+
error: str | None = None,
|
|
258
|
+
) -> Text:
|
|
259
|
+
checkout = Text()
|
|
260
|
+
checkout.append(base, style=PRIMARY_TEXT_COLOR)
|
|
261
|
+
checkout.append(" · ", style=SECONDARY_TEXT_COLOR)
|
|
262
|
+
if clean:
|
|
263
|
+
checkout.append("clean", style=f"{STYLE_BOLD} {SUCCESS_COLOR}")
|
|
264
|
+
elif changes is None and error is None:
|
|
265
|
+
checkout.append("changed · loading line totals…", style=PRIMARY_TEXT_COLOR)
|
|
266
|
+
elif changes is None:
|
|
267
|
+
checkout.append(
|
|
268
|
+
f"changed · line totals unavailable: {error or 'unknown error'}",
|
|
269
|
+
style=f"{STYLE_BOLD} {AMBER}",
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
checkout.append(f"+{changes.added_lines}", style=SUCCESS_COLOR)
|
|
273
|
+
checkout.append(" ", style=SECONDARY_TEXT_COLOR)
|
|
274
|
+
checkout.append(f"{MINUS_SIGN}{changes.deleted_lines}", style=ALERT_COLOR)
|
|
275
|
+
details: list[str] = []
|
|
276
|
+
if changes.untracked_paths:
|
|
277
|
+
label = "path" if changes.untracked_paths == 1 else "paths"
|
|
278
|
+
details.append(f"{changes.untracked_paths} untracked {label}")
|
|
279
|
+
if changes.binary_entries:
|
|
280
|
+
label = "entry" if changes.binary_entries == 1 else "entries"
|
|
281
|
+
details.append(f"{changes.binary_entries} binary {label}")
|
|
282
|
+
if details:
|
|
283
|
+
checkout.append(f" · {' · '.join(details)}", style=AMBER)
|
|
284
|
+
return checkout
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class _CandidatePath(Static):
|
|
288
|
+
class Chosen(Message):
|
|
289
|
+
def __init__(self, index: int) -> None:
|
|
290
|
+
super().__init__()
|
|
291
|
+
self.index = index
|
|
292
|
+
|
|
293
|
+
def __init__(self, path: str, index: int) -> None:
|
|
294
|
+
super().__init__(path, markup=False, classes="candidate-path")
|
|
295
|
+
self._index = index
|
|
296
|
+
|
|
297
|
+
def on_click(self, event: events.Click) -> None:
|
|
298
|
+
event.stop()
|
|
299
|
+
self.post_message(self.Chosen(self._index))
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class CandidateRows(Vertical, can_focus=True):
|
|
303
|
+
"""Render detected paths beside their own Add actions, wrapping paths in place."""
|
|
304
|
+
|
|
305
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
306
|
+
Binding("up", "move(-1)", "Previous", show=False),
|
|
307
|
+
Binding("down", "move(1)", "Next", show=False),
|
|
308
|
+
Binding("enter", "open", "Add", show=False),
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
class Opened(Message):
|
|
312
|
+
"""The selected candidate was added."""
|
|
313
|
+
|
|
314
|
+
def __init__(self, candidate_rows: CandidateRows, index: int) -> None:
|
|
315
|
+
super().__init__()
|
|
316
|
+
self._candidate_rows = candidate_rows
|
|
317
|
+
self.index = index
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def control(self) -> CandidateRows:
|
|
321
|
+
"""The candidate rows that posted this message."""
|
|
322
|
+
return self._candidate_rows
|
|
323
|
+
|
|
324
|
+
def __init__(self, widget_id: str, field_id: str, candidates: tuple[str, ...]) -> None:
|
|
325
|
+
super().__init__(id=widget_id, classes="candidate-list")
|
|
326
|
+
self._field_id = field_id
|
|
327
|
+
self._candidates = candidates
|
|
328
|
+
self._selected = 0
|
|
329
|
+
self._rows: list[Horizontal] = []
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def candidates(self) -> tuple[str, ...]:
|
|
333
|
+
"""Detected project-relative paths in display order."""
|
|
334
|
+
return self._candidates
|
|
335
|
+
|
|
336
|
+
def compose(self) -> ComposeResult:
|
|
337
|
+
if not self._candidates:
|
|
338
|
+
yield Static(NO_CANDIDATES, classes="candidate-empty", markup=False)
|
|
339
|
+
for index, path in enumerate(self._candidates):
|
|
340
|
+
with Horizontal(classes="wizard-candidate-row") as row:
|
|
341
|
+
row.set_class(index == self._selected, "-selected")
|
|
342
|
+
yield _CandidatePath(path, index)
|
|
343
|
+
yield Button(
|
|
344
|
+
"Add",
|
|
345
|
+
id=_candidate_add_id(self._field_id, index),
|
|
346
|
+
variant="primary",
|
|
347
|
+
)
|
|
348
|
+
self._rows.append(row)
|
|
349
|
+
|
|
350
|
+
@on(_CandidatePath.Chosen)
|
|
351
|
+
def _path_chosen(self, message: _CandidatePath.Chosen) -> None:
|
|
352
|
+
message.stop()
|
|
353
|
+
self._choose(message.index)
|
|
354
|
+
self.action_open()
|
|
355
|
+
|
|
356
|
+
def set_included(self, included: set[str]) -> None:
|
|
357
|
+
"""Disable Add for paths already included in the editable field."""
|
|
358
|
+
for index, path in enumerate(self._candidates):
|
|
359
|
+
button = self.query_one(f"#{_candidate_add_id(self._field_id, index)}", Button)
|
|
360
|
+
button.disabled = path in included
|
|
361
|
+
|
|
362
|
+
def action_move(self, step: int) -> None:
|
|
363
|
+
"""Move the selected candidate within the detected paths."""
|
|
364
|
+
self._choose(min(max(self._selected + step, 0), max(len(self._candidates) - 1, 0)))
|
|
365
|
+
|
|
366
|
+
def action_open(self) -> None:
|
|
367
|
+
"""Add the selected candidate to its editable path field."""
|
|
368
|
+
if self._candidates:
|
|
369
|
+
self.post_message(self.Opened(self, self._selected))
|
|
370
|
+
|
|
371
|
+
def _choose(self, index: int) -> None:
|
|
372
|
+
if not 0 <= index < len(self._candidates) or index == self._selected:
|
|
373
|
+
return
|
|
374
|
+
self._selected = index
|
|
375
|
+
for row_index, row in enumerate(self._rows):
|
|
376
|
+
row.set_class(row_index == index, "-selected")
|
|
377
|
+
self._rows[index].scroll_visible(animate=False)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class InitWizardBody(VerticalScroll):
|
|
381
|
+
def on_descendant_focus(self, event: events.DescendantFocus) -> None:
|
|
382
|
+
self.scroll_to_widget(event.widget, animate=False, immediate=True)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class InitWizardApp(App[InitWizardResult | None]):
|
|
386
|
+
"""Guide an operator through project setup and show the final Doctor checks."""
|
|
387
|
+
|
|
388
|
+
ENABLE_COMMAND_PALETTE = False
|
|
389
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
390
|
+
Binding("ctrl+q", "block_quit", show=False, priority=True),
|
|
391
|
+
Binding("ctrl+f", "show_art", "Show artwork", show=False),
|
|
392
|
+
("pageup", "scroll_body_up", "Scroll up"),
|
|
393
|
+
("pagedown", "scroll_body_down", "Scroll down"),
|
|
394
|
+
]
|
|
395
|
+
CSS = (
|
|
396
|
+
POPUP_CSS
|
|
397
|
+
+ WIZARD_LAYER_CSS
|
|
398
|
+
+ """
|
|
399
|
+
Screen {
|
|
400
|
+
layers: art panel;
|
|
401
|
+
align: center middle;
|
|
402
|
+
border: solid $neutral-border;
|
|
403
|
+
background: $background;
|
|
404
|
+
overflow: hidden;
|
|
405
|
+
}
|
|
406
|
+
#wizard-header { width: 100%; height: 1; }
|
|
407
|
+
#wizard-title { width: 1fr; height: 1; color: $primary-text; text-style: bold; }
|
|
408
|
+
#wizard-step { width: auto; height: 1; color: $steel-blue; text-style: bold; }
|
|
409
|
+
#wizard-body {
|
|
410
|
+
height: auto; max-height: 1fr; min-height: 0; padding: 1 0 0 0;
|
|
411
|
+
background: $surface; color: $primary-text;
|
|
412
|
+
scrollbar-size-vertical: $scrollbar-width;
|
|
413
|
+
}
|
|
414
|
+
#wizard-frame.stable > #wizard-body { height: 1fr; min-height: 0; }
|
|
415
|
+
.wizard-page { height: auto; padding: 0; }
|
|
416
|
+
#wizard-frame.height-clamped { min-height: 0; }
|
|
417
|
+
#wizard-frame.compact { padding: 0 1; }
|
|
418
|
+
#wizard-frame.compact > #wizard-header { height: auto; min-height: 1; align: left top; }
|
|
419
|
+
#wizard-frame.compact #wizard-title { height: auto; min-height: 1; }
|
|
420
|
+
#wizard-frame.compact > #wizard-body { padding: 0; }
|
|
421
|
+
#wizard-frame.narrow #wizard-hint-left { height: 2; min-height: 2; }
|
|
422
|
+
#wizard-finish-before, #wizard-finish-after { height: auto; min-height: 0; padding: 0; }
|
|
423
|
+
#wizard-frame Select { height: 1; margin: 0; }
|
|
424
|
+
.wizard-description { margin-bottom: 1; }
|
|
425
|
+
.wizard-page Button {
|
|
426
|
+
width: auto; min-width: 0; height: 1; min-height: 1;
|
|
427
|
+
margin: 0 1 0 0; padding: 0 1; border: none;
|
|
428
|
+
}
|
|
429
|
+
.wizard-page Static { height: auto; color: $primary-text; }
|
|
430
|
+
#catalog-discovery, #wizard-error { height: auto; }
|
|
431
|
+
#wizard-error { color: $dusty-rose; text-style: bold; }
|
|
432
|
+
#wizard-navigation-hint {
|
|
433
|
+
layout: grid; grid-size: 2 0; grid-columns: 1fr auto; grid-gutter: 0 1;
|
|
434
|
+
height: auto; min-height: 1; width: 100%;
|
|
435
|
+
}
|
|
436
|
+
#wizard-hint-left { width: 100%; height: auto; min-height: 1; color: $secondary-text; }
|
|
437
|
+
#wizard-hint-right {
|
|
438
|
+
width: auto; height: 1; color: $secondary-text; text-align: right;
|
|
439
|
+
}
|
|
440
|
+
#wizard-navigation { height: 1; min-height: 1; width: 100%; align: right middle; }
|
|
441
|
+
#wizard-navigation Button {
|
|
442
|
+
width: auto; min-width: 0; height: 1; min-height: 1;
|
|
443
|
+
margin: 0 1 0 0; padding: 0 1; border: none;
|
|
444
|
+
border-top: none; border-right: none; border-bottom: none; border-left: none;
|
|
445
|
+
}
|
|
446
|
+
#wizard-navigation Button:focus {
|
|
447
|
+
color: $primary-text; background: $selected-surface; text-style: none;
|
|
448
|
+
border: none; border-top: none; border-right: none; border-bottom: none; border-left: none;
|
|
449
|
+
}
|
|
450
|
+
#wizard-navigation Button:disabled {
|
|
451
|
+
color: $secondary-text;
|
|
452
|
+
border: none; border-top: none; border-right: none; border-bottom: none; border-left: none;
|
|
453
|
+
}
|
|
454
|
+
#wizard-navigation Button.-primary {
|
|
455
|
+
color: $steel-blue; background: $surface; text-style: bold;
|
|
456
|
+
}
|
|
457
|
+
#wizard-navigation Button.-primary:focus {
|
|
458
|
+
color: $steel-blue; background: $selected-surface; text-style: bold;
|
|
459
|
+
}
|
|
460
|
+
#wizard-navigation Button.-primary:disabled {
|
|
461
|
+
color: $secondary-text; background: $surface; text-style: none;
|
|
462
|
+
}
|
|
463
|
+
.candidate-list { width: 100%; height: auto; }
|
|
464
|
+
.wizard-candidate-row { width: 100%; height: auto; }
|
|
465
|
+
.wizard-candidate-row.-selected { background: $selected-surface; }
|
|
466
|
+
.wizard-candidate-row .candidate-path { width: 1fr; height: auto; }
|
|
467
|
+
#wizard-frame .wizard-candidate-row Button {
|
|
468
|
+
width: auto; min-width: 4; height: 1; min-height: 1;
|
|
469
|
+
margin: 0; padding: 0 1; border: none;
|
|
470
|
+
color: $steel-blue; background: $surface; text-style: bold;
|
|
471
|
+
}
|
|
472
|
+
#wizard-frame .wizard-candidate-row Button:focus {
|
|
473
|
+
border: none; color: $primary-text; background: $selected-surface; text-style: bold;
|
|
474
|
+
}
|
|
475
|
+
#wizard-frame .wizard-candidate-row Button:disabled {
|
|
476
|
+
border: none; color: $secondary-text; background: $surface; text-style: none;
|
|
477
|
+
}
|
|
478
|
+
#wizard-page-2 .section-heading, #wizard-page-3 .section-heading {
|
|
479
|
+
height: auto; color: $steel-blue; text-style: bold;
|
|
480
|
+
}
|
|
481
|
+
#wizard-cow-result { height: auto; }
|
|
482
|
+
"""
|
|
483
|
+
+ LADDER_ACTION_CSS
|
|
484
|
+
+ POPUP_COMPACT_CONTROL_CSS
|
|
485
|
+
+ """
|
|
486
|
+
.wizard-page .palette-row Label { color: $secondary-text; text-style: none; }
|
|
487
|
+
.wizard-page .wizard-long-label Label { height: auto; }
|
|
488
|
+
"""
|
|
489
|
+
+ f"""
|
|
490
|
+
#wizard-frame .wizard-wide-label Label {{ width: {DETAIL_LABEL_WIDTH}; }}
|
|
491
|
+
#wizard-frame .wizard-long-label Label {{ width: {DETAIL_LABEL_WIDTH}; }}
|
|
492
|
+
.orchestration-field {{ height: auto; margin-bottom: 1; }}
|
|
493
|
+
.wizard-page .orchestration-unit {{
|
|
494
|
+
width: auto; height: 1; padding: 0 1; color: $secondary-text;
|
|
495
|
+
}}
|
|
496
|
+
.wizard-page .orchestration-help {{
|
|
497
|
+
height: auto; padding-left: {DETAIL_LABEL_WIDTH}; color: $secondary-text;
|
|
498
|
+
}}
|
|
499
|
+
.wizard-detail-row {{ width: 100%; height: auto; }}
|
|
500
|
+
.wizard-detail-row .wizard-detail-label {{
|
|
501
|
+
width: {DETAIL_LABEL_WIDTH}; height: auto; color: $secondary-text; text-style: none;
|
|
502
|
+
}}
|
|
503
|
+
.wizard-detail-row .wizard-detail-value {{ width: 1fr; height: auto; }}
|
|
504
|
+
"""
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
def __init__(
|
|
508
|
+
self,
|
|
509
|
+
*,
|
|
510
|
+
project_root: Path,
|
|
511
|
+
project: DetectedProject,
|
|
512
|
+
detectors: ProjectDetectors,
|
|
513
|
+
doctor: Doctor,
|
|
514
|
+
doctor_renderer: Callable[..., RenderableType],
|
|
515
|
+
orchestration_defaults: OrchestrationConfig,
|
|
516
|
+
) -> None:
|
|
517
|
+
super().__init__()
|
|
518
|
+
install_theme(self)
|
|
519
|
+
self._root = project_root.resolve()
|
|
520
|
+
self._config_target = project_config_path(self._root)
|
|
521
|
+
self._project = project
|
|
522
|
+
self._detectors = detectors
|
|
523
|
+
self._doctor = doctor
|
|
524
|
+
self._doctor_renderer = doctor_renderer
|
|
525
|
+
self._orchestration_defaults = orchestration_defaults
|
|
526
|
+
self._step = 0
|
|
527
|
+
self._cow_supported: bool | None = None
|
|
528
|
+
self._config_path: Path | None = None
|
|
529
|
+
self._doctor_report: DoctorReport | None = None
|
|
530
|
+
self._doctor_report_width: int | None = None
|
|
531
|
+
self._doctor_error: str | None = None
|
|
532
|
+
self._error = ""
|
|
533
|
+
self._model_catalogs: dict[str, ModelCatalog] = {}
|
|
534
|
+
self._catalog_errors: dict[str, str] = {}
|
|
535
|
+
self._agent_capabilities: dict[str, AgentCapabilityResult] = {}
|
|
536
|
+
self._catalogs_loaded = False
|
|
537
|
+
self._frame_ready = False
|
|
538
|
+
self._writing_config = False
|
|
539
|
+
self._finishing = False
|
|
540
|
+
self._last_focus_by_step: dict[int, str] = {}
|
|
541
|
+
self._art_timer: Timer | None = None
|
|
542
|
+
self._art_focus_id = ""
|
|
543
|
+
self._art_scroll_y = 0.0
|
|
544
|
+
|
|
545
|
+
def compose(self) -> ComposeResult:
|
|
546
|
+
art = _wizard_art()
|
|
547
|
+
yield Static(art, id="wizard-art", markup=False)
|
|
548
|
+
with PopupFrame(widget_id="wizard-frame", classes=POPUP_FRAME_CLASS):
|
|
549
|
+
with Horizontal(classes=POPUP_HEADER_CLASS, id="wizard-header"):
|
|
550
|
+
yield Static(
|
|
551
|
+
_step_title(self._step),
|
|
552
|
+
id="wizard-title",
|
|
553
|
+
classes=POPUP_TITLE_CLASS,
|
|
554
|
+
markup=False,
|
|
555
|
+
)
|
|
556
|
+
yield Static(_step_counter(self._step), id="wizard-step", markup=False)
|
|
557
|
+
yield Rule(classes=POPUP_RULE_CLASS, id="wizard-rule")
|
|
558
|
+
with InitWizardBody(id="wizard-body", classes=POPUP_BODY_CLASS):
|
|
559
|
+
yield from self._repository_page()
|
|
560
|
+
with Vertical(id="wizard-page-1", classes="wizard-page"):
|
|
561
|
+
yield LadderEditor()
|
|
562
|
+
yield from self._trackers_page()
|
|
563
|
+
yield from self._caches_page()
|
|
564
|
+
yield from self._orchestration_page()
|
|
565
|
+
yield from self._agent_settings_page()
|
|
566
|
+
yield from self._integration_page()
|
|
567
|
+
yield from self._finish_page()
|
|
568
|
+
yield Static("", id="wizard-error", markup=False)
|
|
569
|
+
with Horizontal(id="wizard-navigation-hint"):
|
|
570
|
+
yield Static("", id="wizard-hint-left", markup=False)
|
|
571
|
+
right_hint = Text()
|
|
572
|
+
right_hint.append("Ctrl+F", style=f"{STYLE_BOLD} {STEEL_BLUE}")
|
|
573
|
+
right_hint.append(" ")
|
|
574
|
+
right_hint.append("富士山", style=SECONDARY_TEXT_COLOR)
|
|
575
|
+
yield Static(right_hint, id="wizard-hint-right", markup=False)
|
|
576
|
+
yield Rule(classes=POPUP_RULE_CLASS, id="wizard-navigation-rule")
|
|
577
|
+
with Horizontal(id="wizard-navigation"):
|
|
578
|
+
yield Button("Back", id="wizard-back")
|
|
579
|
+
yield Button("Next", variant="primary", id="wizard-next")
|
|
580
|
+
yield Button("Run doctor", variant="primary", id="wizard-finish")
|
|
581
|
+
yield Button("Cancel", id="wizard-cancel")
|
|
582
|
+
|
|
583
|
+
def on_resize(self, event: events.Resize) -> None:
|
|
584
|
+
del event
|
|
585
|
+
self.call_after_refresh(self._adapt_to_viewport)
|
|
586
|
+
|
|
587
|
+
def _adapt_to_viewport(self) -> None:
|
|
588
|
+
frame = self.query_one("#wizard-frame", PopupFrame)
|
|
589
|
+
compact = (
|
|
590
|
+
self.size.height <= WIZARD_COMPACT_HEIGHT or self.size.width <= WIZARD_COMPACT_WIDTH
|
|
591
|
+
)
|
|
592
|
+
frame.set_class(compact, "compact")
|
|
593
|
+
frame.set_class(self.size.width <= WIZARD_HINT_NARROW_WIDTH, "narrow")
|
|
594
|
+
frame.set_class(self.size.height < WIZARD_FRAME_HEIGHT, "height-clamped")
|
|
595
|
+
frame.update_preferred_size()
|
|
596
|
+
if self._config_path is not None:
|
|
597
|
+
self.call_after_refresh(self._update_doctor_report)
|
|
598
|
+
self.call_after_refresh(self._update_navigation_hint)
|
|
599
|
+
|
|
600
|
+
def on_mount(self) -> None:
|
|
601
|
+
self.query_one("#wizard-frame", PopupFrame).visible = False
|
|
602
|
+
for input_widget in self.query(Input):
|
|
603
|
+
input_widget.cursor_blink = False
|
|
604
|
+
self._update_pages()
|
|
605
|
+
self._update_pr_feedback_row()
|
|
606
|
+
self._refresh_candidate_rows()
|
|
607
|
+
self.call_after_refresh(self._capture_agent_page_size)
|
|
608
|
+
self.run_worker(self._load_model_catalogs(), group="model-catalogs", exclusive=True)
|
|
609
|
+
if not self._project.worktree_clean:
|
|
610
|
+
self.run_worker(
|
|
611
|
+
self._load_checkout_line_changes(), group="checkout-lines", exclusive=True
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
def on_unmount(self) -> None:
|
|
615
|
+
self._stop_art_timer()
|
|
616
|
+
|
|
617
|
+
def _capture_agent_page_size(self) -> None:
|
|
618
|
+
editor = self.query_one("#ladder-editor", LadderEditor)
|
|
619
|
+
editor.set_scope(Role.HELPER)
|
|
620
|
+
self._step = AGENTS_STEP_INDEX
|
|
621
|
+
self._update_pages()
|
|
622
|
+
self.call_after_refresh(self._mark_frame_content_ready)
|
|
623
|
+
|
|
624
|
+
def _mark_frame_content_ready(self) -> None:
|
|
625
|
+
frame = self.query_one("#wizard-frame", PopupFrame)
|
|
626
|
+
frame.content_ready()
|
|
627
|
+
self.call_after_refresh(self._capture_completed_frame_size)
|
|
628
|
+
|
|
629
|
+
def _capture_completed_frame_size(self) -> None:
|
|
630
|
+
self.query_one("#wizard-frame", PopupFrame).update_preferred_size()
|
|
631
|
+
self.call_after_refresh(self._restore_repository_page)
|
|
632
|
+
|
|
633
|
+
def _restore_repository_page(self) -> None:
|
|
634
|
+
self.query_one("#ladder-editor", LadderEditor).set_scope(Role.LEADER)
|
|
635
|
+
self._step = REPOSITORY_STEP_INDEX
|
|
636
|
+
self._update_pages()
|
|
637
|
+
self.call_after_refresh(self._enable_wizard)
|
|
638
|
+
|
|
639
|
+
def _enable_wizard(self) -> None:
|
|
640
|
+
self.query_one("#wizard-frame", PopupFrame).visible = True
|
|
641
|
+
self._frame_ready = True
|
|
642
|
+
self._update_pages()
|
|
643
|
+
self._adapt_to_viewport()
|
|
644
|
+
self.call_after_refresh(self._focus_step)
|
|
645
|
+
|
|
646
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
647
|
+
if not self.query_one("#wizard-frame", PopupFrame).visible:
|
|
648
|
+
return
|
|
649
|
+
button_id = event.button.id or ""
|
|
650
|
+
for field_id, _, candidates in self._candidate_rows():
|
|
651
|
+
for index, candidate in enumerate(candidates):
|
|
652
|
+
if button_id == _candidate_add_id(field_id, index):
|
|
653
|
+
self._add_candidate(field_id, candidate)
|
|
654
|
+
event.stop()
|
|
655
|
+
return
|
|
656
|
+
match button_id:
|
|
657
|
+
case "wizard-back":
|
|
658
|
+
await self._move(-1)
|
|
659
|
+
case "wizard-next":
|
|
660
|
+
await self._move(1)
|
|
661
|
+
case "wizard-finish":
|
|
662
|
+
if self._config_path is None:
|
|
663
|
+
await self._finish()
|
|
664
|
+
else:
|
|
665
|
+
self._stop_art_timer()
|
|
666
|
+
self.exit(self._result())
|
|
667
|
+
case "wizard-cancel":
|
|
668
|
+
self.action_cancel()
|
|
669
|
+
case _:
|
|
670
|
+
return
|
|
671
|
+
|
|
672
|
+
@on(Input.Changed)
|
|
673
|
+
def _input_changed(self, event: Input.Changed) -> None:
|
|
674
|
+
if event.input.id in {TRACKERS_INPUT_ID, CACHES_INPUT_ID}:
|
|
675
|
+
self._refresh_candidate_rows()
|
|
676
|
+
self.call_after_refresh(self._update_navigation_hint)
|
|
677
|
+
self._update_finish_summary()
|
|
678
|
+
|
|
679
|
+
@on(Select.Changed)
|
|
680
|
+
def _select_changed(self, event: Select.Changed) -> None:
|
|
681
|
+
if event.select.id == INTEGRATION_SELECT_ID:
|
|
682
|
+
self._update_pr_feedback_row()
|
|
683
|
+
self._update_finish_summary()
|
|
684
|
+
|
|
685
|
+
@on(CandidateRows.Opened)
|
|
686
|
+
def _candidate_opened(self, message: CandidateRows.Opened) -> None:
|
|
687
|
+
list_id = message.control.id
|
|
688
|
+
if list_id == TRACKERS_LIST_ID:
|
|
689
|
+
field_id = TRACKERS_INPUT_ID
|
|
690
|
+
elif list_id == CACHES_LIST_ID:
|
|
691
|
+
field_id = CACHES_INPUT_ID
|
|
692
|
+
else:
|
|
693
|
+
return
|
|
694
|
+
message.stop()
|
|
695
|
+
self._add_candidate(field_id, message.control.candidates[message.index])
|
|
696
|
+
|
|
697
|
+
def _add_candidate(self, field_id: str, candidate: str) -> None:
|
|
698
|
+
field = self.query_one(f"#{field_id}", Input)
|
|
699
|
+
entries = [entry.strip() for entry in field.value.split(PATH_SEPARATOR) if entry.strip()]
|
|
700
|
+
if candidate not in entries:
|
|
701
|
+
entries.append(candidate)
|
|
702
|
+
field.value = f"{PATH_SEPARATOR} ".join(entries)
|
|
703
|
+
field.cursor_position = len(field.value)
|
|
704
|
+
self._refresh_candidate_rows()
|
|
705
|
+
self.call_after_refresh(field.focus)
|
|
706
|
+
|
|
707
|
+
def _refresh_candidate_rows(self) -> None:
|
|
708
|
+
for field_id, list_id, _candidates in self._candidate_rows():
|
|
709
|
+
input_widget = self.query_one(f"#{field_id}", Input)
|
|
710
|
+
candidate_list = self.query_one(f"#{list_id}", CandidateRows)
|
|
711
|
+
included = {
|
|
712
|
+
entry.strip() for entry in input_widget.value.split(PATH_SEPARATOR) if entry.strip()
|
|
713
|
+
}
|
|
714
|
+
candidate_list.set_included(included)
|
|
715
|
+
|
|
716
|
+
def _candidate_rows(
|
|
717
|
+
self,
|
|
718
|
+
) -> tuple[tuple[str, str, tuple[str, ...]], ...]:
|
|
719
|
+
return (
|
|
720
|
+
(TRACKERS_INPUT_ID, TRACKERS_LIST_ID, self._project.trackers),
|
|
721
|
+
(CACHES_INPUT_ID, CACHES_LIST_ID, self._project.caches),
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
async def _load_checkout_line_changes(self) -> None:
|
|
725
|
+
try:
|
|
726
|
+
changes = await self._detectors.working_tree_line_changes()
|
|
727
|
+
except (OSError, ShikiruError) as error:
|
|
728
|
+
checkout = _repository_checkout(
|
|
729
|
+
self._project.base,
|
|
730
|
+
clean=False,
|
|
731
|
+
error=str(error) or type(error).__name__,
|
|
732
|
+
)
|
|
733
|
+
else:
|
|
734
|
+
checkout = _repository_checkout(
|
|
735
|
+
self._project.base,
|
|
736
|
+
clean=False,
|
|
737
|
+
changes=changes,
|
|
738
|
+
)
|
|
739
|
+
if self._config_path is not None or not self._wizard_mounted():
|
|
740
|
+
return
|
|
741
|
+
self.query_one(f"#{REPOSITORY_CHECKOUT_ID}", WizardDetailRow).update_value(checkout)
|
|
742
|
+
|
|
743
|
+
def action_show_art(self) -> None:
|
|
744
|
+
if self._art_timer is not None:
|
|
745
|
+
self._stop_art_timer()
|
|
746
|
+
self._art_timer = self.set_timer(ART_REVEAL_SECONDS, self._restore_art_panel)
|
|
747
|
+
return
|
|
748
|
+
focused = self.focused
|
|
749
|
+
if focused is not None and (
|
|
750
|
+
isinstance(focused, (Input, Select))
|
|
751
|
+
or any(isinstance(ancestor, Select) for ancestor in focused.ancestors)
|
|
752
|
+
):
|
|
753
|
+
return
|
|
754
|
+
if any(select.expanded for select in self.query(Select[str])):
|
|
755
|
+
return
|
|
756
|
+
frame = self.query_one("#wizard-frame", PopupFrame)
|
|
757
|
+
if not frame.visible:
|
|
758
|
+
return
|
|
759
|
+
body = self.query_one("#wizard-body", InitWizardBody)
|
|
760
|
+
self._art_focus_id = focused.id if focused is not None and focused.id else ""
|
|
761
|
+
self._art_scroll_y = body.scroll_y
|
|
762
|
+
frame.visible = False
|
|
763
|
+
self._art_timer = self.set_timer(ART_REVEAL_SECONDS, self._restore_art_panel)
|
|
764
|
+
|
|
765
|
+
def _restore_art_panel(self) -> None:
|
|
766
|
+
self._art_timer = None
|
|
767
|
+
self.query_one("#wizard-frame", PopupFrame).visible = True
|
|
768
|
+
self.call_after_refresh(self._restore_art_state)
|
|
769
|
+
|
|
770
|
+
def _restore_art_state(self) -> None:
|
|
771
|
+
if self._art_focus_id:
|
|
772
|
+
target = self.query(f"#{self._art_focus_id}")
|
|
773
|
+
if target:
|
|
774
|
+
target.first().focus(scroll_visible=False)
|
|
775
|
+
self.call_after_refresh(self._restore_art_scroll)
|
|
776
|
+
|
|
777
|
+
def _restore_art_scroll(self) -> None:
|
|
778
|
+
body = self.query_one("#wizard-body", InitWizardBody)
|
|
779
|
+
body.scroll_to(
|
|
780
|
+
y=min(max(self._art_scroll_y, 0), body.max_scroll_y),
|
|
781
|
+
animate=False,
|
|
782
|
+
immediate=True,
|
|
783
|
+
)
|
|
784
|
+
self._update_navigation_hint()
|
|
785
|
+
|
|
786
|
+
def _stop_art_timer(self) -> None:
|
|
787
|
+
if self._art_timer is not None:
|
|
788
|
+
self._art_timer.stop()
|
|
789
|
+
self._art_timer = None
|
|
790
|
+
|
|
791
|
+
def _update_navigation_hint(self) -> None:
|
|
792
|
+
self.call_after_refresh(self._render_navigation_hint)
|
|
793
|
+
|
|
794
|
+
def _render_navigation_hint(self) -> None:
|
|
795
|
+
text = EXIT_HINT if self._config_path is not None else self._step_hint()
|
|
796
|
+
body = self.query_one("#wizard-body", InitWizardBody)
|
|
797
|
+
overflows = body.max_scroll_y > 0
|
|
798
|
+
if self.size.width <= WIZARD_HINT_NARROW_WIDTH:
|
|
799
|
+
hint = Group(key_hint(text), key_hint(SCROLL_HINT)) if overflows else key_hint(text)
|
|
800
|
+
else:
|
|
801
|
+
if overflows:
|
|
802
|
+
text = f"{text} · {SCROLL_HINT}" if text else SCROLL_HINT
|
|
803
|
+
hint = key_hint(text)
|
|
804
|
+
self.query_one("#wizard-hint-left", Static).update(hint)
|
|
805
|
+
|
|
806
|
+
def _step_hint(self) -> str:
|
|
807
|
+
if self._step == TRACKERS_STEP_INDEX and not self._project.trackers:
|
|
808
|
+
return FIELD_EDIT_HINT
|
|
809
|
+
if self._step == COW_STEP_INDEX and not self._project.caches:
|
|
810
|
+
return FIELD_EDIT_HINT
|
|
811
|
+
if self.size.width <= WIZARD_HINT_NARROW_WIDTH:
|
|
812
|
+
if self._step == AGENTS_STEP_INDEX:
|
|
813
|
+
return NARROW_AGENT_HINT
|
|
814
|
+
if self._step in {TRACKERS_STEP_INDEX, COW_STEP_INDEX}:
|
|
815
|
+
return NARROW_CANDIDATE_HINT
|
|
816
|
+
return WIZARD_HINTS[self._step]
|
|
817
|
+
|
|
818
|
+
def _update_pr_feedback_row(self) -> None:
|
|
819
|
+
integration = cast("Select[str]", self.query_one(f"#{INTEGRATION_SELECT_ID}", Select)).value
|
|
820
|
+
self.query_one(f"#{PR_FEEDBACK_ROW_ID}").display = integration == PULL_REQUEST_INTEGRATION
|
|
821
|
+
|
|
822
|
+
def _update_finish_summary(self) -> None:
|
|
823
|
+
integration_value = cast(
|
|
824
|
+
"Select[str]", self.query_one(f"#{INTEGRATION_SELECT_ID}", Select)
|
|
825
|
+
).value
|
|
826
|
+
integration = (
|
|
827
|
+
INTEGRATION_LABELS[integration_value]
|
|
828
|
+
if isinstance(integration_value, str)
|
|
829
|
+
else INTEGRATION_UNSELECTED
|
|
830
|
+
)
|
|
831
|
+
parts = (*(self._field_summary(field) for field in ORCHESTRATION_FIELDS), integration)
|
|
832
|
+
self.query_one("#wizard-summary", WizardDetailRow).update_value(" · ".join(parts))
|
|
833
|
+
|
|
834
|
+
def _field_summary(self, field: OrchestrationField) -> str:
|
|
835
|
+
try:
|
|
836
|
+
value = field.parse(self.query_one(f"#{field.input_id}", Input).value)
|
|
837
|
+
except OrchestrationFieldError:
|
|
838
|
+
return f"{field.summary} needs a value"
|
|
839
|
+
return f"{field.summary} {value} {field.unit}"
|
|
840
|
+
|
|
841
|
+
def action_cancel(self) -> None:
|
|
842
|
+
if self._writing_config:
|
|
843
|
+
return
|
|
844
|
+
self._stop_art_timer()
|
|
845
|
+
if self._config_path is None:
|
|
846
|
+
self.exit(None)
|
|
847
|
+
else:
|
|
848
|
+
self.exit(self._result())
|
|
849
|
+
|
|
850
|
+
def action_block_quit(self) -> None:
|
|
851
|
+
return
|
|
852
|
+
|
|
853
|
+
def action_scroll_body_up(self) -> None:
|
|
854
|
+
self.query_one("#wizard-body", InitWizardBody).action_page_up()
|
|
855
|
+
|
|
856
|
+
def action_scroll_body_down(self) -> None:
|
|
857
|
+
self.query_one("#wizard-body", InitWizardBody).action_page_down()
|
|
858
|
+
|
|
859
|
+
async def _load_model_catalogs(self) -> None:
|
|
860
|
+
for agent in self._project.agents:
|
|
861
|
+
self._agent_capabilities[agent] = self._detectors.agent_capabilities(agent, {})
|
|
862
|
+
try:
|
|
863
|
+
catalog = await self._detectors.model_catalog(agent, {})
|
|
864
|
+
except (OSError, ShikiruError, ValueError) as error:
|
|
865
|
+
self._catalog_errors[agent] = str(error) or type(error).__name__
|
|
866
|
+
continue
|
|
867
|
+
if catalog is None:
|
|
868
|
+
self._catalog_errors[agent] = LEGACY_CATALOG_DETAIL
|
|
869
|
+
elif catalog.error is not None:
|
|
870
|
+
self._catalog_errors[agent] = catalog.error
|
|
871
|
+
elif any(choice.offered for choice in catalog.models):
|
|
872
|
+
self._model_catalogs[agent] = catalog
|
|
873
|
+
else:
|
|
874
|
+
self._catalog_errors[agent] = "agent reported no offered model identifiers"
|
|
875
|
+
if not self._wizard_mounted():
|
|
876
|
+
return
|
|
877
|
+
self.query_one("#ladder-editor", LadderEditor).set_model_catalogs(
|
|
878
|
+
self._model_catalogs, self._catalog_errors
|
|
879
|
+
)
|
|
880
|
+
self.query_one("#ladder-editor", LadderEditor).set_agent_capabilities(
|
|
881
|
+
self._agent_capabilities
|
|
882
|
+
)
|
|
883
|
+
self.query_one("#catalog-discovery", Static).display = False
|
|
884
|
+
self._catalogs_loaded = True
|
|
885
|
+
self._update_pages()
|
|
886
|
+
|
|
887
|
+
async def _move(self, direction: int) -> None:
|
|
888
|
+
if (
|
|
889
|
+
not self.query_one("#wizard-frame", PopupFrame).visible
|
|
890
|
+
or self._config_path is not None
|
|
891
|
+
or self._writing_config
|
|
892
|
+
or self._finishing
|
|
893
|
+
):
|
|
894
|
+
return
|
|
895
|
+
if direction > 0 and self._step == REPOSITORY_STEP_INDEX and not self._catalogs_loaded:
|
|
896
|
+
return
|
|
897
|
+
next_step = self._step + direction
|
|
898
|
+
if not 0 <= next_step < len(WIZARD_STEPS):
|
|
899
|
+
return
|
|
900
|
+
self._stop_art_timer()
|
|
901
|
+
if direction > 0 and self._step == AGENTS_STEP_INDEX:
|
|
902
|
+
try:
|
|
903
|
+
self._read_ladders()
|
|
904
|
+
except ConfigError as error:
|
|
905
|
+
self._show_step_error(AGENTS_STEP_INDEX, "ladder-input", error)
|
|
906
|
+
return
|
|
907
|
+
if direction > 0 and self._step == AGENT_SETTINGS_STEP_INDEX:
|
|
908
|
+
try:
|
|
909
|
+
self._read_agent_options()
|
|
910
|
+
except AgentSettingError as error:
|
|
911
|
+
self._show_step_error(AGENT_SETTINGS_STEP_INDEX, error.input_id, error)
|
|
912
|
+
return
|
|
913
|
+
focused = self.focused
|
|
914
|
+
if focused is not None and focused.id:
|
|
915
|
+
self._last_focus_by_step[self._step] = focused.id
|
|
916
|
+
self._error = ""
|
|
917
|
+
if direction > 0 and next_step == COW_STEP_INDEX:
|
|
918
|
+
await self._probe_cow()
|
|
919
|
+
self._step = next_step
|
|
920
|
+
self._update_pages()
|
|
921
|
+
self.call_after_refresh(self._focus_step)
|
|
922
|
+
|
|
923
|
+
def _focus_previous(self) -> bool:
|
|
924
|
+
previous_id = self._last_focus_by_step.get(self._step)
|
|
925
|
+
previous = self.query(f"#{previous_id}") if previous_id else None
|
|
926
|
+
if previous:
|
|
927
|
+
previous.first().focus()
|
|
928
|
+
return True
|
|
929
|
+
return False
|
|
930
|
+
|
|
931
|
+
def _focus_step(self) -> None:
|
|
932
|
+
if self._focus_previous():
|
|
933
|
+
return
|
|
934
|
+
if self._step in BUTTON_FOCUS_IDS:
|
|
935
|
+
self.query_one(BUTTON_FOCUS_IDS[self._step], Button).focus()
|
|
936
|
+
elif self._step == AGENTS_STEP_INDEX:
|
|
937
|
+
self.query_one("#ladder-scopes", RowList).focus()
|
|
938
|
+
elif self._step == TRACKERS_STEP_INDEX:
|
|
939
|
+
self._focus_candidate_list(TRACKERS_LIST_ID, TRACKERS_INPUT_ID)
|
|
940
|
+
elif self._step == COW_STEP_INDEX:
|
|
941
|
+
self._focus_candidate_list(CACHES_LIST_ID, CACHES_INPUT_ID)
|
|
942
|
+
elif self._step == ORCHESTRATION_STEP_INDEX:
|
|
943
|
+
self.query_one(f"#{MAX_PARALLEL.input_id}", Input).focus()
|
|
944
|
+
elif self._step == AGENT_SETTINGS_STEP_INDEX:
|
|
945
|
+
self._focus_first_agent_setting()
|
|
946
|
+
elif self._step == INTEGRATION_STEP_INDEX:
|
|
947
|
+
self.query_one(f"#{INTEGRATION_SELECT_ID}", Select).focus()
|
|
948
|
+
|
|
949
|
+
def _focus_first_agent_setting(self) -> None:
|
|
950
|
+
editors = self.query(".agent-setting-editor")
|
|
951
|
+
if editors:
|
|
952
|
+
editors.first().focus()
|
|
953
|
+
|
|
954
|
+
def _focus_candidate_list(self, list_id: str, input_id: str) -> None:
|
|
955
|
+
candidate_list = self.query_one(f"#{list_id}", CandidateRows)
|
|
956
|
+
if candidate_list.candidates:
|
|
957
|
+
candidate_list.focus()
|
|
958
|
+
else:
|
|
959
|
+
self.query_one(f"#{input_id}", Input).focus()
|
|
960
|
+
|
|
961
|
+
async def _probe_cow(self) -> None:
|
|
962
|
+
try:
|
|
963
|
+
self._cow_supported = await self._detectors.check_cow_support()
|
|
964
|
+
except (OSError, ShikiruError) as error:
|
|
965
|
+
self._error = f"CoW check failed: {error}"
|
|
966
|
+
self.query_one("#cow-result", WizardDetailRow).update_value(
|
|
967
|
+
Text(self._error, style=f"{STYLE_BOLD} {ALERT_COLOR}")
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
async def _finish(self) -> None:
|
|
971
|
+
if self._finishing:
|
|
972
|
+
return
|
|
973
|
+
try:
|
|
974
|
+
ladders = self._read_ladders()
|
|
975
|
+
except ConfigError as error:
|
|
976
|
+
self._show_step_error(AGENTS_STEP_INDEX, "ladder-input", error)
|
|
977
|
+
return
|
|
978
|
+
paths = self._read_both_path_lists()
|
|
979
|
+
if paths is None:
|
|
980
|
+
return
|
|
981
|
+
trackers, caches = paths
|
|
982
|
+
try:
|
|
983
|
+
integration = self._read_integration()
|
|
984
|
+
except ConfigError as error:
|
|
985
|
+
self._show_step_error(INTEGRATION_STEP_INDEX, INTEGRATION_SELECT_ID, error)
|
|
986
|
+
return
|
|
987
|
+
try:
|
|
988
|
+
orchestration = self._read_orchestration(integration)
|
|
989
|
+
except OrchestrationFieldError as error:
|
|
990
|
+
self._show_step_error(ORCHESTRATION_STEP_INDEX, error.input_id, error)
|
|
991
|
+
return
|
|
992
|
+
try:
|
|
993
|
+
agent_options = self._read_agent_options()
|
|
994
|
+
except AgentSettingError as error:
|
|
995
|
+
self._show_step_error(AGENT_SETTINGS_STEP_INDEX, error.input_id, error)
|
|
996
|
+
return
|
|
997
|
+
self._finishing = True
|
|
998
|
+
self.query_one("#wizard-finish", Button).disabled = True
|
|
999
|
+
try:
|
|
1000
|
+
self._writing_config = True
|
|
1001
|
+
config_path = await self._detectors.initialize_config(
|
|
1002
|
+
trackers=trackers,
|
|
1003
|
+
caches=caches,
|
|
1004
|
+
ladders=ladders.ladders,
|
|
1005
|
+
complexity=ladders.complexity,
|
|
1006
|
+
orchestration=orchestration,
|
|
1007
|
+
agent_options=agent_options,
|
|
1008
|
+
)
|
|
1009
|
+
self._config_path = config_path.resolve()
|
|
1010
|
+
self._writing_config = False
|
|
1011
|
+
self._doctor_report = await self._doctor.run()
|
|
1012
|
+
self._doctor_error = None
|
|
1013
|
+
self._error = ""
|
|
1014
|
+
except (OSError, ShikiruError, ValueError) as error:
|
|
1015
|
+
self._writing_config = False
|
|
1016
|
+
if self._config_path is None:
|
|
1017
|
+
self._show_error(str(error))
|
|
1018
|
+
else:
|
|
1019
|
+
self._doctor_error = f"Doctor could not complete: {error}"
|
|
1020
|
+
finally:
|
|
1021
|
+
self._finishing = False
|
|
1022
|
+
self._update_pages()
|
|
1023
|
+
|
|
1024
|
+
def _read_both_path_lists(self) -> tuple[tuple[str, ...], tuple[str, ...]] | None:
|
|
1025
|
+
try:
|
|
1026
|
+
trackers = self._read_paths(TRACKERS_INPUT_ID)
|
|
1027
|
+
except ConfigError as error:
|
|
1028
|
+
self._show_step_error(TRACKERS_STEP_INDEX, TRACKERS_INPUT_ID, error)
|
|
1029
|
+
return None
|
|
1030
|
+
try:
|
|
1031
|
+
caches = self._read_paths(CACHES_INPUT_ID)
|
|
1032
|
+
except ConfigError as error:
|
|
1033
|
+
self._show_step_error(COW_STEP_INDEX, CACHES_INPUT_ID, error)
|
|
1034
|
+
return None
|
|
1035
|
+
return trackers, caches
|
|
1036
|
+
|
|
1037
|
+
def _show_step_error(self, step: int, focus_id: str, error: ConfigError) -> None:
|
|
1038
|
+
self._step = step
|
|
1039
|
+
self._error = ""
|
|
1040
|
+
self._update_pages()
|
|
1041
|
+
if step == AGENTS_STEP_INDEX:
|
|
1042
|
+
self.query_one("#ladder-editor", LadderEditor).show_validation_error(str(error))
|
|
1043
|
+
else:
|
|
1044
|
+
self._show_error(str(error))
|
|
1045
|
+
if focus_id:
|
|
1046
|
+
self.call_after_refresh(lambda: self.query_one(f"#{focus_id}").focus())
|
|
1047
|
+
|
|
1048
|
+
def _result(self) -> InitWizardResult:
|
|
1049
|
+
if self._config_path is None:
|
|
1050
|
+
message = "wizard result requested before config was written"
|
|
1051
|
+
raise RuntimeError(message)
|
|
1052
|
+
return InitWizardResult(self._config_path, self._doctor_report, self._doctor_error)
|
|
1053
|
+
|
|
1054
|
+
def _read_ladders(self) -> LadderConfiguration:
|
|
1055
|
+
return self.query_one("#ladder-editor", LadderEditor).configuration()
|
|
1056
|
+
|
|
1057
|
+
def _read_paths(self, field: str) -> tuple[str, ...]:
|
|
1058
|
+
value = self.query_one(f"#{field}", Input).value
|
|
1059
|
+
if not value.strip():
|
|
1060
|
+
return ()
|
|
1061
|
+
entries = tuple(item.strip() for item in value.split(PATH_SEPARATOR))
|
|
1062
|
+
if any(not item for item in entries):
|
|
1063
|
+
message = "remove empty comma-separated entries"
|
|
1064
|
+
raise ConfigError(message)
|
|
1065
|
+
return entries
|
|
1066
|
+
|
|
1067
|
+
def _read_field(self, field: OrchestrationField) -> int | float:
|
|
1068
|
+
return field.parse(self.query_one(f"#{field.input_id}", Input).value)
|
|
1069
|
+
|
|
1070
|
+
def _read_orchestration(self, integration: str) -> OrchestrationChoices:
|
|
1071
|
+
return OrchestrationChoices(
|
|
1072
|
+
max_parallel=int(self._read_field(MAX_PARALLEL)),
|
|
1073
|
+
max_iterations=int(self._read_field(MAX_ITERATIONS)),
|
|
1074
|
+
integration=integration,
|
|
1075
|
+
push_interval_seconds=int(self._read_field(PUSH_INTERVAL)),
|
|
1076
|
+
max_quota_wait=int(self._read_field(MAX_QUOTA_WAIT)),
|
|
1077
|
+
stall=int(self._read_field(STALL_TIMEOUT)),
|
|
1078
|
+
tool=int(self._read_field(TOOL_TIMEOUT)),
|
|
1079
|
+
max_cpu_pct=int(self._read_field(MAX_CPU)),
|
|
1080
|
+
min_free_ram_gb=float(self._read_field(MIN_FREE_RAM)),
|
|
1081
|
+
pr_feedback=self._read_pr_feedback(),
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
def _read_pr_feedback(self) -> str:
|
|
1085
|
+
value = cast("Select[str]", self.query_one(f"#{PR_FEEDBACK_SELECT_ID}", Select)).value
|
|
1086
|
+
return cast("str", value)
|
|
1087
|
+
|
|
1088
|
+
def _read_agent_options(self) -> dict[str, dict[str, object]]:
|
|
1089
|
+
options: dict[str, dict[str, object]] = {}
|
|
1090
|
+
for agent_index, offered in enumerate(self._project.agent_settings):
|
|
1091
|
+
values: dict[str, object] = {}
|
|
1092
|
+
for key, setting in enumerate(offered.settings):
|
|
1093
|
+
input_id = _agent_setting_id(agent_index, key)
|
|
1094
|
+
try:
|
|
1095
|
+
value = setting.parse(self._read_agent_setting(input_id, setting))
|
|
1096
|
+
except AgentSettingError as error:
|
|
1097
|
+
raise AgentSettingError(error.key, str(error), input_id) from error
|
|
1098
|
+
if value is not None:
|
|
1099
|
+
values[setting.key] = value
|
|
1100
|
+
rejection = self._detectors.agent_options_rejection(offered.agent, values)
|
|
1101
|
+
if rejection is not None:
|
|
1102
|
+
keys = [setting.key for setting in offered.settings]
|
|
1103
|
+
position = keys.index(rejection.key) if rejection.key in keys else None
|
|
1104
|
+
focus = "" if position is None else _agent_setting_id(agent_index, position)
|
|
1105
|
+
message = f"The {offered.agent} agent rejected its settings: {rejection.detail}"
|
|
1106
|
+
raise AgentSettingError(rejection.key or offered.agent, message, focus)
|
|
1107
|
+
options[offered.agent] = values
|
|
1108
|
+
return options
|
|
1109
|
+
|
|
1110
|
+
def _read_agent_setting(self, input_id: str, setting: AgentSetting) -> str:
|
|
1111
|
+
widget = self.query_one(f"#{input_id}")
|
|
1112
|
+
if setting.kind is SettingKind.CHOICE:
|
|
1113
|
+
value = cast("Select[str]", widget).value
|
|
1114
|
+
return value if isinstance(value, str) else ""
|
|
1115
|
+
return cast("Input", widget).value
|
|
1116
|
+
|
|
1117
|
+
def _read_integration(self) -> str:
|
|
1118
|
+
value = cast("Select[str]", self.query_one(f"#{INTEGRATION_SELECT_ID}", Select)).value
|
|
1119
|
+
if not isinstance(value, str) or value not in INTEGRATION_LABELS:
|
|
1120
|
+
message = "choose an integration mode"
|
|
1121
|
+
raise ConfigError(message)
|
|
1122
|
+
return value
|
|
1123
|
+
|
|
1124
|
+
def _show_error(self, message: str) -> None:
|
|
1125
|
+
self._error = message
|
|
1126
|
+
error = self.query_one("#wizard-error", Static)
|
|
1127
|
+
error.update(message)
|
|
1128
|
+
error.display = True
|
|
1129
|
+
self.query_one("#wizard-body", InitWizardBody).scroll_to_widget(
|
|
1130
|
+
error, animate=False, immediate=True
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
def _wizard_mounted(self) -> bool:
|
|
1134
|
+
return len(self.query("#wizard-frame")) > 0
|
|
1135
|
+
|
|
1136
|
+
def _update_pages(self) -> None:
|
|
1137
|
+
self.query_one("#wizard-title", Static).update(_step_title(self._step))
|
|
1138
|
+
self.query_one("#wizard-step", Static).update(_step_counter(self._step))
|
|
1139
|
+
for index in range(len(WIZARD_STEPS)):
|
|
1140
|
+
self.query_one(f"#wizard-page-{index}").display = index == self._step
|
|
1141
|
+
self.query_one("#wizard-body", InitWizardBody).scroll_home(animate=False)
|
|
1142
|
+
back = self.query_one("#wizard-back", Button)
|
|
1143
|
+
back.display = self._config_path is None
|
|
1144
|
+
back.disabled = self._step == 0 or self._finishing
|
|
1145
|
+
next_button = self.query_one("#wizard-next", Button)
|
|
1146
|
+
next_button.display = self._step < FINISH_STEP_INDEX and self._config_path is None
|
|
1147
|
+
next_button.disabled = (
|
|
1148
|
+
(self._step == REPOSITORY_STEP_INDEX and not self._catalogs_loaded)
|
|
1149
|
+
or not self._frame_ready
|
|
1150
|
+
or self._finishing
|
|
1151
|
+
or self._writing_config
|
|
1152
|
+
)
|
|
1153
|
+
finish_button = self.query_one("#wizard-finish", Button)
|
|
1154
|
+
finish_button.display = self._step == FINISH_STEP_INDEX or self._config_path is not None
|
|
1155
|
+
finish_button.label = "Exit" if self._config_path is not None else "Run doctor"
|
|
1156
|
+
finish_button.disabled = self._finishing
|
|
1157
|
+
if self.focused is next_button and not next_button.display:
|
|
1158
|
+
finish_button.focus()
|
|
1159
|
+
cancel = self.query_one("#wizard-cancel", Button)
|
|
1160
|
+
cancel.display = self._config_path is None
|
|
1161
|
+
cancel.disabled = self._writing_config
|
|
1162
|
+
cow_result = self.query_one("#cow-result", WizardDetailRow)
|
|
1163
|
+
if self._cow_supported is not None:
|
|
1164
|
+
cow_result.update_value(self._cow_status())
|
|
1165
|
+
wizard_error = self.query_one("#wizard-error", Static)
|
|
1166
|
+
wizard_error.update(self._error if self._config_path is None else "")
|
|
1167
|
+
wizard_error.display = self._config_path is None and bool(self._error)
|
|
1168
|
+
self._update_finish_summary()
|
|
1169
|
+
self.query_one("#wizard-finish-before").display = self._config_path is None
|
|
1170
|
+
self.query_one("#wizard-finish-after").display = self._config_path is not None
|
|
1171
|
+
if self._config_path is None:
|
|
1172
|
+
self._update_doctor_report()
|
|
1173
|
+
else:
|
|
1174
|
+
self.call_after_refresh(self._update_doctor_report)
|
|
1175
|
+
self.call_after_refresh(self._update_navigation_hint)
|
|
1176
|
+
|
|
1177
|
+
def _update_doctor_report(self) -> None:
|
|
1178
|
+
report_widget = self.query_one("#doctor-report", Static)
|
|
1179
|
+
if self._config_path is None:
|
|
1180
|
+
report_widget.update("")
|
|
1181
|
+
self._doctor_report_width = None
|
|
1182
|
+
return
|
|
1183
|
+
width = report_widget.content_region.width
|
|
1184
|
+
if width < 1:
|
|
1185
|
+
return
|
|
1186
|
+
if width == self._doctor_report_width:
|
|
1187
|
+
self.call_after_refresh(self._update_navigation_hint)
|
|
1188
|
+
return
|
|
1189
|
+
self._doctor_report_width = width
|
|
1190
|
+
self._render_doctor_report(report_widget, width)
|
|
1191
|
+
self.call_after_refresh(self._settle_doctor_report_width)
|
|
1192
|
+
|
|
1193
|
+
def _settle_doctor_report_width(self) -> None:
|
|
1194
|
+
report_widget = self.query_one("#doctor-report", Static)
|
|
1195
|
+
width = report_widget.content_region.width
|
|
1196
|
+
if width > 0 and width != self._doctor_report_width:
|
|
1197
|
+
self._doctor_report_width = width
|
|
1198
|
+
self._render_doctor_report(report_widget, width)
|
|
1199
|
+
self.call_after_refresh(self._update_navigation_hint)
|
|
1200
|
+
|
|
1201
|
+
def _render_doctor_report(self, report_widget: Static, width: int) -> None:
|
|
1202
|
+
self.query_one("#doctor-config", WizardDetailRow).update_value(str(self._config_path))
|
|
1203
|
+
if self._doctor_report is not None:
|
|
1204
|
+
report_widget.update(self._doctor_renderer(self._doctor_report, width=width))
|
|
1205
|
+
else:
|
|
1206
|
+
detail = self._doctor_error or "Doctor did not return a report."
|
|
1207
|
+
report_widget.update(Text(detail, style=ALERT_COLOR))
|
|
1208
|
+
|
|
1209
|
+
def _cow_status(self) -> Text:
|
|
1210
|
+
status = Text()
|
|
1211
|
+
if self._cow_supported is None:
|
|
1212
|
+
status.append("checking this project filesystem", style=PRIMARY_TEXT_COLOR)
|
|
1213
|
+
elif self._cow_supported:
|
|
1214
|
+
status.append(
|
|
1215
|
+
"supported on this project filesystem", style=f"{STYLE_BOLD} {SUCCESS_COLOR}"
|
|
1216
|
+
)
|
|
1217
|
+
else:
|
|
1218
|
+
status.append(
|
|
1219
|
+
"unavailable · cache copies will be skipped",
|
|
1220
|
+
style=f"{STYLE_BOLD} {AMBER}",
|
|
1221
|
+
)
|
|
1222
|
+
return status
|
|
1223
|
+
|
|
1224
|
+
def _repository_page(self) -> ComposeResult:
|
|
1225
|
+
with Vertical(id="wizard-page-0", classes="wizard-page"):
|
|
1226
|
+
yield Static(
|
|
1227
|
+
"Read-only facts from the current checkout.",
|
|
1228
|
+
classes="wizard-description",
|
|
1229
|
+
markup=False,
|
|
1230
|
+
)
|
|
1231
|
+
yield _detail_row("Project", str(self._root))
|
|
1232
|
+
yield _detail_row("Base", self._project.base)
|
|
1233
|
+
yield _detail_row("Config target", str(self._config_target))
|
|
1234
|
+
yield _detail_row(
|
|
1235
|
+
"Checkout",
|
|
1236
|
+
_repository_checkout(self._project.base, clean=self._project.worktree_clean),
|
|
1237
|
+
widget_id=REPOSITORY_CHECKOUT_ID,
|
|
1238
|
+
)
|
|
1239
|
+
yield Static("Discovering supported agent model choices…", id="catalog-discovery")
|
|
1240
|
+
|
|
1241
|
+
def _trackers_page(self) -> ComposeResult:
|
|
1242
|
+
with Vertical(id="wizard-page-2", classes="wizard-page"):
|
|
1243
|
+
yield Static(
|
|
1244
|
+
"Tracker files start blank. Select project-relative Markdown candidates "
|
|
1245
|
+
"used as task trackers; an empty field sends no tracker files to tasks.",
|
|
1246
|
+
classes="wizard-description",
|
|
1247
|
+
markup=False,
|
|
1248
|
+
)
|
|
1249
|
+
yield Static(_section_heading("Detected tracker files"), classes="section-heading")
|
|
1250
|
+
yield CandidateRows(TRACKERS_LIST_ID, TRACKERS_INPUT_ID, self._project.trackers)
|
|
1251
|
+
with Horizontal(classes="palette-row wizard-wide-label"):
|
|
1252
|
+
yield Label("Tracker files")
|
|
1253
|
+
yield Input(id=TRACKERS_INPUT_ID)
|
|
1254
|
+
|
|
1255
|
+
def _caches_page(self) -> ComposeResult:
|
|
1256
|
+
with Vertical(id="wizard-page-3", classes="wizard-page"):
|
|
1257
|
+
yield Static(
|
|
1258
|
+
"Cache paths start blank and are registered for task worktrees. Select "
|
|
1259
|
+
"detected ignored directories to add; an empty field copies no caches. "
|
|
1260
|
+
"CoW is checked on this project filesystem.",
|
|
1261
|
+
classes="wizard-description",
|
|
1262
|
+
markup=False,
|
|
1263
|
+
)
|
|
1264
|
+
yield Static(_section_heading("Detected cache directories"), classes="section-heading")
|
|
1265
|
+
yield _detail_row("Copy-on-write", self._cow_status(), widget_id="cow-result")
|
|
1266
|
+
yield CandidateRows(CACHES_LIST_ID, CACHES_INPUT_ID, self._project.caches)
|
|
1267
|
+
with Horizontal(classes="palette-row wizard-wide-label"):
|
|
1268
|
+
yield Label("Cache paths")
|
|
1269
|
+
yield Input(id=CACHES_INPUT_ID)
|
|
1270
|
+
|
|
1271
|
+
def _orchestration_page(self) -> ComposeResult:
|
|
1272
|
+
with Vertical(id="wizard-page-4", classes="wizard-page"):
|
|
1273
|
+
yield Static(
|
|
1274
|
+
"Set how many tasks run at once and when new work may start. "
|
|
1275
|
+
"Each field starts at the project default.",
|
|
1276
|
+
classes="wizard-description",
|
|
1277
|
+
markup=False,
|
|
1278
|
+
)
|
|
1279
|
+
for field in ORCHESTRATION_FIELDS:
|
|
1280
|
+
yield self._orchestration_field(field)
|
|
1281
|
+
|
|
1282
|
+
def _orchestration_field(self, field: OrchestrationField) -> Vertical:
|
|
1283
|
+
return Vertical(
|
|
1284
|
+
Horizontal(
|
|
1285
|
+
Label(field.label),
|
|
1286
|
+
Input(
|
|
1287
|
+
value=str(field.default(self._orchestration_defaults)),
|
|
1288
|
+
id=field.input_id,
|
|
1289
|
+
),
|
|
1290
|
+
Static(field.unit, classes="orchestration-unit", markup=False),
|
|
1291
|
+
classes="palette-row wizard-long-label",
|
|
1292
|
+
),
|
|
1293
|
+
Static(field.explanation, classes="orchestration-help", markup=False),
|
|
1294
|
+
classes="orchestration-field",
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
def _agent_settings_page(self) -> ComposeResult:
|
|
1298
|
+
with Vertical(id="wizard-page-5", classes="wizard-page"):
|
|
1299
|
+
yield Static(
|
|
1300
|
+
"Review the options each agent plugin offers; they are written to "
|
|
1301
|
+
".shikiru/config.toml so you can change them later.",
|
|
1302
|
+
classes="wizard-description",
|
|
1303
|
+
markup=False,
|
|
1304
|
+
)
|
|
1305
|
+
for agent_index, offered in enumerate(self._project.agent_settings):
|
|
1306
|
+
yield from self._agent_settings_section(agent_index, offered)
|
|
1307
|
+
|
|
1308
|
+
def _agent_settings_section(self, agent_index: int, offered: AgentSettings) -> ComposeResult:
|
|
1309
|
+
yield Static(_section_heading(offered.agent), classes="section-heading", markup=False)
|
|
1310
|
+
if not offered.supported:
|
|
1311
|
+
yield Static(
|
|
1312
|
+
f"{offered.error}; the plugin's own defaults apply and nothing is written.",
|
|
1313
|
+
classes="orchestration-help",
|
|
1314
|
+
markup=False,
|
|
1315
|
+
)
|
|
1316
|
+
for key, setting in enumerate(offered.settings):
|
|
1317
|
+
yield self._agent_setting_field(agent_index, key, setting)
|
|
1318
|
+
|
|
1319
|
+
def _agent_setting_field(self, agent: int, key: int, setting: AgentSetting) -> Vertical:
|
|
1320
|
+
input_id = _agent_setting_id(agent, key)
|
|
1321
|
+
editor: Input | Select[str]
|
|
1322
|
+
if setting.kind is SettingKind.CHOICE:
|
|
1323
|
+
editor = Select(
|
|
1324
|
+
[(choice, choice) for choice in setting.choices],
|
|
1325
|
+
value=setting.display() if setting.default is not None else Select.BLANK,
|
|
1326
|
+
allow_blank=setting.optional,
|
|
1327
|
+
prompt=UNSET_CHOICE_LABEL,
|
|
1328
|
+
id=input_id,
|
|
1329
|
+
classes="agent-setting-editor",
|
|
1330
|
+
)
|
|
1331
|
+
else:
|
|
1332
|
+
editor = Input(value=setting.display(), id=input_id, classes="agent-setting-editor")
|
|
1333
|
+
return Vertical(
|
|
1334
|
+
Horizontal(
|
|
1335
|
+
Label(setting.label),
|
|
1336
|
+
editor,
|
|
1337
|
+
Static(setting.unit, classes="orchestration-unit", markup=False),
|
|
1338
|
+
classes="palette-row wizard-long-label",
|
|
1339
|
+
),
|
|
1340
|
+
Static(setting.help, classes="orchestration-help", markup=False),
|
|
1341
|
+
classes="orchestration-field",
|
|
1342
|
+
)
|
|
1343
|
+
|
|
1344
|
+
def _integration_page(self) -> ComposeResult:
|
|
1345
|
+
with Vertical(id="wizard-page-6", classes="wizard-page"):
|
|
1346
|
+
yield Static(
|
|
1347
|
+
"Choose how completed work is integrated back into the repository.",
|
|
1348
|
+
classes="wizard-description",
|
|
1349
|
+
markup=False,
|
|
1350
|
+
)
|
|
1351
|
+
with Horizontal(classes="palette-row wizard-long-label"):
|
|
1352
|
+
yield Label("Integration mode")
|
|
1353
|
+
yield Select(
|
|
1354
|
+
INTEGRATION_OPTIONS,
|
|
1355
|
+
value=self._orchestration_defaults.integration,
|
|
1356
|
+
allow_blank=False,
|
|
1357
|
+
id=INTEGRATION_SELECT_ID,
|
|
1358
|
+
)
|
|
1359
|
+
yield _detail_row("Local merge", "merge a finished task branch locally")
|
|
1360
|
+
yield _detail_row(
|
|
1361
|
+
"Local squash and merge", "land a finished task as one commit on the base"
|
|
1362
|
+
)
|
|
1363
|
+
yield _detail_row("Pull request", "open a pull request for review")
|
|
1364
|
+
with Horizontal(id=PR_FEEDBACK_ROW_ID, classes="palette-row wizard-long-label"):
|
|
1365
|
+
yield Label("Pull request feedback")
|
|
1366
|
+
yield Select(
|
|
1367
|
+
PR_FEEDBACK_OPTIONS,
|
|
1368
|
+
value=self._orchestration_defaults.pr_feedback,
|
|
1369
|
+
allow_blank=False,
|
|
1370
|
+
id=PR_FEEDBACK_SELECT_ID,
|
|
1371
|
+
)
|
|
1372
|
+
|
|
1373
|
+
def _finish_page(self) -> ComposeResult:
|
|
1374
|
+
with Vertical(id="wizard-page-7", classes="wizard-page"):
|
|
1375
|
+
with Vertical(id="wizard-finish-before"):
|
|
1376
|
+
yield Static(
|
|
1377
|
+
"Finish writes .shikiru/config.toml, excludes .shikiru/ in Git, "
|
|
1378
|
+
"and runs Doctor.",
|
|
1379
|
+
id="wizard-finish-hint",
|
|
1380
|
+
classes="wizard-description",
|
|
1381
|
+
markup=False,
|
|
1382
|
+
)
|
|
1383
|
+
yield _detail_row(
|
|
1384
|
+
"Config", str(self._config_target), widget_id="wizard-config-target"
|
|
1385
|
+
)
|
|
1386
|
+
yield _detail_row("Summary", "", widget_id="wizard-summary")
|
|
1387
|
+
with Vertical(id="wizard-finish-after"):
|
|
1388
|
+
yield _detail_row("Config", str(self._config_target), widget_id="doctor-config")
|
|
1389
|
+
yield Static("", id="doctor-report", markup=False)
|