<!--
One peer, and every worktree this machine could carry with it.
A peer is a MACHINE. Whether anything crosses is decided per worktree, and
per repository inside it, so this card is a list of relationships rather than
a single on/off switch.
-->
<script lang="ts">
import { peers } from "../../stores/peers.svelte.js";
import type { PeerSummary, Service } from "../../api/types/peer.js";
import RelationshipPanel from "./RelationshipPanel.svelte";
import AppIcon from "../layout/AppIcon.svelte";
interface Props {
peer: PeerSummary;
}
let { peer }: Props = $props();
let expanded = $state(false);
const worktrees = $derived(peers.worktrees[peer.id] ?? []);
async function toggle() {
expanded = !expanded;
if (expanded) {
await peers.loadWorkspaces(peer.id);
await peers.loadServices(peer.id);
}
}
/**
* What this machine will do with that one.
*
* Both independent service switches live here, from the same peer store.
* This machine page is the single place to operate the relationship before
* choosing a worktree.
*/
function switchOf(service: Service): "on" | "off" {
return (
(peers.services[peer.id] ?? []).find((entry) => entry.service === service)?.switch ?? "on"
);
}
function describes(service: Service): string {
return (
(peers.services[peer.id] ?? []).find((entry) => entry.service === service)?.describes ?? ""
);
}
async function flip(service: Service) {
await peers.setService(peer.id, service, switchOf(service) === "on" ? "off" : "on");
}
/**
* A state that has held too long is worth saying, because "connecting" and
* "has been trying to connect for ten minutes" look identical otherwise.
*/
const stuck = $derived(
peer.state === "connecting" && peer.state_for_seconds > 60,
);
</script>
<article class="peer">
<header>
<button class="disclose" onclick={toggle} aria-expanded={expanded}>
<AppIcon name={expanded ? "chevron-down" : "chevron-right"} size={11} />
<span class="name">{peer.name}</span>
</button>
<span class="state {peer.state}">{peer.state}</span>
{#if stuck}
<span class="stuck">for {Math.round(peer.state_for_seconds / 60)} min</span>
{/if}
{#if peer.detail}
<span class="detail">{peer.detail}</span>
{/if}
<span class="spacer"></span>
<span class="fingerprint" title="Compare this with the other machine">
{peer.fingerprint}
</span>
<button
class="forget"
disabled={peers.busy === `forget:${peer.id}`}
onclick={() => peers.forget(peer.id)}
title="End this relationship. Baselines go with it; files already carried stay."
>
Forget
</button>
</header>
{#if expanded}
<div class="services">
{#each ["files"] as const as service (service)}
<label class="service" title={describes(service)}>
<input
type="checkbox"
checked={switchOf(service) === "on"}
disabled={peers.busy === `service:${peer.id}:${service}`}
onchange={() => void flip(service)}
/>
File sync
<span class="service-switch">{switchOf(service)}</span>
</label>
{/each}
</div>
{#if switchOf("files") === "off"}
<p class="subtle">
Working trees are not being compared with this machine.
</p>
{:else if !worktrees.length}
<p class="subtle">
Nothing to compare: either the peer is not reachable, or the two
machines have no worktree in common.
</p>
{:else}
{#each worktrees as worktree (worktree.id)}
<RelationshipPanel {peer} {worktree} />
{/each}
{/if}
{/if}
</article>
<style>
.services {
display: flex;
gap: var(--wb-spacing-16);
margin: var(--wb-spacing-6) 0 var(--wb-spacing-2);
}
.service {
display: flex;
align-items: center;
gap: var(--wb-spacing-6);
font-size: var(--wb-font-size-12);
cursor: pointer;
}
.service-switch {
font-size: var(--wb-font-size-10);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--vscode-descriptionForeground);
}
.peer {
border: 1px solid var(--vscode-panel-border);
border-radius: var(--wb-corner-radius-4);
padding: var(--wb-spacing-8) var(--wb-spacing-12);
margin-bottom: var(--wb-spacing-10);
}
header {
display: flex;
align-items: center;
gap: var(--wb-spacing-10);
}
.disclose {
background: none;
border: none;
color: var(--vscode-editor-foreground);
cursor: pointer;
padding: 0;
font-size: var(--wb-font-size-13);
}
.name {
font-weight: 600;
}
.state {
font-size: var(--wb-font-size-10);
text-transform: uppercase;
letter-spacing: 0.05em;
padding: var(--wb-spacing-1) var(--wb-spacing-6);
border-radius: var(--wb-corner-radius-8);
background: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
}
.state.connected {
background: var(--vscode-testing-iconPassed, #2d7d46);
color: #fff;
}
.state.error {
background: var(--vscode-inputValidation-errorBackground, #5a1d1d);
color: var(--vscode-errorForeground);
}
.stuck,
.detail,
.fingerprint,
.subtle {
color: var(--vscode-descriptionForeground);
font-size: var(--wb-font-size-11);
}
.fingerprint {
font-family: var(--vscode-editor-font-family, monospace);
}
.spacer {
flex: 1;
}
.forget {
background: none;
border: 1px solid var(--vscode-panel-border);
color: var(--vscode-descriptionForeground);
cursor: pointer;
padding: var(--wb-spacing-2) var(--wb-spacing-8);
border-radius: var(--wb-corner-radius-2);
font-size: var(--wb-font-size-11);
}
</style>