fix: make control ui chat scroll page

This commit is contained in:
Peter Steinberger
2026-01-05 00:15:13 +00:00
parent bcdfe461d4
commit d6933b074a
5 changed files with 77 additions and 7 deletions

View File

@@ -13,6 +13,7 @@
### Fixes ### Fixes
- Android: tapping the foreground service notification brings the app to the front. (#179) — thanks @Syhids - Android: tapping the foreground service notification brings the app to the front. (#179) — thanks @Syhids
- Cron tool passes `id` to the gateway for update/remove/run/runs (keeps `jobId` input). (#180) — thanks @adamgall - Cron tool passes `id` to the gateway for update/remove/run/runs (keeps `jobId` input). (#180) — thanks @adamgall
- Control UI: chat view uses page scroll with sticky header/sidebar and fixed composer (no inner scroll frame).
- macOS: treat location permission as always-only to avoid iOS-only enums. (#165) — thanks @Nachx639 - macOS: treat location permission as always-only to avoid iOS-only enums. (#165) — thanks @Nachx639
- macOS: make generated gateway protocol models `Sendable` for Swift 6 strict concurrency. (#195) — thanks @andranik-sahakyan - macOS: make generated gateway protocol models `Sendable` for Swift 6 strict concurrency. (#195) — thanks @andranik-sahakyan
- WhatsApp: suppress typing indicator during heartbeat background tasks. (#190) — thanks @mcinteerj - WhatsApp: suppress typing indicator during heartbeat background tasks. (#190) — thanks @mcinteerj

View File

@@ -429,6 +429,16 @@
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
} }
.chat {
display: flex;
flex-direction: column;
min-height: 0;
}
.shell--chat .chat {
flex: 1;
}
.chat-header { .chat-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -460,8 +470,9 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
max-height: 60vh; flex: 1;
overflow: auto; max-height: none;
overflow: visible;
padding: 14px 12px; padding: 14px 12px;
min-width: 0; min-width: 0;
border-radius: 16px; border-radius: 16px;
@@ -731,6 +742,16 @@
gap: 10px; gap: 10px;
} }
.shell--chat .chat-compose {
position: sticky;
bottom: 0;
z-index: 5;
margin-top: 0;
padding-top: 12px;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, var(--panel) 35%);
border-top: 1px solid var(--border);
}
.chat-compose__field { .chat-compose__field {
gap: 4px; gap: 4px;
} }

View File

@@ -1,4 +1,6 @@
.shell { .shell {
--shell-pad: 18px;
--shell-gap: 18px;
min-height: 100vh; min-height: 100vh;
display: grid; display: grid;
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr); grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
@@ -6,13 +8,16 @@
grid-template-areas: grid-template-areas:
"topbar topbar" "topbar topbar"
"nav content"; "nav content";
gap: 18px; gap: var(--shell-gap);
padding: 18px; padding: var(--shell-pad);
animation: dashboard-enter 0.6s ease-out; animation: dashboard-enter 0.6s ease-out;
} }
.topbar { .topbar {
grid-area: topbar; grid-area: topbar;
position: sticky;
top: var(--shell-pad);
z-index: 20;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -51,6 +56,16 @@
.nav { .nav {
grid-area: nav; grid-area: nav;
position: sticky;
top: calc(
var(--shell-pad) + var(--topbar-height, 0px) + var(--shell-gap)
);
align-self: start;
max-height: calc(
100vh - var(--topbar-height, 0px) - var(--shell-gap) -
var(--shell-pad) - var(--shell-pad)
);
overflow: auto;
padding: 16px; padding: 16px;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 20px; border-radius: 20px;
@@ -132,6 +147,14 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
min-height: 0;
}
.shell--chat .content {
min-height: calc(
100vh - var(--topbar-height, 0px) - var(--shell-gap) -
var(--shell-pad) - var(--shell-pad)
);
} }
.content-header { .content-header {
@@ -204,16 +227,19 @@
@media (max-width: 1100px) { @media (max-width: 1100px) {
.shell { .shell {
--shell-pad: 12px;
--shell-gap: 12px;
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: auto auto 1fr; grid-template-rows: auto auto 1fr;
grid-template-areas: grid-template-areas:
"topbar" "topbar"
"nav" "nav"
"content"; "content";
padding: 12px;
} }
.nav { .nav {
position: static;
max-height: none;
display: flex; display: flex;
gap: 16px; gap: 16px;
overflow-x: auto; overflow-x: auto;
@@ -234,6 +260,7 @@
} }
.topbar { .topbar {
position: static;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 12px; gap: 12px;

View File

@@ -184,9 +184,10 @@ export function renderApp(state: AppViewState) {
const sessionsCount = state.sessionsResult?.count ?? null; const sessionsCount = state.sessionsResult?.count ?? null;
const cronNext = state.cronStatus?.nextWakeAtMs ?? null; const cronNext = state.cronStatus?.nextWakeAtMs ?? null;
const chatDisabledReason = state.connected ? null : "Disconnected from gateway."; const chatDisabledReason = state.connected ? null : "Disconnected from gateway.";
const isChat = state.tab === "chat";
return html` return html`
<div class="shell"> <div class="shell ${isChat ? "shell--chat" : ""}">
<header class="topbar"> <header class="topbar">
<div class="brand"> <div class="brand">
<div class="brand-title">Clawdbot Control</div> <div class="brand-title">Clawdbot Control</div>
@@ -211,7 +212,7 @@ export function renderApp(state: AppViewState) {
`, `,
)} )}
</aside> </aside>
<main class="content"> <main class="content ${isChat ? "content--chat" : ""}">
<section class="content-header"> <section class="content-header">
<div> <div>
<div class="page-title">${titleForTab(state.tab)}</div> <div class="page-title">${titleForTab(state.tab)}</div>

View File

@@ -348,6 +348,7 @@ export class ClawdbotApp extends LitElement {
private popStateHandler = () => this.onPopState(); private popStateHandler = () => this.onPopState();
private themeMedia: MediaQueryList | null = null; private themeMedia: MediaQueryList | null = null;
private themeMediaHandler: ((event: MediaQueryListEvent) => void) | null = null; private themeMediaHandler: ((event: MediaQueryListEvent) => void) | null = null;
private topbarObserver: ResizeObserver | null = null;
createRenderRoot() { createRenderRoot() {
return this; return this;
@@ -365,10 +366,16 @@ export class ClawdbotApp extends LitElement {
this.startNodesPolling(); this.startNodesPolling();
} }
protected firstUpdated() {
this.observeTopbar();
}
disconnectedCallback() { disconnectedCallback() {
window.removeEventListener("popstate", this.popStateHandler); window.removeEventListener("popstate", this.popStateHandler);
this.stopNodesPolling(); this.stopNodesPolling();
this.detachThemeListener(); this.detachThemeListener();
this.topbarObserver?.disconnect();
this.topbarObserver = null;
super.disconnectedCallback(); super.disconnectedCallback();
} }
@@ -437,6 +444,19 @@ export class ClawdbotApp extends LitElement {
}); });
} }
private observeTopbar() {
if (typeof ResizeObserver === "undefined") return;
const topbar = this.querySelector(".topbar");
if (!topbar) return;
const update = () => {
const { height } = topbar.getBoundingClientRect();
this.style.setProperty("--topbar-height", `${height}px`);
};
update();
this.topbarObserver = new ResizeObserver(() => update());
this.topbarObserver.observe(topbar);
}
private startNodesPolling() { private startNodesPolling() {
if (this.nodesPollInterval != null) return; if (this.nodesPollInterval != null) return;
this.nodesPollInterval = window.setInterval( this.nodesPollInterval = window.setInterval(