import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useChatContext } from '../state/ChatProvider';
import { formatTokenCount } from '../utils/format';
import { SlashPicker } from './SlashPicker';
import { ModelSelector } from './ModelSelector';
import { postMessage } from '../vscode';
interface WorkspaceFile {
path: string;
fileName: string;
relativePath: string;
}
export function InputArea() {
const { state, send, stop, dispatch } = useChatContext();
const [text, setText] = useState('');
const [showSlash, setShowSlash] = useState(false);
const [slashFilter, setSlashFilter] = useState('');
const [showFilePicker, setShowFilePicker] = useState(false);
const [fileQuery, setFileQuery] = useState('');
const [workspaceFiles, setWorkspaceFiles] = useState<WorkspaceFile[]>([]);
const inputBoxRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const fileSearchRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const el = textareaRef.current;
if (!el) return;
el.style.height = 'auto';
el.style.height = `${Math.min(el.scrollHeight, 200)}px`;
}, [text]);
useEffect(() => {
function handleMessage(e: MessageEvent) {
if (e.data?.type === 'focusInput') textareaRef.current?.focus();
if (e.data?.type === 'workspaceFiles') {
setWorkspaceFiles(e.data.files || []);
}
if (e.data?.type === 'setDraft') setText(e.data.text);
}
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, []);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const sessionBody = container.closest<HTMLElement>('.session-body');
if (!sessionBody) return;
const updateInputInset = () => {
sessionBody.style.setProperty('--input-inset', `${container.offsetHeight + 32}px`);
};
updateInputInset();
const resizeObserver = new ResizeObserver(updateInputInset);
resizeObserver.observe(container);
window.addEventListener('resize', updateInputInset);
return () => {
resizeObserver.disconnect();
window.removeEventListener('resize', updateInputInset);
sessionBody.style.removeProperty('--input-inset');
};
}, []);
useEffect(() => {
if (!showFilePicker && !showSlash) return;
if (showFilePicker) {
requestAnimationFrame(() => fileSearchRef.current?.focus());
}
function closePickers(e: MouseEvent) {
const target = e.target as Node;
if (!document.body.contains(target)) return;
if (showFilePicker) {
const insidePicker = (target as HTMLElement).closest?.('.file-picker');
if (!insidePicker) {
setShowFilePicker(false);
setFileQuery('');
}
}
if (showSlash && inputBoxRef.current && !inputBoxRef.current.contains(target)) {
setShowSlash(false);
}
}
document.addEventListener('mousedown', closePickers, true);
return () => document.removeEventListener('mousedown', closePickers, true);
}, [showFilePicker, showSlash]);
const handleChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
const val = e.target.value;
setText(val);
if (/^\/\S*$/.test(val)) {
setSlashFilter(val.slice(1).split(/\s/)[0]);
setShowSlash(true);
} else {
setShowSlash(false);
}
}, []);
const handleSend = useCallback(() => {
const trimmed = text.trim();
if (!trimmed) return;
send(trimmed);
setText('');
setShowSlash(false);
}, [text, send]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (showSlash) return;
if (e.nativeEvent.isComposing || e.keyCode === 229) return;
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); }
},
[handleSend, showSlash],
);
const handleSlashSelect = useCallback((command: string) => {
setText(command + ' ');
setShowSlash(false);
textareaRef.current?.focus();
}, []);
const handleSlashButton = useCallback(() => {
setShowFilePicker((fp) => {
if (fp) { setFileQuery(''); return false; }
return fp;
});
setShowSlash((open) => {
if (open) { setText(''); return false; }
setText('/');
setSlashFilter('');
return true;
});
textareaRef.current?.focus();
}, []);
const handleAttachClick = useCallback(() => {
setShowFilePicker((prev) => {
const next = !prev;
if (next) {
setShowSlash(false);
postMessage({ type: 'searchWorkspaceFiles', query: '' });
} else {
setFileQuery('');
}
return next;
});
}, []);
const handleFileSearch = useCallback((query: string) => {
setFileQuery(query);
postMessage({ type: 'searchWorkspaceFiles', query });
}, []);
const handleFileSelect = useCallback((f: WorkspaceFile) => {
postMessage({ type: 'attachFile', path: f.path });
setShowFilePicker(false);
setFileQuery('');
textareaRef.current?.focus();
}, []);
const hasText = Boolean(text.trim());
return (
<div className="input-container" ref={containerRef}>
<div className="input-box" ref={inputBoxRef}>
{showSlash && (
<SlashPicker filter={slashFilter} onSelect={handleSlashSelect} onClose={() => setShowSlash(false)} />
)}
{showFilePicker && (
<div className="file-picker">
<input
ref={fileSearchRef}
className="file-picker-search"
type="text"
placeholder="Search project files..."
value={fileQuery}
onChange={(e) => handleFileSearch(e.target.value)}
/>
<div className="file-picker-list">
{workspaceFiles.length === 0 ? (
<div className="file-picker-empty">
{fileQuery ? 'No matching files' : 'Type to search workspace files'}
</div>
) : (
workspaceFiles.map((f) => (
<button
key={f.path}
type="button"
className="file-picker-item"
onClick={() => handleFileSelect(f)}
>
<span className="file-picker-item-icon">📄</span>
<span className="file-picker-item-body">
<span className="file-picker-item-name">{f.fileName}</span>
<span className="file-picker-item-path">{f.relativePath}</span>
</span>
</button>
))
)}
</div>
</div>
)}
{state.contextFiles.length > 0 && (
<div className="attached-files">
{state.contextFiles.map((f) => (
<span
key={f.path + (f.startLine || '')}
className={`attached-file-pill ${f.type === 'selection' ? 'clickable' : ''}`}
title={f.type === 'selection' && f.startLine
? `${f.path}:${f.startLine}-${f.endLine}`
: f.path
}
onClick={f.type === 'selection' ? () => postMessage({ type: 'openFile', path: f.path, startLine: f.startLine, endLine: f.endLine }) : undefined}
>
<span className="pill-icon">{f.type === 'selection' ? '📋' : '📄'}</span>
<span className="pill-name">
{f.type === 'selection' && f.startLine
? `${f.fileName}:${f.startLine}-${f.endLine}`
: f.fileName
}
</span>
<button className="pill-close" onClick={(e) => { e.stopPropagation(); dispatch({ type: 'REMOVE_CONTEXT_FILE', path: f.path, startLine: f.startLine }); }}>×</button>
</span>
))}
</div>
)}
<textarea
ref={textareaRef}
className="message-input"
value={text}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder="Type a message..."
rows={1}
/>
<div className="input-footer">
<button className="footer-slash-btn" onClick={handleSlashButton} title="Commands">
/
</button>
<button className="footer-attach-btn" onClick={handleAttachClick} title="Attach file">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
</svg>
</button>
<span className="footer-spacer" />
{state.tokenCount && <span className="footer-tokens">{formatTokenCount(state.tokenCount.total)}</span>}
<ModelSelector placement="up" onOpen={() => setShowSlash(false)} />
{state.isGenerating ? (
<>
{hasText && (
<button className="btn-send" onClick={handleSend} title="Queue message">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="19" x2="12" y2="5" /><polyline points="5 12 12 5 19 12" />
</svg>
</button>
)}
<button className="btn-stop" onClick={stop} title="Stop">
<div style={{ width: 8, height: 8, background: 'currentColor', borderRadius: 1 }} />
</button>
</>
) : (
<button className="btn-send" onClick={handleSend} disabled={!hasText} title="Send">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="19" x2="12" y2="5" /><polyline points="5 12 12 5 19 12" />
</svg>
</button>
)}
</div>
</div>
</div>
);
}