feat: add recent session switchers
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user