swarm-ui: let a closed stream reconnect and a failed caps read retry
When the server answers an event stream with an error status the browser gives up and the badge stayed on disconnected with nothing to press. The terminal, its state header and the messages page now offer a reconnect, and the one rule for the state after an error moved into a tested helper. The resource caps panel gets a retry beside its error, and its loading state announces itself.
This commit is contained in:
parent
9b1fe85706
commit
31b7452e56
4 changed files with 118 additions and 34 deletions
|
|
@ -117,10 +117,22 @@ export function AgentLimits({ agent }: { agent: string }) {
|
|||
// Shown as text, not a hover title. Caps already loaded stay under a
|
||||
// failed refetch, so a save that worked is not hidden by a read that did not.
|
||||
const errorPanel = error ? (
|
||||
<ApiErrorPanel context="reading the resource caps failed" problem={error} />
|
||||
<>
|
||||
<ApiErrorPanel
|
||||
context="reading the resource caps failed"
|
||||
problem={error}
|
||||
/>
|
||||
<Button onClick={() => setReload((n) => n + 1)}>retry</Button>
|
||||
</>
|
||||
) : null;
|
||||
if (caps === null) {
|
||||
return errorPanel ?? <span class="ui-agent-limits-muted">…</span>;
|
||||
return (
|
||||
errorPanel ?? (
|
||||
<span class="ui-agent-limits-muted" role="status">
|
||||
loading the resource caps…
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -203,8 +203,10 @@ export function AgentTermPreview({
|
|||
leading?: ComponentChildren;
|
||||
}) {
|
||||
const paths = termPaths(agentName, subagent);
|
||||
const { rows, connection } = useSwarmTermStream(paths.stream);
|
||||
const { header } = useSwarmAgentStateStream(paths.state);
|
||||
const { rows, connection, reconnect } = useSwarmTermStream(paths.stream);
|
||||
const { header, reconnect: reconnectState } = useSwarmAgentStateStream(
|
||||
paths.state,
|
||||
);
|
||||
const { open: openTab } = useDynamicTabs();
|
||||
const logRef = useRef<HTMLDivElement>(null);
|
||||
const [stickToBottom, setStickToBottom] = useState(true);
|
||||
|
|
@ -246,6 +248,17 @@ export function AgentTermPreview({
|
|||
tone={CONNECTION_TONE[connection]}
|
||||
value={CONNECTION_LABEL[connection]}
|
||||
/>
|
||||
{connection === "closed" && (
|
||||
<Badge
|
||||
variant="quiet"
|
||||
value="reconnect"
|
||||
title={`open ${paths.label}'s terminal stream again`}
|
||||
onClick={() => {
|
||||
reconnect();
|
||||
reconnectState();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{header && showHeaderBadges && (
|
||||
<StateBadges header={header} ctx={ctx} cost={cost} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,12 @@
|
|||
// `status_text`/`running`).
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
export type ConnectionState = "connecting" | "open" | "reconnecting" | "closed";
|
||||
import {
|
||||
connectionAfterError,
|
||||
type ConnectionState,
|
||||
} from "../../ui/stream/connection.js";
|
||||
|
||||
export type { ConnectionState };
|
||||
|
||||
export interface TokenUsage {
|
||||
input_tokens?: number;
|
||||
|
|
@ -53,6 +58,8 @@ export interface AgentStateHeader {
|
|||
export interface UseSwarmAgentStateStreamResult {
|
||||
header: AgentStateHeader | null;
|
||||
connection: ConnectionState;
|
||||
/** Opens the stream again, for one the browser gave up on. */
|
||||
reconnect: () => void;
|
||||
}
|
||||
|
||||
export function useSwarmAgentStateStream(
|
||||
|
|
@ -60,6 +67,8 @@ export function useSwarmAgentStateStream(
|
|||
): UseSwarmAgentStateStreamResult {
|
||||
const [header, setHeader] = useState<AgentStateHeader | null>(null);
|
||||
const [connection, setConnection] = useState<ConnectionState>("connecting");
|
||||
// Bumped by `reconnect`, which reruns the effect below.
|
||||
const [attempt, setAttempt] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setHeader(null);
|
||||
|
|
@ -78,13 +87,9 @@ export function useSwarmAgentStateStream(
|
|||
// badge cluster over — the next valid header replaces it.
|
||||
}
|
||||
};
|
||||
es.onerror = () => {
|
||||
setConnection(
|
||||
es.readyState === 0 /* CONNECTING */ ? "reconnecting" : "closed",
|
||||
);
|
||||
};
|
||||
es.onerror = () => setConnection(connectionAfterError(es.readyState));
|
||||
return () => es.close();
|
||||
}, [streamUrl]);
|
||||
}, [streamUrl, attempt]);
|
||||
|
||||
return { header, connection };
|
||||
return { header, connection, reconnect: () => setAttempt((n) => n + 1) };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,12 @@
|
|||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import type { TermMsg, TermRow } from "@hive/shared/term-msg.js";
|
||||
|
||||
export type ConnectionState = "connecting" | "open" | "reconnecting" | "closed";
|
||||
import {
|
||||
connectionAfterError,
|
||||
type ConnectionState,
|
||||
} from "../../ui/stream/connection.js";
|
||||
|
||||
export type { ConnectionState };
|
||||
|
||||
function appendRow(rows: TermRow[], row: TermRow): TermRow[] {
|
||||
const last = rows[rows.length - 1];
|
||||
|
|
@ -25,6 +30,8 @@ function appendRow(rows: TermRow[], row: TermRow): TermRow[] {
|
|||
export interface UseSwarmTermStreamResult {
|
||||
rows: TermRow[];
|
||||
connection: ConnectionState;
|
||||
/** Opens the stream again, for one the browser gave up on. */
|
||||
reconnect: () => void;
|
||||
}
|
||||
|
||||
export function useSwarmTermStream(
|
||||
|
|
@ -33,6 +40,8 @@ export function useSwarmTermStream(
|
|||
const [rows, setRows] = useState<TermRow[]>([]);
|
||||
const [connection, setConnection] = useState<ConnectionState>("connecting");
|
||||
const keySeqRef = useRef(0);
|
||||
// Bumped by `reconnect`, which reruns the effect below.
|
||||
const [attempt, setAttempt] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setRows([]);
|
||||
|
|
@ -59,13 +68,9 @@ export function useSwarmTermStream(
|
|||
}
|
||||
setRows((prev) => appendRow(prev, { ...msg, key, fromHistory: false }));
|
||||
};
|
||||
es.onerror = () => {
|
||||
setConnection(
|
||||
es.readyState === 0 /* CONNECTING */ ? "reconnecting" : "closed",
|
||||
);
|
||||
};
|
||||
es.onerror = () => setConnection(connectionAfterError(es.readyState));
|
||||
return () => es.close();
|
||||
}, [streamUrl]);
|
||||
}, [streamUrl, attempt]);
|
||||
|
||||
return { rows, connection };
|
||||
return { rows, connection, reconnect: () => setAttempt((n) => n + 1) };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue