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() {
|
export function Root() {
|
||||||
useApplyThemeOverride(THEME_KEY);
|
useApplyThemeOverride(THEME_KEY);
|
||||||
useApplyMotionOverride(MOTION_KEY);
|
useApplyMotionOverride(MOTION_KEY);
|
||||||
|
|
@ -82,6 +90,17 @@ export function Root() {
|
||||||
const { todos, refresh: refreshTodos } = useTodos();
|
const { todos, refresh: refreshTodos } = useTodos();
|
||||||
const [openPanel, setOpenPanel] = useState<OpenPanel>(null);
|
const [openPanel, setOpenPanel] = useState<OpenPanel>(null);
|
||||||
const liveStreamRef = useRef<LiveStreamHandle>(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,
|
// Ticks the status badge's "thinking Xm Ys" age once a second,
|
||||||
// independent of the ~4s `/api/state` poll cadence — without its own
|
// independent of the ~4s `/api/state` poll cadence — without its own
|
||||||
|
|
@ -111,7 +130,7 @@ export function Root() {
|
||||||
? `${state.label} // ${state.hive_name}`
|
? `${state.label} // ${state.hive_name}`
|
||||||
: `${tab} // hyperhive`;
|
: `${tab} // hyperhive`;
|
||||||
}, [state?.label, state?.qualified_label, state?.hive_name]);
|
}, [state?.label, state?.qualified_label, state?.hive_name]);
|
||||||
const termInput = (
|
const termInput = hideInput ? null : (
|
||||||
<footer class="agent-composer">
|
<footer class="agent-composer">
|
||||||
<TermInput
|
<TermInput
|
||||||
label={state?.label ?? "…"}
|
label={state?.label ?? "…"}
|
||||||
|
|
@ -191,22 +210,24 @@ export function Root() {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header label="…" pills={pills}>
|
{hideHeader ? null : (
|
||||||
<StatusChips
|
<Header label="…" pills={pills}>
|
||||||
statusLabel="… connecting"
|
<StatusChips
|
||||||
statusTone="neutral"
|
statusLabel="… connecting"
|
||||||
model=""
|
statusTone="neutral"
|
||||||
availableModels={[]}
|
model=""
|
||||||
onSelectModel={() => {}}
|
availableModels={[]}
|
||||||
effort=""
|
onSelectModel={() => {}}
|
||||||
availableEfforts={[]}
|
effort=""
|
||||||
onSelectEffort={() => {}}
|
availableEfforts={[]}
|
||||||
paused={false}
|
onSelectEffort={() => {}}
|
||||||
onTogglePause={() => {}}
|
paused={false}
|
||||||
thinking={false}
|
onTogglePause={() => {}}
|
||||||
onCancelTurn={() => Promise.resolve()}
|
thinking={false}
|
||||||
/>
|
onCancelTurn={() => Promise.resolve()}
|
||||||
</Header>
|
/>
|
||||||
|
</Header>
|
||||||
|
)}
|
||||||
<main className="agent-main">
|
<main className="agent-main">
|
||||||
<LiveStream ref={liveStreamRef} />
|
<LiveStream ref={liveStreamRef} />
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -266,51 +287,53 @@ export function Root() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header
|
{hideHeader ? null : (
|
||||||
label={state.label}
|
<Header
|
||||||
hiveLabel={
|
label={state.label}
|
||||||
[state.swarm_name, state.hive_name].filter(Boolean).join(" / ") ||
|
hiveLabel={
|
||||||
null
|
[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();
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
/>
|
pills={pills}
|
||||||
</Header>
|
>
|
||||||
|
<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">
|
<main className="agent-main">
|
||||||
{state.status === "needs_login_idle" ||
|
{state.status === "needs_login_idle" ||
|
||||||
state.status === "needs_login_in_progress" ? (
|
state.status === "needs_login_in_progress" ? (
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,19 @@
|
||||||
--agent-frost-blur: blur(12px) saturate(140%);
|
--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,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue