Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { FileConfigProvider } from './config/file-provider.js';
import { FileStorageAdapter } from './storage/file.js';
import type { SystemConfig, ToolDiscoveryConfig } from './types/config.js';
import { DEFAULT_CONNECTION_POOL } from './types/service.js';
import type { TagFilter } from './types/tool.js';
import { getPackageVersion } from './utils/package-version.js';
import { silenceStderrForShutdown } from './utils/silence-stderr-shutdown.js';
Expand Down Expand Up @@ -292,11 +293,7 @@ function initializeConfigDir(configDir: string): void {
logLevel: 'INFO',
configDir,
mcpServers: {},
connectionPool: {
maxConnections: 5,
idleTimeout: 60000,
connectionTimeout: 30000,
},
connectionPool: { ...DEFAULT_CONNECTION_POOL },
healthCheck: {
enabled: true,
interval: 30000,
Expand Down
7 changes: 2 additions & 5 deletions src/config/file-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
ValidationError,
} from '../types/config.js';
import type { ServiceDefinition } from '../types/service.js';
import { DEFAULT_CONNECTION_POOL } from '../types/service.js';
import type { StorageAdapter } from '../types/storage.js';
import * as log from '../utils/logger.js';

Expand Down Expand Up @@ -683,11 +684,7 @@ export class FileConfigProvider implements ConfigProvider {
logLevel: 'INFO',
configDir: this.configDir,
mcpServers: {},
connectionPool: {
maxConnections: 5,
idleTimeout: 60000,
connectionTimeout: 30000,
},
connectionPool: { ...DEFAULT_CONNECTION_POOL },
healthCheck: {
enabled: true,
interval: 30000,
Expand Down
22 changes: 16 additions & 6 deletions src/tui/app-optimized.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ export const TuiAppOptimized: React.FC<TuiAppProps> = ({

const terminalHeight = stdout?.rows || 24;

// Vertical space consumed by chrome above the content area:
// Header (double border title 3 rows + stats 1 row + margin 1 = 5).
// StatusBar adds a round-bordered box (border 2 + 1 line + margin 1 = 4) while a message is visible.
const OUTER_CHROME_LINES = 5;
const STATUS_BAR_LINES = statusMessage ? 4 : 0;
const contentHeight = Math.max(8, terminalHeight - OUTER_CHROME_LINES - STATUS_BAR_LINES);

// Calculate global tool statistics from in-memory cache
const globalToolStats = React.useMemo(() => {
let total = 0;
Expand Down Expand Up @@ -469,12 +476,11 @@ export const TuiAppOptimized: React.FC<TuiAppProps> = ({
process.exit(0);
}

// Tools view
// Tools view: ServiceTools handles its own input (search Esc layering,
// tool navigation/toggle). Do not intercept Esc here — that would bypass
// ServiceTools' layered Esc (exit search → clear filter → back) and jump
// straight to the service list.
if (view === 'tools') {
if (key.escape) {
setView('list');
setRefreshKey(k => k + 1);
}
return;
}

Expand Down Expand Up @@ -632,11 +638,15 @@ export const TuiAppOptimized: React.FC<TuiAppProps> = ({
{view === 'tools' && editingService && (
<ServiceTools
service={editingService}
onBack={() => setView('list')}
onBack={() => {
setView('list');
setRefreshKey(k => k + 1);
}}
onToggleTool={handleToggleTool}
onBatchToggleTools={handleBatchToggleTools}
toolStates={editingService.toolStates || {}}
onToolsDiscovered={handleToolsDiscovered}
terminalHeight={contentHeight}
/>
)}
</Box>
Expand Down
19 changes: 13 additions & 6 deletions src/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ export const TuiApp: React.FC<TuiAppProps> = ({ configDir, config: propConfig, c

const terminalHeight = stdout?.rows || 24;

// Vertical space consumed by chrome above the content area:
// app header box (border 2 + padding 2 + 1 line + margin 1 = 6) + info bar (3 lines + margin 1 = 4).
// A transient status message adds a single-bordered box (6 lines).
const OUTER_CHROME_LINES = 10;
const STATUS_MESSAGE_LINES = statusMessage ? 6 : 0;
const contentHeight = Math.max(8, terminalHeight - OUTER_CHROME_LINES - STATUS_MESSAGE_LINES);

// Calculate global tool statistics
const globalToolStats = React.useMemo(() => {
let totalTools = 0;
Expand Down Expand Up @@ -366,12 +373,8 @@ export const TuiApp: React.FC<TuiAppProps> = ({ configDir, config: propConfig, c
return;
}

// Tools view - handle back
// Tools view: ServiceTools handles its own input including Esc layering.
if (view === 'tools') {
if (key.escape) {
setView('list');
setRefreshKey(k => k + 1);
}
return;
}

Expand Down Expand Up @@ -544,11 +547,15 @@ export const TuiApp: React.FC<TuiAppProps> = ({ configDir, config: propConfig, c
{view === 'tools' && editingService && (
<ServiceTools
service={editingService}
onBack={() => setView('list')}
onBack={() => {
setView('list');
setRefreshKey(k => k + 1);
}}
onToggleTool={handleToggleTool}
onBatchToggleTools={handleBatchToggleTools}
toolStates={editingService.toolStates || {}}
onToolsDiscovered={handleToolsDiscovered}
terminalHeight={contentHeight}
/>
)}

Expand Down
24 changes: 4 additions & 20 deletions src/tui/components/ServiceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Box, Text, useInput, useStdout } from 'ink';
import TextInput from 'ink-text-input';
import SelectInput from 'ink-select-input';
import type { ServiceDefinition, TransportType } from '../../types/service.js';
import { fieldHelp, fieldPlaceholder } from './service-field-config.js';

export interface ServiceFormProps {
/** Existing service to edit (undefined for new service) */
Expand Down Expand Up @@ -138,26 +139,7 @@ function getFieldLabel(field: FormField): string {
* Get field help text
*/
function getFieldHelp(field: FormField): string {
const help: Record<FormField, string> = {
name: 'Unique identifier for this service',
transport: 'Protocol used to communicate with the service',
command: 'Command to start the MCP server (e.g., npx, node, python)',
url: 'HTTP(S) URL of the MCP server',
args: 'Command-line arguments (e.g., -y, @modelcontextprotocol/server-filesystem, /tmp)',
env: 'Environment variables to pass to the process (e.g., NODE_ENV=production, DEBUG=true).',
headers: 'Custom HTTP headers (e.g., Authorization: Bearer token, Content-Type: application/json).',
tags: 'Labels for categorization and filtering (e.g., local, storage, api)',
enabled: 'Whether this service should be active',
maxConnections: 'Maximum number of concurrent connections (default: 5)',
idleTimeout: 'Time before idle connections are closed (default: 60000)',
connectionTimeout: 'Maximum time to wait for connection (default: 30000)',
triggerHintsStart: 'Reason the LLM should call this service at conversation start (e.g., "recall role memory").',
triggerHintsEnd: 'Reason the LLM should call this service before conversation ends (e.g., "persist new memory").',
triggerHintsPhrases: 'Extra trigger phrases the LLM should treat as a search signal (e.g., "我是X, switch role").',
confirm: 'Review and save the configuration',
quickMode: 'Use quick mode with defaults for advanced options',
};
return help[field];
return fieldHelp[field];
}

/**
Expand Down Expand Up @@ -473,13 +455,15 @@ export const ServiceForm: React.FC<ServiceFormProps> = ({

// Render text input field
const renderTextInput = (field: FormField) => {
const placeholder = fieldPlaceholder[field];
return (
<TextInput
value={formData[field as keyof FormData] as string}
onChange={(value) => {
setFormData({ ...formData, [field]: value });
}}
onSubmit={() => goToNextField()}
{...(placeholder ? { placeholder } : {})}
/>
);
};
Expand Down
33 changes: 18 additions & 15 deletions src/tui/components/ServiceFormUnified.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Box, Text, useInput, useStdout } from 'ink';
import TextInput from 'ink-text-input';
import SelectInput from 'ink-select-input';
import type { ServiceDefinition, TransportType } from '../../types/service.js';
import { fieldHelp, fieldPlaceholder } from './service-field-config.js';

export interface ServiceFormUnifiedProps {
/** Existing service to edit (undefined for new service) */
Expand Down Expand Up @@ -91,14 +92,14 @@ function getFieldConfigs(transport: TransportType): FieldConfig[] {
{
field: 'name',
label: 'Service Name',
help: 'Unique identifier (letters, numbers, hyphens, underscores)',
help: fieldHelp.name,
required: true,
type: 'text',
},
{
field: 'transport',
label: 'Transport Type',
help: 'Protocol for communication',
help: fieldHelp.transport,
required: true,
type: 'select',
},
Expand All @@ -108,23 +109,23 @@ function getFieldConfigs(transport: TransportType): FieldConfig[] {
configs.push({
field: 'command',
label: 'Command',
help: 'Command to start the MCP server (e.g., npx, node, python)',
help: fieldHelp.command,
required: true,
type: 'text',
dependsOn: { field: 'transport', value: 'stdio' },
});
configs.push({
field: 'args',
label: 'Arguments',
help: 'Command-line arguments (comma-separated, optional)',
help: fieldHelp.args,
required: false,
type: 'text',
dependsOn: { field: 'transport', value: 'stdio' },
});
configs.push({
field: 'env',
label: 'Environment Variables',
help: 'Environment variables to pass to the process (KEY=VALUE pairs, comma-separated, optional).',
help: fieldHelp.env,
required: false,
type: 'text',
dependsOn: { field: 'transport', value: 'stdio' },
Expand All @@ -133,15 +134,15 @@ function getFieldConfigs(transport: TransportType): FieldConfig[] {
configs.push({
field: 'url',
label: 'URL',
help: 'HTTP(S) URL of the MCP server',
help: fieldHelp.url,
required: true,
type: 'text',
dependsOn: { field: 'transport', value: transport },
});
configs.push({
field: 'headers',
label: 'Headers',
help: 'Custom HTTP headers (Key: Value pairs, comma-separated, optional).',
help: fieldHelp.headers,
required: false,
type: 'text',
dependsOn: { field: 'transport', value: transport },
Expand All @@ -152,56 +153,56 @@ function getFieldConfigs(transport: TransportType): FieldConfig[] {
{
field: 'tags',
label: 'Tags',
help: 'Labels for categorization (comma-separated, optional)',
help: fieldHelp.tags,
required: false,
type: 'text',
},
{
field: 'enabled',
label: 'Enabled',
help: 'Whether this service should be active',
help: fieldHelp.enabled,
required: false,
type: 'select',
},
{
field: 'maxConnections',
label: 'Max Connections',
help: 'Maximum concurrent connections (1-100, default: 5)',
help: fieldHelp.maxConnections,
required: false,
type: 'text',
},
{
field: 'idleTimeout',
label: 'Idle Timeout',
help: 'Time before idle connections close in ms (min: 1000, default: 60000)',
help: fieldHelp.idleTimeout,
required: false,
type: 'text',
},
{
field: 'connectionTimeout',
label: 'Connection Timeout',
help: 'Maximum time to wait for connection in ms (min: 1000, default: 30000)',
help: fieldHelp.connectionTimeout,
required: false,
type: 'text',
},
{
field: 'triggerHintsStart',
label: 'Trigger: On Session Start',
help: 'Reason for the LLM to call this service at conversation start (optional, e.g. "recall role memory").',
help: fieldHelp.triggerHintsStart,
required: false,
type: 'text',
},
{
field: 'triggerHintsEnd',
label: 'Trigger: On Session End',
help: 'Reason to call before the conversation ends (optional, e.g. "persist new memory").',
help: fieldHelp.triggerHintsEnd,
required: false,
type: 'text',
},
{
field: 'triggerHintsPhrases',
label: 'Trigger Phrases',
help: 'Extra phrases that should make the LLM search this service (comma-separated, optional).',
help: fieldHelp.triggerHintsPhrases,
required: false,
type: 'text',
}
Expand Down Expand Up @@ -615,6 +616,7 @@ export const ServiceFormUnified: React.FC<ServiceFormUnifiedProps> = ({

// Render text input
const renderTextInput = (field: FormField) => {
const placeholder = fieldPlaceholder[field];
return (
<TextInput
value={formData[field as keyof FormData] as string}
Expand All @@ -629,6 +631,7 @@ export const ServiceFormUnified: React.FC<ServiceFormUnifiedProps> = ({
setTouched(prev => new Set(prev).add(field));
goToNextField();
}}
{...(placeholder ? { placeholder } : {})}
/>
);
};
Expand Down
22 changes: 19 additions & 3 deletions src/tui/components/ServiceJsonEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import React, { useState, useEffect } from 'react';
import { Box, Text, useInput } from 'ink';
import type { ServiceDefinition } from '../../types/service.js';
import { DEFAULT_CONNECTION_POOL } from '../../types/service.js';

export interface ServiceJsonEditorProps {
/** Initial JSON content (for editing existing service) */
Expand Down Expand Up @@ -191,9 +192,9 @@ function getExampleJson(): string {
"tags": ["local", "storage"],
"enabled": true,
"connectionPool": {
"maxConnections": 5,
"idleTimeout": 60000,
"connectionTimeout": 30000
"maxConnections": DEFAULT_CONNECTION_POOL.maxConnections,
"idleTimeout": DEFAULT_CONNECTION_POOL.idleTimeout,
"connectionTimeout": DEFAULT_CONNECTION_POOL.connectionTimeout
}
},
"github": {
Expand All @@ -205,6 +206,21 @@ function getExampleJson(): string {
},
"tags": ["remote", "api"],
"enabled": true
},
"remote-api": {
"transport": "http",
"url": "https://example.com/mcp",
"headers": {
"Authorization": "Bearer token",
"Content-Type": "application/json"
},
"tags": ["remote", "api"],
"enabled": true,
"connectionPool": {
"maxConnections": DEFAULT_CONNECTION_POOL.maxConnections,
"idleTimeout": DEFAULT_CONNECTION_POOL.idleTimeout,
"connectionTimeout": DEFAULT_CONNECTION_POOL.connectionTimeout
}
}
}, null, 2);
}
Expand Down
Loading