diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d5d14b66fb..619446d5934 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- TUI/sessions: bound the session picker to recent rows and use exact lookup-style refreshes for the active session, so dusty stores no longer make TUI hydrate weeks-old transcripts before becoming responsive. Thanks @vincentkoc. - Doctor/gateway: report recent supervisor restart handoffs in `openclaw doctor --deep`, using the installed service environment when available so service-managed clean exits are visible in guided diagnostics. Thanks @shakkernerd. - Gateway/status: show recent supervisor restart handoffs in `openclaw gateway status --deep`, including JSON details, so clean service-managed restarts are reported as restart handoffs instead of opaque stopped-service diagnostics. Thanks @shakkernerd. - Providers/Fireworks: expose Kimi models as thinking-off-only and keep K2.5/K2.6 requests on `thinking: disabled`, so manual model switches do not send Fireworks-rejected `reasoning*` parameters. Refs #74289. Thanks @frankekn. diff --git a/docs/web/tui.md b/docs/web/tui.md index f5254b68dd9..a3fd287aeea 100644 --- a/docs/web/tui.md +++ b/docs/web/tui.md @@ -82,7 +82,7 @@ Notes: - Model picker: list available models and set the session override. - Agent picker: choose a different agent. -- Session picker: shows only sessions for the current agent. +- Session picker: shows up to 50 sessions for the current agent updated in the last 7 days. Use `/session ` to jump to an older known session. - Settings: toggle deliver, tool output expansion, and thinking visibility. ## Keyboard shortcuts diff --git a/src/tui/gateway-chat.ts b/src/tui/gateway-chat.ts index 1a0cc38a13b..a298b864c72 100644 --- a/src/tui/gateway-chat.ts +++ b/src/tui/gateway-chat.ts @@ -220,15 +220,7 @@ export class GatewayChatClient implements TuiBackend { } async listSessions(opts?: SessionsListParams) { - return await this.client.request("sessions.list", { - limit: opts?.limit, - activeMinutes: opts?.activeMinutes, - includeGlobal: opts?.includeGlobal, - includeUnknown: opts?.includeUnknown, - includeDerivedTitles: opts?.includeDerivedTitles, - includeLastMessage: opts?.includeLastMessage, - agentId: opts?.agentId, - }); + return await this.client.request("sessions.list", opts ?? {}); } async listAgents() { diff --git a/src/tui/tui-command-handlers.test.ts b/src/tui/tui-command-handlers.test.ts index 57a85a0eb8d..d49d749a116 100644 --- a/src/tui/tui-command-handlers.test.ts +++ b/src/tui/tui-command-handlers.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it, vi } from "vitest"; import { createCommandHandlers } from "./tui-command-handlers.js"; +import { + TUI_RECENT_SESSIONS_ACTIVE_MINUTES, + TUI_SESSION_PICKER_LIMIT, +} from "./tui-session-list-policy.js"; type LoadHistoryMock = ReturnType & (() => Promise); type RunAuthFlow = NonNullable[0]["runAuthFlow"]>; @@ -16,6 +20,7 @@ async function flushAsyncSelect() { function createHarness(params?: { sendChat?: ReturnType; getGatewayStatus?: ReturnType; + listSessions?: ReturnType; patchSession?: ReturnType; resetSession?: ReturnType; runAuthFlow?: RunAuthFlow; @@ -32,6 +37,7 @@ function createHarness(params?: { }) { const sendChat = params?.sendChat ?? vi.fn().mockResolvedValue({ runId: "r1" }); const getGatewayStatus = params?.getGatewayStatus ?? vi.fn().mockResolvedValue({}); + const listSessions = params?.listSessions ?? vi.fn().mockResolvedValue({ sessions: [] }); const patchSession = params?.patchSession ?? vi.fn().mockResolvedValue({}); const resetSession = params?.resetSession ?? vi.fn().mockResolvedValue({ ok: true }); const setSession = params?.setSession ?? (vi.fn().mockResolvedValue(undefined) as SetSessionMock); @@ -64,8 +70,8 @@ function createHarness(params?: { sessionInfo: {}, }; - const { handleCommand } = createCommandHandlers({ - client: { sendChat, getGatewayStatus, patchSession, resetSession } as never, + const { handleCommand, openSessionSelector } = createCommandHandlers({ + client: { sendChat, getGatewayStatus, listSessions, patchSession, resetSession } as never, chatLog: { addUser, addSystem } as never, tui: { requestRender } as never, opts: params?.opts ?? {}, @@ -92,7 +98,9 @@ function createHarness(params?: { return { handleCommand, getGatewayStatus, + listSessions, sendChat, + openSessionSelector, openOverlay, closeOverlay, patchSession, @@ -114,6 +122,31 @@ function createHarness(params?: { } describe("tui command handlers", () => { + it("bounds session picker hydration to recent TUI sessions", async () => { + const listSessions = vi.fn().mockResolvedValue({ + sessions: [ + { + key: "agent:main:main", + displayName: "main", + updatedAt: Date.now(), + }, + ], + }); + const { openSessionSelector } = createHarness({ listSessions }); + + await openSessionSelector(); + + expect(listSessions).toHaveBeenCalledWith({ + limit: TUI_SESSION_PICKER_LIMIT, + activeMinutes: TUI_RECENT_SESSIONS_ACTIVE_MINUTES, + includeGlobal: false, + includeUnknown: false, + includeDerivedTitles: true, + includeLastMessage: true, + agentId: "main", + }); + }); + it("renders the sending indicator before chat.send resolves", async () => { let resolveSend: (value: { runId: string }) => void = () => { throw new Error("sendChat promise resolver was not initialized"); diff --git a/src/tui/tui-command-handlers.ts b/src/tui/tui-command-handlers.ts index 378cbc05b50..6d9ba339eda 100644 --- a/src/tui/tui-command-handlers.ts +++ b/src/tui/tui-command-handlers.ts @@ -18,6 +18,10 @@ import { } from "./components/selectors.js"; import type { TuiBackend } from "./tui-backend.js"; import { sanitizeRenderableText } from "./tui-formatters.js"; +import { + TUI_RECENT_SESSIONS_ACTIVE_MINUTES, + TUI_SESSION_PICKER_LIMIT, +} from "./tui-session-list-policy.js"; import { formatStatusSummary } from "./tui-status-summary.js"; import type { AgentSummary, @@ -190,6 +194,8 @@ export function createCommandHandlers(context: CommandHandlerContext) { const openSessionSelector = async () => { try { const result = await client.listSessions({ + limit: TUI_SESSION_PICKER_LIMIT, + activeMinutes: TUI_RECENT_SESSIONS_ACTIVE_MINUTES, includeGlobal: false, includeUnknown: false, includeDerivedTitles: true, diff --git a/src/tui/tui-session-actions.test.ts b/src/tui/tui-session-actions.test.ts index e8ede6deee6..9316d530f04 100644 --- a/src/tui/tui-session-actions.test.ts +++ b/src/tui/tui-session-actions.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import type { TuiBackend } from "./tui-backend.js"; import { createSessionActions } from "./tui-session-actions.js"; +import { TUI_SESSION_LOOKUP_LIMIT } from "./tui-session-list-policy.js"; import type { TuiStateAccess } from "./tui-types.js"; describe("tui session actions", () => { @@ -96,6 +97,13 @@ describe("tui session actions", () => { await Promise.resolve(); expect(listSessions).toHaveBeenCalledTimes(1); + expect(listSessions).toHaveBeenNthCalledWith(1, { + limit: TUI_SESSION_LOOKUP_LIMIT, + search: "agent:main:main", + includeGlobal: false, + includeUnknown: false, + agentId: "main", + }); resolveFirst?.({ ts: Date.now(), diff --git a/src/tui/tui-session-actions.ts b/src/tui/tui-session-actions.ts index a7e3d12e416..9a853292ebe 100644 --- a/src/tui/tui-session-actions.ts +++ b/src/tui/tui-session-actions.ts @@ -10,6 +10,7 @@ import { normalizeOptionalString } from "../shared/string-coerce.js"; import type { ChatLog } from "./components/chat-log.js"; import type { TuiAgentsList, TuiBackend } from "./tui-backend.js"; import { asString, extractTextFromMessage, isCommandMessage } from "./tui-formatters.js"; +import { TUI_SESSION_LOOKUP_LIMIT } from "./tui-session-list-policy.js"; import type { SessionInfo, TuiOptions, TuiStateAccess } from "./tui-types.js"; type SessionActionBtwPresenter = { @@ -234,6 +235,8 @@ export function createSessionActions(context: SessionActionContext) { }; const listAgentId = resolveListAgentId(); const result = await client.listSessions({ + limit: TUI_SESSION_LOOKUP_LIMIT, + search: state.currentSessionKey, includeGlobal: false, includeUnknown: false, agentId: listAgentId, diff --git a/src/tui/tui-session-list-policy.ts b/src/tui/tui-session-list-policy.ts new file mode 100644 index 00000000000..68965ac0109 --- /dev/null +++ b/src/tui/tui-session-list-policy.ts @@ -0,0 +1,3 @@ +export const TUI_RECENT_SESSIONS_ACTIVE_MINUTES = 7 * 24 * 60; +export const TUI_SESSION_PICKER_LIMIT = 50; +export const TUI_SESSION_LOOKUP_LIMIT = 5; diff --git a/src/tui/tui.ts b/src/tui/tui.ts index 0c6fd0df84d..e0f98c772bc 100644 --- a/src/tui/tui.ts +++ b/src/tui/tui.ts @@ -42,6 +42,7 @@ import { import { createLocalShellRunner } from "./tui-local-shell.js"; import { createOverlayHandlers } from "./tui-overlays.js"; import { createSessionActions } from "./tui-session-actions.js"; +import { TUI_SESSION_LOOKUP_LIMIT } from "./tui-session-list-policy.js"; import { createEditorSubmitHandler, createSubmitBurstCoalescer, @@ -635,6 +636,8 @@ export async function runTui(opts: RunTuiOptions): Promise { } const sessions = await client .listSessions({ + limit: TUI_SESSION_LOOKUP_LIMIT, + search: rememberedKey, includeGlobal: false, includeUnknown: false, agentId: currentAgentId,