658 lines
29 KiB
Markdown
658 lines
29 KiB
Markdown
# 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:
|
||
|
||
```python
|
||
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:
|
||
|
||
```python
|
||
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:
|
||
|
||
```python
|
||
["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:
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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:
|
||
|
||
```python
|
||
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:
|
||
|
||
```python
|
||
@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:
|
||
|
||
```python
|
||
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**
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```qml
|
||
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`:
|
||
|
||
```qml
|
||
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:
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```qml
|
||
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**
|
||
|
||
```bash
|
||
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 1–4.
|
||
- 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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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.
|