feat(tui): add agent picker and agents list rpc

This commit is contained in:
Peter Steinberger
2026-01-09 00:53:11 +01:00
parent a5f0f62e0d
commit 714e170c16
14 changed files with 471 additions and 20 deletions

View File

@@ -39,6 +39,7 @@ Use SSH tunneling or Tailscale to reach the Gateway WS.
- Ctrl+C: clear input (press twice to exit)
- Ctrl+D: exit
- Ctrl+L: model picker
- Ctrl+G: agent picker
- Ctrl+P: session picker
- Ctrl+O: toggle tool output expansion
- Ctrl+T: toggle thinking visibility
@@ -46,6 +47,7 @@ Use SSH tunneling or Tailscale to reach the Gateway WS.
## Slash commands
- `/help`
- `/status`
- `/agent <id>` (or `/agents`)
- `/session <key>` (or `/sessions`)
- `/model <provider/model>` (or `/model list`, `/models`)
- `/think <off|minimal|low|medium|high>`

View File

@@ -3,6 +3,12 @@ import {
type AgentEvent,
AgentEventSchema,
AgentParamsSchema,
type AgentSummary,
AgentSummarySchema,
type AgentsListParams,
AgentsListParamsSchema,
type AgentsListResult,
AgentsListResultSchema,
type AgentWaitParams,
AgentWaitParamsSchema,
type ChatAbortParams,
@@ -163,6 +169,9 @@ export const validateAgentWaitParams = ajv.compile<AgentWaitParams>(
AgentWaitParamsSchema,
);
export const validateWakeParams = ajv.compile<WakeParams>(WakeParamsSchema);
export const validateAgentsListParams = ajv.compile<AgentsListParams>(
AgentsListParamsSchema,
);
export const validateNodePairRequestParams = ajv.compile<NodePairRequestParams>(
NodePairRequestParamsSchema,
);
@@ -332,6 +341,9 @@ export {
ProvidersStatusParamsSchema,
WebLoginStartParamsSchema,
WebLoginWaitParamsSchema,
AgentSummarySchema,
AgentsListParamsSchema,
AgentsListResultSchema,
ModelsListParamsSchema,
SkillsStatusParamsSchema,
SkillsInstallParamsSchema,
@@ -394,6 +406,9 @@ export type {
ProvidersStatusParams,
WebLoginStartParams,
WebLoginWaitParams,
AgentSummary,
AgentsListParams,
AgentsListResult,
SkillsStatusParams,
SkillsInstallParams,
SkillsUpdateParams,

View File

@@ -314,6 +314,7 @@ export const SessionsListParamsSchema = Type.Object(
includeGlobal: Type.Optional(Type.Boolean()),
includeUnknown: Type.Optional(Type.Boolean()),
spawnedBy: Type.Optional(NonEmptyString),
agentId: Type.Optional(NonEmptyString),
},
{ additionalProperties: false },
);
@@ -590,6 +591,29 @@ export const ModelChoiceSchema = Type.Object(
{ additionalProperties: false },
);
export const AgentSummarySchema = Type.Object(
{
id: NonEmptyString,
name: Type.Optional(NonEmptyString),
},
{ additionalProperties: false },
);
export const AgentsListParamsSchema = Type.Object(
{},
{ additionalProperties: false },
);
export const AgentsListResultSchema = Type.Object(
{
defaultId: NonEmptyString,
mainKey: NonEmptyString,
scope: Type.Union([Type.Literal("per-sender"), Type.Literal("global")]),
agents: Type.Array(AgentSummarySchema),
},
{ additionalProperties: false },
);
export const ModelsListParamsSchema = Type.Object(
{},
{ additionalProperties: false },
@@ -927,6 +951,9 @@ export const ProtocolSchemas: Record<string, TSchema> = {
ProvidersStatusParams: ProvidersStatusParamsSchema,
WebLoginStartParams: WebLoginStartParamsSchema,
WebLoginWaitParams: WebLoginWaitParamsSchema,
AgentSummary: AgentSummarySchema,
AgentsListParams: AgentsListParamsSchema,
AgentsListResult: AgentsListResultSchema,
ModelChoice: ModelChoiceSchema,
ModelsListParams: ModelsListParamsSchema,
ModelsListResult: ModelsListResultSchema,
@@ -1000,6 +1027,9 @@ export type TalkModeParams = Static<typeof TalkModeParamsSchema>;
export type ProvidersStatusParams = Static<typeof ProvidersStatusParamsSchema>;
export type WebLoginStartParams = Static<typeof WebLoginStartParamsSchema>;
export type WebLoginWaitParams = Static<typeof WebLoginWaitParamsSchema>;
export type AgentSummary = Static<typeof AgentSummarySchema>;
export type AgentsListParams = Static<typeof AgentsListParamsSchema>;
export type AgentsListResult = Static<typeof AgentsListResultSchema>;
export type ModelChoice = Static<typeof ModelChoiceSchema>;
export type ModelsListParams = Static<typeof ModelsListParamsSchema>;
export type ModelsListResult = Static<typeof ModelsListResultSchema>;

View File

@@ -1,5 +1,6 @@
import { ErrorCodes, errorShape } from "./protocol/index.js";
import { agentHandlers } from "./server-methods/agent.js";
import { agentsHandlers } from "./server-methods/agents.js";
import { chatHandlers } from "./server-methods/chat.js";
import { configHandlers } from "./server-methods/config.js";
import { connectHandlers } from "./server-methods/connect.js";
@@ -45,6 +46,7 @@ const handlers: GatewayRequestHandlers = {
...sendHandlers,
...usageHandlers,
...agentHandlers,
...agentsHandlers,
};
export async function handleGatewayRequest(

View File

@@ -0,0 +1,29 @@
import { loadConfig } from "../../config/config.js";
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateAgentsListParams,
} from "../protocol/index.js";
import { listAgentsForGateway } from "../session-utils.js";
import type { GatewayRequestHandlers } from "./types.js";
export const agentsHandlers: GatewayRequestHandlers = {
"agents.list": ({ params, respond }) => {
if (!validateAgentsListParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid agents.list params: ${formatValidationErrors(validateAgentsListParams.errors)}`,
),
);
return;
}
const cfg = loadConfig();
const result = listAgentsForGateway(cfg);
respond(true, result, undefined);
},
};

View File

@@ -0,0 +1,49 @@
import { describe, expect, test } from "vitest";
import {
connectOk,
installGatewayTestHooks,
rpcReq,
startServerWithClient,
testState,
} from "./test-helpers.js";
installGatewayTestHooks();
describe("gateway server agents", () => {
test("lists configured agents via agents.list RPC", async () => {
testState.routingConfig = {
defaultAgentId: "work",
agents: {
work: { name: "Work" },
home: { name: "Home" },
},
};
const { ws } = await startServerWithClient();
const hello = await connectOk(ws);
expect(
(hello as unknown as { features?: { methods?: string[] } }).features
?.methods,
).toEqual(expect.arrayContaining(["agents.list"]));
const res = await rpcReq<{
defaultId: string;
mainKey: string;
scope: string;
agents: Array<{ id: string; name?: string }>;
}>(ws, "agents.list", {});
expect(res.ok).toBe(true);
expect(res.payload?.defaultId).toBe("work");
expect(res.payload?.mainKey).toBe("main");
expect(res.payload?.scope).toBe("per-sender");
expect(res.payload?.agents.map((agent) => agent.id)).toEqual([
"work",
"home",
]);
const work = res.payload?.agents.find((agent) => agent.id === "work");
const home = res.payload?.agents.find((agent) => agent.id === "home");
expect(work?.name).toBe("Work");
expect(home?.name).toBe("Home");
});
});

View File

@@ -320,4 +320,84 @@ describe("gateway server sessions", () => {
ws.close();
await server.close();
});
test("filters sessions by agentId", async () => {
const dir = await fs.mkdtemp(
path.join(os.tmpdir(), "clawdbot-sessions-agents-"),
);
testState.sessionConfig = {
store: path.join(dir, "{agentId}", "sessions.json"),
};
testState.routingConfig = {
defaultAgentId: "home",
agents: {
home: {},
work: {},
},
};
const homeDir = path.join(dir, "home");
const workDir = path.join(dir, "work");
await fs.mkdir(homeDir, { recursive: true });
await fs.mkdir(workDir, { recursive: true });
await fs.writeFile(
path.join(homeDir, "sessions.json"),
JSON.stringify(
{
"agent:home:main": {
sessionId: "sess-home-main",
updatedAt: Date.now(),
},
"agent:home:discord:group:dev": {
sessionId: "sess-home-group",
updatedAt: Date.now() - 1000,
},
},
null,
2,
),
"utf-8",
);
await fs.writeFile(
path.join(workDir, "sessions.json"),
JSON.stringify(
{
"agent:work:main": {
sessionId: "sess-work-main",
updatedAt: Date.now(),
},
},
null,
2,
),
"utf-8",
);
const { ws } = await startServerWithClient();
await connectOk(ws);
const homeSessions = await rpcReq<{
sessions: Array<{ key: string }>;
}>(ws, "sessions.list", {
includeGlobal: false,
includeUnknown: false,
agentId: "home",
});
expect(homeSessions.ok).toBe(true);
expect(homeSessions.payload?.sessions.map((s) => s.key).sort()).toEqual([
"agent:home:discord:group:dev",
"agent:home:main",
]);
const workSessions = await rpcReq<{
sessions: Array<{ key: string }>;
}>(ws, "sessions.list", {
includeGlobal: false,
includeUnknown: false,
agentId: "work",
});
expect(workSessions.ok).toBe(true);
expect(workSessions.payload?.sessions.map((s) => s.key)).toEqual([
"agent:work:main",
]);
});
});

View File

@@ -227,6 +227,7 @@ const METHODS = [
"wizard.status",
"talk.mode",
"models.list",
"agents.list",
"skills.status",
"skills.install",
"skills.update",

View File

@@ -17,8 +17,10 @@ import {
resolveSessionTranscriptPath,
resolveStorePath,
type SessionEntry,
type SessionScope,
} from "../config/sessions.js";
import {
DEFAULT_MAIN_KEY,
normalizeAgentId,
parseAgentSessionKey,
} from "../routing/session-key.js";
@@ -56,6 +58,11 @@ export type GatewaySessionRow = {
lastAccountId?: string;
};
export type GatewayAgentRow = {
id: string;
name?: string;
};
export type SessionsListResult = {
ts: number;
path: string;
@@ -237,6 +244,39 @@ function listConfiguredAgentIds(cfg: ClawdbotConfig): string[] {
return sorted;
}
export function listAgentsForGateway(cfg: ClawdbotConfig): {
defaultId: string;
mainKey: string;
scope: SessionScope;
agents: GatewayAgentRow[];
} {
const defaultId = normalizeAgentId(cfg.routing?.defaultAgentId);
const mainKey =
(cfg.session?.mainKey ?? DEFAULT_MAIN_KEY).trim() || DEFAULT_MAIN_KEY;
const scope = cfg.session?.scope ?? "per-sender";
const configured = cfg.routing?.agents;
const configuredById = new Map<string, { name?: string }>();
if (configured && typeof configured === "object") {
for (const [key, value] of Object.entries(configured)) {
if (!value || typeof value !== "object") continue;
configuredById.set(normalizeAgentId(key), {
name:
typeof value.name === "string" && value.name.trim()
? value.name.trim()
: undefined,
});
}
}
const agents = listConfiguredAgentIds(cfg).map((id) => {
const meta = configuredById.get(id);
return {
id,
name: meta?.name,
};
});
return { defaultId, mainKey, scope, agents };
}
function canonicalizeSessionKeyForAgent(agentId: string, key: string): string {
if (key === "global" || key === "unknown") return key;
if (key.startsWith("agent:")) return key;
@@ -394,6 +434,8 @@ export function listSessionsFromStore(params: {
const includeGlobal = opts.includeGlobal === true;
const includeUnknown = opts.includeUnknown === true;
const spawnedBy = typeof opts.spawnedBy === "string" ? opts.spawnedBy : "";
const agentId =
typeof opts.agentId === "string" ? normalizeAgentId(opts.agentId) : "";
const activeMinutes =
typeof opts.activeMinutes === "number" &&
Number.isFinite(opts.activeMinutes)
@@ -404,6 +446,12 @@ export function listSessionsFromStore(params: {
.filter(([key]) => {
if (!includeGlobal && key === "global") return false;
if (!includeUnknown && key === "unknown") return false;
if (agentId) {
if (key === "global" || key === "unknown") return false;
const parsed = parseAgentSessionKey(key);
if (!parsed) return false;
return normalizeAgentId(parsed.agentId) === agentId;
}
return true;
})
.filter(([key, entry]) => {

View File

@@ -85,6 +85,7 @@ export const agentCommand = hoisted.agentCommand;
export const testState = {
agentConfig: undefined as Record<string, unknown> | undefined,
routingConfig: undefined as Record<string, unknown> | undefined,
sessionStorePath: undefined as string | undefined,
sessionConfig: undefined as Record<string, unknown> | undefined,
allowFrom: undefined as string[] | undefined,
@@ -246,6 +247,7 @@ vi.mock("../config/config.js", async () => {
workspace: path.join(os.tmpdir(), "clawd-gateway-test"),
...testState.agentConfig,
},
routing: testState.routingConfig,
whatsapp: {
allowFrom: testState.allowFrom,
},
@@ -354,6 +356,7 @@ export function installGatewayTestHooks() {
testState.sessionConfig = undefined;
testState.sessionStorePath = undefined;
testState.agentConfig = undefined;
testState.routingConfig = undefined;
testState.allowFrom = undefined;
testIsNixMode.value = false;
cronIsolatedRun.mockClear();

View File

@@ -31,6 +31,8 @@ export function getSlashCommands(): SlashCommand[] {
return [
{ name: "help", description: "Show slash command help" },
{ name: "status", description: "Show gateway status summary" },
{ name: "agent", description: "Switch agent (or open picker)" },
{ name: "agents", description: "Open agent picker" },
{ name: "session", description: "Switch session (or open picker)" },
{ name: "sessions", description: "Open session picker" },
{
@@ -108,6 +110,7 @@ export function helpText(): string {
"Slash commands:",
"/help",
"/status",
"/agent <id> (or /agents)",
"/session <key> (or /sessions)",
"/model <provider/model> (or /models)",
"/think <off|minimal|low|medium|high>",

View File

@@ -4,6 +4,7 @@ export class CustomEditor extends Editor {
onEscape?: () => void;
onCtrlC?: () => void;
onCtrlD?: () => void;
onCtrlG?: () => void;
onCtrlL?: () => void;
onCtrlO?: () => void;
onCtrlP?: () => void;
@@ -28,6 +29,10 @@ export class CustomEditor extends Editor {
this.onCtrlP();
return;
}
if (matchesKey(data, Key.ctrl("g")) && this.onCtrlG) {
this.onCtrlG();
return;
}
if (matchesKey(data, Key.ctrl("t")) && this.onCtrlT) {
this.onCtrlT();
return;

View File

@@ -57,6 +57,16 @@ export type GatewaySessionList = {
}>;
};
export type GatewayAgentsList = {
defaultId: string;
mainKey: string;
scope: "per-sender" | "global";
agents: Array<{
id: string;
name?: string;
}>;
};
export type GatewayModelChoice = {
id: string;
name: string;
@@ -165,9 +175,14 @@ export class GatewayChatClient {
activeMinutes: opts?.activeMinutes,
includeGlobal: opts?.includeGlobal,
includeUnknown: opts?.includeUnknown,
agentId: opts?.agentId,
});
}
async listAgents() {
return await this.client.request<GatewayAgentsList>("agents.list", {});
}
async patchSession(opts: SessionsPatchParams) {
return await this.client.request("sessions.patch", opts);
}

View File

@@ -7,6 +7,11 @@ import {
TUI,
} from "@mariozechner/pi-tui";
import { loadConfig } from "../config/config.js";
import {
buildAgentMainSessionKey,
normalizeAgentId,
parseAgentSessionKey,
} from "../routing/session-key.js";
import { getSlashCommands, helpText, parseCommand } from "./commands.js";
import { ChatLog } from "./components/chat-log.js";
import { CustomEditor } from "./components/custom-editor.js";
@@ -14,7 +19,7 @@ import {
createSelectList,
createSettingsList,
} from "./components/selectors.js";
import { GatewayChatClient } from "./gateway-chat.js";
import { type GatewayAgentsList, GatewayChatClient } from "./gateway-chat.js";
import { editorTheme, theme } from "./theme/theme.js";
export type TuiOptions = {
@@ -53,6 +58,13 @@ type SessionInfo = {
displayName?: string;
};
type SessionScope = "per-sender" | "global";
type AgentSummary = {
id: string;
name?: string;
};
function extractTextBlocks(
content: unknown,
opts?: { includeThinking?: boolean },
@@ -106,9 +118,18 @@ function asString(value: unknown, fallback = ""): string {
export async function runTui(opts: TuiOptions) {
const config = loadConfig();
const defaultSession =
(opts.session ?? config.session?.mainKey ?? "main").trim() || "main";
let currentSessionKey = defaultSession;
const initialSessionInput = (opts.session ?? "").trim();
let sessionScope: SessionScope = (config.session?.scope ??
"per-sender") as SessionScope;
let sessionMainKey = (config.session?.mainKey ?? "main").trim() || "main";
let agentDefaultId = normalizeAgentId(
config.routing?.defaultAgentId ?? "main",
);
let currentAgentId = agentDefaultId;
let agents: AgentSummary[] = [];
const agentNames = new Map<string, string>();
let currentSessionKey = "";
let initialSessionApplied = false;
let currentSessionId: string | null = null;
let activeChatRunId: string | null = null;
const finalizedRuns = new Map<string, number>();
@@ -144,10 +165,39 @@ export async function runTui(opts: TuiOptions) {
tui.addChild(root);
tui.setFocus(editor);
const formatSessionKey = (key: string) => {
if (key === "global" || key === "unknown") return key;
const parsed = parseAgentSessionKey(key);
return parsed?.rest ?? key;
};
const formatAgentLabel = (id: string) => {
const name = agentNames.get(id);
return name ? `${id} (${name})` : id;
};
const resolveSessionKey = (raw?: string) => {
const trimmed = (raw ?? "").trim();
if (sessionScope === "global") return "global";
if (!trimmed) {
return buildAgentMainSessionKey({
agentId: currentAgentId,
mainKey: sessionMainKey,
});
}
if (trimmed === "global" || trimmed === "unknown") return trimmed;
if (trimmed.startsWith("agent:")) return trimmed;
return `agent:${currentAgentId}:${trimmed}`;
};
currentSessionKey = resolveSessionKey(initialSessionInput);
const updateHeader = () => {
const sessionLabel = formatSessionKey(currentSessionKey);
const agentLabel = formatAgentLabel(currentAgentId);
header.setText(
theme.header(
`clawdbot tui - ${client.connection.url} - session ${currentSessionKey}`,
`clawdbot tui - ${client.connection.url} - agent ${agentLabel} - session ${sessionLabel}`,
),
);
};
@@ -158,9 +208,11 @@ export async function runTui(opts: TuiOptions) {
const updateFooter = () => {
const connection = isConnected ? "connected" : "disconnected";
const sessionKeyLabel = formatSessionKey(currentSessionKey);
const sessionLabel = sessionInfo.displayName
? `${currentSessionKey} (${sessionInfo.displayName})`
: currentSessionKey;
? `${sessionKeyLabel} (${sessionInfo.displayName})`
: sessionKeyLabel;
const agentLabel = formatAgentLabel(currentAgentId);
const modelLabel = sessionInfo.model ?? "unknown";
const tokens = formatTokens(
sessionInfo.totalTokens ?? null,
@@ -172,7 +224,7 @@ export async function runTui(opts: TuiOptions) {
const deliver = deliverDefault ? "on" : "off";
footer.setText(
theme.dim(
`${connection} | session ${sessionLabel} | model ${modelLabel} | think ${think} | verbose ${verbose} | reasoning ${reasoning} | ${tokens} | deliver ${deliver}`,
`${connection} | agent ${agentLabel} | session ${sessionLabel} | model ${modelLabel} | think ${think} | verbose ${verbose} | reasoning ${reasoning} | ${tokens} | deliver ${deliver}`,
),
);
};
@@ -188,11 +240,74 @@ export async function runTui(opts: TuiOptions) {
tui.setFocus(component);
};
const initialSessionAgentId = (() => {
if (!initialSessionInput) return null;
const parsed = parseAgentSessionKey(initialSessionInput);
return parsed ? normalizeAgentId(parsed.agentId) : null;
})();
const applyAgentsResult = (result: GatewayAgentsList) => {
agentDefaultId = normalizeAgentId(result.defaultId);
sessionMainKey = result.mainKey.trim() || sessionMainKey;
sessionScope = result.scope ?? sessionScope;
agents = result.agents.map((agent) => ({
id: normalizeAgentId(agent.id),
name: agent.name?.trim() || undefined,
}));
agentNames.clear();
for (const agent of agents) {
if (agent.name) agentNames.set(agent.id, agent.name);
}
if (!initialSessionApplied) {
if (initialSessionAgentId) {
if (agents.some((agent) => agent.id === initialSessionAgentId)) {
currentAgentId = initialSessionAgentId;
}
} else if (!agents.some((agent) => agent.id === currentAgentId)) {
currentAgentId =
agents[0]?.id ?? normalizeAgentId(result.defaultId ?? currentAgentId);
}
const nextSessionKey = resolveSessionKey(initialSessionInput);
if (nextSessionKey !== currentSessionKey) {
currentSessionKey = nextSessionKey;
}
initialSessionApplied = true;
} else if (!agents.some((agent) => agent.id === currentAgentId)) {
currentAgentId =
agents[0]?.id ?? normalizeAgentId(result.defaultId ?? currentAgentId);
}
updateHeader();
updateFooter();
};
const refreshAgents = async () => {
try {
const result = await client.listAgents();
applyAgentsResult(result);
} catch (err) {
chatLog.addSystem(`agents list failed: ${String(err)}`);
}
};
const updateAgentFromSessionKey = (key: string) => {
const parsed = parseAgentSessionKey(key);
if (!parsed) return;
const next = normalizeAgentId(parsed.agentId);
if (next !== currentAgentId) {
currentAgentId = next;
}
};
const refreshSessionInfo = async () => {
try {
const listAgentId =
currentSessionKey === "global" || currentSessionKey === "unknown"
? undefined
: currentAgentId;
const result = await client.listSessions({
includeGlobal: false,
includeUnknown: false,
agentId: listAgentId,
});
const entry = result.sessions.find(
(row) => row.key === currentSessionKey,
@@ -272,12 +387,15 @@ export async function runTui(opts: TuiOptions) {
tui.requestRender();
};
const setSession = async (key: string) => {
currentSessionKey = key;
const setSession = async (rawKey: string) => {
const nextKey = resolveSessionKey(rawKey);
updateAgentFromSessionKey(nextKey);
currentSessionKey = nextKey;
activeChatRunId = null;
currentSessionId = null;
historyLoaded = false;
updateHeader();
updateFooter();
await loadHistory();
};
@@ -429,15 +547,51 @@ export async function runTui(opts: TuiOptions) {
}
};
const setAgent = async (id: string) => {
currentAgentId = normalizeAgentId(id);
await setSession("");
};
const openAgentSelector = async () => {
await refreshAgents();
if (agents.length === 0) {
chatLog.addSystem("no agents found");
tui.requestRender();
return;
}
const items = agents.map((agent) => ({
value: agent.id,
label: agent.name ? `${agent.id} (${agent.name})` : agent.id,
description: agent.id === agentDefaultId ? "default" : "",
}));
const selector = createSelectList(items, 9);
selector.onSelect = (item) => {
void (async () => {
closeOverlay();
await setAgent(item.value);
tui.requestRender();
})();
};
selector.onCancel = () => {
closeOverlay();
tui.requestRender();
};
openOverlay(selector);
tui.requestRender();
};
const openSessionSelector = async () => {
try {
const result = await client.listSessions({
includeGlobal: false,
includeUnknown: false,
agentId: currentAgentId,
});
const items = result.sessions.map((session) => ({
value: session.key,
label: session.displayName ?? session.key,
label: session.displayName
? `${session.displayName} (${formatSessionKey(session.key)})`
: formatSessionKey(session.key),
description: session.updatedAt
? new Date(session.updatedAt).toLocaleString()
: "",
@@ -528,6 +682,16 @@ export async function runTui(opts: TuiOptions) {
chatLog.addSystem(`status failed: ${String(err)}`);
}
break;
case "agent":
if (!args) {
await openAgentSelector();
} else {
await setAgent(args);
}
break;
case "agents":
await openAgentSelector();
break;
case "session":
if (!args) {
await openSessionSelector();
@@ -744,6 +908,9 @@ export async function runTui(opts: TuiOptions) {
editor.onCtrlL = () => {
void openModelSelector();
};
editor.onCtrlG = () => {
void openAgentSelector();
};
editor.onCtrlP = () => {
void openSessionSelector();
};
@@ -760,17 +927,19 @@ export async function runTui(opts: TuiOptions) {
client.onConnected = () => {
isConnected = true;
setStatus("connected");
updateHeader();
if (!historyLoaded) {
void loadHistory().then(() => {
void (async () => {
await refreshAgents();
updateHeader();
if (!historyLoaded) {
await loadHistory();
chatLog.addSystem("gateway connected");
tui.requestRender();
});
} else {
chatLog.addSystem("gateway reconnected");
}
updateFooter();
tui.requestRender();
} else {
chatLog.addSystem("gateway reconnected");
}
updateFooter();
tui.requestRender();
})();
};
client.onDisconnected = (reason) => {