agent: add ?hide= URL param to suppress header/composer chrome
For embedding companions (e.g. trollshell WebKitGTK) that supply their own header/input chrome around the per-agent page and only want the raw terminal feed. Comma-separated so both axes can hide in one param: ?hide=header,input. Zeroes --agent-header-h/--agent-composer-h via body classes rather than conditionally computing padding, so every dependent calc() (overlay offset, terminal scroll padding) collapses in one place.
This commit is contained in:
parent
5af1f6a8e5
commit
ec51ec924a
2 changed files with 97 additions and 61 deletions
|
|
@ -75,6 +75,14 @@ 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.
|
||||
const hideParam = new URLSearchParams(window.location.search).get("hide");
|
||||
const HIDDEN = new Set(hideParam ? hideParam.split(",").filter(Boolean) : []);
|
||||
|
||||
export function Root() {
|
||||
useApplyThemeOverride(THEME_KEY);
|
||||
useApplyMotionOverride(MOTION_KEY);
|
||||
|
|
@ -82,6 +90,17 @@ export function Root() {
|
|||
const { todos, refresh: refreshTodos } = useTodos();
|
||||
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.
|
||||
useEffect(() => {
|
||||
document.body.classList.toggle("agent-hide-header", hideHeader);
|
||||
document.body.classList.toggle("agent-hide-input", hideInput);
|
||||
}, [hideHeader, hideInput]);
|
||||
|
||||
// Ticks the status badge's "thinking Xm Ys" age once a second,
|
||||
// independent of the ~4s `/api/state` poll cadence — without its own
|
||||
|
|
@ -111,7 +130,7 @@ export function Root() {
|
|||
? `${state.label} // ${state.hive_name}`
|
||||
: `${tab} // hyperhive`;
|
||||
}, [state?.label, state?.qualified_label, state?.hive_name]);
|
||||
const termInput = (
|
||||
const termInput = hideInput ? null : (
|
||||
<footer class="agent-composer">
|
||||
<TermInput
|
||||
label={state?.label ?? "…"}
|
||||
|
|
@ -191,22 +210,24 @@ export function Root() {
|
|||
if (!state) {
|
||||
return (
|
||||
<>
|
||||
<Header label="…" pills={pills}>
|
||||
<StatusChips
|
||||
statusLabel="… connecting"
|
||||
statusTone="neutral"
|
||||
model=""
|
||||
availableModels={[]}
|
||||
onSelectModel={() => {}}
|
||||
effort=""
|
||||
availableEfforts={[]}
|
||||
onSelectEffort={() => {}}
|
||||
paused={false}
|
||||
onTogglePause={() => {}}
|
||||
thinking={false}
|
||||
onCancelTurn={() => Promise.resolve()}
|
||||
/>
|
||||
</Header>
|
||||
{hideHeader ? null : (
|
||||
<Header label="…" pills={pills}>
|
||||
<StatusChips
|
||||
statusLabel="… connecting"
|
||||
statusTone="neutral"
|
||||
model=""
|
||||
availableModels={[]}
|
||||
onSelectModel={() => {}}
|
||||
effort=""
|
||||
availableEfforts={[]}
|
||||
onSelectEffort={() => {}}
|
||||
paused={false}
|
||||
onTogglePause={() => {}}
|
||||
thinking={false}
|
||||
onCancelTurn={() => Promise.resolve()}
|
||||
/>
|
||||
</Header>
|
||||
)}
|
||||
<main className="agent-main">
|
||||
<LiveStream ref={liveStreamRef} />
|
||||
</main>
|
||||
|
|
@ -266,51 +287,53 @@ export function Root() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
label={state.label}
|
||||
hiveLabel={
|
||||
[state.swarm_name, state.hive_name].filter(Boolean).join(" / ") ||
|
||||
null
|
||||
}
|
||||
pills={pills}
|
||||
>
|
||||
<StatusChips
|
||||
statusLabel={`${primaryStatus.glyph} ${primaryStatus.text}`}
|
||||
statusTone={primaryStatus.tone}
|
||||
statusTooltip={primaryStatus.tooltip}
|
||||
model={state.model}
|
||||
resolvedModel={state.resolved_model ?? undefined}
|
||||
availableModels={state.available_models}
|
||||
onSelectModel={(name) => {
|
||||
postModel(name).then(() => refresh());
|
||||
}}
|
||||
effort={state.effort}
|
||||
availableEfforts={state.available_efforts}
|
||||
onSelectEffort={(level) => {
|
||||
postEffort(level).then(() => refresh());
|
||||
}}
|
||||
ctxLabel={ctx !== null ? fmtTokens(ctx) : undefined}
|
||||
costLabel={cost !== null ? fmtTokens(cost) : undefined}
|
||||
paused={state.paused}
|
||||
onTogglePause={() => {
|
||||
submitPauseResume(
|
||||
resolveDashboardBase(state.dashboard_port),
|
||||
state.label,
|
||||
state.paused ? "resume" : "pause",
|
||||
).then(() => refresh());
|
||||
}}
|
||||
thinking={effectiveTurnState === "thinking"}
|
||||
onCancelTurn={() =>
|
||||
postCancelTurn().then((r) => {
|
||||
if (!r.ok)
|
||||
liveStreamRef.current?.pushNote(
|
||||
`✗ /cancel failed${r.detail ? ": " + r.detail : ""}`,
|
||||
);
|
||||
refresh();
|
||||
})
|
||||
{hideHeader ? null : (
|
||||
<Header
|
||||
label={state.label}
|
||||
hiveLabel={
|
||||
[state.swarm_name, state.hive_name].filter(Boolean).join(" / ") ||
|
||||
null
|
||||
}
|
||||
/>
|
||||
</Header>
|
||||
pills={pills}
|
||||
>
|
||||
<StatusChips
|
||||
statusLabel={`${primaryStatus.glyph} ${primaryStatus.text}`}
|
||||
statusTone={primaryStatus.tone}
|
||||
statusTooltip={primaryStatus.tooltip}
|
||||
model={state.model}
|
||||
resolvedModel={state.resolved_model ?? undefined}
|
||||
availableModels={state.available_models}
|
||||
onSelectModel={(name) => {
|
||||
postModel(name).then(() => refresh());
|
||||
}}
|
||||
effort={state.effort}
|
||||
availableEfforts={state.available_efforts}
|
||||
onSelectEffort={(level) => {
|
||||
postEffort(level).then(() => refresh());
|
||||
}}
|
||||
ctxLabel={ctx !== null ? fmtTokens(ctx) : undefined}
|
||||
costLabel={cost !== null ? fmtTokens(cost) : undefined}
|
||||
paused={state.paused}
|
||||
onTogglePause={() => {
|
||||
submitPauseResume(
|
||||
resolveDashboardBase(state.dashboard_port),
|
||||
state.label,
|
||||
state.paused ? "resume" : "pause",
|
||||
).then(() => refresh());
|
||||
}}
|
||||
thinking={effectiveTurnState === "thinking"}
|
||||
onCancelTurn={() =>
|
||||
postCancelTurn().then((r) => {
|
||||
if (!r.ok)
|
||||
liveStreamRef.current?.pushNote(
|
||||
`✗ /cancel failed${r.detail ? ": " + r.detail : ""}`,
|
||||
);
|
||||
refresh();
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Header>
|
||||
)}
|
||||
<main className="agent-main">
|
||||
{state.status === "needs_login_idle" ||
|
||||
state.status === "needs_login_in_progress" ? (
|
||||
|
|
|
|||
|
|
@ -30,6 +30,19 @@
|
|||
--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.
|
||||
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. */
|
||||
body.agent-hide-header {
|
||||
--agent-header-h: 0px;
|
||||
--agent-header-real-h: 0px;
|
||||
}
|
||||
body.agent-hide-input {
|
||||
--agent-composer-h: 0px;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
|
|
|
|||
Loading…
Reference in a new issue