- Updated the README to reflect new features in Claude Code, including the ability to spawn subagents via the Task tool and coordinate teams with improved visibility. - Added details on the rendering of subagent sessions as expandable inline cards, including execution traces, metrics, and tool calls. - Enhanced description of teammate messages, highlighting color-coded cards and team lifecycle visibility. - Clarified session summary metrics to differentiate between teammate and subagent counts for better user insights. This commit significantly improves the documentation of team and subagent visualization features, providing users with a clearer understanding of the capabilities and enhancements in the application.
226 lines
7.7 KiB
TypeScript
226 lines
7.7 KiB
TypeScript
/**
|
|
* SSH IPC Handlers - Manages SSH connection lifecycle from renderer requests.
|
|
*
|
|
* Channels:
|
|
* - ssh:connect - Connect to SSH host, create new context
|
|
* - ssh:disconnect - Disconnect and switch back to local context
|
|
* - ssh:getState - Get current connection state
|
|
* - ssh:test - Test connection without switching
|
|
*/
|
|
|
|
import { createLogger } from '@shared/utils/logger';
|
|
|
|
// Channel constants (mirrored from preload/constants/ipcChannels.ts to respect module boundaries)
|
|
const SSH_CONNECT = 'ssh:connect';
|
|
const SSH_DISCONNECT = 'ssh:disconnect';
|
|
const SSH_GET_STATE = 'ssh:getState';
|
|
const SSH_TEST = 'ssh:test';
|
|
const SSH_GET_CONFIG_HOSTS = 'ssh:getConfigHosts';
|
|
const SSH_RESOLVE_HOST = 'ssh:resolveHost';
|
|
const SSH_SAVE_LAST_CONNECTION = 'ssh:saveLastConnection';
|
|
const SSH_GET_LAST_CONNECTION = 'ssh:getLastConnection';
|
|
import * as path from 'path';
|
|
|
|
import { configManager, ServiceContext } from '../services';
|
|
|
|
import type {
|
|
ServiceContextRegistry,
|
|
SshConnectionConfig,
|
|
SshConnectionManager,
|
|
SshConnectionStatus,
|
|
} from '../services';
|
|
import type { SshLastConnection } from '@shared/types';
|
|
import type { IpcMain } from 'electron';
|
|
|
|
const logger = createLogger('IPC:ssh');
|
|
|
|
// =============================================================================
|
|
// Module State
|
|
// =============================================================================
|
|
|
|
let connectionManager: SshConnectionManager;
|
|
let registry: ServiceContextRegistry;
|
|
let onContextRewire: (context: ServiceContext) => void;
|
|
|
|
// =============================================================================
|
|
// Initialization
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Initialize SSH handlers with required services.
|
|
* @param manager - The SSH connection manager instance
|
|
* @param contextRegistry - The service context registry
|
|
* @param onRewire - Rewire-only callback (no renderer notification) for renderer-initiated switches
|
|
*/
|
|
export function initializeSshHandlers(
|
|
manager: SshConnectionManager,
|
|
contextRegistry: ServiceContextRegistry,
|
|
onRewire: (context: ServiceContext) => void
|
|
): void {
|
|
connectionManager = manager;
|
|
registry = contextRegistry;
|
|
onContextRewire = onRewire;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Handler Registration
|
|
// =============================================================================
|
|
|
|
export function registerSshHandlers(ipcMain: IpcMain): void {
|
|
ipcMain.handle(SSH_CONNECT, async (_event, config: SshConnectionConfig) => {
|
|
try {
|
|
// Connect to SSH host
|
|
await connectionManager.connect(config);
|
|
|
|
// Get provider and remote path
|
|
const provider = connectionManager.getProvider();
|
|
const remoteProjectsPath = connectionManager.getRemoteProjectsPath() ?? undefined;
|
|
const remoteTodosPath = remoteProjectsPath
|
|
? path.join(path.dirname(remoteProjectsPath), 'todos')
|
|
: undefined;
|
|
|
|
// Generate context ID
|
|
const contextId = `ssh-${config.host}`;
|
|
|
|
// Destroy existing SSH context if any (reconnection case)
|
|
if (registry.has(contextId)) {
|
|
logger.info(`Destroying existing SSH context: ${contextId}`);
|
|
registry.destroy(contextId);
|
|
}
|
|
|
|
// Create new SSH context
|
|
const sshContext = new ServiceContext({
|
|
id: contextId,
|
|
type: 'ssh',
|
|
fsProvider: provider,
|
|
projectsDir: remoteProjectsPath,
|
|
todosDir: remoteTodosPath,
|
|
});
|
|
|
|
// Register and start SSH context
|
|
registry.registerContext(sshContext);
|
|
sshContext.start();
|
|
|
|
// Switch to SSH context
|
|
registry.switch(contextId);
|
|
|
|
// Re-wire file watcher events only (renderer's connectSsh() handles state)
|
|
onContextRewire(sshContext);
|
|
|
|
return { success: true, data: connectionManager.getStatus() };
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
logger.error('SSH connect failed:', message);
|
|
return { success: false, error: message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle(SSH_DISCONNECT, async () => {
|
|
try {
|
|
// Get current SSH context ID before disconnecting
|
|
const currentContextId = registry.getActiveContextId();
|
|
const isSshContext = currentContextId.startsWith('ssh-');
|
|
|
|
// Disconnect from SSH
|
|
connectionManager.disconnect();
|
|
|
|
// If we were on an SSH context, destroy it
|
|
if (isSshContext) {
|
|
// Switch back to local first (this also starts local file watcher)
|
|
registry.switch('local');
|
|
|
|
// Destroy the SSH context
|
|
registry.destroy(currentContextId);
|
|
|
|
// Re-wire file watcher events only (renderer's disconnectSsh() handles state)
|
|
const localContext = registry.getActive();
|
|
onContextRewire(localContext);
|
|
}
|
|
|
|
return { success: true, data: connectionManager.getStatus() };
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
logger.error('SSH disconnect failed:', message);
|
|
return { success: false, error: message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle(SSH_GET_STATE, async (): Promise<SshConnectionStatus> => {
|
|
return connectionManager.getStatus();
|
|
});
|
|
|
|
ipcMain.handle(SSH_TEST, async (_event, config: SshConnectionConfig) => {
|
|
try {
|
|
const result = await connectionManager.testConnection(config);
|
|
return { success: true, data: result };
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
return { success: false, error: message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle(SSH_GET_CONFIG_HOSTS, async () => {
|
|
try {
|
|
const hosts = await connectionManager.getConfigHosts();
|
|
return { success: true, data: hosts };
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
logger.error('Failed to get SSH config hosts:', message);
|
|
return { success: true, data: [] };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle(SSH_RESOLVE_HOST, async (_event, alias: string) => {
|
|
try {
|
|
const entry = await connectionManager.resolveHostConfig(alias);
|
|
return { success: true, data: entry };
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
logger.error(`Failed to resolve SSH host "${alias}":`, message);
|
|
return { success: true, data: null };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle(SSH_SAVE_LAST_CONNECTION, async (_event, config: SshLastConnection) => {
|
|
try {
|
|
configManager.updateConfig('ssh', {
|
|
lastConnection: {
|
|
host: config.host,
|
|
port: config.port,
|
|
username: config.username,
|
|
authMethod: config.authMethod,
|
|
privateKeyPath: config.privateKeyPath,
|
|
},
|
|
});
|
|
return { success: true };
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
logger.error('Failed to save SSH connection:', message);
|
|
return { success: false, error: message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle(SSH_GET_LAST_CONNECTION, async () => {
|
|
try {
|
|
const config = configManager.getConfig();
|
|
return { success: true, data: config.ssh.lastConnection };
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
logger.error('Failed to get last SSH connection:', message);
|
|
return { success: true, data: null };
|
|
}
|
|
});
|
|
|
|
logger.info('SSH handlers registered');
|
|
}
|
|
|
|
export function removeSshHandlers(ipcMain: IpcMain): void {
|
|
ipcMain.removeHandler(SSH_CONNECT);
|
|
ipcMain.removeHandler(SSH_DISCONNECT);
|
|
ipcMain.removeHandler(SSH_GET_STATE);
|
|
ipcMain.removeHandler(SSH_TEST);
|
|
ipcMain.removeHandler(SSH_GET_CONFIG_HOSTS);
|
|
ipcMain.removeHandler(SSH_RESOLVE_HOST);
|
|
ipcMain.removeHandler(SSH_SAVE_LAST_CONNECTION);
|
|
ipcMain.removeHandler(SSH_GET_LAST_CONNECTION);
|
|
}
|