SourceControlPanel.tsxā¢6.78 kB
import React, { useState, useEffect, useCallback } from 'react';
import { useTool } from '@modelcontextprotocol/sdk/react';
import Spinner from './Spinner.js';
interface SourceControlPanelProps {
addToTerminal: (output: string) => void;
refreshFileTree: () => void;
}
type RepoStatus = {
isRepo: boolean;
branch?: string;
remoteUrl?: string;
files?: string[];
};
const SourceControlPanel: React.FC<SourceControlPanelProps> = ({ addToTerminal, refreshFileTree }) => {
const [status, setStatus] = useState<RepoStatus | null>(null);
const [commitMessage, setCommitMessage] = useState('');
const [authorName, setAuthorName] = useState('PyForge User');
const [authorEmail, setAuthorEmail] = useState('user@pyforge.dev');
const { call: getStatus, isPending: isFetchingStatus } = useTool('github_get_status');
const { call: commitAndPush, isPending: isPushing } = useTool('github_commit_and_push');
const fetchStatus = useCallback(async () => {
const result = await getStatus({});
try {
const textResponse = result?.content?.[0]?.type === 'text' ? result.content[0].text : '{}';
const parsedStatus = JSON.parse(textResponse);
setStatus(parsedStatus);
} catch (e) {
setStatus({ isRepo: false });
addToTerminal(`Could not parse git status: ${e}`);
}
}, [getStatus, addToTerminal]);
useEffect(() => {
fetchStatus();
}, [fetchStatus]);
const handleCommitAndPush = async () => {
if (!commitMessage.trim()) {
alert('Please provide a commit message.');
return;
}
if (!status?.isRepo || !status.branch) {
alert('Not in a valid git repository.');
return;
}
addToTerminal(`Committing and pushing to ${status.branch}...`);
const result = await commitAndPush({
branch: status.branch,
commitMessage,
authorName,
authorEmail,
});
const output = result?.content?.[0]?.type === 'text' ? result.content[0].text : 'Commit and push failed.';
addToTerminal(output);
if(output.startsWith('Successfully')) {
setCommitMessage('');
fetchStatus(); // Refresh status after push
refreshFileTree(); // Refresh file explorer
}
};
const isLoading = isFetchingStatus || isPushing;
const renderContent = () => {
if (isFetchingStatus && !status) {
return <div style={styles.section}><Spinner /></div>;
}
if (!status?.isRepo) {
return (
<div style={styles.section}>
<h3 style={styles.listHeader}>No Repository Found</h3>
<p style={styles.helpText}>The workspace is not a git repository.</p>
<p style={styles.helpText}>Please use the terminal to clone a repository into the workspace root:</p>
<pre style={styles.codeBlock}>git clone [repository_url] .</pre>
<button onClick={fetchStatus} disabled={isLoading} style={styles.button}>
{isFetchingStatus ? <Spinner size={16} /> : 'Refresh Status'}
</button>
</div>
);
}
return (
<div style={styles.section}>
<div style={styles.repoInfo}>
<p style={styles.helpText}><strong>Branch:</strong> {status.branch}</p>
<p style={styles.helpText}><strong>Remote:</strong> {status.remoteUrl}</p>
</div>
<h3 style={styles.listHeader}>Changes ({status.files?.length || 0})</h3>
{(status.files && status.files.length > 0) ? (
<ul style={styles.fileList}>
{status.files.map(file => <li key={file} style={styles.fileItem}>{file}</li>)}
</ul>
) : <p style={styles.helpText}>No changes detected.</p>}
<div style={{ borderTop: '1px solid var(--border-color)', paddingTop: '15px'}}>
<h3 style={styles.listHeader}>Commit & Push</h3>
<div style={styles.formGroup}>
<label style={styles.label}>Commit Message</label>
<textarea value={commitMessage} onChange={e => setCommitMessage(e.target.value)} style={{...styles.input, height: '60px'}} placeholder="Your commit message..."/>
</div>
<div style={styles.formGroup}>
<label style={styles.label}>Author Name</label>
<input type="text" value={authorName} onChange={e => setAuthorName(e.target.value)} style={styles.input} />
</div>
<div style={styles.formGroup}>
<label style={styles.label}>Author Email</label>
<input type="email" value={authorEmail} onChange={e => setAuthorEmail(e.target.value)} style={styles.input} />
</div>
<button onClick={handleCommitAndPush} disabled={isPushing} style={styles.button}>
{isPushing ? <Spinner size={16} /> : `Commit & Push to '${status.branch}'`}
</button>
</div>
</div>
);
};
return (
<div style={styles.container}>
<div style={styles.header}>SOURCE CONTROL</div>
{renderContent()}
</div>
);
};
const styles: { [key: string]: React.CSSProperties } = {
container: { display: 'flex', flexDirection: 'column', height: '100%', color: 'var(--text-primary)' },
header: { padding: '10px', fontWeight: 'bold', borderBottom: '1px solid var(--border-color)', flexShrink: 0 },
section: { padding: '10px', display: 'flex', flexDirection: 'column', gap: '15px', overflowY: 'auto', flex: 1 },
formGroup: { display: 'flex', flexDirection: 'column', gap: '5px', marginBottom: '10px' },
label: { fontSize: '12px', color: 'var(--text-secondary)' },
input: { flex: 1, padding: '5px', backgroundColor: 'var(--background-tertiary)', border: '1px solid var(--border-color)', color: 'var(--text-primary)', borderRadius: '3px', resize: 'vertical' },
button: { padding: '8px 10px', backgroundColor: 'var(--accent-primary)', color: 'white', border: 'none', borderRadius: '3px', cursor: 'pointer', minWidth: '70px', display: 'flex', justifyContent: 'center', alignItems: 'center' },
listHeader: { marginTop: 0, fontSize: '14px', color: 'var(--text-primary)' },
helpText: { fontSize: '12px', color: 'var(--text-secondary)', margin: 0, wordBreak: 'break-word' },
repoInfo: { display: 'flex', flexDirection: 'column', gap: '5px', padding: '10px', backgroundColor: 'var(--background-tertiary)', borderRadius: '3px' },
codeBlock: { padding: '10px', backgroundColor: 'var(--background-primary)', borderRadius: '3px', color: 'var(--text-secondary)', fontSize: '12px', fontFamily: 'monospace' },
fileList: { listStyle: 'none', padding: '0 0 0 10px', margin: 0, maxHeight: '150px', overflowY: 'auto' },
fileItem: { padding: '2px 0', fontSize: '13px', fontFamily: 'monospace', color: 'var(--text-secondary)' },
};
export default SourceControlPanel;