fix(cli): wire command:result + system:reload socket events in TUI (#187)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #187.
This commit is contained in:
2026-03-16 13:21:11 +00:00
committed by jason.woltje
parent b6effdcd6b
commit f0476cae92
6 changed files with 189 additions and 77 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
import { Box, useApp, useInput } from 'ink';
import type { ParsedCommand } from '@mosaic/types';
import { TopBar } from './components/top-bar.js';
@@ -73,6 +73,11 @@ export function TuiApp({
const [sidebarSelectedIndex, setSidebarSelectedIndex] = useState(0);
// Controlled input state — held here so Ctrl+C can clear it
const [tuiInput, setTuiInput] = useState('');
// Ctrl+C double-press: first press with empty input shows hint; second exits
const ctrlCPendingExit = useRef(false);
const handleLocalCommand = useCallback(
(parsed: ParsedCommand) => {
switch (parsed.command) {
@@ -97,6 +102,20 @@ export function TuiApp({
case 'clear':
socket.clearMessages();
break;
case 'new':
case 'n':
void conversations
.createConversation()
.then((conv) => {
if (conv) {
socket.switchConversation(conv.id);
appMode.setMode('chat');
}
})
.catch(() => {
socket.addSystemMessage('Failed to create new conversation.');
});
break;
case 'stop':
// Currently no stop mechanism exposed — show feedback
socket.addSystemMessage('Stop is not available for the current session.');
@@ -117,12 +136,12 @@ export function TuiApp({
const handleGatewayCommand = useCallback(
(parsed: ParsedCommand) => {
if (!socket.socketRef.current?.connected || !socket.conversationId) {
if (!socket.socketRef.current?.connected) {
socket.addSystemMessage('Not connected to gateway. Command cannot be executed.');
return;
}
socket.socketRef.current.emit('command:execute', {
conversationId: socket.conversationId,
conversationId: socket.conversationId ?? '',
command: parsed.command,
args: parsed.args ?? undefined,
});
@@ -153,9 +172,21 @@ export function TuiApp({
);
useInput((ch, key) => {
// Ctrl+C: clear input → show hint → second empty press exits
if (key.ctrl && ch === 'c') {
exit();
if (tuiInput) {
setTuiInput('');
ctrlCPendingExit.current = false;
} else if (ctrlCPendingExit.current) {
exit();
} else {
ctrlCPendingExit.current = true;
socket.addSystemMessage('Press Ctrl+C again to exit.');
}
return;
}
// Any other key resets the pending-exit flag
ctrlCPendingExit.current = false;
// Ctrl+L: toggle sidebar (refresh on open)
if (key.ctrl && ch === 'l') {
const willOpen = !appMode.sidebarOpen;
@@ -260,12 +291,15 @@ export function TuiApp({
)}
<InputBar
value={tuiInput}
onChange={setTuiInput}
onSubmit={socket.sendMessage}
onSystemMessage={socket.addSystemMessage}
onLocalCommand={handleLocalCommand}
onGatewayCommand={handleGatewayCommand}
isStreaming={socket.isStreaming}
connected={socket.connected}
focused={appMode.mode === 'chat'}
placeholder={inputPlaceholder}
allCommands={commandRegistry.getAll()}
/>