fix(tui): bound session list recency (#77752)

This commit is contained in:
Vincent Koc
2026-05-05 01:25:43 -07:00
committed by GitHub
parent 9c4a335007
commit 42d8255ce9
9 changed files with 61 additions and 12 deletions

View File

@@ -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.

View File

@@ -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 <key>` to jump to an older known session.
- Settings: toggle deliver, tool output expansion, and thinking visibility.
## Keyboard shortcuts

View File

@@ -220,15 +220,7 @@ export class GatewayChatClient implements TuiBackend {
}
async listSessions(opts?: SessionsListParams) {
return await this.client.request<GatewaySessionList>("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<GatewaySessionList>("sessions.list", opts ?? {});
}
async listAgents() {

View File

@@ -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<typeof vi.fn> & (() => Promise<void>);
type RunAuthFlow = NonNullable<Parameters<typeof createCommandHandlers>[0]["runAuthFlow"]>;
@@ -16,6 +20,7 @@ async function flushAsyncSelect() {
function createHarness(params?: {
sendChat?: ReturnType<typeof vi.fn>;
getGatewayStatus?: ReturnType<typeof vi.fn>;
listSessions?: ReturnType<typeof vi.fn>;
patchSession?: ReturnType<typeof vi.fn>;
resetSession?: ReturnType<typeof vi.fn>;
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");

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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;

View File

@@ -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<TuiResult> {
}
const sessions = await client
.listSessions({
limit: TUI_SESSION_LOOKUP_LIMIT,
search: rememberedKey,
includeGlobal: false,
includeUnknown: false,
agentId: currentAgentId,