From c37205e8f5d0f381810b378cf89f128ec0abaa3f Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 6 Nov 2026 19:37:26 +0100 Subject: [PATCH] swarm-ui: the terminal message box has a placeholder and a spinner instead of a label and a sent line The label 'message ' is the input's placeholder (the label stays for screen readers). The send button shows a spinner over its label while the request is pending and keeps its width, and the 'sent' line is gone: the box empties when the message is held. Spinner is a component of its own now, used by the button and the refresh picker. --- docs/2-handbook/swarm/ui.md | 10 ++++--- .../src/agent/terminal/AgentInputForm.css | 3 -- .../src/agent/terminal/AgentInputForm.tsx | 12 +++----- .../swarm-ui/src/pages/ComponentsPage.tsx | 5 ++++ .../swarm-ui/src/ui/button/Button.css | 14 +++++++++ .../swarm-ui/src/ui/button/Button.tsx | 18 ++++++++++-- .../swarm-ui/src/ui/form-field/FormField.css | 9 ++++++ .../swarm-ui/src/ui/form-field/FormField.tsx | 10 +++++-- .../ui/refresh-interval/RefreshInterval.css | 29 ------------------- .../ui/refresh-interval/RefreshInterval.tsx | 3 +- .../swarm-ui/src/ui/spinner/Spinner.css | 26 +++++++++++++++++ .../swarm-ui/src/ui/spinner/Spinner.tsx | 10 +++++++ .../swarm-ui/src/ui/text-field/TextField.tsx | 5 +++- 13 files changed, 103 insertions(+), 51 deletions(-) create mode 100644 frontend/packages/swarm-ui/src/ui/spinner/Spinner.css create mode 100644 frontend/packages/swarm-ui/src/ui/spinner/Spinner.tsx diff --git a/docs/2-handbook/swarm/ui.md b/docs/2-handbook/swarm/ui.md index 602b005b..066313df 100644 --- a/docs/2-handbook/swarm/ui.md +++ b/docs/2-handbook/swarm/ui.md @@ -447,10 +447,12 @@ tab, follow a link in the app, or press the browser's back button. Choose ### Sending a message to an agent Under an agent's terminal, in the preview and in its own tab, is a **message** -box. **send** hands the text to the agent's harness, and the agent takes it as -an inbox message from your authelia username. **sent** means the harness was -listening and holds the message. If the harness isn't running, or isn't -connected to the swarm queue, the box shows an error and the agent gets nothing. +box, with the agent's name as its placeholder. **send** hands the text to the agent's +harness, and the agent takes it as an inbox message from your authelia username. +While the request is pending, a spinner replaces the button's label. When it +ends, the box empties: the harness was listening and holds the message. If the +harness isn't running, or isn't connected to the swarm queue, the box shows an +error and the agent gets nothing. While the agent is in a turn, a **cancel turn** badge appears among the state badges over the terminal, in its own tab. It asks the agent's harness to stop the turn, then shows whether one was running ("turn cancelled" or "nothing was diff --git a/frontend/packages/swarm-ui/src/agent/terminal/AgentInputForm.css b/frontend/packages/swarm-ui/src/agent/terminal/AgentInputForm.css index 6514f376..74efb86e 100644 --- a/frontend/packages/swarm-ui/src/agent/terminal/AgentInputForm.css +++ b/frontend/packages/swarm-ui/src/agent/terminal/AgentInputForm.css @@ -11,6 +11,3 @@ .ui-agent-input-form > :first-child { flex: 1; } -.ui-agent-input-sent { - margin-block-start: 0.25rem; -} diff --git a/frontend/packages/swarm-ui/src/agent/terminal/AgentInputForm.tsx b/frontend/packages/swarm-ui/src/agent/terminal/AgentInputForm.tsx index 0b39639f..020ba1ce 100644 --- a/frontend/packages/swarm-ui/src/agent/terminal/AgentInputForm.tsx +++ b/frontend/packages/swarm-ui/src/agent/terminal/AgentInputForm.tsx @@ -7,7 +7,6 @@ import { useState } from "preact/hooks"; import { ApiErrorPanel } from "@hive/shared/api-error-panel.js"; import { TextField } from "../../ui/text-field/TextField.js"; import { Button } from "../../ui/button/Button.js"; -import { Hint } from "../../ui/hint/Hint.js"; import { useSubmit } from "../../ui/submit/useSubmit.js"; import "./AgentInputForm.css"; @@ -34,19 +33,16 @@ export function AgentInputForm({ agentName }: { agentName: string }) { - - {state.status === "done" && ( - - sent - - )} {state.status === "error" && ( )} diff --git a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx index 5d2939bf..3f02ef59 100644 --- a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx @@ -611,6 +611,11 @@ export function ComponentsPage() { + + + diff --git a/frontend/packages/swarm-ui/src/ui/button/Button.css b/frontend/packages/swarm-ui/src/ui/button/Button.css index 1c5e42a6..1114b7bd 100644 --- a/frontend/packages/swarm-ui/src/ui/button/Button.css +++ b/frontend/packages/swarm-ui/src/ui/button/Button.css @@ -24,3 +24,17 @@ opacity: 0.6; cursor: default; } +.ui-button { + position: relative; +} +/* Busy: the label keeps its space, hidden, and the spinner sits over it. */ +.ui-button-busy .ui-button-label { + visibility: hidden; +} +.ui-button-spinner { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/frontend/packages/swarm-ui/src/ui/button/Button.tsx b/frontend/packages/swarm-ui/src/ui/button/Button.tsx index cdf77e4b..decb138d 100644 --- a/frontend/packages/swarm-ui/src/ui/button/Button.tsx +++ b/frontend/packages/swarm-ui/src/ui/button/Button.tsx @@ -5,6 +5,7 @@ // so far; a third reading gets added the day something needs it, not // speculatively. import type { ComponentChildren } from "preact"; +import { Spinner } from "../spinner/Spinner.js"; import "./Button.css"; export type ButtonVariant = "primary" | "default"; @@ -13,6 +14,7 @@ export function Button({ variant = "default", type = "button", disabled, + busy = false, onClick, describedBy, children, @@ -20,6 +22,10 @@ export function Button({ variant?: ButtonVariant; type?: "button" | "submit"; disabled?: boolean; + /** A request from this button is in flight: a spinner covers the label, + * which keeps its space so the button does not change width, and the button + * cannot be pressed again. */ + busy?: boolean; onClick?: (e: Event) => void; /** Id of the element that says why the button is disabled. */ describedBy?: string; @@ -27,13 +33,19 @@ export function Button({ }) { return ( ); } diff --git a/frontend/packages/swarm-ui/src/ui/form-field/FormField.css b/frontend/packages/swarm-ui/src/ui/form-field/FormField.css index f8ac6a15..2b936f9b 100644 --- a/frontend/packages/swarm-ui/src/ui/form-field/FormField.css +++ b/frontend/packages/swarm-ui/src/ui/form-field/FormField.css @@ -45,3 +45,12 @@ box-sizing: border-box; min-height: 2.75em; } +/* A label that stays for assistive tech and is off the screen. */ +.ui-form-field-label-hidden { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} diff --git a/frontend/packages/swarm-ui/src/ui/form-field/FormField.tsx b/frontend/packages/swarm-ui/src/ui/form-field/FormField.tsx index dc6db9e2..7b4959dc 100644 --- a/frontend/packages/swarm-ui/src/ui/form-field/FormField.tsx +++ b/frontend/packages/swarm-ui/src/ui/form-field/FormField.tsx @@ -16,21 +16,27 @@ export function FormField({ label, htmlFor, labelId, + hideLabel = false, children, }: { label: string; htmlFor?: string; labelId?: string; + /** Keep the label for assistive tech and take it off the screen, for a + * control whose placeholder says the same. */ + hideLabel?: boolean; children: ComponentChildren; }) { + const labelClass = + "ui-form-field-label" + (hideLabel ? " ui-form-field-label-hidden" : ""); return (
{htmlFor !== undefined ? ( -