From c6047e04e4efb5da3bb15a188aed4ebe3b48f824 Mon Sep 17 00:00:00 2001 From: "Maksym Hryzodub [DREAM]" Date: Fri, 18 Sep 2026 15:40:19 +0300 Subject: [PATCH] feat(admin): tabs in the Add peer dialog, status beside Info, no autofill (CLEAN-98) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things the dialog got wrong once a ranch had more than a handful of agents. The address form sat at the bottom of the same scrolling column as the candidate list, so on a ranch with a dozen agents nothing on the first screen said an external import existed at all. The two ways in are now tabs — "On this ranch" and "External agent" — with the dialog description saying what the open tab does. With nothing left to connect on this ranch the dialog opens on the address tab, unless the operator has already picked one themselves. In a candidate row the status badge trailed the agent name, so it landed at a different distance in every row while the Info chip stayed on the right edge. Status now rides with that chip: name on the left, status + Info on the right, lined up down the whole list. The card address field sits directly above a password field, which is exactly the shape Chrome and the password managers read as a login form — they were dropping a saved account into it. Both fields now carry autocomplete off (new-password on the credential) plus the 1Password/LastPass/Dashlane ignore attributes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SZ9dCrzZqd5SwR4erLYe12 --- .../agent/peer/components/peer/Picker.vue | 415 ++++++++++-------- package.json | 2 +- 2 files changed, 245 insertions(+), 172 deletions(-) diff --git a/admin/slices/agent/peer/components/peer/Picker.vue b/admin/slices/agent/peer/components/peer/Picker.vue index 63657e22..320b84b4 100644 --- a/admin/slices/agent/peer/components/peer/Picker.vue +++ b/admin/slices/agent/peer/components/peer/Picker.vue @@ -31,6 +31,11 @@ import { * The preview expands under the clicked row, accordion-style: at ten * candidates a panel below the list sits under the fold, and a click that * renders off-screen reads as a click that did nothing (CLEAN-94). + * + * The two ways in — an agent of this ranch, or any A2A address — are tabs, not + * one scrolling column (CLEAN-98). Stacked, the address form sat below every + * candidate: on a ranch with a dozen agents nothing on the first screen said an + * external import existed at all. */ const props = defineProps<{ open: boolean; agentId: string }>(); const emit = defineEmits<{ 'update:open': [value: boolean]; connected: [] }>(); @@ -46,7 +51,24 @@ const error = ref(null); const FILTER_FROM = 6; +type AddMode = 'ranch' | 'external'; + +const mode = ref('ranch'); +/** True once the operator has picked a tab themselves; after that nothing + * moves them, least of all the empty-ranch default below. */ +const modeTouched = ref(false); + +function selectMode(value: unknown): void { + if (value !== 'ranch' && value !== 'external') return; + mode.value = value; + modeTouched.value = true; +} + const candidates = computed(() => store.candidates(props.agentId)); +/** What the tab counts: agents that can still be added, not the whole ranch. */ +const available = computed( + () => candidates.value.filter((c) => !c.connected).length, +); const visible = computed(() => { const q = filter.value.trim().toLowerCase(); @@ -61,7 +83,7 @@ const isOpen = computed({ watch( () => props.open, - (open) => { + async (open) => { if (!open) return; filter.value = ''; selected.value = null; @@ -71,7 +93,14 @@ watch( urlToken.value = ''; urlPreview.value = null; urlError.value = null; - void store.loadCandidates(props.agentId); + mode.value = 'ranch'; + modeTouched.value = false; + await store.loadCandidates(props.agentId); + // With nothing left to connect on this ranch, the address form IS the + // dialog — opening on an empty list would read as "no way to add a peer". + if (!modeTouched.value && candidates.value.length === 0) { + mode.value = 'external'; + } }, ); @@ -197,193 +226,237 @@ const urlPreviewAdvertisesNothing = computed( Add peer - Agents on this ranch this one isn't connected to yet. Click an agent - to read its card, then connect. + + -
-
- - -
+ + + + On this ranch + {{ available }} + + External agent + -
    -
  • - + - -
    -
    - - - -
    - -

    {{ error }}

    -
    -
  • -
+ + -

- No agent matches "{{ filter }}". -

-

- No unconnected agents left on this ranch. -

+

+ No agent matches "{{ filter }}". +

+

+ No unconnected agents left on this ranch. Anything outside + it comes in under External agent. +

+ - -
-

Import an external agent

-

- Any A2A 1.0 agent outside this ranch, by its address. Importing - the same address again updates the entry — no duplicates. -

-
- - - -
+ + + +
+ + + +
-
-
- - + +
+ + +
+
- -
-

- {{ urlError }} -

+

+ {{ urlError }} +

+
-
+