@@ -7,6 +7,7 @@
|
|||||||
- Memory: allow custom OpenAI-compatible embedding endpoints for memory search (remote baseUrl/apiKey/headers). (#819 — thanks @mukhtharcm)
|
- Memory: allow custom OpenAI-compatible embedding endpoints for memory search (remote baseUrl/apiKey/headers). (#819 — thanks @mukhtharcm)
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
- Control UI: keep chat scroll position unless user is near the bottom. (#217 — thanks @thewilloftheshadow)
|
||||||
- Fallback: treat credential validation failures ("no credentials found", "no API key found") as auth errors that trigger model fallback. (#822 — thanks @sebslight)
|
- Fallback: treat credential validation failures ("no credentials found", "no API key found") as auth errors that trigger model fallback. (#822 — thanks @sebslight)
|
||||||
- Telegram: preserve forum topic thread ids, including General topic replies. (#727 — thanks @thewilloftheshadow)
|
- Telegram: preserve forum topic thread ids, including General topic replies. (#727 — thanks @thewilloftheshadow)
|
||||||
- Telegram: persist polling update offsets across restarts to avoid duplicate updates. (#739 — thanks @thewilloftheshadow)
|
- Telegram: persist polling update offsets across restarts to avoid duplicate updates. (#739 — thanks @thewilloftheshadow)
|
||||||
|
|||||||
@@ -494,6 +494,7 @@ export function renderApp(state: AppViewState) {
|
|||||||
...state.settings,
|
...state.settings,
|
||||||
useNewChatLayout: !state.settings.useNewChatLayout,
|
useNewChatLayout: !state.settings.useNewChatLayout,
|
||||||
}),
|
}),
|
||||||
|
onChatScroll: (event) => state.handleChatScroll(event),
|
||||||
onDraftChange: (next) => (state.chatMessage = next),
|
onDraftChange: (next) => (state.chatMessage = next),
|
||||||
onSend: () => state.handleSendChat(),
|
onSend: () => state.handleSendChat(),
|
||||||
canAbort: Boolean(state.chatRunId),
|
canAbort: Boolean(state.chatRunId),
|
||||||
|
|||||||
@@ -400,6 +400,7 @@ export class ClawdbotApp extends LitElement {
|
|||||||
private chatScrollFrame: number | null = null;
|
private chatScrollFrame: number | null = null;
|
||||||
private chatScrollTimeout: number | null = null;
|
private chatScrollTimeout: number | null = null;
|
||||||
private chatHasAutoScrolled = false;
|
private chatHasAutoScrolled = false;
|
||||||
|
private chatUserNearBottom = true;
|
||||||
private nodesPollInterval: number | null = null;
|
private nodesPollInterval: number | null = null;
|
||||||
private logsPollInterval: number | null = null;
|
private logsPollInterval: number | null = null;
|
||||||
private logsScrollFrame: number | null = null;
|
private logsScrollFrame: number | null = null;
|
||||||
@@ -525,10 +526,12 @@ export class ClawdbotApp extends LitElement {
|
|||||||
if (!target) return;
|
if (!target) return;
|
||||||
const distanceFromBottom =
|
const distanceFromBottom =
|
||||||
target.scrollHeight - target.scrollTop - target.clientHeight;
|
target.scrollHeight - target.scrollTop - target.clientHeight;
|
||||||
const shouldStick = force || distanceFromBottom < 200;
|
const shouldStick =
|
||||||
|
force || this.chatUserNearBottom || distanceFromBottom < 200;
|
||||||
if (!shouldStick) return;
|
if (!shouldStick) return;
|
||||||
if (force) this.chatHasAutoScrolled = true;
|
if (force) this.chatHasAutoScrolled = true;
|
||||||
target.scrollTop = target.scrollHeight;
|
target.scrollTop = target.scrollHeight;
|
||||||
|
this.chatUserNearBottom = true;
|
||||||
const retryDelay = force ? 150 : 120;
|
const retryDelay = force ? 150 : 120;
|
||||||
this.chatScrollTimeout = window.setTimeout(() => {
|
this.chatScrollTimeout = window.setTimeout(() => {
|
||||||
this.chatScrollTimeout = null;
|
this.chatScrollTimeout = null;
|
||||||
@@ -536,8 +539,11 @@ export class ClawdbotApp extends LitElement {
|
|||||||
if (!latest) return;
|
if (!latest) return;
|
||||||
const latestDistanceFromBottom =
|
const latestDistanceFromBottom =
|
||||||
latest.scrollHeight - latest.scrollTop - latest.clientHeight;
|
latest.scrollHeight - latest.scrollTop - latest.clientHeight;
|
||||||
if (!force && latestDistanceFromBottom >= 250) return;
|
const shouldStickRetry =
|
||||||
|
force || this.chatUserNearBottom || latestDistanceFromBottom < 200;
|
||||||
|
if (!shouldStickRetry) return;
|
||||||
latest.scrollTop = latest.scrollHeight;
|
latest.scrollTop = latest.scrollHeight;
|
||||||
|
this.chatUserNearBottom = true;
|
||||||
}, retryDelay);
|
}, retryDelay);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -600,6 +606,14 @@ export class ClawdbotApp extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleChatScroll(event: Event) {
|
||||||
|
const container = event.currentTarget as HTMLElement | null;
|
||||||
|
if (!container) return;
|
||||||
|
const distanceFromBottom =
|
||||||
|
container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||||
|
this.chatUserNearBottom = distanceFromBottom < 200;
|
||||||
|
}
|
||||||
|
|
||||||
handleLogsScroll(event: Event) {
|
handleLogsScroll(event: Event) {
|
||||||
const container = event.currentTarget as HTMLElement | null;
|
const container = event.currentTarget as HTMLElement | null;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -630,6 +644,7 @@ export class ClawdbotApp extends LitElement {
|
|||||||
|
|
||||||
resetChatScroll() {
|
resetChatScroll() {
|
||||||
this.chatHasAutoScrolled = false;
|
this.chatHasAutoScrolled = false;
|
||||||
|
this.chatUserNearBottom = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleToolOutput(id: string, expanded: boolean) {
|
toggleToolOutput(id: string, expanded: boolean) {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export type ChatProps = {
|
|||||||
onOpenSidebar?: (content: string) => void;
|
onOpenSidebar?: (content: string) => void;
|
||||||
onCloseSidebar?: () => void;
|
onCloseSidebar?: () => void;
|
||||||
onSplitRatioChange?: (ratio: number) => void;
|
onSplitRatioChange?: (ratio: number) => void;
|
||||||
|
onChatScroll?: (event: Event) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function renderChat(props: ChatProps) {
|
export function renderChat(props: ChatProps) {
|
||||||
@@ -109,7 +110,12 @@ export function renderChat(props: ChatProps) {
|
|||||||
class="chat-main"
|
class="chat-main"
|
||||||
style="flex: ${sidebarOpen ? `0 0 ${splitRatio * 100}%` : "1 1 100%"}"
|
style="flex: ${sidebarOpen ? `0 0 ${splitRatio * 100}%` : "1 1 100%"}"
|
||||||
>
|
>
|
||||||
<div class="chat-thread" role="log" aria-live="polite">
|
<div
|
||||||
|
class="chat-thread"
|
||||||
|
role="log"
|
||||||
|
aria-live="polite"
|
||||||
|
@scroll=${props.onChatScroll}
|
||||||
|
>
|
||||||
${props.loading
|
${props.loading
|
||||||
? html`<div class="muted">Loading chat…</div>`
|
? html`<div class="muted">Loading chat…</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
|||||||
Reference in New Issue
Block a user