feat: add recent session switchers

This commit is contained in:
Peter Steinberger
2026-01-01 23:45:56 +01:00
parent c7c13f2d5e
commit 7c0379ce05
14 changed files with 427 additions and 35 deletions

View File

@@ -324,7 +324,11 @@ export function renderApp(state: AppViewState) {
sessionKey: state.sessionKey,
onSessionKeyChange: (next) => {
state.sessionKey = next;
state.chatMessage = "";
state.chatStream = null;
state.chatRunId = null;
state.applySettings({ ...state.settings, sessionKey: next });
void loadChatHistory(state);
},
thinkingLevel: state.chatThinkingLevel,
loading: state.chatLoading,
@@ -335,6 +339,7 @@ export function renderApp(state: AppViewState) {
connected: state.connected,
canSend: state.connected && hasConnectedMobileNode,
disabledReason: chatDisabledReason,
sessions: state.sessionsResult,
onRefresh: () => loadChatHistory(state),
onDraftChange: (next) => (state.chatMessage = next),
onSend: () => state.handleSendChat(),

View File

@@ -363,7 +363,7 @@ export class ClawdisApp extends LitElement {
if (this.tab === "skills") await loadSkills(this);
if (this.tab === "nodes") await loadNodes(this);
if (this.tab === "chat") {
await loadChatHistory(this);
await Promise.all([loadChatHistory(this), loadSessions(this)]);
this.scheduleChatScroll();
}
if (this.tab === "config") await loadConfig(this);

View File

@@ -1,5 +1,7 @@
import { html, nothing } from "lit";
import type { SessionsListResult } from "../types";
export type ChatProps = {
sessionKey: string;
onSessionKeyChange: (next: string) => void;
@@ -12,6 +14,7 @@ export type ChatProps = {
connected: boolean;
canSend: boolean;
disabledReason: string | null;
sessions: SessionsListResult | null;
onRefresh: () => void;
onDraftChange: (next: string) => void;
onSend: () => void;
@@ -20,6 +23,7 @@ export type ChatProps = {
export function renderChat(props: ChatProps) {
const canInteract = props.connected;
const canCompose = props.canSend && !props.sending;
const sessionOptions = resolveSessionOptions(props.sessionKey, props.sessions);
const composePlaceholder = (() => {
if (!props.connected) return "Connect to the gateway to start chatting…";
if (!props.canSend) return "Connect an iOS/Android node to enable Web Chat + Talk…";
@@ -32,12 +36,16 @@ export function renderChat(props: ChatProps) {
<div class="chat-header__left">
<label class="field chat-session">
<span>Session Key</span>
<input
<select
.value=${props.sessionKey}
?disabled=${!canInteract}
@input=${(e: Event) =>
props.onSessionKeyChange((e.target as HTMLInputElement).value)}
/>
@change=${(e: Event) =>
props.onSessionKeyChange((e.target as HTMLSelectElement).value)}
>
${sessionOptions.map(
(entry) => html`<option value=${entry.key}>${entry.key}</option>`,
)}
</select>
</label>
<button
class="btn"
@@ -104,6 +112,55 @@ export function renderChat(props: ChatProps) {
`;
}
type SessionOption = {
key: string;
updatedAt?: number | null;
};
function resolveSessionOptions(
currentKey: string,
sessions: SessionsListResult | null,
) {
const now = Date.now();
const cutoff = now - 24 * 60 * 60 * 1000;
const entries = Array.isArray(sessions?.sessions) ? sessions?.sessions ?? [] : [];
const sorted = [...entries].sort(
(a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0),
);
const recent: SessionOption[] = [];
const seen = new Set<string>();
for (const entry of sorted) {
if (seen.has(entry.key)) continue;
seen.add(entry.key);
if ((entry.updatedAt ?? 0) < cutoff) continue;
recent.push(entry);
}
const result: SessionOption[] = [];
const included = new Set<string>();
const mainKey = "main";
const mainEntry = sorted.find((entry) => entry.key === mainKey);
if (mainEntry) {
result.push(mainEntry);
included.add(mainKey);
} else if (currentKey === mainKey) {
result.push({ key: mainKey, updatedAt: null });
included.add(mainKey);
}
for (const entry of recent) {
if (included.has(entry.key)) continue;
result.push(entry);
included.add(entry.key);
}
if (!included.has(currentKey)) {
result.push({ key: currentKey, updatedAt: null });
}
return result;
}
function renderMessage(message: unknown, opts?: { streaming?: boolean }) {
const m = message as Record<string, unknown>;
const role = typeof m.role === "string" ? m.role : "unknown";