feat: add web search, file edit, MCP management, file refs, and /stop to CLI/TUI (#348)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful

This commit was merged in pull request #348.
This commit is contained in:
2026-04-02 18:08:30 +00:00
parent ea371d760d
commit 0d12471868
15 changed files with 1162 additions and 4 deletions

View File

@@ -15,6 +15,7 @@ import { useConversations } from './hooks/use-conversations.js';
import { useSearch } from './hooks/use-search.js';
import { executeHelp, executeStatus, executeHistory, commandRegistry } from './commands/index.js';
import { fetchConversationMessages } from './gateway-api.js';
import { expandFileRefs, hasFileRefs, handleAttachCommand } from './file-ref.js';
export interface TuiAppProps {
gatewayUrl: string;
@@ -85,6 +86,36 @@ export function TuiApp({
// combo is handled by the top-level useInput handler (e.g. Ctrl+T → 't').
const ctrlJustFired = useRef(false);
// Wrap sendMessage to expand @file references before sending
const sendMessageWithFileRefs = useCallback(
(content: string) => {
if (!hasFileRefs(content)) {
socket.sendMessage(content);
return;
}
void expandFileRefs(content)
.then(({ expandedMessage, filesAttached, errors }) => {
for (const err of errors) {
socket.addSystemMessage(err);
}
if (filesAttached.length > 0) {
socket.addSystemMessage(
`📎 Attached ${filesAttached.length} file(s): ${filesAttached.join(', ')}`,
);
}
socket.sendMessage(expandedMessage);
})
.catch((err: unknown) => {
socket.addSystemMessage(
`File expansion failed: ${err instanceof Error ? err.message : String(err)}`,
);
// Send original message without expansion
socket.sendMessage(content);
});
},
[socket],
);
const handleLocalCommand = useCallback(
(parsed: ParsedCommand) => {
switch (parsed.command) {
@@ -123,9 +154,36 @@ export function TuiApp({
socket.addSystemMessage('Failed to create new conversation.');
});
break;
case 'attach': {
if (!parsed.args) {
socket.addSystemMessage('Usage: /attach <file-path>');
break;
}
void handleAttachCommand(parsed.args)
.then(({ content, error }) => {
if (error) {
socket.addSystemMessage(`Attach error: ${error}`);
} else if (content) {
// Send the file content as a user message
socket.sendMessage(content);
}
})
.catch((err: unknown) => {
socket.addSystemMessage(
`Attach failed: ${err instanceof Error ? err.message : String(err)}`,
);
});
break;
}
case 'stop':
// Currently no stop mechanism exposed — show feedback
socket.addSystemMessage('Stop is not available for the current session.');
if (socket.isStreaming && socket.socketRef.current?.connected && socket.conversationId) {
socket.socketRef.current.emit('abort', {
conversationId: socket.conversationId,
});
socket.addSystemMessage('Abort signal sent.');
} else {
socket.addSystemMessage('No active stream to stop.');
}
break;
case 'cost': {
const u = socket.tokenUsage;
@@ -348,7 +406,7 @@ export function TuiApp({
}
setTuiInput(val);
}}
onSubmit={socket.sendMessage}
onSubmit={sendMessageWithFileRefs}
onSystemMessage={socket.addSystemMessage}
onLocalCommand={handleLocalCommand}
onGatewayCommand={handleGatewayCommand}