Files
goose/MONACO_QUICK_START.md
spencrmartin 0caa22e999 Add quick start guide for Monaco Editor integration
Step-by-step implementation guide with:
- Installation instructions
- Complete code examples
- Testing procedures
- Troubleshooting tips
- Performance optimization
- Customization options

Ready for developers to start implementation.
2025-10-31 11:28:18 -04:00

11 KiB

Monaco Editor Integration - Quick Start Guide

🚀 Getting Started

Prerequisites

# Ensure you're on the right branch
git checkout spence/ideinput

# Navigate to the UI directory
cd ui/desktop

Installation

# Install Monaco Editor React wrapper
npm install @monaco-editor/react

# Or with yarn
yarn add @monaco-editor/react

Package: @monaco-editor/react v4.6.0
Peer Dependency: monaco-editor (auto-loaded from CDN)


📁 File Structure

ui/desktop/src/components/
├── RichChatInput.tsx          # Main rich text input (modify)
├── MonacoCodeInput.tsx        # NEW: Monaco wrapper component
├── ChatInput.tsx              # Parent component (no changes)
├── SpellCheckTooltip.tsx      # Existing (no changes)
├── ActionPill.tsx             # Existing (no changes)
└── MentionPill.tsx            # Existing (no changes)

🔧 Step-by-Step Implementation

Step 1: Create MonacoCodeInput Component

File: ui/desktop/src/components/MonacoCodeInput.tsx

import React, { useRef, useEffect } from 'react';
import Editor, { OnMount, OnChange } from '@monaco-editor/react';
import type * as monaco from 'monaco-editor';

interface MonacoCodeInputProps {
  language: string;
  value: string;
  onChange: (value: string) => void;
  onSend?: () => void;
  onExit?: () => void;
  height?: string | number;
  theme?: 'vs-dark' | 'light';
}

export const MonacoCodeInput: React.FC<MonacoCodeInputProps> = ({
  language,
  value,
  onChange,
  onSend,
  onExit,
  height = 'auto',
  theme = 'vs-dark',
}) => {
  const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);

  const handleEditorDidMount: OnMount = (editor, monaco) => {
    editorRef.current = editor;
    editor.focus();

    // Cmd/Ctrl+Enter to send
    editor.addCommand(
      monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
      () => onSend?.()
    );

    // Escape to exit
    editor.addCommand(
      monaco.KeyCode.Escape,
      () => onExit?.()
    );

    // Position cursor at end
    const model = editor.getModel();
    if (model) {
      const lineCount = model.getLineCount();
      const lastLineLength = model.getLineLength(lineCount);
      editor.setPosition({ lineNumber: lineCount, column: lastLineLength + 1 });
    }
  };

  const handleEditorChange: OnChange = (value) => {
    onChange(value || '');
  };

  // Calculate height based on line count
  const calculatedHeight = React.useMemo(() => {
    if (typeof height === 'number' || height !== 'auto') return height;
    
    const lines = value.split('\n').length;
    const lineHeight = 21;
    const padding = 16;
    const minHeight = 100;
    const maxHeight = 400;
    
    return Math.min(Math.max(lines * lineHeight + padding, minHeight), maxHeight);
  }, [value, height]);

  return (
    <div className="monaco-code-input-wrapper">
      <Editor
        height={calculatedHeight}
        language={language}
        value={value}
        theme={theme}
        options={{
          minimap: { enabled: false },
          scrollBeyondLastLine: false,
          fontSize: 14,
          fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, monospace',
          lineNumbers: 'on',
          renderLineHighlight: 'line',
          automaticLayout: true,
          wordWrap: 'on',
          padding: { top: 8, bottom: 8 },
          suggest: {
            showKeywords: true,
            showSnippets: true,
          },
          quickSuggestions: {
            other: true,
            comments: false,
            strings: false,
          },
        }}
        onMount={handleEditorDidMount}
        onChange={handleEditorChange}
        loading={
          <div className="flex items-center justify-center h-32">
            <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500" />
          </div>
        }
      />
    </div>
  );
};

Step 2: Integrate into RichChatInput

File: ui/desktop/src/components/RichChatInput.tsx

2.1 Add Import

import { MonacoCodeInput } from './MonacoCodeInput';
import { lazy, Suspense } from 'react';

// Lazy load Monaco for better performance
const MonacoCodeInputLazy = lazy(() => import('./MonacoCodeInput').then(m => ({ default: m.MonacoCodeInput })));

2.2 Update renderContent() - Replace SyntaxHighlighter

Find this section (around line 600):

// OLD CODE - REMOVE THIS:
<SyntaxHighlighter
  language={codeMode.language}
  style={vscDarkPlus}
  customStyle={{...}}
>
  {codeContent}
</SyntaxHighlighter>

Replace with:

// NEW CODE - ADD THIS:
<Suspense fallback={
  <div className="flex items-center justify-center h-32">
    <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500" />
  </div>
}>
  <MonacoCodeInputLazy
    language={codeMode.language}
    value={codeContent}
    onChange={(newCode) => {
      const beforeCode = value.slice(0, codeMode.startPos);
      const afterCode = value.slice(codeMode.startPos + codeContent.length);
      const newValue = beforeCode + newCode + afterCode;
      onChange(newValue, codeMode.startPos + newCode.length);
    }}
    onSend={() => {
      // Trigger send via parent's onKeyDown
      const syntheticEvent = new CustomEvent('submit', {
        detail: { value },
      }) as unknown as React.FormEvent;
      onKeyDown?.(syntheticEvent as any);
    }}
    onExit={() => {
      // Exit code mode
      setCodeMode(null);
    }}
    height="auto"
    theme="vs-dark"
  />
</Suspense>

Step 3: Add Styling

File: ui/desktop/src/styles/main.css (or component CSS)

/* Monaco Code Input Wrapper */
.monaco-code-input-wrapper {
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  overflow: hidden;
  background: #1E1E1E;
  margin-top: 8px;
}

/* Smooth transition when entering code mode */
.monaco-code-input-wrapper {
  animation: slideDown 0.2s ease-out;
}

@keyframes slideDown {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Loading spinner */
.monaco-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100px;
}

🧪 Testing

Manual Testing

  1. Start the app: npm run dev
  2. Open chat input
  3. Type: #python
  4. Verify Monaco loads
  5. Type code and test autocomplete
  6. Press Cmd+Enter to send
  7. Press Escape to exit

Test Cases

// Test 1: Code mode activation
// Type: #python
// Expected: Monaco editor appears

// Test 2: Autocomplete
// Type: def hel
// Expected: Autocomplete shows "hello"

// Test 3: Multi-cursor
// Hold Cmd/Ctrl + click multiple locations
// Expected: Multiple cursors appear

// Test 4: Send message
// Type code + press Cmd+Enter
// Expected: Message sent with code

// Test 5: Exit code mode
// Press Escape
// Expected: Back to normal input

🎯 Quick Reference

Keyboard Shortcuts

Shortcut Action
Enter New line (in code mode)
Cmd/Ctrl+Enter Send message
Escape Exit code mode
Cmd/Ctrl+F Find (Monaco built-in)
Cmd/Ctrl+D Multi-cursor (Monaco built-in)

Supported Languages

JavaScript, TypeScript, Python, Java, C++, C, Go, Rust, Ruby, PHP, Swift, Kotlin, Scala, HTML, CSS, JSON, YAML, SQL, Bash, Shell, PowerShell, R, MATLAB, Lua, Perl, Haskell, Elixir, Clojure, Dart, JSX, TSX

Props - MonacoCodeInput

interface MonacoCodeInputProps {
  language: string;        // Language for syntax highlighting
  value: string;          // Code content
  onChange: (value: string) => void;  // Called on edit
  onSend?: () => void;    // Called on Cmd+Enter
  onExit?: () => void;    // Called on Escape
  height?: string | number;  // Editor height
  theme?: 'vs-dark' | 'light';  // Color theme
}

🐛 Troubleshooting

Monaco doesn't load

Problem: Editor shows loading spinner indefinitely
Solution: Check network tab for CDN errors, ensure internet connection

Autocomplete not working

Problem: No suggestions appear
Solution: Verify language is supported, check Monaco language services loaded

Height not adjusting

Problem: Editor too small or too large
Solution: Check calculatedHeight logic, verify min/max constraints

Keyboard shortcuts not working

Problem: Cmd+Enter doesn't send
Solution: Check onSend prop is passed, verify addCommand in onMount

Memory leak

Problem: Memory grows over time
Solution: Ensure editor is disposed in cleanup: editorRef.current?.dispose()


📊 Performance Tips

1. Lazy Loading

// Always use lazy loading for Monaco
const MonacoCodeInput = lazy(() => import('./MonacoCodeInput'));

2. Preloading (Optional)

// Preload Monaco when user types #
if (value.endsWith('#')) {
  import('./MonacoCodeInput');
}

3. Debounce onChange

// Debounce onChange for better performance
const debouncedOnChange = useMemo(
  () => debounce(onChange, 100),
  [onChange]
);

4. Cleanup

// Always cleanup on unmount
useEffect(() => {
  return () => {
    editorRef.current?.dispose();
  };
}, []);

🎨 Customization

Custom Theme

// Define custom theme
monaco.editor.defineTheme('goose-dark', {
  base: 'vs-dark',
  inherit: true,
  rules: [
    { token: 'comment', foreground: '6A9955' },
    { token: 'keyword', foreground: 'C586C0' },
  ],
  colors: {
    'editor.background': '#1E1E1E',
  },
});

// Use custom theme
<MonacoCodeInput theme="goose-dark" />

Custom Options

<Editor
  options={{
    fontSize: 16,
    lineNumbers: 'off',
    minimap: { enabled: true },
    // ... more options
  }}
/>

📚 Resources


Checklist

Before Starting

  • Read MONACO_INTEGRATION_PLAN.md
  • Review MONACO_ARCHITECTURE.md
  • Understand current RichChatInput.tsx

Implementation

  • Install @monaco-editor/react
  • Create MonacoCodeInput.tsx
  • Update RichChatInput.tsx
  • Add CSS styling
  • Test basic functionality

Testing

  • Manual testing (all languages)
  • Keyboard shortcuts work
  • Performance is acceptable
  • No memory leaks
  • Accessibility verified

Deployment

  • Create feature flag
  • Beta test with team
  • Monitor performance
  • Gather feedback
  • Full rollout

Status: Ready to implement!
Estimated Time: 2-3 hours for basic integration
Branch: spence/ideinput

Good luck! 🚀