import { randomUUID } from "node:crypto" ;
import type { Component, SelectItem, TUI } from "@mariozechner/pi-tui" ;
import { normalizeGroupActivation } from "../auto-reply/group-activation.js" ;
import {
formatThinkingLevels,
normalizeUsageDisplay,
resolveResponseUsageMode,
} from "../auto-reply/thinking.js" ;
import type { SessionsPatchResult } from "../gateway/protocol/index.js" ;
import { formatRelativeTimestamp } from "../infra/format-time/format-relative.ts" ;
import { normalizeAgentId } from "../routing/session-key.js" ;
import { helpText, parseCommand } from "./commands.js" ;
import type { ChatLog } from "./components/chat-log.js" ;
import {
createFilterableSelectList,
createSearchableSelectList,
createSettingsList,
} from "./components/selectors.js" ;
import type { TuiBackend } from "./tui-backend.js" ;
import { sanitizeRenderableText } from "./tui-formatters.js" ;
import { formatStatusSummary } from "./tui-status-summary.js" ;
import type {
AgentSummary,
GatewayStatusSummary,
TuiOptions,
TuiStateAccess,
} from "./tui-types.js" ;
type CommandHandlerContext = {
client: TuiBackend;
chatLog: ChatLog;
tui: TUI;
opts: TuiOptions;
state: TuiStateAccess;
deliverDefault: boolean ;
openOverlay: (component: Component) => void ;
closeOverlay: () => void ;
refreshSessionInfo: () => Promise<void >;
loadHistory: () => Promise<void >;
setSession: (key: string) => Promise<void >;
refreshAgents: () => Promise<void >;
abortActive: () => Promise<void >;
setActivityStatus: (text: string) => void ;
formatSessionKey: (key: string) => string;
applySessionInfoFromPatch: (result: SessionsPatchResult) => void ;
noteLocalRunId?: (runId: string) => void ;
noteLocalBtwRunId?: (runId: string) => void ;
forgetLocalRunId?: (runId: string) => void ;
forgetLocalBtwRunId?: (runId: string) => void ;
runAuthFlow?: (params: {
provider?: string;
}) => Promise<{ exitCode: number | null ; signal: NodeJS.Signals | null }>;
requestExit: () => void ;
};
function isBtwCommand(text: string): boolean {
return /^\/btw(?::|\s|$)/i.test(text.trim());
}
export function createCommandHandlers(context: CommandHandlerContext) {
const {
client,
chatLog,
tui,
opts,
state,
deliverDefault,
openOverlay,
closeOverlay,
refreshSessionInfo,
loadHistory,
setSession,
refreshAgents,
abortActive,
setActivityStatus,
formatSessionKey,
applySessionInfoFromPatch,
noteLocalBtwRunId,
forgetLocalRunId,
forgetLocalBtwRunId,
runAuthFlow,
requestExit,
} = context;
const setAgent = async (id: string) => {
state.currentAgentId = normalizeAgentId(id);
await setSession("" );
};
const closeOverlayAndRender = () => {
closeOverlay();
tui.requestRender();
};
const openSelector = (
selector: {
onSelect?: (item: SelectItem) => void ;
onCancel?: () => void ;
},
onSelect: (value: string) => Promise<void >,
) => {
selector.onSelect = (item) => {
void (async () => {
await onSelect(item.value);
closeOverlayAndRender();
})();
};
selector.onCancel = closeOverlayAndRender;
openOverlay(selector as Component);
tui.requestRender();
};
const openModelSelector = async () => {
try {
const models = await client.listModels();
if (models.length === 0 ) {
chatLog.addSystem("no models available" );
tui.requestRender();
return ;
}
const items = models.map((model) => ({
value: `${model.provider}/${model.id}`,
label: `${model.provider}/${model.id}`,
description: model.name && model.name !== model.id ? model.name : "" ,
}));
const selector = createSearchableSelectList(items, 9 );
openSelector(selector, async (value) => {
try {
const result = await client.patchSession({
key: state.currentSessionKey,
model: value,
});
chatLog.addSystem(`model set to ${value}`);
applySessionInfoFromPatch(result);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`model set failed: ${String(err)}`);
}
});
} catch (err) {
chatLog.addSystem(`model list failed: ${String(err)}`);
tui.requestRender();
}
};
const openAgentSelector = async () => {
await refreshAgents();
if (state.agents.length === 0 ) {
chatLog.addSystem("no agents found" );
tui.requestRender();
return ;
}
const items = state.agents.map((agent: AgentSummary) => ({
value: agent.id,
label: agent.name ? `${agent.id} (${agent.name})` : agent.id,
description: agent.id === state.agentDefaultId ? "default" : "" ,
}));
const selector = createSearchableSelectList(items, 9 );
openSelector(selector, async (value) => {
await setAgent(value);
});
};
const openSessionSelector = async () => {
try {
const result = await client.listSessions({
includeGlobal: false ,
includeUnknown: false ,
includeDerivedTitles: true ,
includeLastMessage: true ,
agentId: state.currentAgentId,
});
const items = result.sessions.map((session) => {
const title = session.derivedTitle ?? session.displayName;
const formattedKey = formatSessionKey(session.key);
// Avoid redundant "title (key)" when title matches key
const label = title && title !== formattedKey ? `${title} (${formattedKey})` : formattedKey;
// Build description: time + message preview
const timePart = session.updatedAt
? formatRelativeTimestamp(session.updatedAt, { dateFallback: true , fallback: "" })
: "" ;
const preview = session.lastMessagePreview?.replace(/\s+/g, " " ).trim();
const description =
timePart && preview ? `${timePart} · ${preview}` : (preview ?? timePart);
return {
value: session.key,
label,
description,
searchText: [
session.displayName,
session.label,
session.subject,
session.sessionId,
session.key,
session.lastMessagePreview,
]
.filter(Boolean )
.join(" " ),
};
});
const selector = createFilterableSelectList(items, 9 );
openSelector(selector, async (value) => {
await setSession(value);
});
} catch (err) {
chatLog.addSystem(`sessions list failed: ${String(err)}`);
tui.requestRender();
}
};
const openSettings = () => {
const items = [
{
id: "tools" ,
label: "Tool output" ,
currentValue: state.toolsExpanded ? "expanded" : "collapsed" ,
values: ["collapsed" , "expanded" ],
},
{
id: "thinking" ,
label: "Show thinking" ,
currentValue: state.showThinking ? "on" : "off" ,
values: ["off" , "on" ],
},
];
const settings = createSettingsList(
items,
(id, value) => {
if (id === "tools" ) {
state.toolsExpanded = value === "expanded" ;
chatLog.setToolsExpanded(state.toolsExpanded);
}
if (id === "thinking" ) {
state.showThinking = value === "on" ;
void loadHistory();
}
tui.requestRender();
},
() => {
closeOverlay();
tui.requestRender();
},
);
openOverlay(settings);
tui.requestRender();
};
const handleCommand = async (raw: string) => {
const { name, args } = parseCommand(raw);
if (!name) {
return ;
}
switch (name) {
case "help" :
chatLog.addSystem(
helpText({
local: opts.local,
provider: state.sessionInfo.modelProvider,
model: state.sessionInfo.model,
}),
);
break ;
case "auth" : {
if (!runAuthFlow) {
chatLog.addSystem("auth login is only available in local embedded mode" );
break ;
}
if (state.activeChatRunId || state.pendingOptimisticUserMessage) {
chatLog.addSystem("abort the current run before /auth" );
break ;
}
const provider = args.trim() || state.sessionInfo.modelProvider || undefined;
chatLog.addSystem(
provider
? `opening auth flow for ${provider}; TUI will resume when it exits`
: "opening auth flow; TUI will resume when it exits" ,
);
tui.requestRender();
setActivityStatus("auth" );
try {
const result = await runAuthFlow({ provider });
await refreshSessionInfo();
if (result.exitCode === 0 && !result.signal) {
chatLog.addSystem(
provider ? `auth flow finished for ${provider}` : "auth flow finished" ,
);
setActivityStatus("idle" );
} else {
const failureSuffix = result.signal
? ` (signal ${result.signal})`
: typeof result.exitCode === "number"
? ` (exit ${String(result.exitCode)})`
: "" ;
chatLog.addSystem(`auth flow failed${failureSuffix}`);
setActivityStatus("error" );
}
} catch (err) {
chatLog.addSystem(`auth flow failed: ${sanitizeRenderableText(String(err))}`);
setActivityStatus("error" );
}
break ;
}
case "gateway-status" :
try {
const status = await client.getGatewayStatus();
if (typeof status === "string" ) {
chatLog.addSystem(status);
break ;
}
if (status && typeof status === "object" ) {
const lines = formatStatusSummary(status as GatewayStatusSummary);
for (const line of lines) {
chatLog.addSystem(line);
}
break ;
}
chatLog.addSystem("status: unknown response" );
} catch (err) {
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();
} else {
await setSession(args);
}
break ;
case "sessions" :
await openSessionSelector();
break ;
case "model" :
if (!args) {
await openModelSelector();
} else {
try {
const result = await client.patchSession({
key: state.currentSessionKey,
model: args,
});
chatLog.addSystem(`model set to ${args}`);
applySessionInfoFromPatch(result);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`model set failed: ${String(err)}`);
}
}
break ;
case "models" :
await openModelSelector();
break ;
case "think" :
if (!args) {
const levels = formatThinkingLevels(
state.sessionInfo.modelProvider,
state.sessionInfo.model,
"|" ,
);
chatLog.addSystem(`usage: /think <${levels}>`);
break ;
}
try {
const result = await client.patchSession({
key: state.currentSessionKey,
thinkingLevel: args,
});
chatLog.addSystem(`thinking set to ${args}`);
applySessionInfoFromPatch(result);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`think failed: ${String(err)}`);
}
break ;
case "verbose" :
if (!args) {
chatLog.addSystem("usage: /verbose <on|off>" );
break ;
}
try {
const result = await client.patchSession({
key: state.currentSessionKey,
verboseLevel: args,
});
chatLog.addSystem(`verbose set to ${args}`);
applySessionInfoFromPatch(result);
await loadHistory();
} catch (err) {
chatLog.addSystem(`verbose failed: ${String(err)}`);
}
break ;
case "trace" :
if (!args) {
chatLog.addSystem("usage: /trace <on|off>" );
break ;
}
try {
const result = await client.patchSession({
key: state.currentSessionKey,
traceLevel: args,
});
chatLog.addSystem(`trace set to ${args}`);
applySessionInfoFromPatch(result);
await loadHistory();
} catch (err) {
chatLog.addSystem(`trace failed: ${String(err)}`);
}
break ;
case "fast" :
if (!args || args === "status" ) {
chatLog.addSystem(`fast mode: ${state.sessionInfo.fastMode ? "on" : "off" }`);
break ;
}
if (args !== "on" && args !== "off" ) {
chatLog.addSystem("usage: /fast <status|on|off>" );
break ;
}
try {
const result = await client.patchSession({
key: state.currentSessionKey,
fastMode: args === "on" ,
});
chatLog.addSystem(`fast mode ${args === "on" ? "enabled" : "disabled" }`);
applySessionInfoFromPatch(result);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`fast failed: ${String(err)}`);
}
break ;
case "reasoning" :
if (!args) {
chatLog.addSystem("usage: /reasoning <on|off>" );
break ;
}
try {
const result = await client.patchSession({
key: state.currentSessionKey,
reasoningLevel: args,
});
chatLog.addSystem(`reasoning set to ${args}`);
applySessionInfoFromPatch(result);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`reasoning failed: ${String(err)}`);
}
break ;
case "usage" : {
const normalized = args ? normalizeUsageDisplay(args) : undefined;
if (args && !normalized) {
chatLog.addSystem("usage: /usage <off|tokens|full>" );
break ;
}
const currentRaw = state.sessionInfo.responseUsage;
const current = resolveResponseUsageMode(currentRaw);
const next =
normalized ?? (current === "off" ? "tokens" : current === "tokens" ? "full" : "off" );
try {
const result = await client.patchSession({
key: state.currentSessionKey,
responseUsage: next === "off" ? null : next,
});
chatLog.addSystem(`usage footer: ${next}`);
applySessionInfoFromPatch(result);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`usage failed: ${String(err)}`);
}
break ;
}
case "elevated" :
if (!args) {
chatLog.addSystem("usage: /elevated <on|off|ask|full>" );
break ;
}
if (!["on" , "off" , "ask" , "full" ].includes(args)) {
chatLog.addSystem("usage: /elevated <on|off|ask|full>" );
break ;
}
try {
const result = await client.patchSession({
key: state.currentSessionKey,
elevatedLevel: args,
});
chatLog.addSystem(`elevated set to ${args}`);
applySessionInfoFromPatch(result);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`elevated failed: ${String(err)}`);
}
break ;
case "activation" : {
if (!args) {
chatLog.addSystem("usage: /activation <mention|always>" );
break ;
}
const activation = normalizeGroupActivation(args);
if (!activation) {
chatLog.addSystem("usage: /activation <mention|always>" );
break ;
}
try {
const result = await client.patchSession({
key: state.currentSessionKey,
groupActivation: activation,
});
chatLog.addSystem(`activation set to ${activation}`);
applySessionInfoFromPatch(result);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`activation failed: ${String(err)}`);
}
break ;
}
case "new" :
try {
// Clear token counts immediately to avoid stale display (#1523)
state.sessionInfo.inputTokens = null ;
state.sessionInfo.outputTokens = null ;
state.sessionInfo.totalTokens = null ;
tui.requestRender();
// Generate unique session key to isolate this TUI client (#39217)
// This ensures /new creates a fresh session that doesn't broadcast
// to other connected TUI clients sharing the original session key.
const uniqueKey = `tui-${randomUUID()}`;
await setSession(uniqueKey);
chatLog.addSystem(`new session: ${uniqueKey}`);
} catch (err) {
chatLog.addSystem(`new session failed: ${sanitizeRenderableText(String(err))}`);
}
break ;
case "reset" :
try {
// Clear token counts immediately to avoid stale display (#1523)
state.sessionInfo.inputTokens = null ;
state.sessionInfo.outputTokens = null ;
state.sessionInfo.totalTokens = null ;
tui.requestRender();
await client.resetSession(state.currentSessionKey, name);
chatLog.addSystem(`session ${state.currentSessionKey} reset`);
await loadHistory();
} catch (err) {
chatLog.addSystem(`reset failed: ${sanitizeRenderableText(String(err))}`);
}
break ;
case "abort" :
await abortActive();
break ;
case "settings" :
openSettings();
break ;
case "exit" :
case "quit" :
requestExit();
break ;
default :
await sendMessage(raw);
break ;
}
tui.requestRender();
};
const sendMessage = async (text: string) => {
if (!state.isConnected) {
chatLog.addSystem(
opts.local
? "local runtime not ready — message not sent"
: "not connected to gateway — message not sent" ,
);
setActivityStatus("disconnected" );
tui.requestRender();
return ;
}
const isBtw = isBtwCommand(text);
const runId = randomUUID();
try {
if (!isBtw) {
chatLog.addUser(text);
state.pendingOptimisticUserMessage = true ;
setActivityStatus("sending" );
} else {
noteLocalBtwRunId?.(runId);
}
tui.requestRender();
await client.sendChat({
sessionKey: state.currentSessionKey,
message: text,
thinking: opts.thinking,
deliver: deliverDefault,
timeoutMs: opts.timeoutMs,
runId,
});
if (!isBtw) {
setActivityStatus("waiting" );
tui.requestRender();
}
} catch (err) {
if (isBtw) {
forgetLocalBtwRunId?.(runId);
}
if (!isBtw && state.activeChatRunId) {
forgetLocalRunId?.(state.activeChatRunId);
}
if (!isBtw) {
state.pendingOptimisticUserMessage = false ;
state.activeChatRunId = null ;
}
chatLog.addSystem(`${isBtw ? "btw failed" : "send failed" }: ${String(err)}`);
if (!isBtw) {
setActivityStatus("error" );
}
tui.requestRender();
}
};
return {
handleCommand,
sendMessage,
openModelSelector,
openAgentSelector,
openSessionSelector,
openSettings,
setAgent,
};
}
Messung V0.5 in Prozent C=99 H=68 G=84
¤ Dauer der Verarbeitung: 0.15 Sekunden
(vorverarbeitet am 2026-06-05)
¤
*© Formatika GbR, Deutschland