feat: support opencode

This commit is contained in:
weishu
2026-01-29 10:34:57 +08:00
parent 66ab96bf38
commit 70b5c22c8f
47 changed files with 2737 additions and 28 deletions
+188
View File
@@ -0,0 +1,188 @@
import React, { useEffect, useState } from 'react';
import { Box, Text, useStdout } from 'ink';
import { MessageBuffer, type BufferedMessage } from './messageBuffer';
import { useSwitchControls } from './useSwitchControls';
interface OpencodeDisplayProps {
messageBuffer: MessageBuffer;
logPath?: string;
onExit?: () => void;
onSwitchToLocal?: () => void;
}
function extractTag(messages: BufferedMessage[], tag: 'MODEL' | 'MODE'): string | null {
const prefix = `[${tag}:`;
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (message.type !== 'system') {
continue;
}
if (!message.content.startsWith(prefix)) {
continue;
}
const match = message.content.match(/\[\w+:(.+?)\]/);
if (match && match[1]) {
return match[1];
}
}
return null;
}
export const OpencodeDisplay: React.FC<OpencodeDisplayProps> = ({
messageBuffer,
logPath,
onExit,
onSwitchToLocal
}) => {
const [messages, setMessages] = useState<BufferedMessage[]>([]);
const [model, setModel] = useState<string | null>(null);
const [permissionMode, setPermissionMode] = useState<string | null>(null);
const { confirmationMode, actionInProgress } = useSwitchControls({
onExit,
onSwitch: onSwitchToLocal
});
const { stdout } = useStdout();
const terminalWidth = stdout.columns || 80;
const terminalHeight = stdout.rows || 24;
useEffect(() => {
setMessages(messageBuffer.getMessages());
const unsubscribe = messageBuffer.onUpdate((newMessages) => {
setMessages(newMessages);
const nextModel = extractTag(newMessages, 'MODEL');
if (nextModel) {
setModel(nextModel);
}
const nextMode = extractTag(newMessages, 'MODE');
if (nextMode) {
setPermissionMode(nextMode);
}
});
return () => {
unsubscribe();
};
}, [messageBuffer]);
const getMessageColor = (type: BufferedMessage['type']): string => {
switch (type) {
case 'user': return 'magenta';
case 'assistant': return 'cyan';
case 'system': return 'blue';
case 'tool': return 'yellow';
case 'result': return 'green';
case 'status': return 'gray';
default: return 'white';
}
};
const formatMessage = (msg: BufferedMessage): string => {
const lines = msg.content.split('\n');
const maxLineLength = Math.max(1, terminalWidth - 10);
return lines.map(line => {
if (line.length <= maxLineLength) return line;
const chunks: string[] = [];
for (let i = 0; i < line.length; i += maxLineLength) {
chunks.push(line.slice(i, i + maxLineLength));
}
return chunks.join('\n');
}).join('\n');
};
const visibleMessages = messages.filter((msg) => {
if (msg.type === 'system' && msg.content.startsWith('[MODEL:')) {
return false;
}
if (msg.type === 'system' && msg.content.startsWith('[MODE:')) {
return false;
}
return true;
});
return (
<Box flexDirection="column" width={terminalWidth} height={terminalHeight}>
<Box
flexDirection="column"
width={terminalWidth}
height={terminalHeight - 4}
borderStyle="round"
borderColor="gray"
paddingX={1}
overflow="hidden"
>
<Box flexDirection="column" marginBottom={1}>
<Text color="gray" bold>OpenCode Agent Messages</Text>
<Text color="gray" dimColor>{'-'.repeat(Math.min(terminalWidth - 4, 60))}</Text>
</Box>
<Box flexDirection="column" height={terminalHeight - 10} overflow="hidden">
{visibleMessages.length === 0 ? (
<Text color="gray" dimColor>Waiting for messages...</Text>
) : (
visibleMessages
.slice(-Math.max(1, terminalHeight - 10))
.map((msg) => (
<Box key={msg.id} flexDirection="column" marginBottom={1}>
<Text color={getMessageColor(msg.type)} dimColor>
{formatMessage(msg)}
</Text>
</Box>
))
)}
</Box>
</Box>
<Box
width={terminalWidth}
borderStyle="round"
borderColor={
actionInProgress ? 'gray' :
confirmationMode === 'exit' ? 'red' :
confirmationMode === 'switch' ? 'yellow' :
'green'
}
paddingX={2}
justifyContent="center"
alignItems="center"
flexDirection="column"
>
<Box flexDirection="column" alignItems="center">
{actionInProgress === 'exiting' ? (
<Text color="gray" bold>
Exiting agent...
</Text>
) : actionInProgress === 'switching' ? (
<Text color="gray" bold>
Switching to local mode...
</Text>
) : confirmationMode === 'exit' ? (
<Text color="red" bold>
Press Ctrl-C again to exit the agent
</Text>
) : confirmationMode === 'switch' ? (
<Text color="yellow" bold>
Press space again to switch to local mode
</Text>
) : (
<Text color="green" bold>
OpenCode running {onSwitchToLocal ? '(Space to switch to local, Ctrl-C to exit)' : '(Ctrl-C to exit)'}
</Text>
)}
{(model || permissionMode) && (
<Text color="gray" dimColor>
{[model ? `Model: ${model}` : null, permissionMode ? `Permission: ${permissionMode}` : null]
.filter(Boolean)
.join(' | ')}
</Text>
)}
{process.env.DEBUG && logPath && (
<Text color="gray" dimColor>
Debug logs: {logPath}
</Text>
)}
</Box>
</Box>
</Box>
);
};