Fix MCP form input and add reactive MCP server state

- Fix space key not working in MCP add form by handling evt.name === "space"
  - Add support for multi-character paste sequences via evt.sequence
  - Add MCPServerDisplay type to types/tui.ts for UI display
  - Add mcpServers reactive state to app store with setMcpServers,
    addMcpServer, and updateMcpServerStatus actions
  - Update session.tsx to use store's mcpServers instead of static props
  - Update execute.tsx to update store when server is added/connected
  - Remove duplicate MCPServer interfaces from app.tsx, session.tsx,
    and mcp-select.tsx in favor of shared MCPServerDisplay type
This commit is contained in:
2026-02-02 14:10:19 -05:00
parent 3b277c3925
commit fbd9afa177
6 changed files with 110 additions and 30 deletions

View File

@@ -105,7 +105,20 @@ const defaultHandleMCPAdd = async (data: MCPAddFormData): Promise<void> => {
data.isGlobal,
);
await connectServer(data.name);
// Add to store with "connecting" status
appStore.addMcpServer({
id: data.name,
name: data.name,
status: "disconnected",
description: data.command,
});
try {
await connectServer(data.name);
appStore.updateMcpServerStatus(data.name, "connected");
} catch {
appStore.updateMcpServerStatus(data.name, "error");
}
};
const defaultHandleBrainSetJwtToken = async (jwtToken: string): Promise<void> => {

View File

@@ -28,6 +28,7 @@ import { ToastProvider, Toast, useToast } from "@tui-solid/ui/toast";
import { Home } from "@tui-solid/routes/home";
import { Session } from "@tui-solid/routes/session";
import type { TuiInput, TuiOutput } from "@tui-solid/types";
import type { MCPServerDisplay } from "@/types/tui";
import type { PermissionScope, LearningScope } from "@/types/tui";
import type { MCPAddFormData } from "@/types/mcp";
@@ -37,13 +38,6 @@ interface AgentOption {
description?: string;
}
interface MCPServer {
id: string;
name: string;
status: "connected" | "disconnected" | "error";
description?: string;
}
interface AppProps extends TuiInput {
onExit: (output: TuiOutput) => void;
onSubmit: (input: string) => Promise<void>;
@@ -72,7 +66,7 @@ interface AppProps extends TuiInput {
} | null;
agents?: AgentOption[];
currentAgent?: string;
mcpServers?: MCPServer[];
mcpServers?: MCPServerDisplay[];
files?: string[];
}
@@ -450,7 +444,7 @@ export interface TuiRenderOptions extends TuiInput {
} | null;
agents?: AgentOption[];
currentAgent?: string;
mcpServers?: MCPServer[];
mcpServers?: MCPServerDisplay[];
files?: string[];
}

View File

@@ -2,23 +2,17 @@ import { createSignal, createMemo, For, Show } from "solid-js";
import { useKeyboard } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { useTheme } from "@tui-solid/context/theme";
interface MCPServer {
id: string;
name: string;
status: "connected" | "disconnected" | "error";
description?: string;
}
import type { MCPServerDisplay } from "@/types/tui";
interface MCPSelectProps {
servers: MCPServer[];
servers: MCPServerDisplay[];
onSelect: (serverId: string) => void;
onAddNew: () => void;
onClose: () => void;
isActive?: boolean;
}
const STATUS_COLORS: Record<MCPServer["status"], string> = {
const STATUS_COLORS: Record<MCPServerDisplay["status"], string> = {
connected: "success",
disconnected: "textDim",
error: "error",

View File

@@ -14,6 +14,7 @@ import type {
CommandMenuState,
StreamingLogState,
SuggestionState,
MCPServerDisplay,
} from "@/types/tui";
import type { ProviderModel } from "@/types/providers";
import type { BrainConnectionStatus, BrainUser } from "@/types/brain";
@@ -45,6 +46,7 @@ interface AppStore {
streamingLog: StreamingLogState;
suggestions: SuggestionState;
cascadeEnabled: boolean;
mcpServers: MCPServerDisplay[];
brain: {
status: BrainConnectionStatus;
user: BrainUser | null;
@@ -89,6 +91,7 @@ interface AppContextValue {
streamingLogIsActive: Accessor<boolean>;
suggestions: Accessor<SuggestionState>;
cascadeEnabled: Accessor<boolean>;
mcpServers: Accessor<MCPServerDisplay[]>;
brain: Accessor<{
status: BrainConnectionStatus;
user: BrainUser | null;
@@ -184,6 +187,11 @@ interface AppContextValue {
setBrainShowBanner: (show: boolean) => void;
dismissBrainBanner: () => void;
// MCP actions
setMcpServers: (servers: MCPServerDisplay[]) => void;
addMcpServer: (server: MCPServerDisplay) => void;
updateMcpServerStatus: (id: string, status: MCPServerDisplay["status"]) => void;
// Computed
isInputLocked: () => boolean;
}
@@ -249,6 +257,7 @@ export const { provider: AppStoreProvider, use: useAppStore } =
streamingLog: createInitialStreamingState(),
suggestions: createInitialSuggestionState(),
cascadeEnabled: true,
mcpServers: [],
brain: {
status: "disconnected" as BrainConnectionStatus,
user: null,
@@ -303,6 +312,7 @@ export const { provider: AppStoreProvider, use: useAppStore } =
const streamingLogIsActive = (): boolean => store.streamingLog.isStreaming;
const suggestions = (): SuggestionState => store.suggestions;
const cascadeEnabled = (): boolean => store.cascadeEnabled;
const mcpServers = (): MCPServerDisplay[] => store.mcpServers;
const brain = () => store.brain;
// Mode actions
@@ -522,6 +532,36 @@ export const { provider: AppStoreProvider, use: useAppStore } =
setStore("brain", { ...store.brain, showBanner: false });
};
// MCP actions
const setMcpServers = (servers: MCPServerDisplay[]): void => {
setStore("mcpServers", servers);
};
const addMcpServer = (server: MCPServerDisplay): void => {
setStore(
produce((s) => {
// Replace if exists, otherwise add
const existingIndex = s.mcpServers.findIndex((srv) => srv.id === server.id);
if (existingIndex !== -1) {
s.mcpServers[existingIndex] = server;
} else {
s.mcpServers.push(server);
}
}),
);
};
const updateMcpServerStatus = (id: string, status: MCPServerDisplay["status"]): void => {
setStore(
produce((s) => {
const server = s.mcpServers.find((srv) => srv.id === id);
if (server) {
server.status = status;
}
}),
);
};
// Session stats actions
const startThinking = (): void => {
setStore("sessionStats", {
@@ -758,6 +798,7 @@ export const { provider: AppStoreProvider, use: useAppStore } =
streamingLogIsActive,
suggestions,
cascadeEnabled,
mcpServers,
brain,
// Mode actions
@@ -820,6 +861,11 @@ export const { provider: AppStoreProvider, use: useAppStore } =
setBrainShowBanner,
dismissBrainBanner,
// MCP actions
setMcpServers,
addMcpServer,
updateMcpServerStatus,
// Session stats actions
startThinking,
stopThinking,
@@ -887,6 +933,7 @@ const defaultAppState = {
isCompacting: false,
streamingLog: createInitialStreamingState(),
suggestions: createInitialSuggestionState(),
mcpServers: [] as MCPServerDisplay[],
brain: {
status: "disconnected" as BrainConnectionStatus,
user: null,
@@ -926,6 +973,7 @@ export const appStore = {
isCompacting: storeRef.isCompacting(),
streamingLog: storeRef.streamingLog(),
suggestions: storeRef.suggestions(),
mcpServers: storeRef.mcpServers(),
brain: storeRef.brain(),
};
},
@@ -1134,4 +1182,19 @@ export const appStore = {
if (!storeRef) return;
storeRef.dismissBrainBanner();
},
setMcpServers: (servers: MCPServerDisplay[]): void => {
if (!storeRef) return;
storeRef.setMcpServers(servers);
},
addMcpServer: (server: MCPServerDisplay): void => {
if (!storeRef) return;
storeRef.addMcpServer(server);
},
updateMcpServerStatus: (id: string, status: MCPServerDisplay["status"]): void => {
if (!storeRef) return;
storeRef.updateMcpServerStatus(id, status);
},
};

View File

@@ -1,4 +1,4 @@
import { Show, Switch, Match, createSignal } from "solid-js";
import { Show, Switch, Match, createSignal, createMemo, onMount } from "solid-js";
import { useTheme } from "@tui-solid/context/theme";
import { useAppStore } from "@tui-solid/context/app";
import { Header } from "@tui-solid/components/header";
@@ -23,7 +23,7 @@ import { CenteredModal } from "@tui-solid/components/centered-modal";
import { DebugLogPanel } from "@tui-solid/components/debug-log-panel";
import { BrainMenu } from "@tui-solid/components/brain-menu";
import { BRAIN_DISABLED } from "@constants/brain";
import type { PermissionScope, LearningScope, InteractionMode } from "@/types/tui";
import type { PermissionScope, LearningScope, InteractionMode, MCPServerDisplay } from "@/types/tui";
import type { MCPAddFormData } from "@/types/mcp";
interface AgentOption {
@@ -32,13 +32,6 @@ interface AgentOption {
description?: string;
}
interface MCPServer {
id: string;
name: string;
status: "connected" | "disconnected" | "error";
description?: string;
}
interface ProviderStatus {
available: boolean;
error?: string;
@@ -72,7 +65,7 @@ interface SessionProps {
} | null;
agents?: AgentOption[];
currentAgent?: string;
mcpServers?: MCPServer[];
mcpServers?: MCPServerDisplay[];
files?: string[];
providerStatuses?: Record<string, ProviderStatus>;
providerScores?: Record<string, number>;
@@ -82,6 +75,16 @@ export function Session(props: SessionProps) {
const theme = useTheme();
const app = useAppStore();
// Initialize MCP servers from props on mount
onMount(() => {
if (props.mcpServers && props.mcpServers.length > 0) {
app.setMcpServers(props.mcpServers);
}
});
// Use store's mcpServers (reactive, updated when servers are added)
const mcpServers = createMemo(() => app.mcpServers());
// Local state for help menu
const [selectedHelpTopic, setSelectedHelpTopic] = createSignal<string | null>(
null
@@ -291,7 +294,7 @@ export function Session(props: SessionProps) {
<Match when={app.mode() === "mcp_select"}>
<CenteredModal>
<MCPSelect
servers={props.mcpServers ?? []}
servers={mcpServers()}
onSelect={props.onMCPSelect}
onAddNew={handleMCPAddNew}
onClose={handleMCPClose}

View File

@@ -246,6 +246,19 @@ export interface StreamingLogState {
isStreaming: boolean;
}
// ============================================================================
// MCP Types (for UI display)
// ============================================================================
export type MCPServerStatus = "connected" | "disconnected" | "error";
export interface MCPServerDisplay {
id: string;
name: string;
status: MCPServerStatus;
description?: string;
}
// ============================================================================
// Component Props Types
// ============================================================================