Skip to content

Terminal API

Terminal APIは、開発環境内でのターミナル統合と管理のための包括的な機能を提供します。

概要

Terminal APIでは以下のことができます:

  • ターミナルインスタンスの作成と管理
  • プログラムによるコマンドの実行
  • ターミナル出力とイベントの監視
  • ターミナルの外観と動作のカスタマイズ
  • シェル環境との統合
  • ターミナルの入力と出力の処理
  • 複数のターミナルタイプのサポート
  • ターミナルベースのユーザーインターフェースの提供

基本的な使用方法

ターミナル管理

typescript
import { TraeAPI } from '@trae/api';

// ターミナルマネージャー
class TerminalManager {
  private terminals: Map<string, TerminalInstance> = new Map();
  private activeTerminal: TerminalInstance | null = null;
  private eventEmitter = new TraeAPI.EventEmitter<TerminalEvent>();
  private terminalCounter = 0;

  constructor() {
    this.setupEventListeners();
  }

  // ターミナルを作成
  async createTerminal(options: TerminalOptions = {}): Promise<TerminalInstance> {
    const terminalId = `terminal-${++this.terminalCounter}`;
    const terminal = new TerminalInstance(terminalId, options);
    
    this.terminals.set(terminalId, terminal);
    
    // 最初のターミナルの場合はアクティブに設定
    if (!this.activeTerminal) {
      this.activeTerminal = terminal;
    }
    
    console.log(`ターミナルを作成: ${terminalId}`);
    this.eventEmitter.fire({ type: 'terminalCreated', terminal });
    
    return terminal;
  }

  // IDでターミナルを取得
  getTerminal(terminalId: string): TerminalInstance | null {
    return this.terminals.get(terminalId) || null;
  }

  // すべてのターミナルを取得
  getAllTerminals(): TerminalInstance[] {
    return Array.from(this.terminals.values());
  }

  // アクティブなターミナルを取得
  getActiveTerminal(): TerminalInstance | null {
    return this.activeTerminal;
  }

  // アクティブなターミナルを設定
  setActiveTerminal(terminalId: string): boolean {
    const terminal = this.terminals.get(terminalId);
    if (terminal) {
      this.activeTerminal = terminal;
      this.eventEmitter.fire({ type: 'terminalActivated', terminal });
      return true;
    }
    return false;
  }

  // ターミナルを閉じる
  async closeTerminal(terminalId: string): Promise<boolean> {
    const terminal = this.terminals.get(terminalId);
    if (!terminal) {
      return false;
    }

    await terminal.dispose();
    this.terminals.delete(terminalId);
    
    // 必要に応じてアクティブなターミナルを更新
    if (this.activeTerminal === terminal) {
      const remaining = Array.from(this.terminals.values());
      this.activeTerminal = remaining.length > 0 ? remaining[0] : null;
    }
    
    console.log(`ターミナルを閉じました: ${terminalId}`);
    this.eventEmitter.fire({ type: 'terminalClosed', terminalId });
    
    return true;
  }

  // Execute command in terminal
  async executeCommand(command: string, options: ExecuteOptions = {}): Promise<ExecutionResult> {
    let terminal = options.terminalId ? this.getTerminal(options.terminalId) : this.activeTerminal;
    
    if (!terminal) {
      // Create new terminal if none exists
      terminal = await this.createTerminal({
        name: 'Command Terminal',
        cwd: options.cwd
      });
    }
    
    return terminal.executeCommand(command, options);
  }

  // Execute command and return output
  async executeCommandWithOutput(command: string, options: ExecuteOptions = {}): Promise<string> {
    const result = await this.executeCommand(command, options);
    return new Promise((resolve, reject) => {
      let output = '';
      
      const outputListener = result.onOutput(data => {
        output += data;
      });
      
      const exitListener = result.onExit(code => {
        outputListener.dispose();
        exitListener.dispose();
        
        if (code === 0) {
          resolve(output);
        } else {
          reject(new Error(`Command failed with exit code ${code}: ${output}`));
        }
      });
    });
  }

  // Setup event listeners
  private setupEventListeners(): void {
    // Listen for workspace changes
    TraeAPI.workspace.onDidChangeWorkspaceFolders(() => {
      // Update terminal working directories if needed
      this.updateTerminalWorkingDirectories();
    });
  }

  private updateTerminalWorkingDirectories(): void {
    const workspaceFolders = TraeAPI.workspace.workspaceFolders;
    if (!workspaceFolders || workspaceFolders.length === 0) {
      return;
    }
    
    const primaryWorkspace = workspaceFolders[0].uri.fsPath;
    
    for (const terminal of this.terminals.values()) {
      if (terminal.options.followWorkspace) {
        terminal.changeWorkingDirectory(primaryWorkspace);
      }
    }
  }

  // Event handling
  onDidChangeTerminal(listener: (event: TerminalEvent) => void): TraeAPI.Disposable {
    return this.eventEmitter.event(listener);
  }

  dispose(): void {
    for (const terminal of this.terminals.values()) {
      terminal.dispose();
    }
    this.terminals.clear();
    this.activeTerminal = null;
    this.eventEmitter.dispose();
  }
}

// Terminal instance
class TerminalInstance {
  private process: any = null;
  private isRunning = false;
  private outputBuffer: string = '';
  private eventEmitter = new TraeAPI.EventEmitter<TerminalInstanceEvent>();
  private executionCounter = 0;
  private currentExecution: ExecutionResult | null = null;

  constructor(
    public readonly id: string,
    public readonly options: TerminalOptions
  ) {
    this.initialize();
  }

  private async initialize(): Promise<void> {
    try {
      await this.createProcess();
      console.log(`Terminal ${this.id} initialized`);
      this.eventEmitter.fire({ type: 'initialized' });
    } catch (error) {
      console.error(`Failed to initialize terminal ${this.id}:`, error);
      this.eventEmitter.fire({ type: 'error', error: error as Error });
    }
  }

  private async createProcess(): Promise<void> {
    const { spawn } = require('child_process');
    const shell = this.getShell();
    const args = this.getShellArgs();
    const env = this.getEnvironment();
    
    this.process = spawn(shell, args, {
      cwd: this.options.cwd || process.cwd(),
      env,
      stdio: ['pipe', 'pipe', 'pipe']
    });
    
    this.isRunning = true;
    
    // Setup process event handlers
    this.process.stdout.on('data', (data: Buffer) => {
      const text = data.toString();
      this.outputBuffer += text;
      this.eventEmitter.fire({ type: 'output', data: text, stream: 'stdout' });
    });
    
    this.process.stderr.on('data', (data: Buffer) => {
      const text = data.toString();
      this.outputBuffer += text;
      this.eventEmitter.fire({ type: 'output', data: text, stream: 'stderr' });
    });
    
    this.process.on('exit', (code: number, signal: string) => {
      this.isRunning = false;
      console.log(`Terminal ${this.id} exited with code ${code}`);
      this.eventEmitter.fire({ type: 'exit', code, signal });
    });
    
    this.process.on('error', (error: Error) => {
      console.error(`Terminal ${this.id} error:`, error);
      this.eventEmitter.fire({ type: 'error', error });
    });
  }

  private getShell(): string {
    if (this.options.shell) {
      return this.options.shell;
    }
    
    // Platform-specific default shells
    switch (process.platform) {
      case 'win32':
        return process.env.COMSPEC || 'cmd.exe';
      case 'darwin':
      case 'linux':
      default:
        return process.env.SHELL || '/bin/bash';
    }
  }

  private getShellArgs(): string[] {
    if (this.options.shellArgs) {
      return this.options.shellArgs;
    }
    
    // Platform-specific default args
    switch (process.platform) {
      case 'win32':
        return ['/K']; // Keep window open
      case 'darwin':
      case 'linux':
      default:
        return ['-i']; // Interactive mode
    }
  }

  private getEnvironment(): NodeJS.ProcessEnv {
    const env = { ...process.env };
    
    // Add custom environment variables
    if (this.options.env) {
      Object.assign(env, this.options.env);
    }
    
    // Set terminal-specific variables
    env.TERM = this.options.termType || 'xterm-256color';
    env.TRAE_TERMINAL_ID = this.id;
    
    return env;
  }

  // Execute command
  async executeCommand(command: string, options: ExecuteOptions = {}): Promise<ExecutionResult> {
    if (!this.isRunning) {
      throw new Error(`Terminal ${this.id} is not running`);
    }
    
    const executionId = `exec-${++this.executionCounter}`;
    const execution = new ExecutionResult(executionId, command, options);
    
    this.currentExecution = execution;
    
    // Send command to terminal
    this.sendInput(command + '\n');
    
    console.log(`Executing command in terminal ${this.id}: ${command}`);
    this.eventEmitter.fire({ type: 'commandExecuted', command, executionId });
    
    return execution;
  }

  // Send input to terminal
  sendInput(input: string): void {
    if (this.process && this.isRunning) {
      this.process.stdin.write(input);
      this.eventEmitter.fire({ type: 'input', data: input });
    }
  }

  // Send key sequence
  sendKey(key: string): void {
    const keySequences: { [key: string]: string } = {
      'enter': '\n',
      'tab': '\t',
      'escape': '\x1b',
      'backspace': '\x08',
      'delete': '\x7f',
      'up': '\x1b[A',
      'down': '\x1b[B',
      'right': '\x1b[C',
      'left': '\x1b[D',
      'home': '\x1b[H',
      'end': '\x1b[F',
      'pageup': '\x1b[5~',
      'pagedown': '\x1b[6~',
      'ctrl+c': '\x03',
      'ctrl+d': '\x04',
      'ctrl+z': '\x1a'
    };
    
    const sequence = keySequences[key.toLowerCase()] || key;
    this.sendInput(sequence);
  }

  // Change working directory
  async changeWorkingDirectory(path: string): Promise<boolean> {
    try {
      const command = process.platform === 'win32' ? `cd /d "${path}"` : `cd "${path}"`;
      await this.executeCommand(command);
      
      console.log(`Changed working directory to: ${path}`);
      this.eventEmitter.fire({ type: 'workingDirectoryChanged', path });
      
      return true;
    } catch (error) {
      console.error(`Failed to change working directory:`, error);
      return false;
    }
  }

  // Clear terminal
  clear(): void {
    const clearCommand = process.platform === 'win32' ? 'cls' : 'clear';
    this.sendInput(clearCommand + '\n');
    this.outputBuffer = '';
    this.eventEmitter.fire({ type: 'cleared' });
  }

  // Get terminal output
  getOutput(): string {
    return this.outputBuffer;
  }

  // Get terminal info
  getInfo(): TerminalInfo {
    return {
      id: this.id,
      name: this.options.name || `Terminal ${this.id}`,
      isRunning: this.isRunning,
      shell: this.getShell(),
      cwd: this.options.cwd || process.cwd(),
      pid: this.process?.pid,
      createdAt: this.options.createdAt || Date.now()
    };
  }

  // Event handling
  onDidChangeState(listener: (event: TerminalInstanceEvent) => void): TraeAPI.Disposable {
    return this.eventEmitter.event(listener);
  }

  // Dispose terminal
  async dispose(): Promise<void> {
    if (this.process && this.isRunning) {
      this.process.kill();
      
      // Wait for process to exit
      await new Promise<void>((resolve) => {
        if (!this.isRunning) {
          resolve();
          return;
        }
        
        const exitListener = this.eventEmitter.event(event => {
          if (event.type === 'exit') {
            exitListener.dispose();
            resolve();
          }
        });
        
        // Force kill after timeout
        setTimeout(() => {
          if (this.isRunning && this.process) {
            this.process.kill('SIGKILL');
          }
          exitListener.dispose();
          resolve();
        }, 5000);
      });
    }
    
    this.eventEmitter.dispose();
    console.log(`Terminal ${this.id} disposed`);
  }
}

// Execution result
class ExecutionResult {
  private eventEmitter = new TraeAPI.EventEmitter<ExecutionEvent>();
  private isCompleted = false;
  private exitCode: number | null = null;
  private output = '';
  private startTime = Date.now();

  constructor(
    public readonly id: string,
    public readonly command: string,
    public readonly options: ExecuteOptions
  ) {}

  // Mark execution as completed
  complete(exitCode: number): void {
    if (this.isCompleted) return;
    
    this.isCompleted = true;
    this.exitCode = exitCode;
    
    const duration = Date.now() - this.startTime;
    console.log(`Command execution completed: ${this.command} (exit code: ${exitCode}, duration: ${duration}ms)`);
    
    this.eventEmitter.fire({ type: 'completed', exitCode, duration });
  }

  // Add output
  addOutput(data: string): void {
    this.output += data;
    this.eventEmitter.fire({ type: 'output', data });
  }

  // Get execution info
  getInfo(): ExecutionInfo {
    return {
      id: this.id,
      command: this.command,
      isCompleted: this.isCompleted,
      exitCode: this.exitCode,
      output: this.output,
      startTime: this.startTime,
      duration: this.isCompleted ? Date.now() - this.startTime : null
    };
  }

  // Event handlers
  onOutput(listener: (data: string) => void): TraeAPI.Disposable {
    return this.eventEmitter.event(event => {
      if (event.type === 'output') {
        listener(event.data);
      }
    });
  }

  onExit(listener: (exitCode: number) => void): TraeAPI.Disposable {
    return this.eventEmitter.event(event => {
      if (event.type === 'completed') {
        listener(event.exitCode);
      }
    });
  }

  onCompleted(listener: (info: ExecutionInfo) => void): TraeAPI.Disposable {
    return this.eventEmitter.event(event => {
      if (event.type === 'completed') {
        listener(this.getInfo());
      }
    });
  }

  dispose(): void {
    this.eventEmitter.dispose();
  }
}

// Initialize terminal manager
const terminalManager = new TerminalManager();

Advanced Features

Terminal UI Integration

typescript
// Terminal UI provider
class TerminalUIProvider {
  private terminalPanel: TraeAPI.WebviewPanel | null = null;
  private terminals: Map<string, TerminalView> = new Map();

  constructor(private terminalManager: TerminalManager) {
    this.setupEventListeners();
  }

  // Show terminal panel
  async showTerminalPanel(): Promise<void> {
    if (this.terminalPanel) {
      this.terminalPanel.reveal();
      return;
    }

    this.terminalPanel = TraeAPI.window.createWebviewPanel(
      'terminal',
      'Terminal',
      TraeAPI.ViewColumn.One,
      {
        enableScripts: true,
        retainContextWhenHidden: true
      }
    );

    this.terminalPanel.webview.html = this.getTerminalHTML();
    this.setupWebviewMessageHandling();

    this.terminalPanel.onDidDispose(() => {
      this.terminalPanel = null;
    });
  }

  private getTerminalHTML(): string {
    return `
      <!DOCTYPE html>
      <html>
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Terminal</title>
        <style>
          body {
            margin: 0;
            padding: 0;
            background: #1e1e1e;
            color: #d4d4d4;
            font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
            font-size: 14px;
            overflow: hidden;
          }
          
          .terminal-container {
            display: flex;
            flex-direction: column;
            height: 100vh;
          }
          
          .terminal-tabs {
            display: flex;
            background: #2d2d30;
            border-bottom: 1px solid #3e3e42;
            padding: 0;
            margin: 0;
          }
          
          .terminal-tab {
            padding: 8px 16px;
            background: #2d2d30;
            border: none;
            color: #cccccc;
            cursor: pointer;
            border-right: 1px solid #3e3e42;
            position: relative;
          }
          
          .terminal-tab.active {
            background: #1e1e1e;
            color: #ffffff;
          }
          
          .terminal-tab .close-btn {
            margin-left: 8px;
            color: #cccccc;
            cursor: pointer;
          }
          
          .terminal-tab .close-btn:hover {
            color: #ffffff;
          }
          
          .terminal-content {
            flex: 1;
            position: relative;
          }
          
          .terminal-view {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            display: none;
          }
          
          .terminal-view.active {
            display: block;
          }
          
          .terminal-output {
            height: calc(100% - 30px);
            overflow-y: auto;
            padding: 10px;
            white-space: pre-wrap;
            word-wrap: break-word;
          }
          
          .terminal-input {
            height: 30px;
            background: #1e1e1e;
            border: none;
            border-top: 1px solid #3e3e42;
            color: #d4d4d4;
            padding: 0 10px;
            font-family: inherit;
            font-size: inherit;
            outline: none;
            width: 100%;
            box-sizing: border-box;
          }
          
          .new-terminal-btn {
            padding: 8px 16px;
            background: #0e639c;
            border: none;
            color: #ffffff;
            cursor: pointer;
            margin-left: auto;
          }
          
          .new-terminal-btn:hover {
            background: #1177bb;
          }
          
          .output-line {
            margin: 0;
            padding: 0;
          }
          
          .output-error {
            color: #f48771;
          }
          
          .output-warning {
            color: #dcdcaa;
          }
          
          .output-success {
            color: #4ec9b0;
          }
        </style>
      </head>
      <body>
        <div class="terminal-container">
          <div class="terminal-tabs">
            <button class="new-terminal-btn" onclick="createNewTerminal()">+ New Terminal</button>
          </div>
          <div class="terminal-content" id="terminalContent">
            <!-- Terminal views will be added here -->
          </div>
        </div>
        
        <script>
          const vscode = acquireVsCodeApi();
          let activeTerminalId = null;
          let terminals = new Map();
          
          // Create new terminal
          function createNewTerminal() {
            vscode.postMessage({ type: 'createTerminal' });
          }
          
          // Switch to terminal
          function switchTerminal(terminalId) {
            if (activeTerminalId) {
              document.getElementById('tab-' + activeTerminalId).classList.remove('active');
              document.getElementById('view-' + activeTerminalId).classList.remove('active');
            }
            
            activeTerminalId = terminalId;
            document.getElementById('tab-' + terminalId).classList.add('active');
            document.getElementById('view-' + terminalId).classList.add('active');
            
            // Focus input
            const input = document.getElementById('input-' + terminalId);
            if (input) {
              input.focus();
            }
          }
          
          // Close terminal
          function closeTerminal(terminalId, event) {
            event.stopPropagation();
            vscode.postMessage({ type: 'closeTerminal', terminalId });
          }
          
          // Send command
          function sendCommand(terminalId, command) {
            vscode.postMessage({ type: 'sendCommand', terminalId, command });
          }
          
          // Handle input
          function handleInput(terminalId, event) {
            if (event.key === 'Enter') {
              const input = event.target;
              const command = input.value;
              input.value = '';
              
              // Add command to output
              addOutput(terminalId, '$ ' + command + '\n', 'command');
              
              // Send command
              sendCommand(terminalId, command);
            }
          }
          
          // Add output to terminal
          function addOutput(terminalId, text, type = 'output') {
            const output = document.getElementById('output-' + terminalId);
            if (output) {
              const line = document.createElement('div');
              line.className = 'output-line';
              if (type === 'error') line.className += ' output-error';
              if (type === 'warning') line.className += ' output-warning';
              if (type === 'success') line.className += ' output-success';
              line.textContent = text;
              output.appendChild(line);
              output.scrollTop = output.scrollHeight;
            }
          }
          
          // Handle messages from extension
          window.addEventListener('message', event => {
            const message = event.data;
            
            switch (message.type) {
              case 'terminalCreated':
                createTerminalView(message.terminal);
                break;
              case 'terminalClosed':
                removeTerminalView(message.terminalId);
                break;
              case 'terminalOutput':
                addOutput(message.terminalId, message.data, message.outputType);
                break;
              case 'terminalList':
                message.terminals.forEach(terminal => createTerminalView(terminal));
                break;
            }
          });
          
          // Create terminal view
          function createTerminalView(terminal) {
            const tabsContainer = document.querySelector('.terminal-tabs');
            const contentContainer = document.getElementById('terminalContent');
            
            // Create tab
            const tab = document.createElement('button');
            tab.id = 'tab-' + terminal.id;
            tab.className = 'terminal-tab';
            tab.onclick = () => switchTerminal(terminal.id);
            tab.innerHTML = `
              ${terminal.name}
              <span class="close-btn" onclick="closeTerminal('${terminal.id}', event)">×</span>
            `;
            
            // Insert before new terminal button
            const newBtn = document.querySelector('.new-terminal-btn');
            tabsContainer.insertBefore(tab, newBtn);
            
            // Create view
            const view = document.createElement('div');
            view.id = 'view-' + terminal.id;
            view.className = 'terminal-view';
            view.innerHTML = `
              <div id="output-${terminal.id}" class="terminal-output"></div>
              <input id="input-${terminal.id}" class="terminal-input" 
                     placeholder="Type command and press Enter..."
                     onkeydown="handleInput('${terminal.id}', event)">
            `;
            
            contentContainer.appendChild(view);
            
            // Switch to new terminal if it's the first one
            if (!activeTerminalId) {
              switchTerminal(terminal.id);
            }
            
            terminals.set(terminal.id, terminal);
          }
          
          // Remove terminal view
          function removeTerminalView(terminalId) {
            const tab = document.getElementById('tab-' + terminalId);
            const view = document.getElementById('view-' + terminalId);
            
            if (tab) tab.remove();
            if (view) view.remove();
            
            terminals.delete(terminalId);
            
            // Switch to another terminal if this was active
            if (activeTerminalId === terminalId) {
              activeTerminalId = null;
              const remainingTerminals = Array.from(terminals.keys());
              if (remainingTerminals.length > 0) {
                switchTerminal(remainingTerminals[0]);
              }
            }
          }
          
          // Request initial terminal list
          vscode.postMessage({ type: 'getTerminals' });
        </script>
      </body>
      </html>
    `;
  }

  private setupWebviewMessageHandling(): void {
    if (!this.terminalPanel) return;

    this.terminalPanel.webview.onDidReceiveMessage(async message => {
      switch (message.type) {
        case 'createTerminal':
          const terminal = await this.terminalManager.createTerminal({
            name: `Terminal ${this.terminals.size + 1}`
          });
          this.sendMessage({
            type: 'terminalCreated',
            terminal: terminal.getInfo()
          });
          break;

        case 'closeTerminal':
          await this.terminalManager.closeTerminal(message.terminalId);
          this.sendMessage({
            type: 'terminalClosed',
            terminalId: message.terminalId
          });
          break;

        case 'sendCommand':
          const terminalInstance = this.terminalManager.getTerminal(message.terminalId);
          if (terminalInstance) {
            await terminalInstance.executeCommand(message.command);
          }
          break;

        case 'getTerminals':
          const terminals = this.terminalManager.getAllTerminals();
          this.sendMessage({
            type: 'terminalList',
            terminals: terminals.map(t => t.getInfo())
          });
          break;
      }
    });
  }

  private sendMessage(message: any): void {
    if (this.terminalPanel) {
      this.terminalPanel.webview.postMessage(message);
    }
  }

  private setupEventListeners(): void {
    this.terminalManager.onDidChangeTerminal(event => {
      if (!this.terminalPanel) return;

      switch (event.type) {
        case 'terminalCreated':
          this.sendMessage({
            type: 'terminalCreated',
            terminal: event.terminal.getInfo()
          });
          break;

        case 'terminalClosed':
          this.sendMessage({
            type: 'terminalClosed',
            terminalId: event.terminalId
          });
          break;
      }
    });
  }
}

// Terminal view for individual terminals
class TerminalView {
  constructor(
    public readonly terminal: TerminalInstance,
    private uiProvider: TerminalUIProvider
  ) {
    this.setupEventListeners();
  }

  private setupEventListeners(): void {
    this.terminal.onDidChangeState(event => {
      switch (event.type) {
        case 'output':
          this.uiProvider.sendMessage({
            type: 'terminalOutput',
            terminalId: this.terminal.id,
            data: event.data,
            outputType: event.stream === 'stderr' ? 'error' : 'output'
          });
          break;
      }
    });
  }
}

// Initialize terminal UI
const terminalUIProvider = new TerminalUIProvider(terminalManager);

Shell Integration

typescript
// Shell integration utilities
class ShellIntegration {
  // Detect available shells
  static async getAvailableShells(): Promise<ShellInfo[]> {
    const shells: ShellInfo[] = [];
    
    if (process.platform === 'win32') {
      // Windows shells
      const windowsShells = [
        { name: 'Command Prompt', path: 'cmd.exe', type: 'cmd' },
        { name: 'PowerShell', path: 'powershell.exe', type: 'powershell' },
        { name: 'PowerShell Core', path: 'pwsh.exe', type: 'pwsh' },
        { name: 'Git Bash', path: 'C:\\Program Files\\Git\\bin\\bash.exe', type: 'bash' }
      ];
      
      for (const shell of windowsShells) {
        if (await this.isShellAvailable(shell.path)) {
          shells.push(shell);
        }
      }
    } else {
      // Unix-like shells
      const unixShells = [
        { name: 'Bash', path: '/bin/bash', type: 'bash' },
        { name: 'Zsh', path: '/bin/zsh', type: 'zsh' },
        { name: 'Fish', path: '/usr/local/bin/fish', type: 'fish' },
        { name: 'Dash', path: '/bin/dash', type: 'dash' }
      ];
      
      for (const shell of unixShells) {
        if (await this.isShellAvailable(shell.path)) {
          shells.push(shell);
        }
      }
    }
    
    return shells;
  }
  
  private static async isShellAvailable(path: string): Promise<boolean> {
    try {
      const { access } = require('fs').promises;
      await access(path);
      return true;
    } catch {
      return false;
    }
  }
  
  // Get shell-specific command formatting
  static formatCommand(command: string, shellType: string): string {
    switch (shellType) {
      case 'powershell':
      case 'pwsh':
        // PowerShell specific formatting
        return command;
      case 'cmd':
        // Command Prompt specific formatting
        return command;
      case 'bash':
      case 'zsh':
      case 'fish':
      default:
        // Unix shell formatting
        return command;
    }
  }
  
  // Get shell-specific environment setup
  static getShellEnvironment(shellType: string): { [key: string]: string } {
    const env: { [key: string]: string } = {};
    
    switch (shellType) {
      case 'powershell':
      case 'pwsh':
        env.PSModulePath = process.env.PSModulePath || '';
        break;
      case 'bash':
      case 'zsh':
        env.HISTSIZE = '10000';
        env.HISTFILESIZE = '20000';
        break;
    }
    
    return env;
  }
}

Interfaces

typescript
interface TerminalOptions {
  name?: string;
  shell?: string;
  shellArgs?: string[];
  cwd?: string;
  env?: { [key: string]: string };
  termType?: string;
  followWorkspace?: boolean;
  createdAt?: number;
}

interface ExecuteOptions {
  terminalId?: string;
  cwd?: string;
  env?: { [key: string]: string };
  timeout?: number;
  shell?: string;
}

interface TerminalInfo {
  id: string;
  name: string;
  isRunning: boolean;
  shell: string;
  cwd: string;
  pid?: number;
  createdAt: number;
}

interface ExecutionInfo {
  id: string;
  command: string;
  isCompleted: boolean;
  exitCode: number | null;
  output: string;
  startTime: number;
  duration: number | null;
}

interface ShellInfo {
  name: string;
  path: string;
  type: string;
}

type TerminalEvent = {
  type: 'terminalCreated';
  terminal: TerminalInstance;
} | {
  type: 'terminalClosed';
  terminalId: string;
} | {
  type: 'terminalActivated';
  terminal: TerminalInstance;
};

type TerminalInstanceEvent = {
  type: 'initialized';
} | {
  type: 'output';
  data: string;
  stream: 'stdout' | 'stderr';
} | {
  type: 'input';
  data: string;
} | {
  type: 'exit';
  code: number;
  signal: string;
} | {
  type: 'error';
  error: Error;
} | {
  type: 'commandExecuted';
  command: string;
  executionId: string;
} | {
  type: 'workingDirectoryChanged';
  path: string;
} | {
  type: 'cleared';
};

type ExecutionEvent = {
  type: 'output';
  data: string;
} | {
  type: 'completed';
  exitCode: number;
  duration: number;
};

API Reference

Core Interfaces

typescript
interface TerminalAPI {
  // Terminal management
  createTerminal(options?: TerminalOptions): Promise<TerminalInstance>;
  getTerminal(terminalId: string): TerminalInstance | null;
  getAllTerminals(): TerminalInstance[];
  getActiveTerminal(): TerminalInstance | null;
  setActiveTerminal(terminalId: string): boolean;
  closeTerminal(terminalId: string): Promise<boolean>;
  
  // Command execution
  executeCommand(command: string, options?: ExecuteOptions): Promise<ExecutionResult>;
  executeCommandWithOutput(command: string, options?: ExecuteOptions): Promise<string>;
  
  // Events
  onDidChangeTerminal(listener: (event: TerminalEvent) => void): TraeAPI.Disposable;
}

interface TerminalInstance {
  readonly id: string;
  readonly options: TerminalOptions;
  
  // Command execution
  executeCommand(command: string, options?: ExecuteOptions): Promise<ExecutionResult>;
  sendInput(input: string): void;
  sendKey(key: string): void;
  
  // Terminal control
  changeWorkingDirectory(path: string): Promise<boolean>;
  clear(): void;
  
  // Information
  getOutput(): string;
  getInfo(): TerminalInfo;
  
  // Events
  onDidChangeState(listener: (event: TerminalInstanceEvent) => void): TraeAPI.Disposable;
  
  // Lifecycle
  dispose(): Promise<void>;
}

Best Practices

  1. Resource Management: Properly dispose of terminals and execution results
  2. Error Handling: Handle terminal process errors gracefully
  3. Performance: Use output buffering and efficient event handling
  4. Security: Validate commands and sanitize input
  5. User Experience: Provide clear feedback for terminal operations
  6. Cross-platform: Support different shells and platforms
  7. Integration: Integrate with workspace and project settings
  8. Accessibility: Ensure terminal UI is accessible

究極の AI 駆動 IDE 学習ガイド