import React, { useCallback, useEffect, useRef, useState } from "react";
import { Box, Text, useApp, useInput, useStdout } from "ink";
import { readFile } from "node:fs/promises";
import type { Gateway, GatewayMode, GatewaySessionInfo } from "../../../../gateway/index.js";
import { readPermissionSettings, writePermissionSettings } from "../../../../permission/settings.js";
import { defaultTuiSessionKey } from "../TuiChannel.js";
import { ActivityLine } from "./ActivityLine.js";
import { Header } from "./Header.js";
import { HelpDialog } from "./HelpDialog.js";
import { MessageList } from "./MessageList.js";
import { PermissionPrompt } from "./PermissionPrompt.js";
import { PromptInput } from "./PromptInput.js";
import { SessionSidebar, MIN_SIDEBAR_COLS } from "./SessionSidebar.js";
import { ToolOutputViewer } from "./ToolOutputViewer.js";
import { computeSmartCollapse, groupSessions, flattenSidebarRows } from "./sidebar-helpers.js";
import { applyGatewayEventToTuiState, type TuiAppState, type TuiMessage } from "./types.js";
import { pilotDeckDarkBlueTheme } from "./theme.js";
export type TuiAppProps = {
gateway: Gateway;
connection: "remote" | "in_process";
projectKey?: string;
sessionKey?: string;
model?: string;
cwd?: string;
serverUrl?: string;
onViewOutput?: (path: string) => Promise<void>;
};
export function TuiApp(props: TuiAppProps): React.ReactNode {
const { exit } = useApp();
const { stdout } = useStdout();
const rows = Math.max(10, (stdout?.rows ?? 28) - 7);
const initialSessionKey = props.sessionKey ?? defaultTuiSessionKey(props.projectKey);
const suppressUntilRef = useRef(0);
const [state, setState] = useState<TuiAppState>(() => {
const perm = readPermissionSettings();
return { connection: props.connection,
activeSessionKey: initialSessionKey,
sessions: [],
messages: [],
activity: [],
input: "",
mode: perm.skipPermissions ? "bypassPermissions" : "default",
isRunning: false,
helpOpen: false,
helpTab: "shortcuts",
settingsCursor: 0,
scrollOffset: 0,
focusedIndex: null,
viewerContent: null,
viewerTitle: "",
pendingPermissions: [],
dashboardMode: "closed",
sidebarCursorIndex: 0,
sidebarCollapsed: new Set(),
sidebarGroupBy: "project",
};
});
useEffect(() => {
void props.gateway
.listSessions({ projectKey: props.projectKey, limit: 8 })
.then((result) => setState((current) => ({ ...current, sessions: result.sessions })))
.catch(() => undefined);
}, [props.gateway, props.projectKey]);
const handleInputChange = useCallback((next: string) => {
queueMicrotask(() => {
if (Date.now() < suppressUntilRef.current) {
return;
}
setState((current) => ({ ...current, input: next, focusedIndex: null }));
});
}, []);
const openViewer = useCallback((content: string, title: string) => {
setState((current) => ({ ...current, viewerContent: content, viewerTitle: title }));
}, []);
const closeViewer = useCallback(() => {
setState((current) => ({ ...current, viewerContent: null, viewerTitle: "" }));
}, []);
const openSidebar = useCallback(async () => {
const cols = stdout?.columns ?? 80;
try {
const result = await props.gateway.listSessions({ projectKey: props.projectKey, limit: 20 });
setState((current) => {
const groups = groupSessions(result.sessions, current.sidebarGroupBy);
const smartCollapse = computeSmartCollapse(groups, current.activeSessionKey);
return {
...current,
sessions: result.sessions,
dashboardMode: cols >= MIN_SIDEBAR_COLS ? "sidebar" : "overlay",
sidebarCursorIndex: 0,
sidebarCollapsed: smartCollapse,
};
});
} catch {
const groups = groupSessions(state.sessions, state.sidebarGroupBy);
setState((current) => {
const smartCollapse = computeSmartCollapse(groups, current.activeSessionKey);
return {
...current,
dashboardMode: cols >= MIN_SIDEBAR_COLS ? "sidebar" : "overlay",
sidebarCursorIndex: 0,
sidebarCollapsed: smartCollapse,
};
});
}
}, [props.gateway, props.projectKey, stdout, state.sessions]);
const switchToSession = useCallback(
async (sessionKey: string, summary?: string) => {
void props.gateway.resumeSession({ sessionKey });
setState((c) => ({
...c,
activeSessionKey: sessionKey,
dashboardMode: "closed",
messages: [{ role: "system", text: `Loading session: ${summary || sessionKey}…` }],
scrollOffset: 0,
focusedIndex: null,
}));
try {
const result = await props.gateway.readSessionMessages({
sessionKey,
projectKey: props.projectKey,
limit: 50,
direction: "backward",
});
const tuiMessages: TuiMessage[] = [];
for (const msg of result.messages) {
if (msg.role === "user" && msg.kind === "text" && msg.text) {
tuiMessages.push({ role: "user", text: msg.text });
} else if (msg.role === "assistant" && msg.kind === "text" && msg.text) {
tuiMessages.push({ role: "assistant", text: msg.text });
} else if (msg.role === "assistant" && msg.kind === "thinking" && msg.text) {
const last = tuiMessages.at(-1);
if (last?.role === "assistant") {
last.thinking = (last.thinking ?? "") + msg.text;
}
} else if (msg.role === "tool" && msg.kind === "tool_result") {
tuiMessages.push({
role: "tool",
text: msg.text ?? (msg.ok ? "ok" : "error"),
ok: msg.ok,
toolCallId: msg.toolCallId,
toolName: msg.toolName,
});
} else if (msg.role === "error") {
tuiMessages.push({ role: "error", text: msg.text ?? "error" });
}
}
setState((c) => ({
...c,
messages: tuiMessages.length > 0 ? tuiMessages : [{ role: "system", text: `Session: ${summary || sessionKey}` }],
}));
} catch {
setState((c) => ({
...c,
messages: [{ role: "system", text: `Switched to session: ${summary || sessionKey}` }],
}));
}
},
[props.gateway, props.projectKey],
);
const openToolOutput = useCallback(
async (msg: Extract<TuiMessage, { role: "tool" }>) => {
const SCROLLBACK_LINE_THRESHOLD = 50;
let text = msg.fullText ?? msg.text;
if (msg.resultPath) {
try {
text = await readFile(msg.resultPath, "utf-8");
} catch {
}
}
const lineCount = text.split("\n").length;
if (lineCount < SCROLLBACK_LINE_THRESHOLD && stdout) {
stdout.write(`\n--- ${msg.toolName ?? "tool"} output ---\n${text}\n---\n`);
} else {
openViewer(text, msg.toolName ?? "tool output");
}
},
[stdout, openViewer],
);
const handleSubmit = useCallback(
async (raw: string) => {
const trimmed = raw.trim();
setState((current) => ({ ...current, input: "" }));
if (!trimmed) {
if (!state.isRunning && state.focusedIndex !== null) {
const focused = state.messages[state.focusedIndex];
if (focused?.role === "tool") {
setState((current) => {
const msgs = [...current.messages];
const msg = msgs[state.focusedIndex!];
if (msg?.role === "tool") {
msgs[state.focusedIndex!] = { ...msg, expanded: !msg.expanded };
}
return { ...current, messages: msgs };
});
return;
}
}
if (!state.isRunning) {
const lastTool = [...state.messages].reverse().find(
(m) => m.role === "tool" && ((m.lineCount ?? 0) > 4 || m.resultPath),
) as Extract<TuiMessage, { role: "tool" }> | undefined;
if (lastTool) {
void openToolOutput(lastTool);
}
}
return;
}
if (state.isRunning || state.pendingPermissions.length > 0) {
return;
}
try {
if (
await handleCommand(
trimmed,
props.gateway,
props.projectKey,
setState,
exit,
openViewer,
openToolOutput,
state.messages,
openSidebar,
switchToSession,
)
) {
return;
}
} catch (error) {
setState((c) => ({
...c,
messages: [...c.messages, { role: "error", text: `Command failed: ${error instanceof Error ? error.message : String(error)}` }],
}));
return;
}
setState((current) => ({
...current,
messages: [...current.messages, { role: "user", text: trimmed }],
isRunning: true,
scrollOffset: 0,
focusedIndex: null,
}));
try {
for await (const event of props.gateway.submitTurn({
sessionKey: state.activeSessionKey,
channelKey: "tui",
projectKey: props.projectKey,
message: trimmed,
mode: state.mode,
})) {
setState((current) => {
const partial = applyGatewayEventToTuiState(current, event);
return {
...current,
...partial,
scrollOffset: partial.messages !== current.messages ? 0 : current.scrollOffset,
};
});
}
} catch (error) {
setState((current) => ({
...current,
isRunning: false,
messages: [
...current.messages,
{ role: "error", text: error instanceof Error ? error.message : String(error) },
],
}));
}
},
[exit, props.gateway, props.projectKey, openToolOutput, openViewer, openSidebar, switchToSession, state.activeSessionKey, state.isRunning, state.messages, state.mode, state.focusedIndex, state.pendingPermissions],
);
const scrollPage = Math.max(1, Math.floor(rows / 2));
useInput((input, key) => {
if (state.viewerContent !== null) return;
if (state.pendingPermissions.length > 0) {
const front = state.pendingPermissions[0]!;
const { requestId, toolName, payload } = front;
const dequeue = (c: TuiAppState) => ({ ...c, pendingPermissions: c.pendingPermissions.slice(1) });
if (input === "y") {
void props.gateway.permissionDecide({
sessionKey: state.activeSessionKey,
requestId,
decision: "allow",
remember: false,
});
setState(dequeue);
return;
}
if (input === "a") {
void props.gateway.permissionDecide({
sessionKey: state.activeSessionKey,
requestId,
decision: "allow",
remember: true,
});
const entry = buildPermissionEntry(toolName, payload);
const current = readPermissionSettings();
if (!current.allowedTools.includes(entry)) {
writePermissionSettings({ allowedTools: [...current.allowedTools, entry] });
}
setState(dequeue);
return;
}
if (input === "n") {
void props.gateway.permissionDecide({
sessionKey: state.activeSessionKey,
requestId,
decision: "deny",
reason: "User denied in TUI",
});
setState(dequeue);
return;
}
if (key.escape) {
void props.gateway.abortTurn({ sessionKey: state.activeSessionKey });
setState((c) => ({ ...c, pendingPermissions: [] }));
return;
}
return;
}
if (state.helpOpen) {
const tabOrder: Array<"shortcuts" | "settings" | "about"> = ["shortcuts", "settings", "about"];
if (key.rightArrow || (key.tab && !key.shift)) {
if (state.helpTab !== "settings") {
setState((c) => {
const idx = tabOrder.indexOf(c.helpTab);
return { ...c, helpTab: tabOrder[(idx + 1) % tabOrder.length]! };
});
}
return;
}
if (key.leftArrow || (key.shift && key.tab)) {
if (state.helpTab !== "settings") {
setState((c) => {
const idx = tabOrder.indexOf(c.helpTab);
return { ...c, helpTab: tabOrder[(idx - 1 + tabOrder.length) % tabOrder.length]! };
});
}
return;
}
if (state.helpTab === "settings") {
const SETTINGS_COUNT = 3;
if (key.upArrow) {
setState((c) => ({ ...c, settingsCursor: Math.max(0, c.settingsCursor - 1) }));
return;
}
if (key.downArrow) {
setState((c) => ({ ...c, settingsCursor: Math.min(SETTINGS_COUNT - 1, c.settingsCursor + 1) }));
return;
}
if (key.return || input === " ") {
setState((c) => {
if (c.settingsCursor === 0) {
const modes: Array<"default" | "plan" | "bypassPermissions"> = ["default", "plan", "bypassPermissions"];
const idx = modes.indexOf(c.mode as "default" | "plan" | "bypassPermissions");
const next = modes[(idx + 1) % modes.length]!;
if (next === "bypassPermissions") {
writePermissionSettings({ ...readPermissionSettings(), skipPermissions: true });
} else {
writePermissionSettings({ ...readPermissionSettings(), skipPermissions: false });
}
return { ...c, mode: next };
}
return c;
});
return;
}
}
if (input === "1") {
setState((c) => ({ ...c, helpTab: "shortcuts" }));
return;
}
if (input === "2") {
setState((c) => ({ ...c, helpTab: "settings", settingsCursor: 0 }));
return;
}
if (input === "3") {
setState((c) => ({ ...c, helpTab: "about" }));
return;
}
if (key.escape || input === "q") {
setState((c) => ({ ...c, helpOpen: false }));
return;
}
return;
}
if (state.dashboardMode === "sidebar") {
const groups = groupSessions(state.sessions, state.sidebarGroupBy);
const rows = flattenSidebarRows(groups, state.sidebarCollapsed);
if (key.escape || (key.ctrl && input === "c")) {
setState((c) => ({ ...c, dashboardMode: "closed" }));
return;
}
if (key.ctrl && input === "e") {
suppressUntilRef.current = Date.now() + 100;
setState((c) => ({ ...c, dashboardMode: "closed" }));
return;
}
if (key.ctrl && input === "s") {
suppressUntilRef.current = Date.now() + 100;
setState((c) => {
const next: "project" | "status" = c.sidebarGroupBy === "project" ? "status" : "project";
const newGroups = groupSessions(c.sessions, next);
return {
...c,
sidebarGroupBy: next,
sidebarCursorIndex: 0,
sidebarCollapsed: computeSmartCollapse(newGroups, c.activeSessionKey),
};
});
return;
}
if (key.upArrow && state.input.length === 0) {
setState((c) => ({
...c,
sidebarCursorIndex: Math.max(0, c.sidebarCursorIndex - 1),
}));
return;
}
if (key.downArrow && state.input.length === 0) {
setState((c) => ({
...c,
sidebarCursorIndex: Math.min(rows.length - 1, c.sidebarCursorIndex + 1),
}));
return;
}
if (key.return) {
if (state.input.trim().length > 0) {
} else {
const row = rows[state.sidebarCursorIndex];
if (row?.kind === "header") {
setState((c) => {
const next = new Set(c.sidebarCollapsed);
if (next.has(row.groupKey)) next.delete(row.groupKey);
else next.add(row.groupKey);
return { ...c, sidebarCollapsed: next };
});
} else if (row?.kind === "session") {
const sessionKey = row.session.sessionKey ?? row.session.sessionId;
void switchToSession(sessionKey, row.session.summary);
}
return;
}
}
if (key.rightArrow && state.input.length === 0) {
const row = rows[state.sidebarCursorIndex];
if (row?.kind === "header" && row.collapsed) {
setState((c) => {
const next = new Set(c.sidebarCollapsed);
next.delete(row.groupKey);
return { ...c, sidebarCollapsed: next };
});
} else if (row?.kind === "session") {
const sessionKey = row.session.sessionKey ?? row.session.sessionId;
void switchToSession(sessionKey, row.session.summary);
}
return;
}
if (key.leftArrow && state.input.length === 0) {
const row = rows[state.sidebarCursorIndex];
if (row?.kind === "header" && !row.collapsed) {
setState((c) => {
const next = new Set(c.sidebarCollapsed);
next.add(row.groupKey);
return { ...c, sidebarCollapsed: next };
});
} else if (row?.kind === "session") {
const headerIdx = rows.findIndex(
(r) => r.kind === "header" && r.groupKey === row.groupKey,
);
if (headerIdx >= 0) {
setState((c) => ({ ...c, sidebarCursorIndex: headerIdx }));
}
}
return;
}
}
if (key.ctrl && input === "c") {
if (state.isRunning) {
void props.gateway.abortTurn({ sessionKey: state.activeSessionKey });
} else {
exit();
}
return;
}
if (key.ctrl && input === "e") {
suppressUntilRef.current = Date.now() + 100;
void openSidebar();
return;
}
if (key.escape) {
if (state.isRunning) {
void props.gateway.abortTurn({ sessionKey: state.activeSessionKey });
return;
}
if (state.helpOpen) {
setState((c) => ({ ...c, helpOpen: false }));
return;
}
if (state.scrollOffset > 0 || state.focusedIndex !== null) {
setState((c) => ({ ...c, scrollOffset: 0, focusedIndex: null }));
return;
}
return;
}
if (input === "?" && state.input.length === 0) {
setState((current) => ({ ...current, helpOpen: !current.helpOpen }));
return;
}
if (key.tab && state.input.length === 0) {
setState((current) => {
const toolIndices = current.messages
.map((m, i) => (m.role === "tool" ? i : -1))
.filter((i) => i >= 0);
if (toolIndices.length === 0) return current;
if (key.shift) {
if (current.focusedIndex === null) {
return { ...current, focusedIndex: toolIndices[toolIndices.length - 1]! };
}
const pos = toolIndices.indexOf(current.focusedIndex);
const next = pos <= 0 ? toolIndices[toolIndices.length - 1]! : toolIndices[pos - 1]!;
return { ...current, focusedIndex: next };
} else {
if (current.focusedIndex === null) {
return { ...current, focusedIndex: toolIndices[0]! };
}
const pos = toolIndices.indexOf(current.focusedIndex);
const next = pos >= toolIndices.length - 1 ? toolIndices[0]! : toolIndices[pos + 1]!;
return { ...current, focusedIndex: next };
}
});
return;
}
if (key.pageUp || (key.shift && key.upArrow)) {
setState((current) => {
const maxOffset = Math.max(0, current.messages.length - 1);
return { ...current, scrollOffset: Math.min(maxOffset, current.scrollOffset + scrollPage) };
});
return;
}
if (key.pageDown || (key.shift && key.downArrow)) {
setState((current) => ({
...current,
scrollOffset: Math.max(0, current.scrollOffset - scrollPage),
}));
return;
}
});
if (state.viewerContent !== null) {
return (
<ToolOutputViewer
content={state.viewerContent}
title={state.viewerTitle}
onClose={closeViewer}
/>
);
}
const showSidebar = state.dashboardMode === "sidebar";
const sidebarGroups = groupSessions(state.sessions, state.sidebarGroupBy);
const chatContent = (
<Box flexDirection="column" flexGrow={1} minHeight={12}>
<Header state={state} model={props.model} cwd={props.cwd ?? process.cwd()} serverUrl={props.serverUrl} />
<MessageList
state={state}
rows={rows}
model={props.model}
cwd={props.cwd ?? process.cwd()}
serverUrl={props.serverUrl}
/>
{state.helpOpen ? (
<HelpDialog
activeTab={state.helpTab}
mode={state.mode}
connection={state.connection === "remote" ? (props.serverUrl ? `remote (${props.serverUrl})` : "remote") : "local"}
sessionKey={state.activeSessionKey}
model={props.model}
settingsCursor={state.settingsCursor}
/>
) : null}
{!state.helpOpen && !showSidebar ? <SessionHint sessions={state.sessions} /> : null}
{state.pendingPermissions.length > 0 ? (
<PermissionPrompt
toolName={state.pendingPermissions[0]!.toolName}
payload={state.pendingPermissions[0]!.payload}
queueLength={state.pendingPermissions.length}
/>
) : null}
<ActivityLine state={state} />
<PromptInput
value={state.input}
onChange={handleInputChange}
onSubmit={handleSubmit}
isRunning={state.isRunning}
focus={!state.helpOpen && state.pendingPermissions.length === 0}
/>
</Box>
);
if (showSidebar) {
return (
<Box flexDirection="row" minHeight={12}>
<SessionSidebar
groups={sidebarGroups}
collapsed={state.sidebarCollapsed}
cursorIndex={state.sidebarCursorIndex}
activeSessionKey={state.activeSessionKey}
maxRows={rows}
groupBy={state.sidebarGroupBy}
/>
{chatContent}
</Box>
);
}
return chatContent;
}
async function handleCommand(
command: string,
gateway: Gateway,
projectKey: string | undefined,
setState: React.Dispatch<React.SetStateAction<TuiAppState>>,
exit: () => void,
openViewer: (content: string, title: string) => void,
openToolOutput: (msg: Extract<TuiMessage, { role: "tool" }>) => Promise<void>,
messages: TuiMessage[],
openSidebar: () => Promise<void>,
switchToSession: (sessionKey: string, summary?: string) => Promise<void>,
): Promise<boolean> {
if (!command.startsWith("/")) {
return false;
}
const [name, ...args] = command.split(/\s+/);
switch (name) {
case "/new": {
const result = await gateway.newSession({ channelKey: "tui", projectKey });
setState((current) => ({
...current,
activeSessionKey: result.sessionKey,
messages: [{ role: "system", text: `New session: ${result.sessionKey}` }],
}));
return true;
}
case "/sessions":
case "/switch":
case "/dashboard": {
if (name === "/switch" && args.length > 0) {
const n = parseInt(args[0]!, 10);
if (!isNaN(n) && n >= 1) {
const result = await gateway.listSessions({ projectKey, limit: 20 });
const target = result.sessions[n - 1];
if (target) {
const sessionKey = target.sessionKey ?? target.sessionId;
setState((c) => ({ ...c, sessions: result.sessions }));
void switchToSession(sessionKey, target.summary);
return true;
}
}
}
void openSidebar();
return true;
}
case "/permissions": {
const sub = args[0];
const current = readPermissionSettings();
if (!sub) {
const lines = [
`skipPermissions: ${current.skipPermissions}`,
`allow: ${current.allowedTools.length === 0 ? "(none)" : current.allowedTools.join(", ")}`,
`deny: ${current.disallowedTools.length === 0 ? "(none)" : current.disallowedTools.join(", ")}`,
];
setState((c) => ({ ...c, messages: [...c.messages, { role: "system", text: lines.join("\n") }] }));
return true;
}
const entry = args.slice(1).join(" ").trim();
if (!entry && sub !== "bypass") {
setState((c) => ({
...c,
messages: [
...c.messages,
{
role: "error",
text: "Usage: /permissions [allow|deny|clear <entry>|bypass]",
},
],
}));
return true;
}
if (sub === "allow" && entry) {
writePermissionSettings({ allowedTools: [...current.allowedTools, entry] });
setState((c) => ({ ...c, messages: [...c.messages, { role: "system", text: `Added allow: ${entry}` }] }));
return true;
}
if (sub === "deny" && entry) {
writePermissionSettings({ disallowedTools: [...current.disallowedTools, entry] });
setState((c) => ({ ...c, messages: [...c.messages, { role: "system", text: `Added deny: ${entry}` }] }));
return true;
}
if (sub === "clear" && entry) {
writePermissionSettings({
allowedTools: current.allowedTools.filter((e) => e !== entry),
disallowedTools: current.disallowedTools.filter((e) => e !== entry),
});
setState((c) => ({ ...c, messages: [...c.messages, { role: "system", text: `Cleared: ${entry}` }] }));
return true;
}
if (sub === "bypass") {
writePermissionSettings({ skipPermissions: true });
setState((c) => ({
...c,
mode: "bypassPermissions",
messages: [...c.messages, { role: "system", text: "Permissions bypassed globally (skipPermissions=true)." }],
}));
return true;
}
setState((c) => ({
...c,
messages: [...c.messages, { role: "error", text: `Unknown /permissions subcommand: ${sub}` }],
}));
return true;
}
case "/mode": {
const mode = (args[0] ?? "default") as GatewayMode;
if (mode === "bypassPermissions") {
writePermissionSettings({ skipPermissions: true });
} else {
const perm = readPermissionSettings();
if (perm.skipPermissions) {
writePermissionSettings({ skipPermissions: false });
}
}
setState((current) => ({
...current,
mode,
messages: [...current.messages, { role: "system", text: `Mode: ${mode}` }],
}));
return true;
}
case "/view": {
const n = parseInt(args[0] ?? "", 10);
const tools = messages.filter(
(m): m is Extract<TuiMessage, { role: "tool" }> =>
m.role === "tool" && ((m.lineCount ?? 0) > 4 || !!m.resultPath || !!m.fullText),
);
if (tools.length === 0) {
setState((current) => ({
...current,
messages: [...current.messages, { role: "system", text: "No tool output to view." }],
}));
return true;
}
const target = !isNaN(n) && n >= 1 && n <= tools.length
? tools[n - 1]!
: tools[tools.length - 1]!;
void openToolOutput(target);
return true;
}
case "/clear":
setState((current) => ({ ...current, messages: [], focusedIndex: null }));
return true;
case "/help":
setState((current) => ({ ...current, helpOpen: !current.helpOpen }));
return true;
case "/exit":
exit();
return true;
default:
setState((current) => ({
...current,
messages: [...current.messages, { role: "error", text: `Unknown command ${name}` }],
}));
return true;
}
}
function buildPermissionEntry(toolName: string, payload: unknown): string {
if (toolName !== "bash") return toolName;
const record = typeof payload === "object" && payload ? (payload as Record<string, unknown>) : {};
const command = typeof record.command === "string" ? record.command.trim() : "";
if (!command) return "bash";
const tokens = command.split(/\s+/);
if (tokens[0] === "git" && tokens[1]) return `bash:${tokens[0]} ${tokens[1]}:*`;
return `bash:${tokens[0]}:*`;
}
function SessionHint({ sessions }: { sessions: GatewaySessionInfo[] }): React.ReactNode {
if (sessions.length <= 1) {
return null;
}
const count = sessions.length;
return (
<Text color={pilotDeckDarkBlueTheme.subtle} dimColor>
{count} session{count > 1 ? "s" : ""} · Ctrl+E sidebar · /switch N
</Text>
);
}