Files
Panama/docs/superpowers/plans/2026-08-17-panama-control-center.md
T

29 KiB
Raw Blame History

Panama Control Center Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Expand Panama Quick Settings into the approved Control Center with secure Home Assistant favourites and capability-aware iPhone continuity.

Architecture: Two executable Python helpers own the external boundaries: KDE Connect CLI/D-Bus and the Home Assistant REST API/credential lookup. Quickshell singletons normalize those JSON contracts for focused Home and Phone components inside the existing Quick Settings window; Ongoing and Signal Glass consume transfer lifecycle without duplicating state.

Tech Stack: Python 3.14 standard library, Secret Service CLI, KDE Connect 25.12 CLI/D-Bus, Home Assistant REST API, Quickshell 0.3/QML, Bash contract tests, Python unittest, jq, Hyprland 0.56

Spec: docs/superpowers/specs/2026-08-17-panama-control-center-design.md

Global Constraints

  • Keep Wi-Fi, Bluetooth, Focus, Caffeine, Night Light, audio, account, settings, power, Escape, and outside-click behavior intact.
  • The Control Center width is 430 logical pixels and its top margin is exactly Theme.barHeight + 2.
  • Only one of Wi-Fi, Bluetooth, sink, source, Home, and Phone may be expanded at once.
  • Never print Home Assistant tokens, Authorization headers, clipboard text, certificate fingerprints, IP addresses, response bodies, or full local file paths.
  • Home Assistant tokens remain in the gitignored environment file or Secret Service; never write one into tracked content.
  • Home Assistant toggle commands accept only entity IDs from the configured favourites snapshot.
  • KDE Connect actions derive from advertised plugins; no fake battery or remote-input control is allowed.
  • Every Home toggle, file chooser, file send, clipboard send, and Ring action requires a user click.
  • Home toggle success, phone Ring, and clipboard success remain globally quiet.
  • A running file send appears in Ongoing without a fabricated percentage.
  • Automated tests do not toggle a real Home entity, ring the phone, send clipboard contents, or transfer a real file.
  • No pulse, shimmer, animated gradient, idle animation, or high-frequency polling.

File map

KDE Connect boundary

  • config/dot/quickshell/scripts/panama-kdeconnect — validates device/file inputs, normalizes device/plugin status, and invokes allow-listed actions.
  • tests/quickshell/kdeconnect_bridge_test.py — pure parser, capability, validation, and command tests with fake runners.
  • tests/quickshell/kdeconnect-helper-contract.sh — live read-only status contract with names and IDs redacted.
  • config/dot/quickshell/services/KdeConnect.qml — singleton device, action, transfer, fixture, and recent-exchange state.

Home Assistant boundary

  • config/dot/quickshell/scripts/panama-home-assistant — configuration resolution, normalized REST reads, allow-listed toggles, and URL handoff.
  • tests/quickshell/home_assistant_bridge_test.py — local fake API and credential/configuration precedence tests.
  • tests/quickshell/home-assistant-helper-contract.sh — live read-only probe/snapshot contract with entity content redacted.
  • config/dot/quickshell/services/HomeAssistant.qml — singleton entities, stale state, per-entity action state, fixture, and inline errors.

Control Center presentation and integration

  • config/dot/quickshell/config/Theme.qml — dedicated controlCenterWidth and controlCenterTopGap geometry tokens.
  • config/dot/quickshell/modules/quicksettings/QuickSettings.qml — tighter top attachment and service preparation.
  • config/dot/quickshell/modules/quicksettings/QuickSettingsPanel.qml — expanded composition and exclusive section routing.
  • config/dot/quickshell/modules/quicksettings/ControlSectionHeader.qml — quiet section label and optional trailing action.
  • config/dot/quickshell/modules/quicksettings/HomeControls.qml — four favourites, expanded configured list, loading/stale/failure states.
  • config/dot/quickshell/modules/quicksettings/HomeTile.qml — one domain-aware favourite tile.
  • config/dot/quickshell/modules/quicksettings/PhoneControls.qml — approved compact Continuity Card.
  • config/dot/quickshell/modules/quicksettings/RecentExchange.qml — one session-local completed transfer row.
  • config/dot/quickshell/modules/bar/StatusCluster.qml — reachable-phone glyph inside the existing click target.
  • config/dot/quickshell/services/DeviceEvents.qml — consume shared KDE Connect state instead of a second polling process.
  • config/dot/quickshell/services/Ongoing.qml — append and control one running phone transfer.
  • config/dot/quickshell/services/StatusEvents.qml — open a completed received/sent path through the existing allow-listed action.
  • config/dot/quickshell/shell.qml — diagnostics/fixture IPC for the two services.
  • tests/quickshell/control-center-services-contract.sh — deterministic service, activity, and event contract.
  • tests/quickshell/control-center-contract.sh — panel geometry, routing, components, and live layer contract.

Documentation

  • config/dot/hypr/DESKTOP-PARITY.md — mark both remaining integrations live.
  • config/dot/hypr/README.md — document Control Center, credential resolution, and iOS capability limits.

Task 1: KDE Connect helper contract

Files:

  • Create: config/dot/quickshell/scripts/panama-kdeconnect
  • Create: tests/quickshell/kdeconnect_bridge_test.py
  • Create: tests/quickshell/kdeconnect-helper-contract.sh

Interfaces:

  • Produces: panama-kdeconnect status JSON with available and devices.

  • Produces device fields: id, name, type, paired, reachable, and actions.

  • Produces: send-file DEVICE FILE, send-clipboard DEVICE, and ring DEVICE with one normalized JSON result.

  • Accepts an injectable runner: Callable[[list[str]], CompletedProcess[str]] in pure functions.

  • Step 1: Write failing parser and action tests

Create tests/quickshell/kdeconnect_bridge_test.py with import-by-path and these cases:

class KdeConnectBridgeTest(unittest.TestCase):
    def test_status_omits_network_and_exposes_supported_ios_actions(self):
        listing = "- Gib's iPhone: 90B54E84548E4F109268EC2D0E7D6930 on 192.168.1.7 via LAN (paired and reachable)\n1 device found\n"
        plugins = ["kdeconnect_clipboard", "kdeconnect_findmyphone", "kdeconnect_ping", "kdeconnect_share"]
        device = bridge.normalize_device_line(listing.splitlines()[0], plugins)
        self.assertEqual(device["name"], "Gib's iPhone")
        self.assertEqual(device["actions"], ["clipboard", "ping", "ring", "share"])
        self.assertNotIn("192.168.1.7", json.dumps(device))

    def test_invalid_device_id_is_rejected(self):
        with self.assertRaisesRegex(ValueError, "invalid-device"):
            bridge.validate_device_id("phone; shutdown")

    def test_send_file_requires_a_regular_file(self):
        with tempfile.TemporaryDirectory() as directory:
            with self.assertRaisesRegex(ValueError, "invalid-file"):
                bridge.validate_file(pathlib.Path(directory))

    def test_action_command_uses_separate_arguments(self):
        command = bridge.action_command("ring", "90B54E84548E4F109268EC2D0E7D6930")
        self.assertEqual(command, ["kdeconnect-cli", "-d", "90B54E84548E4F109268EC2D0E7D6930", "--ring"])
  • Step 2: Run the test and verify RED

Run python3 tests/quickshell/kdeconnect_bridge_test.py.

Expected: FAIL because panama-kdeconnect does not exist.

  • Step 3: Implement the minimal helper

Implement public functions named validate_device_id, validate_file, normalize_device_line, action_command, collect_status, invoke_action, and main. Use these exact validation and command-building implementations:

DEVICE_ID = re.compile(r"^[A-Fa-f0-9]{32,64}$")
PLUGIN_ACTIONS = {
    "kdeconnect_clipboard": "clipboard",
    "kdeconnect_findmyphone": "ring",
    "kdeconnect_ping": "ping",
    "kdeconnect_share": "share",
}

def validate_device_id(value: str) -> str:
    if not DEVICE_ID.fullmatch(value):
        raise ValueError("invalid-device")
    return value

def validate_file(value: pathlib.Path) -> pathlib.Path:
    resolved = value.expanduser().resolve(strict=True)
    if not resolved.is_file():
        raise ValueError("invalid-file")
    return resolved

def action_command(action: str, device_id: str, file_path: pathlib.Path | None = None) -> list[str]:
    device_id = validate_device_id(device_id)
    options = {
        "ring": ["--ring"],
        "clipboard": ["--send-clipboard"],
    }
    if action == "share" and file_path is not None:
        return ["kdeconnect-cli", "-d", device_id, "--share", str(validate_file(file_path))]
    if action not in options:
        raise ValueError("unsupported-action")
    return ["kdeconnect-cli", "-d", device_id, *options[action]]

normalize_device_line(line: str, plugins: list[str]) -> dict[str, object] must parse one detailed CLI line and omit transport/address text. collect_status(runner=subprocess.run) -> dict[str, object] must return available plus normalized devices. invoke_action -> dict[str, object] must run only action_command output. main(argv: list[str]) -> int must emit one compact JSON object and map validation failures to the public error code.

Call kdeconnect-cli --list-devices without a shell. For each parsed device, call busctl --user call org.kde.kdeconnect /modules/kdeconnect/devices/DEVICE org.kde.kdeconnect.device loadedPlugins; parse only plugin names. Return generic error codes unavailable, device-offline, unsupported-action, invalid-device, invalid-file, or action-failed, never raw stderr.

For actions, construct exactly one of:

["kdeconnect-cli", "-d", device_id, "--ring"]
["kdeconnect-cli", "-d", device_id, "--send-clipboard"]
["kdeconnect-cli", "-d", device_id, "--share", str(file_path)]

Make the helper executable.

  • Step 4: Run unit tests and verify GREEN

Run python3 tests/quickshell/kdeconnect_bridge_test.py.

Expected: all cases PASS with no device name, address, or file path printed by the test runner.

  • Step 5: Add and run the redacted live contract

Create tests/quickshell/kdeconnect-helper-contract.sh that runs status and asserts:

jq -e '
  .available == true and
  (.devices | type == "array" and length >= 1) and
  ([.devices[] | has("id") and has("name") and has("type") and has("paired") and has("reachable") and has("actions")] | all) and
  ([.devices[].actions[] | IN("clipboard", "ping", "ring", "share")] | all)
' <<<"$status" >/dev/null

Print only PASS (N paired, N reachable; identities redacted).

  • Step 6: Commit Task 1
git add config/dot/quickshell/scripts/panama-kdeconnect \
  tests/quickshell/kdeconnect_bridge_test.py \
  tests/quickshell/kdeconnect-helper-contract.sh
git commit -m "Add the Panama KDE Connect bridge"

Task 2: Home Assistant helper contract

Files:

  • Create: config/dot/quickshell/scripts/panama-home-assistant
  • Create: tests/quickshell/home_assistant_bridge_test.py
  • Create: tests/quickshell/home-assistant-helper-contract.sh

Interfaces:

  • Produces: probe, snapshot, toggle ENTITY_ID, and open.

  • Produces snapshot fields: ok, configured, generatedAt, entities, and error.

  • Produces entity fields: id, name, domain, state, available, and active.

  • Configuration precedence: PANAMA_HOME_ASSISTANT_*, GNOME extension dconf, then Secret Service for the token.

  • Step 1: Write failing configuration, normalization, and fake-API tests

Create tests/quickshell/home_assistant_bridge_test.py with a local ThreadingHTTPServer. Cover:

def test_environment_configuration_wins_over_legacy_sources(self):
    env = {
        "PANAMA_HOME_ASSISTANT_URL": "https://home.example",
        "PANAMA_HOME_ASSISTANT_TOKEN": "fixture-token",
        "PANAMA_HOME_ASSISTANT_ENTITIES": "light.kitchen,light.hall",
    }
    config = bridge.resolve_config(env, legacy=lambda: (_ for _ in ()).throw(AssertionError()))
    self.assertEqual(config.entity_ids, ("light.kitchen", "light.hall"))

def test_snapshot_filters_and_preserves_configured_order(self):
    raw = [
        {"entity_id": "light.hall", "state": "off", "attributes": {"friendly_name": "Hall"}},
        {"entity_id": "sensor.private", "state": "1", "attributes": {}},
        {"entity_id": "light.kitchen", "state": "on", "attributes": {"friendly_name": "Kitchen"}},
    ]
    result = bridge.normalize_entities(raw, ("light.kitchen", "light.hall"))
    self.assertEqual([item["name"] for item in result], ["Kitchen", "Hall"])
    self.assertNotIn("sensor.private", json.dumps(result))

def test_toggle_rejects_unconfigured_entity(self):
    with self.assertRaisesRegex(ValueError, "entity-not-configured"):
        bridge.ensure_configured("light.office", ("light.kitchen",))

def test_authentication_error_is_redacted(self):
    self.assertEqual(bridge.public_http_error(401, "sensitive response"), "authentication-required")

The fake server must also assert the request uses Authorization: Bearer fixture-token, that snapshot calls GET /api/states, and that toggle calls POST /api/services/homeassistant/toggle with only {"entity_id":"light.kitchen"}. Do not print the header.

  • Step 2: Run the tests and verify RED

Run python3 tests/quickshell/home_assistant_bridge_test.py.

Expected: FAIL because panama-home-assistant does not exist.

  • Step 3: Implement configuration and REST boundaries

Implement the exact Config type and these public functions:

@dataclass(frozen=True)
class Config:
    base_url: str
    token: str
    entity_ids: Sequence[str]

def ensure_configured(entity_id: str, configured: Sequence[str]) -> str:
    if entity_id not in configured:
        raise ValueError("entity-not-configured")
    return entity_id

def public_http_error(status: int, body: str) -> str:
    del body
    return "authentication-required" if status in {401, 403} else "request-failed"

def normalize_entities(raw: list[dict[str, object]], configured: Sequence[str]) -> list[dict[str, object]]:
    by_id = {str(item.get("entity_id", "")): item for item in raw}
    result = []
    for entity_id in configured:
        item = by_id.get(entity_id)
        if not item or not isinstance(item.get("attributes"), dict):
            continue
        state = str(item.get("state", "unavailable"))
        available = state not in {"unknown", "unavailable"}
        result.append({
            "id": entity_id,
            "name": str(item["attributes"].get("friendly_name") or entity_id.split(".", 1)[1].replace("_", " ").title()),
            "domain": entity_id.split(".", 1)[0],
            "state": state,
            "available": available,
            "active": available and state not in {"off", "closed", "idle", "standby"},
        })
    return result

resolve_config(env, legacy=load_legacy_config) -> Config implements the documented precedence. load_legacy_config(runner=subprocess.run) -> Config reads only the named dconf and Secret Service values. request_json performs one authorized request. collect_snapshot(config) -> dict calls GET /api/states; toggle(config, entity_id) -> dict validates then calls the Home Assistant toggle service; main(argv: list[str]) -> int emits compact JSON.

Read config/bash/env only through a non-interactive Bash subprocess that emits the three named Panama values and nothing else. The legacy path reads only hass-url, hass-enabled-entities, and secret-tool lookup token_string user_token. Validate the base URL as HTTP(S), strip a trailing slash, reject entity IDs outside ^[a-z_]+\.[a-z0-9_]+$, deduplicate while preserving order, and use urllib.request with a 5-second timeout.

Map state as follows:

available = state not in {"unknown", "unavailable"}
active = available and state not in {"off", "closed", "idle", "standby"}

Return only normalized entities. Map 401/403 to authentication-required, timeouts and transport errors to unreachable, and malformed JSON to invalid-response.

  • Step 4: Run unit tests and verify GREEN

Run python3 tests/quickshell/home_assistant_bridge_test.py.

Expected: all tests PASS and the fake server records no unexpected route.

  • Step 5: Add and run the redacted live contract

The contract runs probe and snapshot, validates the schema, and prints only PASS (configured=true, N favourites; contents redacted). It must pipe JSON to jq without printing friendly names, entity IDs, URL, or raw errors.

  • Step 6: Commit Task 2
git add config/dot/quickshell/scripts/panama-home-assistant \
  tests/quickshell/home_assistant_bridge_test.py \
  tests/quickshell/home-assistant-helper-contract.sh
git commit -m "Add the Panama Home Assistant bridge"

Task 3: Quickshell service state and Ongoing integration

Files:

  • Create: config/dot/quickshell/services/KdeConnect.qml
  • Create: config/dot/quickshell/services/HomeAssistant.qml
  • Create: tests/quickshell/control-center-services-contract.sh
  • Modify: config/dot/quickshell/services/DeviceEvents.qml
  • Modify: config/dot/quickshell/services/Ongoing.qml
  • Modify: config/dot/quickshell/shell.qml

Interfaces:

  • KdeConnect: available, devices, preferredPhone, phoneReachable, transferActive, transferFileName, recentExchange, refresh(), sendFile(path), sendClipboard(), ring(), cancelTransfer(), applyFixture(name), clearFixture().

  • HomeAssistant: phase, entities, visibleEntities, configuredCount, stale, busyEntityId, lastError, refresh(), toggleEntity(id), open(), applyFixture(name), clearFixture().

  • IPC targets: kdeconnect and home-assistant with fixture, reset, refresh, action-safe diagnostics, and status functions.

  • Step 1: Write the failing live fixture contract

Create a shell test that asserts both IPC targets, then exercises synthetic fixtures only:

qs ipc call kdeconnect fixture reachable >/dev/null
jq -e '.fixture and .reachable and .actionCount == 4 and .transferActive == false' \
  <<<"$(qs ipc call kdeconnect status)" >/dev/null

qs ipc call kdeconnect fixture transfer >/dev/null
jq -e '.transferActive and .ongoingCount >= 1' \
  <<<"$(qs ipc call kdeconnect status)" >/dev/null

qs ipc call home-assistant fixture ready >/dev/null
jq -e '.fixture and .phase == "ready" and .configuredCount == 7 and .visibleCount == 4' \
  <<<"$(qs ipc call home-assistant status)" >/dev/null

qs ipc call home-assistant fixture stale >/dev/null
jq -e '.phase == "degraded" and .stale and .configuredCount == 7' \
  <<<"$(qs ipc call home-assistant status)" >/dev/null

Cleanup resets both fixture modes and closes Quick Settings.

  • Step 2: Run the contract and verify RED

Run tests/quickshell/control-center-services-contract.sh.

Expected: FAIL because the IPC targets do not exist.

  • Step 3: Implement KdeConnect.qml

Use one status Process, one action Process, and one low-frequency Timer. Parse one JSON object per completion. Fixture data contains only synthetic names. Expose preferredPhone as the first reachable type === "phone", then the first paired phone. Keep action commands as arrays:

actionProcess.command = [helperPath, "send-file", phone.id, path]
actionProcess.running = true

While send-file runs, set transferActive, basename-only transferFileName, and the device name. On success, set recentExchange and publish one important StatusEvents completion with an open-path action only when a local path is safe to reopen. Ring and clipboard completion publish no event. cancelTransfer() terminates only the owned action process.

  • Step 4: Implement HomeAssistant.qml

Use separate refresh and action Process objects. Preserve entities when a refresh fails after a successful snapshot and set stale=true. Use one busyEntityId because the UI allows one Home request at a time. After a successful toggle, immediately run refresh(). Expose the first four entities as visibleEntities and retain all configured entities for expansion.

  • Step 5: Wire shared activity and transitions

Replace DeviceEvents.qml's private KDE polling process with properties and change handlers bound to KdeConnect.phoneReachable and the preferred phone's name. Add this activity to Ongoing.activities:

if (KdeConnect.transferActive) {
    result.push({
        kind: "phone-transfer",
        glyph: "\u{F03F2}",
        label: "Sending to " + KdeConnect.transferDeviceName,
        detail: KdeConnect.transferFileName,
        state: "Sending",
        tone: "accent",
        action: "Cancel"
    });
}

Route Ongoing.invoke("phone-transfer") to KdeConnect.cancelTransfer(). Add typed diagnostic IPC methods without any command that performs a live Home toggle, Ring, clipboard send, or file send.

  • Step 6: Restart Quickshell and run the service contract GREEN

Run the repository's established shell reload command, wait for the current instance to report IPC, then run:

tests/quickshell/control-center-services-contract.sh

Expected: PASS with fixture state only and no QML warnings in the fresh journal.

  • Step 7: Commit Task 3
git add config/dot/quickshell/services/KdeConnect.qml \
  config/dot/quickshell/services/HomeAssistant.qml \
  config/dot/quickshell/services/DeviceEvents.qml \
  config/dot/quickshell/services/Ongoing.qml \
  config/dot/quickshell/shell.qml \
  tests/quickshell/control-center-services-contract.sh
git commit -m "Add Control Center integration services"

Task 4: Approved Control Center interface

Files:

  • Create: config/dot/quickshell/modules/quicksettings/ControlSectionHeader.qml
  • Create: config/dot/quickshell/modules/quicksettings/HomeControls.qml
  • Create: config/dot/quickshell/modules/quicksettings/HomeTile.qml
  • Create: config/dot/quickshell/modules/quicksettings/PhoneControls.qml
  • Create: config/dot/quickshell/modules/quicksettings/RecentExchange.qml
  • Create: tests/quickshell/control-center-contract.sh
  • Modify: config/dot/quickshell/config/Theme.qml
  • Modify: config/dot/quickshell/modules/quicksettings/QuickSettings.qml
  • Modify: config/dot/quickshell/modules/quicksettings/QuickSettingsPanel.qml
  • Modify: config/dot/quickshell/modules/bar/StatusCluster.qml

Interfaces:

  • HomeControls.expanded: bool and signal toggleExpanded().

  • PhoneControls.expanded: bool and signal toggleExpanded().

  • Existing expandedSection remains the only expansion owner.

  • StatusCluster remains one click target and reads KdeConnect.phoneReachable.

  • Step 1: Write the failing panel contract

Assert source and live behavior:

rg -q 'readonly property int controlCenterWidth: 430' config/dot/quickshell/config/Theme.qml
rg -q 'readonly property int controlCenterTopGap: 2' config/dot/quickshell/config/Theme.qml
rg -q 'margins.top: Theme.barHeight + Theme.controlCenterTopGap' config/dot/quickshell/modules/quicksettings/QuickSettings.qml
rg -q 'HomeControls' config/dot/quickshell/modules/quicksettings/QuickSettingsPanel.qml
rg -q 'PhoneControls' config/dot/quickshell/modules/quicksettings/QuickSettingsPanel.qml
rg -q 'visible: KdeConnect.phoneReachable' config/dot/quickshell/modules/bar/StatusCluster.qml

Load ready fixtures, open Quick Settings, and assert hyprctl layers contains qs-popover-quicksettings. Use IPC status to assert opening Home followed by Phone leaves only Phone expanded.

  • Step 2: Run the contract and verify RED

Run tests/quickshell/control-center-contract.sh.

Expected: FAIL on the missing geometry tokens and components.

  • Step 3: Add geometry and composition

Add exact tokens:

readonly property int controlCenterWidth: 430
readonly property int controlCenterTopGap: 2

Set both Quick Settings implicit widths to Theme.controlCenterWidth, set top margin to Theme.barHeight + Theme.controlCenterTopGap, and retain the current right margin. On open, call KdeConnect.refresh() and HomeAssistant.refresh() before restarting the entrance animation.

Place Home after audio/brightness and Phone after Home, before the footer. Reuse expandedSection values home and phone; do not introduce nested popovers.

  • Step 4: Implement the Home components

HomeTile.qml is a button-like rectangle with entity, busy, and onActivated. It maps light to a lamp glyph, uses Theme.warn at low alpha only when entity.active, and shows Unavailable for !entity.available.

HomeControls.qml uses a four-column resting grid from HomeAssistant.visibleEntities. Expanded mode uses a single-column list of all HomeAssistant.entities. Include deterministic loading, setup, authentication-required, unreachable/stale, and empty states. Retry calls HomeAssistant.refresh(); Open calls HomeAssistant.open().

  • Step 5: Implement the Phone components and chooser

PhoneControls.qml renders the approved identity-first card. Show only actions included in preferredPhone.actions. Use QtQuick.Dialogs.FileDialog with a single existing-file selection. onAccepted passes the selected local file path as a distinct argument to KdeConnect.sendFile(); onRejected does nothing.

Disabled/offline copy is Not nearby. Missing KDE Connect copy is KDE Connect unavailable. RecentExchange.qml is visible only when the service has a real current-session exchange.

  • Step 6: Add reachable-phone status and visual states

Append one phone glyph to the existing StatusCluster only when KdeConnect.phoneReachable; it has no MouseArea and inherits the parent Pill's activation. Use Theme.cyan for reachable, no disconnected glyph, and no animation.

  • Step 7: Restart Quickshell and run the panel contract GREEN

Run tests/quickshell/control-center-contract.sh and inspect the fresh shell journal for error, warning, ReferenceError, and TypeError entries from the current Quickshell start.

Capture resting, Home-expanded, Phone-offline, transfer-active, and Home-stale fixture screenshots to /tmp; do not commit screenshots.

  • Step 8: Commit Task 4
git add config/dot/quickshell/config/Theme.qml \
  config/dot/quickshell/modules/quicksettings \
  config/dot/quickshell/modules/bar/StatusCluster.qml \
  tests/quickshell/control-center-contract.sh
git commit -m "Build the Panama Control Center"

Task 5: Documentation and full verification

Files:

  • Modify: config/dot/hypr/DESKTOP-PARITY.md
  • Modify: config/dot/hypr/README.md

Interfaces:

  • Consumes the complete helper, service, UI, and IPC contracts from Tasks 14.

  • Produces an auditable verification result and an up-to-date remaining roadmap.

  • Step 1: Update desktop documentation

Mark Home Assistant favourites and KDE Connect phone integration Live. Document the 430-pixel Control Center, two-pixel bar attachment, environment/keyring configuration precedence, BlueBubbles messaging boundary, and current iOS capabilities. Remove those two items from System UI still worth adding while retaining printers and the external RustDesk lock-screen test.

  • Step 2: Run every focused test
python3 tests/quickshell/kdeconnect_bridge_test.py
python3 tests/quickshell/home_assistant_bridge_test.py
tests/quickshell/kdeconnect-helper-contract.sh
tests/quickshell/home-assistant-helper-contract.sh
tests/quickshell/control-center-services-contract.sh
tests/quickshell/control-center-contract.sh

Expected: every command exits 0; live contracts print counts only.

  • Step 3: Run full Panama verification

Run hyprctl configerrors and require empty output. Run every executable tests/quickshell/*contract.sh in sorted order and require zero failures. Restart Quickshell from a clean process, verify all IPC targets return, and inspect only the new process's journal for QML errors or warnings.

  • Step 4: Perform read-only live checks

Verify the Control Center reports one reachable phone with the confirmed action names and seven configured Home favourites without printing identities. Open and close the panel, expand Home and Phone, and confirm Wi-Fi, Bluetooth, audio, Focus, Caffeine, Night Light, settings, power, Escape, and outside-click remain usable. Do not trigger an external-device action.

  • Step 5: Review the final diff against the spec

Check every Goal, Non-goal, Interaction, Security, Failure, Testing, and Documentation requirement in docs/superpowers/specs/2026-08-17-panama-control-center-design.md. Run git diff --check and confirm no secret-shaped value, live entity response, device ID, IP address, or absolute personal file path entered tracked content.

  • Step 6: Commit documentation and push the feature
git add config/dot/hypr/DESKTOP-PARITY.md config/dot/hypr/README.md
git commit -m "Document the Panama Control Center"
git push origin main
  • Step 7: Offer the three explicit live action checks

Ask the user to choose actions from the finished Control Center: toggle one Home light, Ring the iPhone, send clipboard text, or send a harmless chosen file. Treat the visual state and resulting external device behavior as the acceptance signal; do not execute an external action from diagnostics or automation.