agentic-project-kit 1.0.1__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.
Files changed (310) hide show
  1. agentic_project_kit/__init__.py +1 -0
  2. agentic_project_kit/absolute_path_portability_audit.py +241 -0
  3. agentic_project_kit/access_level_policy.py +14 -0
  4. agentic_project_kit/access_levels.py +22 -0
  5. agentic_project_kit/action_registry.py +195 -0
  6. agentic_project_kit/action_specs.py +286 -0
  7. agentic_project_kit/agent_command_runner.py +516 -0
  8. agentic_project_kit/audit_test_matrix.py +36 -0
  9. agentic_project_kit/audit_test_matrix_cli.py +43 -0
  10. agentic_project_kit/chat_bootloader.py +257 -0
  11. agentic_project_kit/chat_entrypoint_contract.py +297 -0
  12. agentic_project_kit/checks.py +511 -0
  13. agentic_project_kit/ci_readiness.py +211 -0
  14. agentic_project_kit/cli.py +147 -0
  15. agentic_project_kit/cli_commands/__init__.py +1 -0
  16. agentic_project_kit/cli_commands/absolute_path_audit.py +25 -0
  17. agentic_project_kit/cli_commands/actions.py +21 -0
  18. agentic_project_kit/cli_commands/artifact_gc.py +240 -0
  19. agentic_project_kit/cli_commands/boot.py +55 -0
  20. agentic_project_kit/cli_commands/chat.py +34 -0
  21. agentic_project_kit/cli_commands/checks.py +305 -0
  22. agentic_project_kit/cli_commands/cockpit.py +103 -0
  23. agentic_project_kit/cli_commands/command_taxonomy.py +25 -0
  24. agentic_project_kit/cli_commands/commands.py +129 -0
  25. agentic_project_kit/cli_commands/dev.py +23 -0
  26. agentic_project_kit/cli_commands/direction.py +92 -0
  27. agentic_project_kit/cli_commands/doc_currency_audit.py +25 -0
  28. agentic_project_kit/cli_commands/doc_registry.py +126 -0
  29. agentic_project_kit/cli_commands/docs.py +223 -0
  30. agentic_project_kit/cli_commands/dpa.py +1338 -0
  31. agentic_project_kit/cli_commands/evidence.py +251 -0
  32. agentic_project_kit/cli_commands/github.py +34 -0
  33. agentic_project_kit/cli_commands/governance.py +14 -0
  34. agentic_project_kit/cli_commands/gui.py +23 -0
  35. agentic_project_kit/cli_commands/gui_readiness_gate.py +27 -0
  36. agentic_project_kit/cli_commands/handoff.py +183 -0
  37. agentic_project_kit/cli_commands/human_workflows.py +430 -0
  38. agentic_project_kit/cli_commands/init.py +132 -0
  39. agentic_project_kit/cli_commands/instruction.py +75 -0
  40. agentic_project_kit/cli_commands/mutation_lock_audit.py +24 -0
  41. agentic_project_kit/cli_commands/ns_legacy_audit.py +25 -0
  42. agentic_project_kit/cli_commands/pass_already_done.py +66 -0
  43. agentic_project_kit/cli_commands/patch_failure_discipline_audit.py +26 -0
  44. agentic_project_kit/cli_commands/patch_preflight.py +37 -0
  45. agentic_project_kit/cli_commands/path_literal_audit.py +39 -0
  46. agentic_project_kit/cli_commands/patterns.py +40 -0
  47. agentic_project_kit/cli_commands/planning_docs_audit.py +25 -0
  48. agentic_project_kit/cli_commands/pr.py +133 -0
  49. agentic_project_kit/cli_commands/pr_hygiene.py +27 -0
  50. agentic_project_kit/cli_commands/profiles.py +26 -0
  51. agentic_project_kit/cli_commands/program_redundancy_audit.py +25 -0
  52. agentic_project_kit/cli_commands/project_direction.py +25 -0
  53. agentic_project_kit/cli_commands/release.py +392 -0
  54. agentic_project_kit/cli_commands/release_publish_orchestration.py +36 -0
  55. agentic_project_kit/cli_commands/remote_branch_hygiene.py +152 -0
  56. agentic_project_kit/cli_commands/remote_next.py +57 -0
  57. agentic_project_kit/cli_commands/removed_ns_commands.py +65 -0
  58. agentic_project_kit/cli_commands/rule_registry.py +129 -0
  59. agentic_project_kit/cli_commands/rules.py +222 -0
  60. agentic_project_kit/cli_commands/scaffold.py +46 -0
  61. agentic_project_kit/cli_commands/slice.py +21 -0
  62. agentic_project_kit/cli_commands/standard_gates_audit_suite.py +27 -0
  63. agentic_project_kit/cli_commands/state.py +51 -0
  64. agentic_project_kit/cli_commands/status_current_state_audit.py +30 -0
  65. agentic_project_kit/cli_commands/todo.py +53 -0
  66. agentic_project_kit/cli_commands/transfer.py +19 -0
  67. agentic_project_kit/cli_commands/transfer_context_flow.py +398 -0
  68. agentic_project_kit/cli_commands/transfer_context_helpers.py +398 -0
  69. agentic_project_kit/cli_commands/transfer_diagnostics.py +767 -0
  70. agentic_project_kit/cli_commands/transfer_evidence_flow.py +368 -0
  71. agentic_project_kit/cli_commands/transfer_handoff_flow.py +594 -0
  72. agentic_project_kit/cli_commands/transfer_order_contract.py +74 -0
  73. agentic_project_kit/cli_commands/transfer_post_merge_complete.py +442 -0
  74. agentic_project_kit/cli_commands/transfer_post_merge_settle.py +171 -0
  75. agentic_project_kit/cli_commands/transfer_pr_closeout_complete.py +124 -0
  76. agentic_project_kit/cli_commands/transfer_pr_create_flow.py +743 -0
  77. agentic_project_kit/cli_commands/transfer_pr_merge_flow.py +547 -0
  78. agentic_project_kit/cli_commands/transfer_repo_after_pr.py +160 -0
  79. agentic_project_kit/cli_commands/transfer_repo_pre_pr.py +664 -0
  80. agentic_project_kit/cli_commands/transfer_runner_flow.py +131 -0
  81. agentic_project_kit/cli_commands/transfer_runtime_initial.py +194 -0
  82. agentic_project_kit/cli_commands/transfer_shared.py +274 -0
  83. agentic_project_kit/cli_commands/validation.py +200 -0
  84. agentic_project_kit/cli_commands/version.py +27 -0
  85. agentic_project_kit/cli_commands/work_orders.py +219 -0
  86. agentic_project_kit/cli_commands/workflow.py +471 -0
  87. agentic_project_kit/cli_commands/workflow_guard.py +26 -0
  88. agentic_project_kit/cli_commands/workspace.py +252 -0
  89. agentic_project_kit/cockpit.py +608 -0
  90. agentic_project_kit/cockpit_readiness.py +67 -0
  91. agentic_project_kit/command_authority_audit.py +192 -0
  92. agentic_project_kit/command_inbox_check.py +103 -0
  93. agentic_project_kit/command_manifest.py +591 -0
  94. agentic_project_kit/command_queue_contract.py +161 -0
  95. agentic_project_kit/command_selector.py +245 -0
  96. agentic_project_kit/command_taxonomy.py +264 -0
  97. agentic_project_kit/commit_guard.py +67 -0
  98. agentic_project_kit/communication_artifact_gc.py +495 -0
  99. agentic_project_kit/communication_rule_context.py +437 -0
  100. agentic_project_kit/communication_state.py +115 -0
  101. agentic_project_kit/composed_wrapper_invariants.py +97 -0
  102. agentic_project_kit/composite_short_circuit.py +140 -0
  103. agentic_project_kit/contract.py +251 -0
  104. agentic_project_kit/control_state.py +83 -0
  105. agentic_project_kit/doc_currency_audit.py +536 -0
  106. agentic_project_kit/doc_lifecycle.py +1179 -0
  107. agentic_project_kit/doc_lifecycle_signals.py +446 -0
  108. agentic_project_kit/doc_lifecycle_sweep.py +639 -0
  109. agentic_project_kit/doc_mesh.py +491 -0
  110. agentic_project_kit/doctor.py +379 -0
  111. agentic_project_kit/documentation_registry.py +1122 -0
  112. agentic_project_kit/documentation_system_audit.py +310 -0
  113. agentic_project_kit/dpa_current_handoff_lifecycle.py +549 -0
  114. agentic_project_kit/dpa_dp2_decision_readiness.py +362 -0
  115. agentic_project_kit/dpa_dp3_dp4_adjudication.py +604 -0
  116. agentic_project_kit/dpa_dp5_block_new_gate.py +294 -0
  117. agentic_project_kit/dpa_dp5_stage_adoption.py +615 -0
  118. agentic_project_kit/dpa_dp5_strict_gate.py +187 -0
  119. agentic_project_kit/dpa_final_closeout.py +627 -0
  120. agentic_project_kit/dpa_fixture_evidence.py +680 -0
  121. agentic_project_kit/dpa_maintainer_record_check.py +711 -0
  122. agentic_project_kit/dpa_post_dp2_scope_assessment.py +734 -0
  123. agentic_project_kit/dpa_probe_002_readiness.py +509 -0
  124. agentic_project_kit/dpa_probe_003_readiness.py +417 -0
  125. agentic_project_kit/dpa_probe_004_readiness.py +437 -0
  126. agentic_project_kit/dpa_readiness.py +505 -0
  127. agentic_project_kit/dpa_readonly_probe_execution.py +500 -0
  128. agentic_project_kit/dpa_registry_contracts.py +312 -0
  129. agentic_project_kit/dpa_renderer_readiness.py +407 -0
  130. agentic_project_kit/dpa_repo_adoption_assessment.py +574 -0
  131. agentic_project_kit/dpa_stable_readiness.py +743 -0
  132. agentic_project_kit/dpa_successor_projection.py +168 -0
  133. agentic_project_kit/dpa_workspace_init_projection.py +93 -0
  134. agentic_project_kit/dpa_wrt_ch001_evidence.py +327 -0
  135. agentic_project_kit/entrypoint_slice_runner.py +153 -0
  136. agentic_project_kit/evidence_clean.py +152 -0
  137. agentic_project_kit/evidence_commit_paths.py +214 -0
  138. agentic_project_kit/evidence_finalize_log.py +204 -0
  139. agentic_project_kit/evidence_guard.py +114 -0
  140. agentic_project_kit/evidence_inspector.py +507 -0
  141. agentic_project_kit/evidence_state_contract.py +99 -0
  142. agentic_project_kit/execution_mode_state.py +148 -0
  143. agentic_project_kit/final_summary_contract.py +85 -0
  144. agentic_project_kit/finalize_guard.py +139 -0
  145. agentic_project_kit/gatekeeper_core.py +232 -0
  146. agentic_project_kit/github.py +25 -0
  147. agentic_project_kit/governance.py +59 -0
  148. agentic_project_kit/gui_action_execution.py +105 -0
  149. agentic_project_kit/gui_action_renderer.py +81 -0
  150. agentic_project_kit/gui_action_run_intent.py +98 -0
  151. agentic_project_kit/gui_action_views.py +195 -0
  152. agentic_project_kit/gui_activity_log.py +76 -0
  153. agentic_project_kit/gui_button_catalog.py +605 -0
  154. agentic_project_kit/gui_cockpit.py +365 -0
  155. agentic_project_kit/gui_cockpit_actions.py +695 -0
  156. agentic_project_kit/gui_cockpit_common.py +172 -0
  157. agentic_project_kit/gui_cockpit_header.py +501 -0
  158. agentic_project_kit/gui_cockpit_sidebar.py +387 -0
  159. agentic_project_kit/gui_cockpit_task.py +748 -0
  160. agentic_project_kit/gui_command_projection.py +355 -0
  161. agentic_project_kit/gui_communication_modes.py +130 -0
  162. agentic_project_kit/gui_dry_run.py +219 -0
  163. agentic_project_kit/gui_gatekeeper_status.py +259 -0
  164. agentic_project_kit/gui_layout_plan.py +148 -0
  165. agentic_project_kit/gui_open_folder.py +18 -0
  166. agentic_project_kit/gui_output_status_panel.py +61 -0
  167. agentic_project_kit/gui_panel_state.py +47 -0
  168. agentic_project_kit/gui_presenter.py +62 -0
  169. agentic_project_kit/gui_readiness_gate.py +206 -0
  170. agentic_project_kit/gui_release_flow.py +59 -0
  171. agentic_project_kit/gui_task_editor.py +1077 -0
  172. agentic_project_kit/gui_tk_widgets.py +177 -0
  173. agentic_project_kit/gui_tkinter_renderer.py +87 -0
  174. agentic_project_kit/gui_tkinter_shell.py +1176 -0
  175. agentic_project_kit/gui_transfer_contract.py +28 -0
  176. agentic_project_kit/gui_viewmodel.py +698 -0
  177. agentic_project_kit/gui_window_guard.py +46 -0
  178. agentic_project_kit/handoff_freshness.py +390 -0
  179. agentic_project_kit/handoff_prompt.py +180 -0
  180. agentic_project_kit/handoff_state.py +254 -0
  181. agentic_project_kit/instruction_lint.py +455 -0
  182. agentic_project_kit/llm_context_carriers.py +66 -0
  183. agentic_project_kit/llm_execution_context.py +285 -0
  184. agentic_project_kit/local_command_stack.py +133 -0
  185. agentic_project_kit/local_feature_gate.py +82 -0
  186. agentic_project_kit/local_garbage_collector.py +269 -0
  187. agentic_project_kit/models.py +18 -0
  188. agentic_project_kit/mutation_lock_audit.py +421 -0
  189. agentic_project_kit/next_turn_evidence.py +253 -0
  190. agentic_project_kit/next_turn_merge_if_green.py +459 -0
  191. agentic_project_kit/next_turn_pr_status.py +255 -0
  192. agentic_project_kit/next_turn_result.py +183 -0
  193. agentic_project_kit/next_turn_runner.py +159 -0
  194. agentic_project_kit/next_turn_slot.py +107 -0
  195. agentic_project_kit/next_turn_status.py +274 -0
  196. agentic_project_kit/ns_legacy_reference_audit.py +258 -0
  197. agentic_project_kit/ns_slice_runner.py +16 -0
  198. agentic_project_kit/operational_handoff_projection.py +124 -0
  199. agentic_project_kit/output_contract.py +80 -0
  200. agentic_project_kit/output_repair.py +61 -0
  201. agentic_project_kit/pass_already_done.py +216 -0
  202. agentic_project_kit/patch_artifact_preflight.py +147 -0
  203. agentic_project_kit/patch_cycle_workflow.py +440 -0
  204. agentic_project_kit/patch_failure_discipline_audit.py +223 -0
  205. agentic_project_kit/patch_preflight.py +215 -0
  206. agentic_project_kit/path_literal_audit.py +612 -0
  207. agentic_project_kit/patterns.py +99 -0
  208. agentic_project_kit/planning_docs_consolidation_audit.py +313 -0
  209. agentic_project_kit/portability_shell_gate.py +40 -0
  210. agentic_project_kit/post_merge_handoff_refresh.py +56 -0
  211. agentic_project_kit/post_release.py +252 -0
  212. agentic_project_kit/post_release_closeout.py +616 -0
  213. agentic_project_kit/pr_cleanup.py +172 -0
  214. agentic_project_kit/pr_closeout.py +50 -0
  215. agentic_project_kit/pr_closeout_complete.py +344 -0
  216. agentic_project_kit/pr_create_or_skip.py +71 -0
  217. agentic_project_kit/pr_hygiene.py +210 -0
  218. agentic_project_kit/program_redundancy_audit.py +251 -0
  219. agentic_project_kit/project_direction.py +840 -0
  220. agentic_project_kit/protected_change_planner.py +194 -0
  221. agentic_project_kit/release.py +401 -0
  222. agentic_project_kit/release_doi_safety.py +24 -0
  223. agentic_project_kit/release_gate_core.py +174 -0
  224. agentic_project_kit/release_metadata_authority_gate.py +328 -0
  225. agentic_project_kit/release_metadata_prep.py +43 -0
  226. agentic_project_kit/release_notes.py +823 -0
  227. agentic_project_kit/release_prep_core.py +204 -0
  228. agentic_project_kit/release_prepare.py +369 -0
  229. agentic_project_kit/release_process_guardrails.py +247 -0
  230. agentic_project_kit/release_publish_core.py +114 -0
  231. agentic_project_kit/release_publish_orchestration.py +469 -0
  232. agentic_project_kit/release_state.py +552 -0
  233. agentic_project_kit/release_verify_core.py +175 -0
  234. agentic_project_kit/remote_branch_hygiene.py +223 -0
  235. agentic_project_kit/remote_next.py +89 -0
  236. agentic_project_kit/remote_next_closeout.py +101 -0
  237. agentic_project_kit/remote_next_order_contract.py +391 -0
  238. agentic_project_kit/removed_ns_commands.py +458 -0
  239. agentic_project_kit/removed_source_audit.py +225 -0
  240. agentic_project_kit/rendering.py +13 -0
  241. agentic_project_kit/repair_report.py +79 -0
  242. agentic_project_kit/repo_identity.py +59 -0
  243. agentic_project_kit/result_report_classifier.py +57 -0
  244. agentic_project_kit/rule_ack.py +105 -0
  245. agentic_project_kit/rule_preservation.py +58 -0
  246. agentic_project_kit/rule_refresh.py +174 -0
  247. agentic_project_kit/rule_registry_registration.py +322 -0
  248. agentic_project_kit/rule_registry_report.py +206 -0
  249. agentic_project_kit/rule_registry_validator.py +704 -0
  250. agentic_project_kit/rule_snapshot.py +98 -0
  251. agentic_project_kit/rule_source_validator.py +133 -0
  252. agentic_project_kit/run_summary.py +68 -0
  253. agentic_project_kit/run_summary_renderer.py +238 -0
  254. agentic_project_kit/runtime_validator.py +68 -0
  255. agentic_project_kit/safe_file_replace.py +35 -0
  256. agentic_project_kit/safe_patch.py +47 -0
  257. agentic_project_kit/safe_push.py +218 -0
  258. agentic_project_kit/safe_remove_diagnostic.py +51 -0
  259. agentic_project_kit/scaffold.py +69 -0
  260. agentic_project_kit/site_claims.py +444 -0
  261. agentic_project_kit/site_generator.py +1140 -0
  262. agentic_project_kit/slice_gate.py +225 -0
  263. agentic_project_kit/slice_runner.py +75 -0
  264. agentic_project_kit/standard_gates_audit_suite.py +167 -0
  265. agentic_project_kit/state_freshness.py +135 -0
  266. agentic_project_kit/status_current_state_audit.py +762 -0
  267. agentic_project_kit/successor_handoff_package.py +1763 -0
  268. agentic_project_kit/templates.py +292 -0
  269. agentic_project_kit/terminal_block_guard.py +166 -0
  270. agentic_project_kit/terminal_logging.py +275 -0
  271. agentic_project_kit/todo.py +97 -0
  272. agentic_project_kit/transfer_closeout.py +136 -0
  273. agentic_project_kit/transfer_continue.py +215 -0
  274. agentic_project_kit/transfer_local_runner.py +101 -0
  275. agentic_project_kit/transfer_operation_monitor.py +236 -0
  276. agentic_project_kit/transfer_post_merge_lifecycle.py +356 -0
  277. agentic_project_kit/transfer_post_merge_settle.py +383 -0
  278. agentic_project_kit/transfer_pr_actions.py +164 -0
  279. agentic_project_kit/transfer_remote_next.py +959 -0
  280. agentic_project_kit/transfer_repo_actions.py +2253 -0
  281. agentic_project_kit/transfer_runner.py +347 -0
  282. agentic_project_kit/transfer_safety_context.py +291 -0
  283. agentic_project_kit/transfer_state.py +483 -0
  284. agentic_project_kit/transfer_uplink.py +471 -0
  285. agentic_project_kit/transfer_workflow_next.py +148 -0
  286. agentic_project_kit/typed_work_order_evidence.py +58 -0
  287. agentic_project_kit/typed_work_order_queue.py +166 -0
  288. agentic_project_kit/typed_work_order_runner.py +168 -0
  289. agentic_project_kit/work_cycle.py +317 -0
  290. agentic_project_kit/work_discard_changes.py +129 -0
  291. agentic_project_kit/work_order_patch.py +199 -0
  292. agentic_project_kit/work_order_runner.py +120 -0
  293. agentic_project_kit/work_order_uploader.py +220 -0
  294. agentic_project_kit/work_order_validator.py +185 -0
  295. agentic_project_kit/work_orders.py +246 -0
  296. agentic_project_kit/workflow_guard.py +262 -0
  297. agentic_project_kit/workflow_summary_runner.py +124 -0
  298. agentic_project_kit/workspace.py +665 -0
  299. agentic_project_kit/workspace_adopt.py +471 -0
  300. agentic_project_kit/workspace_dpa_intake.py +406 -0
  301. agentic_project_kit/workspace_init.py +487 -0
  302. agentic_project_kit/workspace_lock.py +129 -0
  303. agentic_project_kit/workspace_remove.py +381 -0
  304. agentic_project_kit/workspace_upgrade.py +324 -0
  305. agentic_project_kit/wrapper_live_status.py +104 -0
  306. agentic_project_kit-1.0.1.dist-info/METADATA +724 -0
  307. agentic_project_kit-1.0.1.dist-info/RECORD +310 -0
  308. agentic_project_kit-1.0.1.dist-info/WHEEL +4 -0
  309. agentic_project_kit-1.0.1.dist-info/entry_points.txt +3 -0
  310. agentic_project_kit-1.0.1.dist-info/licenses/LICENSE +10 -0
@@ -0,0 +1,695 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ import json
5
+ from typing import Any
6
+
7
+ from agentic_project_kit.cockpit import READ_ONLY, run_cockpit_action
8
+ from agentic_project_kit.gui_action_views import (
9
+ GuiActionGroupView,
10
+ GuiActionView,
11
+ explain_safety,
12
+ format_action_details,
13
+ format_action_result,
14
+ grouped_action_views,
15
+ )
16
+ from agentic_project_kit.gui_cockpit_common import (
17
+ THEME,
18
+ action_tree_tag_colors,
19
+ format_state_details,
20
+ )
21
+ from agentic_project_kit.gui_release_flow import (
22
+ humanize_release_result,
23
+ normalize_release_version,
24
+ release_prepare_args,
25
+ release_preview_signature,
26
+ release_ready_args,
27
+ )
28
+ from agentic_project_kit.gui_tk_widgets import attach_tooltip, traffic_light_fill
29
+ from agentic_project_kit.gui_tkinter_shell import run_basic_cockpit_button
30
+
31
+
32
+ class CockpitActionsMixin:
33
+ def _log_gui_action(self, label: str) -> None:
34
+ if hasattr(self, "log_action"):
35
+ self.log_action(label)
36
+
37
+ def _log_gui_result(self, label: str, status: str | None, body: str) -> None:
38
+ if hasattr(self, "log_result"):
39
+ self.log_result(label, status, body)
40
+ return
41
+ self.write_output("\n" + body + "\n")
42
+
43
+ def _completed_result_status(self, completed: Any) -> str:
44
+ if completed.stdout:
45
+ try:
46
+ payload = json.loads(completed.stdout)
47
+ except ValueError:
48
+ payload = None
49
+ if isinstance(payload, dict):
50
+ raw_status = payload.get("result_status", payload.get("STATE", payload.get("state")))
51
+ if raw_status is not None:
52
+ return str(raw_status)
53
+ return "PASS" if getattr(completed, "returncode", 1) == 0 else "ERROR"
54
+
55
+ def _build_next_step_panel(self, parent: Any) -> None:
56
+ import tkinter as tk
57
+ from tkinter import ttk
58
+
59
+ next_step = self.basic_view.next_step
60
+ panel = tk.Frame(
61
+ parent,
62
+ bg=THEME.color_recommended_bg,
63
+ highlightbackground="#7eb1f1",
64
+ highlightthickness=1,
65
+ padx=10,
66
+ pady=6,
67
+ )
68
+ self._register_group_frame("next_step", panel)
69
+ panel.pack(fill=tk.BOTH, expand=True)
70
+
71
+ header = tk.Frame(panel, bg=THEME.color_recommended_bg)
72
+ header.pack(fill=tk.X, pady=(0, 4))
73
+ tk.Label(
74
+ header,
75
+ text="NEXT STEP",
76
+ bg=THEME.color_recommended_bg,
77
+ fg="#174ea6",
78
+ font=THEME.section_font,
79
+ anchor=tk.W,
80
+ ).pack(side=tk.LEFT)
81
+ status = tk.Frame(header, bg=THEME.color_recommended_bg)
82
+ status.pack(side=tk.RIGHT)
83
+ light = tk.Canvas(status, width=14, height=14, bg=THEME.color_recommended_bg, highlightthickness=0)
84
+ fill = traffic_light_fill(self.basic_view.traffic_light_color)
85
+ light.create_oval(3, 3, 11, 11, fill=fill, outline=fill)
86
+ light.pack(side=tk.LEFT, padx=(0, 5))
87
+ tk.Label(
88
+ status,
89
+ text=next_step.state_label,
90
+ bg=THEME.color_recommended_bg,
91
+ fg="#174ea6",
92
+ font=THEME.small_font,
93
+ ).pack(side=tk.LEFT)
94
+
95
+ tk.Label(
96
+ panel,
97
+ text=next_step.title,
98
+ bg=THEME.color_recommended_bg,
99
+ fg="#0b2f27",
100
+ font=THEME.recommended_font,
101
+ anchor=tk.W,
102
+ justify=tk.LEFT,
103
+ wraplength=720,
104
+ ).pack(fill=tk.X)
105
+ tk.Label(
106
+ panel,
107
+ text=next_step.message,
108
+ bg=THEME.color_recommended_bg,
109
+ fg="#174ea6",
110
+ font=THEME.body_font,
111
+ anchor=tk.W,
112
+ justify=tk.LEFT,
113
+ wraplength=720,
114
+ ).pack(fill=tk.X, pady=(5, 0))
115
+
116
+ button_row = tk.Frame(panel, bg=THEME.color_recommended_bg)
117
+ button_row.pack(fill=tk.X, pady=(6, 0))
118
+ primary_state = tk.NORMAL if next_step.primary_enabled else tk.DISABLED
119
+ primary = ttk.Button(
120
+ button_row,
121
+ text=next_step.primary_label,
122
+ command=self.run_recommended_action,
123
+ state=primary_state,
124
+ )
125
+ primary.pack(side=tk.LEFT, padx=(0, 8))
126
+ attach_tooltip(primary, self.basic_view.recommended_action.tooltip)
127
+ why = ttk.Button(button_row, text="Why?", command=self.show_next_step_details)
128
+ why.pack(side=tk.LEFT, padx=(0, 8))
129
+ attach_tooltip(why, "Explain why this next step is recommended and show the current state details.")
130
+ help_button = ttk.Button(button_row, text="Help", command=self.show_cockpit_help)
131
+ help_button.pack(side=tk.LEFT)
132
+ attach_tooltip(help_button, "Show the current help placeholder, project sources, and maintainer contact.")
133
+
134
+ def show_next_step_details(self) -> None:
135
+ self._log_gui_action("Why?")
136
+ next_step = self.basic_view.next_step
137
+ lines = [
138
+ "",
139
+ "NEXT STEP DETAILS",
140
+ f"title={next_step.title}",
141
+ f"primary_action={next_step.primary_label}",
142
+ f"primary_enabled={str(next_step.primary_enabled).lower()}",
143
+ f"reason={next_step.reason}",
144
+ "",
145
+ format_state_details(self.basic_view),
146
+ "",
147
+ ]
148
+ self._log_gui_result("Why?", "INFO", "\n".join(lines))
149
+
150
+ def show_cockpit_help(self) -> None:
151
+ self._log_gui_action("Help")
152
+ lines = [
153
+ "",
154
+ "HELP (under construction)",
155
+ "The cockpit help area is under construction.",
156
+ "Authoritative sources:",
157
+ "- GitHub: https://github.com/vfi64/agentic-project-kit",
158
+ "- Zenodo concept DOI: https://doi.org/10.5281/zenodo.20101359",
159
+ "- Current verified Zenodo DOI: https://doi.org/10.5281/zenodo.20917074",
160
+ "- Maintainer: Volker Fickert",
161
+ "",
162
+ ]
163
+ self._log_gui_result("Help", "INFO", "\n".join(lines))
164
+
165
+ def _build_action_cards(self, parent: Any) -> None:
166
+ import tkinter as tk
167
+ from tkinter import ttk
168
+
169
+ self._section_heading(parent, "Advanced: individual actions")
170
+ actions_row = tk.Frame(parent, bg=THEME.color_panel_bg)
171
+ self._register_group_frame("action_table", actions_row)
172
+ actions_row.pack(fill=tk.X, pady=(0, 10))
173
+ action_list_height = THEME.action_rows_visible * THEME.action_card_height
174
+ action_shell_width = THEME.action_list_width + THEME.action_scrollbar_width
175
+ action_scroll_shell = tk.Frame(
176
+ actions_row,
177
+ bg=THEME.color_panel_bg,
178
+ width=action_shell_width,
179
+ height=action_list_height,
180
+ )
181
+ self.action_scroll_shell = action_scroll_shell
182
+ action_scroll_shell.pack(side=tk.LEFT, fill=tk.BOTH)
183
+ action_scroll_shell.pack_propagate(False)
184
+ self.action_card_canvas = tk.Canvas(
185
+ action_scroll_shell,
186
+ bg=THEME.color_panel_bg,
187
+ width=THEME.action_list_width,
188
+ height=action_list_height,
189
+ highlightthickness=0,
190
+ )
191
+ self.action_card_scrollbar = ttk.Scrollbar(
192
+ action_scroll_shell,
193
+ orient=tk.VERTICAL,
194
+ command=self.action_card_canvas.yview,
195
+ )
196
+ self.action_card_canvas.configure(yscrollcommand=self.action_card_scrollbar.set)
197
+ self.action_card_container = tk.Frame(self.action_card_canvas, bg=THEME.color_panel_bg)
198
+ self.action_card_window = self.action_card_canvas.create_window(
199
+ (0, 0),
200
+ window=self.action_card_container,
201
+ anchor=tk.NW,
202
+ )
203
+ self.action_card_container.bind(
204
+ "<Configure>",
205
+ lambda _event: self.action_card_canvas.configure(
206
+ scrollregion=self.action_card_canvas.bbox(tk.ALL)
207
+ ),
208
+ )
209
+ self.action_card_canvas.bind(
210
+ "<Configure>",
211
+ lambda event: self.action_card_canvas.itemconfigure(
212
+ self.action_card_window,
213
+ width=event.width,
214
+ ),
215
+ )
216
+ self._bind_action_card_scroll_events(self.action_card_canvas)
217
+ self._bind_action_card_scroll_events(self.action_card_container)
218
+ self.action_card_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
219
+ self.action_card_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
220
+ self.action_card_widgets: dict[str, Any] = {}
221
+ self.populate_action_tree()
222
+
223
+ controls = tk.Frame(actions_row, bg=THEME.color_panel_bg)
224
+ controls.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(12, 0))
225
+ inspect_button = ttk.Button(controls, text="Inspect", command=self.inspect_selected)
226
+ attach_tooltip(inspect_button, "Show metadata, command, and safety details for the selected action.")
227
+ inspect_button.grid(row=0, column=0, sticky="ew", padx=(0, 8), pady=(0, 6))
228
+ run_button = ttk.Button(controls, text="Run read-only", command=self.run_selected_read_only)
229
+ attach_tooltip(run_button, "Run only selected read-only cockpit actions through the shared cockpit layer.")
230
+ run_button.grid(row=0, column=1, sticky="ew", pady=(0, 6))
231
+ controls.columnconfigure(0, weight=1)
232
+ controls.columnconfigure(1, weight=1)
233
+
234
+ def _bind_action_card_scroll_events(self, widget: Any) -> None:
235
+ for event_name in ("<MouseWheel>", "<Button-4>", "<Button-5>"):
236
+ widget.bind(event_name, self._on_action_card_mousewheel)
237
+
238
+ def _on_action_card_mousewheel(self, event: Any) -> str:
239
+ number = getattr(event, "num", None)
240
+ delta = getattr(event, "delta", 0)
241
+ if number == 4:
242
+ units = -1
243
+ elif number == 5:
244
+ units = 1
245
+ else:
246
+ units = -1 if delta > 0 else 1
247
+ self.action_card_canvas.yview_scroll(units, "units")
248
+ return "break"
249
+
250
+ def _build_advanced_tools(self, parent: Any) -> None:
251
+ group = self._build_collapsible_group(
252
+ parent,
253
+ group_id="advanced_tools",
254
+ title="Advanced tools",
255
+ subtitle="release, cleanup, discard",
256
+ )
257
+ body = group.body
258
+ self._build_release_and_cleanup_tools(body)
259
+ self._build_discard_tools(body)
260
+
261
+ def _build_release_and_cleanup_tools(self, parent: Any) -> None:
262
+ import tkinter as tk
263
+ from tkinter import ttk
264
+
265
+ row = tk.Frame(parent, bg=THEME.color_panel_bg)
266
+ row.pack(fill=tk.X, pady=(0, 8))
267
+ create_button = ttk.Button(row, text="Create release", command=self.start_create_release)
268
+ create_button.grid(row=0, column=0, sticky="ew", padx=(0, 8), pady=(0, 6))
269
+ attach_tooltip(
270
+ create_button,
271
+ "Ask for a version, run release ready, then enable Confirm create release on PASS.",
272
+ )
273
+ self.release_confirm_button = ttk.Button(
274
+ row,
275
+ text="Confirm create release",
276
+ command=self.confirm_create_release,
277
+ state=tk.DISABLED,
278
+ )
279
+ self.release_confirm_button.grid(row=0, column=1, sticky="ew", pady=(0, 6))
280
+ attach_tooltip(
281
+ self.release_confirm_button,
282
+ "Runs agentic-kit release prepare --write only after a successful readiness preview for the same version and state.",
283
+ )
284
+
285
+ cleanup_row = tk.Frame(row, bg=THEME.color_panel_bg)
286
+ cleanup_row.grid(row=1, column=0, columnspan=2, sticky="ew")
287
+ tk.Label(
288
+ cleanup_row,
289
+ text="Local tmp cutoff",
290
+ bg=THEME.color_panel_bg,
291
+ fg=THEME.color_muted_text,
292
+ font=THEME.small_font,
293
+ ).pack(side=tk.LEFT, padx=(0, 6))
294
+ self.gc_cutoff_var = tk.StringVar(value=self._default_gc_cutoff())
295
+ cutoff_entry = ttk.Entry(cleanup_row, textvariable=self.gc_cutoff_var, width=20)
296
+ attach_tooltip(cutoff_entry, "ISO cutoff for local repository tmp/ cleanup. Default is now.")
297
+ cutoff_entry.pack(side=tk.LEFT, padx=(0, 9))
298
+ gc_button = ttk.Button(cleanup_row, text="Clean local tmp", command=self.preview_log_cleanup)
299
+ attach_tooltip(
300
+ gc_button,
301
+ "Dry-run local artifact-gc for old untracked files and empty directories under repository tmp/. Never touches the remote repository.",
302
+ )
303
+ gc_button.pack(side=tk.LEFT, padx=(0, 9))
304
+ self.gc_confirm_delete_button = ttk.Button(
305
+ cleanup_row,
306
+ text="Confirm delete",
307
+ command=self.confirm_log_cleanup,
308
+ state=tk.DISABLED,
309
+ )
310
+ attach_tooltip(
311
+ self.gc_confirm_delete_button,
312
+ "Deletes local tmp/ candidates only after a successful preview. Never touches the remote repository.",
313
+ )
314
+ self.gc_confirm_delete_button.pack(side=tk.LEFT, padx=(0, 9))
315
+ self.pending_gc_preview = None
316
+ row.columnconfigure(0, weight=1)
317
+ row.columnconfigure(1, weight=1)
318
+
319
+ def _build_discard_tools(self, parent: Any) -> None:
320
+ import tkinter as tk
321
+ from tkinter import ttk
322
+
323
+ row = tk.Frame(parent, bg="#fff3f3", highlightbackground="#e2b1b1", highlightthickness=1, padx=8, pady=7)
324
+ row.pack(fill=tk.X, pady=(0, 8))
325
+ tk.Label(
326
+ row,
327
+ text="DESTRUCTIVE",
328
+ bg="#fff3f3",
329
+ fg="#7a1f1f",
330
+ font=THEME.section_font,
331
+ ).pack(side=tk.LEFT, padx=(0, 8))
332
+ discard_button = ttk.Button(
333
+ row,
334
+ text="Discard all changes",
335
+ command=self.preview_discard_changes,
336
+ state=tk.NORMAL if self._discard_available() else tk.DISABLED,
337
+ )
338
+ attach_tooltip(
339
+ discard_button,
340
+ "Destructive: first previews all feature-branch changes, then requires Confirm discard.",
341
+ )
342
+ discard_button.pack(side=tk.LEFT, padx=(0, 8))
343
+ self.work_discard_confirm_button = ttk.Button(
344
+ row,
345
+ text="Confirm discard",
346
+ command=self.confirm_discard_changes,
347
+ state=tk.DISABLED,
348
+ )
349
+ attach_tooltip(
350
+ self.work_discard_confirm_button,
351
+ "Runs agentic-kit work discard-changes --execute only after a matching dry-run preview.",
352
+ )
353
+ self.work_discard_confirm_button.pack(side=tk.LEFT)
354
+
355
+ def _default_gc_cutoff(self) -> str:
356
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
357
+
358
+ def _gc_cutoff_value(self) -> str:
359
+ cutoff_var = getattr(self, "gc_cutoff_var", None)
360
+ value = cutoff_var.get().strip() if cutoff_var is not None else ""
361
+ if value:
362
+ return value
363
+ value = self._default_gc_cutoff()
364
+ if cutoff_var is not None:
365
+ cutoff_var.set(value)
366
+ return value
367
+
368
+ def _gc_command_args(self, *, cutoff: str, execute: bool) -> tuple[str, ...]:
369
+ args = (
370
+ "artifact-gc",
371
+ "--local-tmp-contents",
372
+ "--older-than",
373
+ cutoff,
374
+ )
375
+ if execute:
376
+ return (*args, "--execute", "--json")
377
+ return (*args, "--json")
378
+
379
+ def _write_gc_result(self, completed: Any) -> dict[str, object] | None:
380
+ output = completed.stdout or completed.stderr
381
+ self._log_gui_result("Clean local tmp", self._completed_result_status(completed), output)
382
+ if not completed.stdout:
383
+ return None
384
+ try:
385
+ payload = json.loads(completed.stdout)
386
+ except ValueError:
387
+ return None
388
+ return payload if isinstance(payload, dict) else None
389
+
390
+ def _disable_confirm_delete(self) -> None:
391
+ self.pending_gc_preview = None
392
+ button = getattr(self, "gc_confirm_delete_button", None)
393
+ if button is not None:
394
+ button.configure(state="disabled")
395
+
396
+ def _disable_confirm_release(self) -> None:
397
+ self.pending_release_preview = None
398
+ button = getattr(self, "release_confirm_button", None)
399
+ if button is not None:
400
+ button.configure(state="disabled")
401
+
402
+ def preview_log_cleanup(self) -> None:
403
+ self._log_gui_action("Clean up logs")
404
+ cutoff = self._gc_cutoff_value()
405
+ args = self._gc_command_args(cutoff=cutoff, execute=False)
406
+ self._log_gui_result("Clean up logs", "INFO", f"Running local tmp cleanup preview for files before {cutoff}...")
407
+ completed = self._agentic_command(*args)
408
+ payload = self._write_gc_result(completed)
409
+ candidate_count = int(payload.get("candidate_count", 0)) if payload else 0
410
+ if completed.returncode == 0 and candidate_count > 0:
411
+ self.pending_gc_preview = {
412
+ "cutoff": cutoff,
413
+ "args": args,
414
+ "candidate_count": candidate_count,
415
+ }
416
+ if self.gc_confirm_delete_button is not None:
417
+ self.gc_confirm_delete_button.configure(state="normal")
418
+ return
419
+ self._disable_confirm_delete()
420
+
421
+ def confirm_log_cleanup(self) -> None:
422
+ self._log_gui_action("Confirm delete")
423
+ if not self.pending_gc_preview:
424
+ self._log_gui_result(
425
+ "Confirm delete",
426
+ "INFO",
427
+ "Run Clean up logs first. Confirm is enabled only after a dry-run preview.",
428
+ )
429
+ return
430
+ cutoff = self._gc_cutoff_value()
431
+ if self.pending_gc_preview.get("cutoff") != cutoff:
432
+ self._log_gui_result(
433
+ "Confirm delete",
434
+ "BLOCKED",
435
+ "Log cleanup cutoff changed after the preview. Run Clean up logs again before confirming.",
436
+ )
437
+ self._disable_confirm_delete()
438
+ return
439
+ self._log_gui_result("Confirm delete", "INFO", f"Deleting local tmp cleanup candidates before {cutoff}...")
440
+ completed = self._agentic_command(*self._gc_command_args(cutoff=cutoff, execute=True))
441
+ self._write_gc_result(completed)
442
+ self._disable_confirm_delete()
443
+
444
+ def _current_release_version(self) -> str | None:
445
+ version_var = getattr(self, "release_version_var", None)
446
+ value = version_var.get() if version_var is not None else ""
447
+ return normalize_release_version(value)
448
+
449
+ def _release_state_signature(self, version: str) -> str:
450
+ work_signature = self._work_cycle_signature() if hasattr(self, "_work_cycle_signature") else ""
451
+ return release_preview_signature(version=version, state_signature=work_signature)
452
+
453
+ def start_create_release(self) -> None:
454
+ from tkinter import simpledialog
455
+
456
+ self._log_gui_action("Create release")
457
+ current = self.release_version_var.get().strip()
458
+ version = simpledialog.askstring(
459
+ "Create release",
460
+ "Enter target version (X.Y.Z):",
461
+ initialvalue=current,
462
+ parent=self.root,
463
+ )
464
+ if version is None:
465
+ self._log_gui_result("Create release", "INFO", "Create release cancelled.")
466
+ self._disable_confirm_release()
467
+ return
468
+ self.release_version_var.set(version.strip())
469
+ self.preview_create_release()
470
+
471
+ def _write_release_result(self, completed: Any, *, preview: bool) -> dict[str, object] | None:
472
+ output = completed.stdout or completed.stderr
473
+ if not completed.stdout:
474
+ self._log_gui_result("Create release", self._completed_result_status(completed), output)
475
+ self._disable_confirm_release()
476
+ return None
477
+ try:
478
+ payload = json.loads(completed.stdout)
479
+ except ValueError:
480
+ self._log_gui_result("Create release", self._completed_result_status(completed), output)
481
+ self._disable_confirm_release()
482
+ return None
483
+ if not isinstance(payload, dict):
484
+ self._log_gui_result("Create release", self._completed_result_status(completed), output)
485
+ self._disable_confirm_release()
486
+ return None
487
+ message = humanize_release_result(payload, preview=preview)
488
+ lines = ["", message.headline, message.detail]
489
+ if message.blockers:
490
+ lines.append("")
491
+ lines.extend(f"- {blocker}" for blocker in message.blockers)
492
+ lines.append("")
493
+ self._log_gui_result("Create release", str(payload.get("result_status", "")), "\n".join(lines))
494
+ if preview and message.allow_confirm:
495
+ version = str(payload.get("version", "")).strip()
496
+ self.pending_release_preview = {
497
+ "version": version,
498
+ "signature": self._release_state_signature(version),
499
+ "payload": payload,
500
+ }
501
+ if self.release_confirm_button is not None:
502
+ self.release_confirm_button.configure(state="normal")
503
+ else:
504
+ self._disable_confirm_release()
505
+ return payload
506
+
507
+ def preview_create_release(self) -> None:
508
+ version = self._current_release_version()
509
+ if version is None:
510
+ self._log_gui_result(
511
+ "Create release",
512
+ "INFO",
513
+ "Enter a release version in X.Y.Z format before creating a release.",
514
+ )
515
+ self._disable_confirm_release()
516
+ return
517
+ completed = self._agentic_command(*release_ready_args(version))
518
+ self._write_release_result(completed, preview=True)
519
+
520
+ def confirm_create_release(self) -> None:
521
+ self._log_gui_action("Confirm create release")
522
+ version = self._current_release_version()
523
+ if not self.pending_release_preview or version is None:
524
+ self._log_gui_result(
525
+ "Confirm create release",
526
+ "INFO",
527
+ "Run Create release first. Confirm is enabled only after a passing readiness preview.",
528
+ )
529
+ self._disable_confirm_release()
530
+ return
531
+ if self.pending_release_preview.get("signature") != self._release_state_signature(version):
532
+ self._log_gui_result(
533
+ "Confirm create release",
534
+ "BLOCKED",
535
+ "Version or state changed after preview, run Create release again before confirming.",
536
+ )
537
+ self._disable_confirm_release()
538
+ return
539
+ completed = self._agentic_command(*release_prepare_args(version))
540
+ self._write_release_result(completed, preview=False)
541
+
542
+
543
+ def selected_action_id(self) -> str | None:
544
+ return self.selected_action_id_value
545
+
546
+ def action_view_by_id(self, action_id: str) -> GuiActionView | None:
547
+ for action in self.actions:
548
+ if action.action_id == action_id:
549
+ return action
550
+ return None
551
+
552
+ def populate_action_tree(self) -> None:
553
+ for child in self.action_card_container.winfo_children():
554
+ child.destroy()
555
+ self.action_card_widgets = {}
556
+ action_groups = grouped_action_views(self.actions)
557
+ for group in action_groups:
558
+ self._create_action_group_heading(group)
559
+ for action in group.actions:
560
+ self._create_action_card(action)
561
+ visible_ids = {action.action_id for group in action_groups for action in group.actions}
562
+ if self.selected_action_id_value not in visible_ids:
563
+ first_group = action_groups[0] if action_groups else None
564
+ self.selected_action_id_value = first_group.actions[0].action_id if first_group and first_group.actions else None
565
+ self._refresh_action_card_selection()
566
+
567
+ def _create_action_group_heading(self, group: GuiActionGroupView) -> None:
568
+ import tkinter as tk
569
+
570
+ frame = tk.Frame(self.action_card_container, bg=THEME.color_panel_bg)
571
+ frame.pack(fill=tk.X, pady=(7 if self.action_card_widgets else 0, 4))
572
+ group_label = tk.Label(
573
+ frame,
574
+ text=group.label.upper(),
575
+ bg=THEME.color_panel_bg,
576
+ fg=THEME.color_muted_text,
577
+ font=THEME.section_font,
578
+ anchor=tk.W,
579
+ )
580
+ group_label.pack(side=tk.LEFT)
581
+ group_description = tk.Label(
582
+ frame,
583
+ text=group.description,
584
+ bg=THEME.color_panel_bg,
585
+ fg=THEME.color_muted_text,
586
+ font=THEME.small_font,
587
+ anchor=tk.E,
588
+ )
589
+ group_description.pack(side=tk.RIGHT)
590
+ for widget in (frame, group_label, group_description):
591
+ self._bind_action_card_scroll_events(widget)
592
+
593
+ def _create_action_card(self, action: GuiActionView) -> None:
594
+ import tkinter as tk
595
+
596
+ bg = action_tree_tag_colors().get(action.safety, "#f4f4f4")
597
+ card = tk.Frame(
598
+ self.action_card_container,
599
+ bg=bg,
600
+ highlightbackground=bg,
601
+ highlightthickness=2,
602
+ padx=7,
603
+ pady=5,
604
+ )
605
+ card.pack(fill=tk.X, pady=(0, 4))
606
+ dot = tk.Canvas(card, width=12, height=12, bg=bg, highlightthickness=0)
607
+ dot.create_oval(2, 2, 10, 10, fill=traffic_light_fill("green" if action.safety == READ_ONLY else "yellow"), outline="")
608
+ dot.pack(side=tk.LEFT, padx=(0, 8))
609
+ label = tk.Label(
610
+ card,
611
+ text=action.label,
612
+ bg=bg,
613
+ fg="#0b2f27" if action.safety == READ_ONLY else "#3d2200",
614
+ font=THEME.action_font,
615
+ anchor=tk.W,
616
+ )
617
+ label.pack(side=tk.LEFT, fill=tk.X, expand=True)
618
+ safety = tk.Label(
619
+ card,
620
+ text=action.safety.replace("_", "-"),
621
+ bg=bg,
622
+ fg="#006b50" if action.safety == READ_ONLY else "#6b3d00",
623
+ font=THEME.small_font,
624
+ )
625
+ safety.pack(side=tk.RIGHT)
626
+ tooltip = f"{action.short_description}. {explain_safety(action.safety)}"
627
+ for widget in (card, dot, label, safety):
628
+ widget.bind("<Button-1>", lambda _event, action_id=action.action_id: self._select_action(action_id))
629
+ self._bind_action_card_scroll_events(widget)
630
+ attach_tooltip(card, tooltip)
631
+ self.action_card_widgets[action.action_id] = card
632
+
633
+ def _select_action(self, action_id: str) -> None:
634
+ self.selected_action_id_value = action_id
635
+ self._refresh_action_card_selection()
636
+
637
+ def _refresh_action_card_selection(self) -> None:
638
+ for action_id, card in self.action_card_widgets.items():
639
+ selected = action_id == self.selected_action_id_value
640
+ bg = card.cget("bg")
641
+ card.configure(
642
+ highlightbackground="#1f5b9d" if selected else bg,
643
+ highlightthickness=2 if selected else 1,
644
+ )
645
+
646
+
647
+ def load_recovery_action(self) -> None:
648
+ if self.recovery_action_id is None:
649
+ return
650
+ self._select_action(self.recovery_action_id)
651
+ action = self.action_view_by_id(self.recovery_action_id)
652
+ label = action.label if action is not None else self.recovery_action_id
653
+ self._log_gui_result("Recovery action", "INFO", f"Recovery action loaded: {label}. Inspect or run read-only manually.")
654
+
655
+ def inspect_selected(self) -> None:
656
+ self._log_gui_action("Inspect")
657
+ action_id = self.selected_action_id()
658
+ if action_id is None:
659
+ self._log_gui_result("Inspect", "INFO", "No cockpit action selected.")
660
+ return
661
+ action = self.action_view_by_id(action_id)
662
+ if action is None:
663
+ self._log_gui_result("Inspect", "ERROR", f"Unknown cockpit action: {action_id}")
664
+ return
665
+ [
666
+ "",
667
+ f"action_id={action.action_id}",
668
+ f"label={action.label}",
669
+ f"category={action.category}",
670
+ f"safety={action.safety}",
671
+ f"can_run_by_default={str(action.can_run_by_default).lower()}",
672
+ f"command={' '.join(action.command)}",
673
+ f"description={action.description}",
674
+ "",
675
+ ]
676
+ self._log_gui_result("Inspect", "INFO", "Inspecting selected action...\n\n" + format_action_details(action))
677
+
678
+ def run_basic_action(self, command_id: str) -> None:
679
+ self._log_gui_action(f"Run {command_id}")
680
+ self._log_gui_result("Run basic action", "INFO", f"Running basic action: {command_id}...")
681
+ self._log_gui_result("Run basic action", None, run_basic_cockpit_button(command_id, project_root=self.project_root))
682
+
683
+ def run_selected_read_only(self) -> None:
684
+ self._log_gui_action("Run read-only")
685
+ action_id = self.selected_action_id()
686
+ if action_id is None:
687
+ self._log_gui_result("Run read-only", "INFO", "No cockpit action selected.")
688
+ return
689
+ action = self.action_view_by_id(action_id)
690
+ if action is not None and action.safety != READ_ONLY:
691
+ self._log_gui_result("Run read-only", "BLOCKED", format_action_details(action))
692
+ return
693
+ self._log_gui_result("Run read-only", "INFO", f"Running read-only action: {action_id}...")
694
+ result = run_cockpit_action(action_id, self.project_root, allow_bounded=False)
695
+ self._log_gui_result("Run read-only", "PASS" if result.allowed and result.executed else "BLOCKED", format_action_result(result))