hive-agent: the per-agent page has no message box; its send route and the broker's operator message are removed
The page's composer (its input, slash commands and completion), POST /send in hive-agent, the OperatorMsg request of the agent socket and its handler in hive-c0re, and the broker-error helper only the send route used are deleted: writing to an agent is the swarm UI's terminal. cancel turn, new session and log out stay in the state badge's menu; POST /api/compact stays, with nothing on the page calling it. The request-line limit keeps its size, and its comment says it is a generous bound now. The docs describe the actions in place of the footer and the prompt.
This commit is contained in:
parent
e149ff3315
commit
23f4f1823b
7 changed files with 77 additions and 505 deletions
|
|
@ -20,7 +20,6 @@ import { HeaderPill } from "./components/HeaderPill.js";
|
|||
import { SidePanel } from "./components/SidePanel.js";
|
||||
import { InboxPanel } from "./components/InboxPanel.js";
|
||||
import { TodosPanel } from "./components/TodosPanel.js";
|
||||
import { TermInput } from "./components/TermInput.js";
|
||||
import { LoginFlow } from "./components/LoginFlow.js";
|
||||
import { useAgentState } from "./hooks/useAgentState.js";
|
||||
import { useTodos } from "./hooks/useTodos.js";
|
||||
|
|
@ -79,11 +78,10 @@ function tokenTotal(u: TokenUsage | null): number | null {
|
|||
);
|
||||
}
|
||||
|
||||
// `?hide=header,input` — an embedding companion window (e.g. a native
|
||||
// desktop shell) supplies its own header/composer chrome around this
|
||||
// page, so it asks for just the terminal feed. Read once per load (the
|
||||
// value can't change without a navigation); comma-separated so both
|
||||
// axes can be hidden in one param.
|
||||
// `?hide=header` — an embedding companion window (e.g. a native desktop
|
||||
// shell) supplies its own header chrome around this page, so it asks for
|
||||
// just the terminal feed. Read once per load (the value can't change without
|
||||
// a navigation); comma-separated so more parts can be hidden in one param.
|
||||
const hideParam = new URLSearchParams(window.location.search).get("hide");
|
||||
const HIDDEN = new Set(hideParam ? hideParam.split(",").filter(Boolean) : []);
|
||||
|
||||
|
|
@ -95,16 +93,13 @@ export function Root() {
|
|||
const [openPanel, setOpenPanel] = useState<OpenPanel>(null);
|
||||
const liveStreamRef = useRef<LiveStreamHandle>(null);
|
||||
const hideHeader = HIDDEN.has("header");
|
||||
const hideInput = HIDDEN.has("input");
|
||||
// Body classes, not inline styles: agent.css's `--agent-header-h`/
|
||||
// `--agent-header-real-h`/`--agent-composer-h` custom properties already
|
||||
// drive every dependent calc() (overlay offset, terminal scroll padding)
|
||||
// — zeroing them here collapses all of that in one place instead of
|
||||
// touching each rule.
|
||||
// `--agent-header-real-h` custom properties already drive every
|
||||
// dependent calc() (overlay offset, terminal scroll padding) — zeroing
|
||||
// them here collapses all of that in one place instead of touching each rule.
|
||||
useEffect(() => {
|
||||
document.body.classList.toggle("agent-hide-header", hideHeader);
|
||||
document.body.classList.toggle("agent-hide-input", hideInput);
|
||||
}, [hideHeader, hideInput]);
|
||||
}, [hideHeader]);
|
||||
|
||||
// Ticks the status badge's "thinking Xm Ys" age once a second,
|
||||
// independent of the ~4s `/api/state` poll cadence — without its own
|
||||
|
|
@ -134,17 +129,6 @@ export function Root() {
|
|||
? `${state.label} // ${state.hive_name}`
|
||||
: `${tab} // hyperhive`;
|
||||
}, [state?.label, state?.qualified_label, state?.hive_name]);
|
||||
const termInput = hideInput ? null : (
|
||||
<footer class="agent-composer">
|
||||
<TermInput
|
||||
label={state?.label ?? "…"}
|
||||
online={state?.status === "online"}
|
||||
onLocalNote={(text) => liveStreamRef.current?.pushNote(text)}
|
||||
onClear={() => liveStreamRef.current?.clear()}
|
||||
/>
|
||||
</footer>
|
||||
);
|
||||
|
||||
const pills = (
|
||||
<>
|
||||
<HeaderPill
|
||||
|
|
@ -237,7 +221,6 @@ export function Root() {
|
|||
<main className="agent-main">
|
||||
<LiveStream ref={liveStreamRef} />
|
||||
</main>
|
||||
{termInput}
|
||||
{panel}
|
||||
</>
|
||||
);
|
||||
|
|
@ -363,7 +346,6 @@ export function Root() {
|
|||
) : null}
|
||||
<LiveStream ref={liveStreamRef} />
|
||||
</main>
|
||||
{termInput}
|
||||
{panel}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
/* ─── full-screen layout overrides ─────────────────────────────────
|
||||
The agent page mounts a full-viewport terminal under a fixed
|
||||
frosted-glass header + composer pair. See docs/web-ui/agent.md::Per-agent
|
||||
frosted-glass header. See docs/web-ui/agent.md::Per-agent
|
||||
page for the layer / pill / side-panel structure; rules below
|
||||
override the in-page defaults from @hive/shared. */
|
||||
|
||||
|
|
@ -25,13 +25,12 @@
|
|||
padding) alongside the two-row main column (title + nav-links
|
||||
on top, state strip below). */
|
||||
--agent-header-h: 6em;
|
||||
--agent-composer-h: 3.6em;
|
||||
--agent-frost-bg: color-mix(in srgb, var(--bg) 72%, transparent);
|
||||
--agent-frost-blur: blur(12px) saturate(140%);
|
||||
}
|
||||
|
||||
/* `?hide=header,input` (per-agent-page embed mode): `Root.tsx` toggles
|
||||
these body classes when it omits `<Header>`/the composer footer.
|
||||
/* `?hide=header` (per-agent-page embed mode): `Root.tsx` toggles
|
||||
this body class when it omits `<Header>`.
|
||||
Zeroing the custom properties here — rather than overriding each
|
||||
dependent rule — collapses every calc() that reads them (overlay
|
||||
offset, terminal scroll padding) in one place. */
|
||||
|
|
@ -39,9 +38,6 @@ body.agent-hide-header {
|
|||
--agent-header-h: 0px;
|
||||
--agent-header-real-h: 0px;
|
||||
}
|
||||
body.agent-hide-input {
|
||||
--agent-composer-h: 0px;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
|
|
@ -266,30 +262,6 @@ h3 {
|
|||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.agent-composer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 30;
|
||||
min-height: var(--agent-composer-h);
|
||||
background: var(--agent-frost-bg);
|
||||
-webkit-backdrop-filter: var(--agent-frost-blur);
|
||||
backdrop-filter: var(--agent-frost-blur);
|
||||
border-top: 1px solid var(--purple-dim);
|
||||
box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.agent-composer .term-input {
|
||||
/* The composer is its own chrome now — drop the in-terminal-wrap
|
||||
padding the legacy layout assumed. */
|
||||
padding: 0.45em 1em;
|
||||
}
|
||||
.agent-composer .term-input .sendform-term {
|
||||
/* No dashed top-border in the floating composer — the box-shadow
|
||||
and frosted border already separate it from the terminal. */
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
.meta {
|
||||
color: var(--muted);
|
||||
font-size: 0.85em;
|
||||
|
|
@ -618,9 +590,8 @@ pre.diff {
|
|||
(StatusChips.tsx's `StatusMenu`, `@hive/shared`'s
|
||||
`.ui-dropdown-item-danger`). */
|
||||
/* Orphaned tombstone — `.btn-new-session` round-pill moved into the
|
||||
overflow menu, then out of the header entirely: it's the
|
||||
`/new-session` slash command now (TermInput.tsx), no button/chip
|
||||
left to style. */
|
||||
overflow menu, then out of the header entirely: it's a row of the
|
||||
state badge's menu now, no button/chip left to style. */
|
||||
/* Markup carries `hive-pill` alongside `state-badge` (shared pill.css
|
||||
supplies the shape); this rule is only the transition + state-
|
||||
specific colour below. */
|
||||
|
|
@ -681,8 +652,8 @@ pre.diff {
|
|||
black frame for the in-page case; the agent page now owns the
|
||||
whole viewport so the frame chrome would be redundant noise.
|
||||
`.live.terminal` similarly drops the in-page max-height cap so
|
||||
it can fill the main area top-to-bottom; the floating header +
|
||||
composer overlay it via fixed positioning. */
|
||||
it can fill the main area top-to-bottom; the floating header
|
||||
overlays it via fixed positioning. */
|
||||
.agent-main .terminal-wrap {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
|
@ -698,94 +669,28 @@ pre.diff {
|
|||
inset: 0;
|
||||
height: auto;
|
||||
max-height: none;
|
||||
/* Scroll behind the floating header/composer, but keep the first
|
||||
/* Scroll behind the floating header, but keep the first
|
||||
and last rows reachable with extra padding inside the scroll
|
||||
area. scroll-padding-* keeps anchor-jumps (the `↓ N new` pill,
|
||||
focus restore) clear of the floats too. */
|
||||
padding-top: calc(var(--agent-header-real-h, var(--agent-header-h)) + 0.8em);
|
||||
padding-bottom: calc(var(--agent-composer-h) + 0.8em);
|
||||
padding-bottom: 0.8em;
|
||||
scroll-padding-top: calc(
|
||||
var(--agent-header-real-h, var(--agent-header-h)) + 0.8em
|
||||
);
|
||||
scroll-padding-bottom: calc(var(--agent-composer-h) + 0.8em);
|
||||
scroll-padding-bottom: 0.8em;
|
||||
overflow: auto;
|
||||
}
|
||||
/* Tail pill (↓ N new): nudged up so it floats clear of the composer
|
||||
rather than colliding with the frosted bar. z-index bumped above
|
||||
the composer (z-30) so the pill sits on the top layer instead of
|
||||
being clipped by the floating chrome. (Pill is anchored in
|
||||
/* Tail pill (↓ N new): z-index bumped above the floating chrome so the
|
||||
pill sits on the top layer instead of being clipped by it. (Pill is anchored in
|
||||
`.agent-main` rather than `.terminal-wrap` — see
|
||||
docs/web-ui/agent.md::Per-agent page Terminal-wrap paragraph for the
|
||||
backdrop-filter stacking-context rationale.) */
|
||||
.agent-main .tail-pill {
|
||||
bottom: calc(var(--agent-composer-h) + 0.6em);
|
||||
bottom: 0.6em;
|
||||
z-index: 35;
|
||||
}
|
||||
|
||||
/* Composer chrome lives inside the fixed `.agent-composer` defined
|
||||
further up. The base rules below stay scoped to whichever ancestor
|
||||
owns it. */
|
||||
.term-input .sendform-term {
|
||||
display: flex;
|
||||
/* mara, live: "bottom input row text not aligned / inconsistent
|
||||
sizes." Real cause: `.prompt` (1em, no line-height override) and
|
||||
`.submit-hint` (0.8em, no line-height override) each get a
|
||||
browser-default line-box at a different em-relative size, so
|
||||
`align-items: flex-start` (their old value) pinned each one's
|
||||
line-box top edge to the row's top — same nominal `padding-top`
|
||||
but a DIFFERENT absolute offset per element, since 0.25em means
|
||||
something different at 1em vs 0.8em font-size. `center` sidesteps
|
||||
the whole "line-box for text this size" mismatch by aligning each
|
||||
item's own box middle instead of a top edge that was never
|
||||
actually equal. Still correct once the textarea grows multi-line
|
||||
(a common composer-bar convention — Slack/Discord centre their
|
||||
send affordance against a grown box too, not pin it to the first
|
||||
line). */
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
/* The dashed in-frame separator is dropped — see the
|
||||
.agent-composer .term-input override above for the floating-bar
|
||||
variant. */
|
||||
border-top: 1px dashed var(--purple-dim);
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
.term-input .prompt {
|
||||
color: var(--green);
|
||||
text-shadow: 0 0 6px color-mix(in srgb, var(--green) 60%, transparent);
|
||||
user-select: none;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.term-input textarea {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
color: var(--fg);
|
||||
font-family: inherit;
|
||||
font-size: 1em;
|
||||
padding: 0.2em 0;
|
||||
caret-color: var(--green);
|
||||
resize: none;
|
||||
overflow-y: auto;
|
||||
line-height: 1.4;
|
||||
min-height: 1.4em;
|
||||
}
|
||||
.term-input textarea::placeholder {
|
||||
color: var(--muted);
|
||||
}
|
||||
.term-input .submit-hint {
|
||||
color: var(--muted);
|
||||
font-size: 0.8em;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.term-input.disabled .prompt {
|
||||
color: var(--muted);
|
||||
text-shadow: none;
|
||||
}
|
||||
.term-input.disabled textarea {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ─── side panel (singleton drawer) ────────────────────────────────
|
||||
Inbox + loose-ends details open here instead of expanding inline. The
|
||||
drawer chrome itself (backdrop, drawer, resize handle, header, title,
|
||||
|
|
|
|||
|
|
@ -33,24 +33,21 @@ import { Row } from "@hive/shared/term-row.js";
|
|||
const NEAR_BOTTOM_PX = 48;
|
||||
const LOAD_MORE_SCROLL_PX = 80;
|
||||
|
||||
/** Imperative escape hatch for TermInput's local-only slash commands
|
||||
* (`/help`, `/clear`) — same shape as app.js's old `termAPI` object,
|
||||
* kept as a ref handle rather than lifting the whole row array up to
|
||||
* Root so LiveStream/useLiveStream still own their state privately. */
|
||||
/** Imperative escape hatch for the page's local-only notes (the result of
|
||||
* a state-badge action) — kept as a ref handle rather than lifting the whole
|
||||
* row array up to Root so LiveStream/useLiveStream still own their state
|
||||
* privately. */
|
||||
export interface LiveStreamHandle {
|
||||
pushNote: (text: string) => void;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
export const LiveStream = forwardRef<LiveStreamHandle, object>(
|
||||
function LiveStream(_props, ref) {
|
||||
const { rows, hasMore, loadingMore, loadMore, pushLocalNote, clearLocal } =
|
||||
const { rows, hasMore, loadingMore, loadMore, pushLocalNote } =
|
||||
useLiveStream();
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({ pushNote: pushLocalNote, clear: clearLocal }),
|
||||
[pushLocalNote, clearLocal],
|
||||
);
|
||||
useImperativeHandle(ref, () => ({ pushNote: pushLocalNote }), [
|
||||
pushLocalNote,
|
||||
]);
|
||||
const logRef = useRef<HTMLDivElement>(null);
|
||||
const [stickToBottom, setStickToBottom] = useState(true);
|
||||
const [unseen, setUnseen] = useState(0);
|
||||
|
|
|
|||
|
|
@ -1,187 +0,0 @@
|
|||
// <TermInput> — the fixed composer at the bottom of the page. Sends a
|
||||
// plain message to this agent's own turn loop (`POST send`), or
|
||||
// intercepts a `/slash` command locally (never sent to the agent).
|
||||
// Ported from app.js's `renderTermInput`/`handleSlashCommand`/
|
||||
// `completeSlash`, with one deliberate UX change: `/new-session` and
|
||||
// `/logout` used to pop the old shadow-DOM `themedConfirm` modal
|
||||
// (@hive/shared/modal.js) before firing — this rewrite's destructive
|
||||
// actions all avoid that widget family (see SidePanel.tsx's/
|
||||
// useConfirmClick.ts's file comments for why), and a modal doesn't fit
|
||||
// a text-input flow anyway. Instead: typing the command once arms it
|
||||
// (a local note explains what confirming does), typing it again within
|
||||
// the same "armed" state fires it — a keyboard-native two-step confirm.
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
import {
|
||||
postCancelTurn,
|
||||
postCompact,
|
||||
postNewSession,
|
||||
postLogout,
|
||||
postSend,
|
||||
} from "../lib/termActions.js";
|
||||
|
||||
export interface TermInputProps {
|
||||
label: string;
|
||||
online: boolean;
|
||||
onLocalNote: (text: string) => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
const SLASH_COMMANDS: { name: string; desc: string }[] = [
|
||||
{ name: "/help", desc: "list slash commands" },
|
||||
{ name: "/clear", desc: "wipe the terminal panel (local-only)" },
|
||||
{ name: "/cancel", desc: "SIGINT the in-flight claude turn" },
|
||||
{ name: "/compact", desc: "compact the persistent claude session" },
|
||||
{
|
||||
name: "/new-session",
|
||||
desc: "fresh claude session next turn (type twice to confirm)",
|
||||
},
|
||||
{
|
||||
name: "/logout",
|
||||
desc: "rotate OAuth creds + park in needs-login (type twice to confirm)",
|
||||
},
|
||||
];
|
||||
|
||||
const MAX_PX = 12 * 16; // ~8 lines @ 1.5 line-height, 1em base
|
||||
|
||||
function completeSlash(prefix: string): string | null {
|
||||
const matches = SLASH_COMMANDS.filter((c) => c.name.startsWith(prefix));
|
||||
if (!matches.length) return null;
|
||||
const idx = matches.findIndex((c) => c.name === prefix);
|
||||
return matches[(idx + 1) % matches.length].name;
|
||||
}
|
||||
|
||||
export function TermInput({
|
||||
label,
|
||||
online,
|
||||
onLocalNote,
|
||||
onClear,
|
||||
}: TermInputProps) {
|
||||
const [value, setValue] = useState("");
|
||||
const taRef = useRef<HTMLTextAreaElement>(null);
|
||||
// Which destructive command is currently "armed" (typed once, waiting
|
||||
// for the confirming repeat) — any other command clears it.
|
||||
const armedRef = useRef<string | null>(null);
|
||||
|
||||
function grow() {
|
||||
const ta = taRef.current;
|
||||
if (!ta) return;
|
||||
ta.style.height = "auto";
|
||||
ta.style.height = Math.min(ta.scrollHeight, MAX_PX) + "px";
|
||||
}
|
||||
|
||||
function reportFailure(label_: string, detail?: string) {
|
||||
onLocalNote(`✗ ${label_} failed${detail ? ": " + detail : ""}`);
|
||||
}
|
||||
|
||||
function handleSlash(line: string): boolean {
|
||||
const trimmed = line.trim();
|
||||
const [cmd] = trimmed.split(/\s+/);
|
||||
const wasArmed = armedRef.current === cmd;
|
||||
armedRef.current = null;
|
||||
|
||||
switch (cmd) {
|
||||
case "/help":
|
||||
onLocalNote("/help");
|
||||
for (const c of SLASH_COMMANDS)
|
||||
onLocalNote(` ${c.name.padEnd(13)} — ${c.desc}`);
|
||||
return true;
|
||||
case "/clear":
|
||||
onClear();
|
||||
onLocalNote(
|
||||
"· terminal cleared (local view only — server history kept)",
|
||||
);
|
||||
return true;
|
||||
case "/cancel":
|
||||
postCancelTurn().then(
|
||||
(r) => !r.ok && reportFailure("/cancel", r.detail),
|
||||
);
|
||||
return true;
|
||||
case "/compact":
|
||||
postCompact().then((r) => !r.ok && reportFailure("/compact", r.detail));
|
||||
return true;
|
||||
case "/new-session":
|
||||
if (wasArmed) {
|
||||
postNewSession().then(
|
||||
(r) => !r.ok && reportFailure("/new-session", r.detail),
|
||||
);
|
||||
} else {
|
||||
armedRef.current = "/new-session";
|
||||
onLocalNote(
|
||||
"⚠ drops all prior --continue context. type /new-session again to confirm.",
|
||||
);
|
||||
}
|
||||
return true;
|
||||
case "/logout":
|
||||
if (wasArmed) {
|
||||
postLogout().then((r) => !r.ok && reportFailure("/logout", r.detail));
|
||||
} else {
|
||||
armedRef.current = "/logout";
|
||||
onLocalNote(
|
||||
"⚠ SIGINTs the current turn + rotates OAuth credentials. type /logout again to confirm.",
|
||||
);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
onLocalNote(`✗ unknown slash command: ${cmd} — try /help`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
const line = value;
|
||||
if (!line.trim()) return;
|
||||
setValue("");
|
||||
requestAnimationFrame(grow);
|
||||
if (line.trim().startsWith("/")) {
|
||||
handleSlash(line);
|
||||
return;
|
||||
}
|
||||
armedRef.current = null;
|
||||
postSend(line).then((r) => !r.ok && reportFailure("send", r.detail));
|
||||
}
|
||||
|
||||
function onInput(e: Event) {
|
||||
setValue((e.currentTarget as HTMLTextAreaElement).value);
|
||||
grow();
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Tab" && value.startsWith("/") && !value.includes(" ")) {
|
||||
const next = completeSlash(value);
|
||||
if (next) {
|
||||
e.preventDefault();
|
||||
setValue(next);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
||||
e.preventDefault();
|
||||
submit();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={`term-input${online ? "" : " disabled"}`}>
|
||||
<form
|
||||
class="sendform-term"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submit();
|
||||
}}
|
||||
>
|
||||
<span class="prompt">operator@{label} ▸</span>
|
||||
<textarea
|
||||
ref={taRef}
|
||||
value={value}
|
||||
placeholder={`message ${label}…`}
|
||||
rows={1}
|
||||
disabled={!online}
|
||||
autocomplete="off"
|
||||
onInput={onInput}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<span class="submit-hint">↵ send · ⇧↵ newline · /help</span>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -28,13 +28,9 @@ export interface UseLiveStreamResult {
|
|||
hasMore: boolean;
|
||||
loadingMore: boolean;
|
||||
loadMore: () => void;
|
||||
/** Append a local-only note row (TermInput's `/help` output, error
|
||||
* feedback on a failed slash-command POST) — never sent anywhere,
|
||||
* purely a client-side echo. */
|
||||
/** Append a local-only note row (error feedback on a failed state-badge
|
||||
* action) — never sent anywhere, purely a client-side echo. */
|
||||
pushLocalNote: (text: string) => void;
|
||||
/** Wipe the local view only (server-side history is untouched) —
|
||||
* TermInput's `/clear`. */
|
||||
clearLocal: () => void;
|
||||
}
|
||||
|
||||
function appendRow(rows: TermRow[], row: TermRow): TermRow[] {
|
||||
|
|
@ -234,9 +230,5 @@ export function useLiveStream(
|
|||
}),
|
||||
);
|
||||
}
|
||||
function clearLocal() {
|
||||
setRows([]);
|
||||
}
|
||||
|
||||
return { rows, hasMore, loadingMore, loadMore, pushLocalNote, clearLocal };
|
||||
return { rows, hasMore, loadingMore, loadMore, pushLocalNote };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
</head>
|
||||
<body class="agent-shell">
|
||||
<!-- The Preact rewrite owns everything below — header, terminal,
|
||||
composer, side panel. See main.tsx/Root.tsx for the component
|
||||
side panel. See main.tsx/Root.tsx for the component
|
||||
tree; docs/web-ui/agent.md for the design. -->
|
||||
<div id="preact-root"></div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,9 @@
|
|||
// Same-origin fetch POST helpers for the term-input slash commands
|
||||
// ({ ok, detail } rather than throwing), reused by TermInput.tsx.
|
||||
async function post(
|
||||
path: string,
|
||||
body?: URLSearchParams,
|
||||
): Promise<{ ok: boolean; detail?: string }> {
|
||||
// Same-origin fetch POST helpers for the state badge's actions
|
||||
// ({ ok, detail } rather than throwing).
|
||||
async function post(path: string): Promise<{ ok: boolean; detail?: string }> {
|
||||
try {
|
||||
const resp = await fetch(path, {
|
||||
method: "POST",
|
||||
headers: body
|
||||
? { "Content-Type": "application/x-www-form-urlencoded" }
|
||||
: undefined,
|
||||
body,
|
||||
redirect: "manual",
|
||||
});
|
||||
const ok =
|
||||
|
|
@ -32,8 +25,5 @@ async function post(
|
|||
}
|
||||
|
||||
export const postCancelTurn = () => post("api/cancel");
|
||||
export const postCompact = () => post("api/compact");
|
||||
export const postNewSession = () => post("api/new-session");
|
||||
export const postLogout = () => post("api/logout");
|
||||
export const postSend = (body: string) =>
|
||||
post("send", new URLSearchParams({ body }));
|
||||
|
|
|
|||
Loading…
Reference in a new issue