SidecarCommando / app.py
smlflg's picture
Initial public upload from Projekte/SidecarCommando
a53fc63 verified
Raw History Blame Contribute Delete
26.9 kB
#!/usr/bin/env python3
"""Native Linux AI Cockpit: JSON state renderer + event writer."""
from __future__ import annotations
import sys
from typing import Any
import gi
from state_store import StateStore
gi.require_version("Adw", "1")
gi.require_version("Gtk", "4.0")
from gi.repository import Adw, Gio, Gtk, Pango # noqa: E402
APP_ID = "dev.sidecarcommando.AICockpit"
SIDECAR_ICONS = {
"focus": "system-run-symbolic",
"scope": "media-playlist-shuffle-symbolic",
"finish": "emblem-ok-symbolic",
"reality": "edit-find-symbolic",
"regulation": "preferences-system-symbolic",
}
CSS = """
window {
background: #030b13;
color: #f3f7ff;
}
.app-shell {
background: radial-gradient(circle at 72% 0%, rgba(74, 42, 120, 0.16), transparent 34%),
linear-gradient(135deg, #030912, #081522 46%, #06101a);
}
.sidebar {
padding: 22px 14px 16px 14px;
background: rgba(4, 13, 22, 0.94);
border-right: 1px solid rgba(144, 166, 196, 0.18);
}
.main {
padding: 18px 22px 18px 22px;
}
.brand-title {
font-size: 23px;
font-weight: 800;
}
.brand-subtitle,
.muted {
color: #a7b2c3;
}
.nav-item {
padding: 12px 14px;
border-radius: 8px;
color: #aab6c8;
}
.nav-item-active {
color: #bd8cff;
background: rgba(108, 72, 170, 0.17);
border-left: 2px solid #8e5aff;
}
.nav-icon,
.work-icon {
color: #9aa9bd;
}
.sidebar-focus {
padding: 14px;
border-radius: 8px;
background: rgba(11, 24, 39, 0.82);
border: 1px solid rgba(144, 166, 196, 0.16);
}
.sidebar-focus-dot {
color: #59e17b;
}
.sidebar-focus-name {
font-weight: 800;
color: #f3f7ff;
}
.focus-panel,
.lead-card,
.sidecar-card,
.next-step,
.work-panel,
.work-section,
.code-panel {
border: 1px solid rgba(144, 166, 196, 0.24);
border-radius: 8px;
background: rgba(13, 25, 40, 0.92);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.24);
}
.focus-panel {
padding: 18px 20px;
}
.focus-target {
border-left: 1px solid rgba(144, 166, 196, 0.18);
padding-left: 22px;
}
.work-panel {
padding: 14px;
}
.eyebrow {
color: #9b6dff;
font-weight: 800;
font-size: 12px;
letter-spacing: 0.06em;
}
.focus-subtitle {
color: #d4dbea;
font-size: 17px;
}
.focus-goal {
color: #59e17b;
font-size: 13px;
font-weight: 800;
}
.focus-action-button,
.owner-button {
padding: 8px 12px;
border-radius: 7px;
background: rgba(36, 50, 68, 0.74);
border: 1px solid rgba(144, 166, 196, 0.22);
}
.section-title {
font-size: 14px;
font-weight: 800;
color: #9b6dff;
}
.field-label {
color: #9aa9bd;
font-size: 12px;
font-weight: 700;
}
entry,
textview {
color: #edf4ff;
background: #0b1725;
border: 1px solid rgba(144, 166, 196, 0.22);
border-radius: 8px;
}
.focus-entry {
color: #f3f7ff;
background: transparent;
border: 0;
box-shadow: none;
padding: 0;
font-size: 30px;
font-weight: 800;
}
.code-panel {
background: rgba(7, 17, 29, 0.96);
}
.code-header {
padding: 10px 12px;
border-bottom: 1px solid rgba(144, 166, 196, 0.18);
}
.code-text {
font-family: monospace;
color: #cdd8e7;
padding: 12px;
font-size: 14px;
}
.work-section {
padding: 12px;
}
.todo-row {
color: #d7e0ee;
}
.todo-done {
color: #59e17b;
}
.action-grid {
margin-top: 2px;
}
.work-footer {
color: #8f9cad;
font-size: 12px;
}
.lead-card {
padding: 22px 20px;
border-color: #9b6dff;
background: linear-gradient(135deg, rgba(44, 22, 79, 0.84), rgba(11, 21, 35, 0.96));
}
.lead-title {
font-size: 26px;
font-weight: 800;
}
.lead-copy {
font-size: 16px;
line-height: 1.55;
}
.sidecar-card {
padding: 18px;
min-height: 194px;
}
.sidecar-title {
font-size: 19px;
font-weight: 800;
}
.sidecar-copy {
color: #d8e1ee;
font-size: 14px;
line-height: 1.45;
}
.badge {
border-radius: 7px;
padding: 5px 10px;
font-size: 12px;
font-weight: 800;
}
.badge-hard {
color: #bd8cff;
border: 1px solid #8e5aff;
background: rgba(98, 57, 160, 0.18);
}
.badge-medium {
color: #59e17b;
border: 1px solid #2bb457;
background: rgba(43, 180, 87, 0.14);
}
.badge-low {
color: #4da9ff;
border: 1px solid #2580d7;
background: rgba(37, 128, 215, 0.13);
}
.sidecar-icon {
min-width: 48px;
min-height: 48px;
border-radius: 999px;
border: 1px solid currentColor;
background: rgba(255, 255, 255, 0.04);
}
.lead-card .sidecar-icon {
min-width: 58px;
min-height: 58px;
}
.sidecar-focus {
color: #bd8cff;
border-color: #9b6dff;
}
.sidecar-scope {
color: #59e17b;
border-color: #2bb457;
background: linear-gradient(135deg, rgba(20, 71, 50, 0.48), rgba(11, 24, 32, 0.95));
}
.sidecar-finish {
color: #f4d044;
border-color: #d5b900;
background: linear-gradient(135deg, rgba(82, 68, 13, 0.38), rgba(16, 25, 27, 0.95));
}
.sidecar-reality {
color: #4da9ff;
border-color: #2580d7;
background: linear-gradient(135deg, rgba(18, 57, 92, 0.42), rgba(10, 22, 34, 0.95));
}
.sidecar-regulation {
color: #ff8a25;
border-color: #e56f00;
background: linear-gradient(135deg, rgba(87, 42, 8, 0.36), rgba(19, 22, 24, 0.95));
}
.next-step {
padding: 16px 20px;
border-color: #2387d6;
background: rgba(9, 35, 58, 0.92);
}
.next-label {
color: #4da9ff;
font-size: 13px;
font-weight: 800;
}
.next-copy {
font-size: 18px;
font-weight: 650;
}
.hint {
color: #8793a4;
font-size: 12px;
}
"""
def text(value: Any, fallback: str = "") -> str:
return value if isinstance(value, str) else fallback
def set_text_view(view: Gtk.TextView, value: str) -> None:
view.get_buffer().set_text(value)
def focus_name(state: dict[str, Any]) -> str:
return text(state.get("active_focus", {}).get("name"), "miniZentrale")
class SidecarCard(Gtk.Box):
def __init__(self, sidecar: dict[str, Any], lead: bool = False) -> None:
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=12)
self.sidecar = sidecar
self.lead = lead
sidecar_id = text(sidecar.get("id"), "unknown")
tone = text(sidecar.get("tone"), "LOW")
self.add_css_class("lead-card" if lead else "sidecar-card")
self.add_css_class(f"sidecar-{sidecar_id}")
if not lead:
self.set_hexpand(True)
self.set_vexpand(True)
header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
header.set_valign(Gtk.Align.START)
self.append(header)
icon = Gtk.Box()
icon.set_size_request(58 if lead else 48, 58 if lead else 48)
icon.set_halign(Gtk.Align.START)
icon.set_valign(Gtk.Align.START)
icon.add_css_class("sidecar-icon")
icon_image = Gtk.Image.new_from_icon_name(SIDECAR_ICONS.get(sidecar_id, "dialog-information-symbolic"))
icon_image.set_pixel_size(30 if lead else 24)
icon_image.set_halign(Gtk.Align.CENTER)
icon_image.set_valign(Gtk.Align.CENTER)
icon.append(icon_image)
header.append(icon)
title_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
title_box.set_hexpand(True)
header.append(title_box)
if lead:
eyebrow = Gtk.Label(label="LEAD SIDECAR")
eyebrow.set_xalign(0)
eyebrow.add_css_class("eyebrow")
title_box.append(eyebrow)
title = Gtk.Label(label=text(sidecar.get("title"), "Sidecar"))
title.set_xalign(0)
title.set_wrap(True)
title.add_css_class("lead-title" if lead else "sidecar-title")
title_box.append(title)
badge = Gtk.Label(label=tone)
badge.add_css_class("badge")
badge.add_css_class(f"badge-{tone.lower()}")
header.append(badge)
self.copy = Gtk.Label()
self.copy.set_xalign(0)
self.copy.set_valign(Gtk.Align.START)
self.copy.set_wrap(True)
self.copy.set_wrap_mode(Pango.WrapMode.WORD_CHAR)
self.copy.add_css_class("lead-copy" if lead else "sidecar-copy")
self.append(self.copy)
def update(self, sentences: list[str]) -> None:
self.copy.set_label("\n".join(sentences[:3]))
class CockpitWindow(Adw.ApplicationWindow):
def __init__(self, app: Adw.Application) -> None:
super().__init__(application=app, title="AI Cockpit")
self.set_default_size(1120, 760)
self.store = StateStore()
self.state = self.store.load_state()
self.focus_entry = Gtk.Entry()
self.focus_entry.set_has_frame(False)
self.focus_entry.set_hexpand(True)
self.focus_entry.set_editable(False)
self.focus_entry.add_css_class("focus-entry")
self.focus_subtitle = Gtk.Label()
self.focus_goal_copy = Gtk.Label()
self.sidebar_focus_name = Gtk.Label()
self.active_time_label = Gtk.Label()
self.file_label = Gtk.Label()
self.work_status_label = Gtk.Label()
self.code_label = Gtk.Label()
self.notes_view = Gtk.TextView()
self.threads_view = Gtk.TextView()
self.todo_list = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=9)
self.saved_label = Gtk.Label()
self.language_label = Gtk.Label()
self.lead_slot = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.quiet_grid = Gtk.Grid(column_spacing=14, row_spacing=14)
self.next_copy = Gtk.Label()
self._build()
self.render_state()
def _build(self) -> None:
shell = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
shell.add_css_class("app-shell")
self.set_content(shell)
shell.append(self._build_sidebar())
scroller = Gtk.ScrolledWindow()
scroller.set_hexpand(True)
shell.append(scroller)
root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16)
root.add_css_class("main")
scroller.set_child(root)
root.append(self._build_focus_panel())
body = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=16)
body.set_hexpand(True)
body.set_vexpand(True)
root.append(body)
work_panel = self._build_work_panel()
work_panel.set_size_request(430, -1)
body.append(work_panel)
output = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16)
output.set_hexpand(True)
output.set_vexpand(True)
body.append(output)
self.lead_slot.set_hexpand(True)
output.append(self.lead_slot)
self.quiet_grid.set_column_homogeneous(True)
self.quiet_grid.set_row_homogeneous(True)
self.quiet_grid.set_hexpand(True)
self.quiet_grid.set_vexpand(True)
output.append(self.quiet_grid)
root.append(self._build_next_step())
hint = Gtk.Label(label="Hinweis: Nur ein Sidecar ist Lead. Die anderen sind leise Karten.")
hint.set_halign(Gtk.Align.CENTER)
hint.add_css_class("hint")
root.append(hint)
def _build_sidebar(self) -> Gtk.Box:
sidebar = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=18)
sidebar.add_css_class("sidebar")
sidebar.set_size_request(214, -1)
brand = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
sidebar.append(brand)
brand_icon = Gtk.Image.new_from_icon_name("applications-science-symbolic")
brand_icon.set_pixel_size(36)
brand_icon.add_css_class("sidecar-focus")
brand.append(brand_icon)
brand_text = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
brand.append(brand_text)
title = Gtk.Label(label="AI Cockpit")
title.set_xalign(0)
title.add_css_class("brand-title")
brand_text.append(title)
subtitle = Gtk.Label(label="Dein agentischer Leitstand")
subtitle.set_xalign(0)
subtitle.add_css_class("brand-subtitle")
brand_text.append(subtitle)
nav = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
sidebar.append(nav)
for index, (icon_name, label) in enumerate(
(
("view-dashboard-symbolic", "Cockpit"),
("find-location-symbolic", "Fokus"),
("package-x-generic-symbolic", "Komponenten"),
("folder-symbolic", "Parks"),
("text-x-generic-symbolic", "Beweise"),
("utilities-system-monitor-symbolic", "AI Log"),
("preferences-system-symbolic", "Einstellungen"),
)
):
nav.append(self._nav_item(icon_name, label, active=index == 0))
spacer = Gtk.Box()
spacer.set_vexpand(True)
sidebar.append(spacer)
focus_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=9)
focus_box.add_css_class("sidebar-focus")
sidebar.append(focus_box)
focus_label = Gtk.Label(label="Aktiver Fokus")
focus_label.set_xalign(0)
focus_label.add_css_class("muted")
focus_box.append(focus_label)
focus_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=7)
focus_box.append(focus_row)
dot = Gtk.Label(label="●")
dot.add_css_class("sidebar-focus-dot")
focus_row.append(dot)
self.sidebar_focus_name.set_xalign(0)
self.sidebar_focus_name.add_css_class("sidebar-focus-name")
focus_row.append(self.sidebar_focus_name)
self.active_time_label.set_xalign(0)
self.active_time_label.add_css_class("muted")
focus_box.append(self.active_time_label)
return sidebar
def _nav_item(self, icon_name: str, label: str, active: bool = False) -> Gtk.Box:
item = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
item.add_css_class("nav-item")
if active:
item.add_css_class("nav-item-active")
icon = Gtk.Image.new_from_icon_name(icon_name)
icon.set_pixel_size(24)
icon.add_css_class("nav-icon")
item.append(icon)
item_label = Gtk.Label(label=label)
item_label.set_xalign(0)
item.append(item_label)
return item
def _build_focus_panel(self) -> Gtk.Box:
focus_panel = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=22)
focus_panel.add_css_class("focus-panel")
focus_icon = Gtk.Box()
focus_icon.set_size_request(70, 70)
focus_icon.set_halign(Gtk.Align.START)
focus_icon.set_valign(Gtk.Align.CENTER)
focus_icon.add_css_class("sidecar-icon")
focus_icon.add_css_class("sidecar-scope")
focus_image = Gtk.Image.new_from_icon_name("find-location-symbolic")
focus_image.set_pixel_size(38)
focus_image.set_halign(Gtk.Align.CENTER)
focus_image.set_valign(Gtk.Align.CENTER)
focus_icon.append(focus_image)
focus_panel.append(focus_icon)
focus_text = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
focus_text.set_hexpand(True)
focus_panel.append(focus_text)
eyebrow = Gtk.Label(label="AKTIVER FOKUS")
eyebrow.set_xalign(0)
eyebrow.add_css_class("eyebrow")
focus_text.append(eyebrow)
focus_text.append(self.focus_entry)
self.focus_subtitle.set_xalign(0)
self.focus_subtitle.add_css_class("focus-subtitle")
focus_text.append(self.focus_subtitle)
target = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
target.set_size_request(360, -1)
target.set_valign(Gtk.Align.CENTER)
target.add_css_class("focus-target")
focus_panel.append(target)
target_label = Gtk.Label(label="FOKUS-ZIEL")
target_label.set_xalign(0)
target_label.add_css_class("focus-goal")
target.append(target_label)
self.focus_goal_copy.set_xalign(0)
self.focus_goal_copy.set_wrap(True)
target.append(self.focus_goal_copy)
return focus_panel
def _build_work_panel(self) -> Gtk.Box:
panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
panel.add_css_class("work-panel")
title = Gtk.Label(label="DEINE AKTUELLE ARBEIT")
title.set_xalign(0)
title.add_css_class("section-title")
panel.append(title)
panel.append(self._build_code_panel())
panel.append(self._text_field("NOTIZEN", self.notes_view, height=88))
panel.append(self._text_field("OFFENE FÄDEN", self.threads_view, height=70))
panel.append(self._build_todo_panel())
panel.append(self._build_action_bar())
footer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
footer.add_css_class("work-footer")
panel.append(footer)
self.saved_label.set_xalign(0)
self.saved_label.set_hexpand(True)
footer.append(self.saved_label)
footer.append(self.language_label)
return panel
def _build_code_panel(self) -> Gtk.Box:
panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
panel.add_css_class("code-panel")
header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
header.add_css_class("code-header")
panel.append(header)
icon = Gtk.Image.new_from_icon_name("text-x-python-symbolic")
icon.set_pixel_size(20)
icon.add_css_class("work-icon")
header.append(icon)
self.file_label.set_xalign(0)
self.file_label.set_hexpand(True)
header.append(self.file_label)
self.work_status_label.add_css_class("muted")
header.append(self.work_status_label)
self.code_label.set_xalign(0)
self.code_label.set_wrap(False)
self.code_label.add_css_class("code-text")
panel.append(self.code_label)
return panel
def _build_todo_panel(self) -> Gtk.Box:
panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=9)
panel.add_css_class("work-section")
title = Gtk.Label(label="TODO")
title.set_xalign(0)
title.add_css_class("field-label")
panel.append(title)
panel.append(self.todo_list)
return panel
def _build_action_bar(self) -> Gtk.Box:
panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=9)
panel.add_css_class("work-section")
title = Gtk.Label(label="OWNER-AKTIONEN")
title.set_xalign(0)
title.add_css_class("field-label")
panel.append(title)
grid = Gtk.Grid(column_spacing=8, row_spacing=8)
grid.add_css_class("action-grid")
panel.append(grid)
actions = (
("Faden parken", "thread_parked"),
("Abschlussaktion setzen", "finish_action_set"),
("Todo erledigen", "todo_completed"),
("Beweis hinzufügen", "evidence_added"),
)
for index, (label, event_type) in enumerate(actions):
button = Gtk.Button(label=label)
button.add_css_class("owner-button")
button.connect("clicked", lambda _button, kind=event_type: self._handle_owner_action(kind))
grid.attach(button, index % 2, index // 2, 1, 1)
return panel
def _todo_row(self, item: dict[str, Any]) -> Gtk.Box:
is_done = item.get("status") == "done"
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=9)
row.add_css_class("todo-row")
check = Gtk.CheckButton()
check.set_active(is_done)
check.set_sensitive(False)
row.append(check)
label = Gtk.Label(label=text(item.get("text"), "Todo"))
label.set_xalign(0)
label.set_hexpand(True)
row.append(label)
state_label = Gtk.Label(label="Erledigt" if is_done else "Offen")
state_label.add_css_class("todo-done" if is_done else "muted")
row.append(state_label)
return row
def _build_next_step(self) -> Gtk.Box:
next_step = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
next_step.add_css_class("next-step")
next_label = Gtk.Label(label="NÄCHSTER ERLAUBTER SCHRITT")
next_label.set_xalign(0)
next_label.add_css_class("next-label")
next_step.append(next_label)
self.next_copy.set_xalign(0)
self.next_copy.set_wrap(True)
self.next_copy.set_wrap_mode(Pango.WrapMode.WORD_CHAR)
self.next_copy.add_css_class("next-copy")
next_step.append(self.next_copy)
return next_step
def _field(self, label: str, widget: Gtk.Widget) -> Gtk.Box:
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
field_label = Gtk.Label(label=label)
field_label.set_xalign(0)
field_label.add_css_class("field-label")
box.append(field_label)
box.append(widget)
return box
def _text_field(self, label: str, view: Gtk.TextView, height: int) -> Gtk.Box:
view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
view.set_monospace(False)
view.set_editable(False)
view.set_cursor_visible(False)
frame = Gtk.ScrolledWindow()
frame.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
frame.set_min_content_height(height)
frame.set_child(view)
return self._field(label, frame)
def render_state(self) -> None:
active_focus = self.state.get("active_focus", {})
current_work = self.state.get("current_work", {})
self.focus_entry.set_text(focus_name(self.state))
self.sidebar_focus_name.set_label(focus_name(self.state))
self.active_time_label.set_label(text(active_focus.get("active_for")))
self.focus_subtitle.set_label(text(active_focus.get("subtitle")))
self.focus_goal_copy.set_label(text(active_focus.get("goal")))
self.file_label.set_label(text(current_work.get("file_path")))
self.work_status_label.set_label(text(current_work.get("status")))
self.code_label.set_label("\n".join(str(line) for line in current_work.get("code_lines", [])))
self.saved_label.set_label(text(current_work.get("saved")))
self.language_label.set_label(text(current_work.get("language")))
set_text_view(self.notes_view, "\n".join(str(note) for note in current_work.get("notes", [])))
set_text_view(self.threads_view, self._threads_text())
self._render_todos()
self._render_cards()
self.next_copy.set_label(text(self.state.get("next_allowed_step")))
def _threads_text(self) -> str:
rows = []
for thread in self.state.get("open_threads", []):
rows.append(
f"{text(thread.get('title'), 'Thread')} · "
f"{text(thread.get('kind'), 'THREAD')} · "
f"{text(thread.get('status'), 'open')}"
)
return "\n".join(rows)
def _render_todos(self) -> None:
while child := self.todo_list.get_first_child():
self.todo_list.remove(child)
for item in self.state.get("todos", []):
self.todo_list.append(self._todo_row(item))
def _render_cards(self) -> None:
while child := self.lead_slot.get_first_child():
self.lead_slot.remove(child)
while child := self.quiet_grid.get_first_child():
self.quiet_grid.remove(child)
sidecars = list(self.state.get("sidecars", []))
lead = next((sidecar for sidecar in sidecars if sidecar.get("is_lead") is True), sidecars[0])
lead_card = SidecarCard(lead, lead=True)
lead_card.update([str(sentence) for sentence in lead.get("sentences", [])])
self.lead_slot.append(lead_card)
quiet_sidecars = [sidecar for sidecar in sidecars if sidecar is not lead]
for index, sidecar in enumerate(quiet_sidecars):
card = SidecarCard(sidecar)
card.update([str(sentence) for sentence in sidecar.get("sentences", [])])
self.quiet_grid.attach(card, index % 2, index // 2, 1, 1)
def _handle_owner_action(self, event_type: str) -> None:
payload: dict[str, Any] = {"active_focus": focus_name(self.state)}
if event_type == "thread_parked":
payload.update(self._park_thread())
elif event_type == "finish_action_set":
payload.update(self._set_finish_action())
elif event_type == "todo_completed":
payload.update(self._complete_todo())
elif event_type == "evidence_added":
payload.update(self._add_evidence_note())
self.store.append_event({"type": event_type, "payload": payload})
self.store.save_state(self.state)
self.render_state()
def _park_thread(self) -> dict[str, Any]:
for thread in self.state.get("open_threads", []):
if thread.get("kind") != "ACTIVE" and thread.get("status") != "parked":
thread["status"] = "parked"
return {"thread_id": thread.get("id"), "result": "parked"}
return {"result": "no_open_thread"}
def _set_finish_action(self) -> dict[str, Any]:
step = "Schließe die kleinste sichtbare Einheit ab: Unit-Test für evaluate_focus."
self.state["next_allowed_step"] = step
return {"next_allowed_step": step}
def _complete_todo(self) -> dict[str, Any]:
for todo in self.state.get("todos", []):
if todo.get("status") != "done":
todo["status"] = "done"
return {"todo_id": todo.get("id"), "result": "done"}
return {"result": "no_open_todo"}
def _add_evidence_note(self) -> dict[str, Any]:
note = "Beweis hinzugefügt: Owner-Aktion protokolliert."
current_work = self.state.setdefault("current_work", {})
notes = current_work.setdefault("notes", [])
if isinstance(notes, list):
notes.append(note)
return {"evidence": note}
class CockpitApp(Adw.Application):
def __init__(self) -> None:
super().__init__(application_id=APP_ID, flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
self._css_provider = Gtk.CssProvider()
self._css_provider.load_from_data(CSS.encode("utf-8"))
self._css_installed = False
def do_startup(self) -> None:
Adw.Application.do_startup(self)
Adw.StyleManager.get_default().set_color_scheme(Adw.ColorScheme.FORCE_DARK)
def do_activate(self) -> None:
window = self.props.active_window
if window is None:
window = CockpitWindow(self)
if not self._css_installed:
Gtk.StyleContext.add_provider_for_display(
window.get_display(),
self._css_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
self._css_installed = True
window.present()
def main() -> int:
app = CockpitApp()
return app.run(sys.argv)
if __name__ == "__main__":
raise SystemExit(main())