diff --git a/src/cli.tsx b/src/cli.tsx index f8e0aa2..b76d217 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -5,12 +5,13 @@ import type { XmtpEnv } from "@xmtp/agent-sdk"; import { privateKeyToAddress } from "viem/accounts"; import { Layout } from "./components/Layout.js"; import type { Command } from "./components/CommandPalette.js"; -import { CommandSuggestion } from "./components/CommandSuggestions.js"; import { useXMTP } from "./hooks/useXMTP.js"; import { useKeyboard } from "./hooks/useKeyboard.js"; import { useStore } from "./store/state.js"; const RED = "#fc4c34"; +const DIM_GREY = "#666666"; +const LIGHT_GREY = "#888888"; // ============================================================================ // Help Display @@ -41,9 +42,8 @@ KEYBINDINGS: Ctrl+C Quit COMMANDS: - /back Return to main conversation list - /list Toggle sidebar visibility - /chat Switch to conversation number + /b Return to main conversation list + /c Switch to conversation number /new
Create new conversation with address /refresh Refresh inbox and update conversations /exit, /quit Exit application @@ -90,92 +90,13 @@ const App: React.FC = ({ env, agentIdentifiers }) => { const [errorTimeout, setErrorTimeout] = useState(null); const [isInputActive, setIsInputActive] = useState(false); - // Command suggestions state - const [showSuggestions, setShowSuggestions] = useState(false); - const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(0); - // Handle suggestion navigation - const handleSuggestionNavigation = (direction: 'up' | 'down') => { - if (!showSuggestions || suggestions.length === 0) return; - - if (direction === 'up') { - setSelectedSuggestionIndex(prev => - prev > 0 ? prev - 1 : suggestions.length - 1 - ); - } else { - setSelectedSuggestionIndex(prev => - prev < suggestions.length - 1 ? prev + 1 : 0 - ); - } - }; - - // Handle suggestion selection - const handleSuggestionSelect = (suggestion: CommandSuggestion) => { - // Find the corresponding command - const command = commands.find(cmd => cmd.id === suggestion.id); - if (command) { - setInputValue(`/${command.name.toLowerCase().replace(/\s+/g, '-')}`); - setShowSuggestions(false); - setSelectedSuggestionIndex(0); - } - }; - - // Handle input change with suggestions + // Handle input change const handleInputChange = (value: string) => { setInputValue(value); - - // Show suggestions when user types "/" - if (value.startsWith("/")) { - setShowSuggestions(true); - setSelectedSuggestionIndex(0); - } else { - setShowSuggestions(false); - setSelectedSuggestionIndex(0); - } }; - // Custom keyboard handler for suggestions - useInput((input, key) => { - if (!isInputActive || !showSuggestions) return; - - // Handle arrow keys for suggestion navigation - if (key.upArrow) { - handleSuggestionNavigation('up'); - return; - } - - if (key.downArrow) { - handleSuggestionNavigation('down'); - return; - } - - // Handle tab for suggestion completion - if (key.tab && suggestions.length > 0) { - const selectedSuggestion = suggestions[selectedSuggestionIndex]; - if (selectedSuggestion) { - handleSuggestionSelect(selectedSuggestion); - } - return; - } - - // Handle enter to execute selected suggestion - if (key.return && suggestions.length > 0) { - const selectedSuggestion = suggestions[selectedSuggestionIndex]; - if (selectedSuggestion) { - // Find the corresponding command and execute it - const command = commands.find(cmd => cmd.id === selectedSuggestion.id); - if (command) { - setInputValue(`/${command.name.toLowerCase().replace(/\s+/g, '-')}`); - setShowSuggestions(false); - setSelectedSuggestionIndex(0); - // Execute the command immediately - command.action(); - } - } - return; - } - }, { isActive: isInputActive && showSuggestions }); // XMTP hook const { @@ -265,23 +186,6 @@ const App: React.FC = ({ env, agentIdentifiers }) => { }, ]; - // Command suggestions logic - const getCommandSuggestions = (query: string): CommandSuggestion[] => { - if (!query.startsWith("/")) return []; - - const commandQuery = query.slice(1).toLowerCase(); - return commands.filter(cmd => - cmd.name.toLowerCase().includes(commandQuery) || - cmd.description.toLowerCase().includes(commandQuery) - ).slice(0, 6).map(cmd => ({ - id: cmd.id, - name: cmd.name, - description: cmd.description, - shortcut: cmd.shortcut, - })); - }; - - const suggestions = getCommandSuggestions(inputValue); // Keyboard navigation useKeyboard({ @@ -309,11 +213,6 @@ const App: React.FC = ({ env, agentIdentifiers }) => { toggleCommandPalette(); return; } - if (showSuggestions) { - setShowSuggestions(false); - setSelectedSuggestionIndex(0); - return; - } if (isInputActive) { setIsInputActive(false); setInputValue(""); @@ -363,7 +262,7 @@ const App: React.FC = ({ env, agentIdentifiers }) => { return; } - if (cmd.startsWith("chat ")) { + if (cmd.startsWith("c ")) { const parts = cmd.split(" "); const index = parseInt(parts[1]) - 1; if ( @@ -417,7 +316,7 @@ const App: React.FC = ({ env, agentIdentifiers }) => { } } - setErrorWithTimeout("Unknown command. Try /back, /list, /chat , /new
, /refresh, or /exit"); + setErrorWithTimeout("Unknown command. Try /b, /c <n>, /new <address>, /refresh, or /exit"); return; } @@ -451,8 +350,8 @@ const App: React.FC = ({ env, agentIdentifiers }) => { {agent && ( - ✓ Agent initialized - + ✓ Agent initialized + Address: {address.slice(0, 10)}...{address.slice(-8)} @@ -481,10 +380,6 @@ const App: React.FC = ({ env, agentIdentifiers }) => { commands={commands} onCommandExecute={(cmd) => cmd.action()} onCommandPaletteClose={() => toggleCommandPalette()} - suggestions={suggestions} - selectedSuggestionIndex={selectedSuggestionIndex} - showSuggestions={showSuggestions} - onSuggestionSelect={handleSuggestionSelect} /> ); }; diff --git a/src/components/ChatView.tsx b/src/components/ChatView.tsx index 496beb3..0db6f00 100644 --- a/src/components/ChatView.tsx +++ b/src/components/ChatView.tsx @@ -47,7 +47,7 @@ export const ChatView: React.FC = ({ if (!conversation) { // Show conversation selector instead of empty state return ( - + = ({ } return ( - + {/* Chat header */} - + {conversation.type === "group" ? "👥 GROUP: " : "💬 DM: "} {conversation.name} @@ -68,7 +68,7 @@ export const ChatView: React.FC = ({ {/* Messages */} - + {visibleMessages.length === 0 ? ( No messages yet... ) : ( diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index 59b9ad3..88c3783 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -2,7 +2,8 @@ import React, { useState, useEffect } from "react"; import { Box, Text, useInput } from "ink"; import TextInput from "ink-text-input"; -const RED = "#fc4c34"; +const DIM_GREY = "#666666"; +const LIGHT_GREY = "#888888"; export interface Command { id: string; @@ -79,21 +80,21 @@ export const CommandPalette: React.FC = ({ {/* Header */} - + Command Palette - (Press Esc to close) + (Press Esc to close) {/* Search input */} - - + + = ({ {/* Command list */} {filteredCommands.length === 0 ? ( - No commands found + No commands found ) : ( filteredCommands.slice(0, 10).map((cmd, index) => { const isSelected = index === selectedIndex; return ( - + {isSelected ? "▶ " : " "} - + {cmd.name} - - {cmd.description} + - {cmd.description} {cmd.shortcut && ( - ({cmd.shortcut}) + ({cmd.shortcut}) )} ); @@ -129,8 +130,8 @@ export const CommandPalette: React.FC = ({ {/* Footer */} - - ↑↓: Navigate • Enter: Execute • Esc: Close + + ↑↓: Navigate • Enter: Execute • Esc: Close ); diff --git a/src/components/CommandSuggestions.tsx b/src/components/CommandSuggestions.tsx deleted file mode 100644 index 1332a4c..0000000 --- a/src/components/CommandSuggestions.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from "react"; -import { Box, Text } from "ink"; - -const RED = "#fc4c34"; - -export interface CommandSuggestion { - id: string; - name: string; - description: string; - shortcut?: string; -} - -interface CommandSuggestionsProps { - suggestions: CommandSuggestion[]; - selectedIndex: number; - query: string; - visible: boolean; -} - -export const CommandSuggestions: React.FC = ({ - suggestions, - selectedIndex, - query, - visible, -}) => { - if (!visible || suggestions.length === 0) { - return null; - } - - return ( - - - - 💡 Command suggestions for "{query}" - - - - - {suggestions.slice(0, 6).map((suggestion, index) => { - const isSelected = index === selectedIndex; - return ( - - {isSelected && ( - - )} - - /{suggestion.name} - - - {suggestion.description} - {suggestion.shortcut && ( - ({suggestion.shortcut}) - )} - - ); - })} - - - - ↑↓: Navigate • Tab: Complete • Enter: Execute • Esc: Close - - - ); -}; \ No newline at end of file diff --git a/src/components/ConversationSelector.tsx b/src/components/ConversationSelector.tsx index 67f262f..d1c72a8 100644 --- a/src/components/ConversationSelector.tsx +++ b/src/components/ConversationSelector.tsx @@ -3,7 +3,8 @@ import { Box, Text } from "ink"; import type { ConversationInfo } from "../types/index.js"; import { formatTime } from "../utils/formatters.js"; -const RED = "#fc4c34"; +const DIM_GREY = "#666666"; +const LIGHT_GREY = "#888888"; interface ConversationSelectorProps { conversations: ConversationInfo[]; @@ -23,7 +24,7 @@ export const ConversationSelector: React.FC = ({ 📱 XMTP Chat - Main Menu - + 💡 TIP: Use ↑↓ arrows to navigate, then press Enter @@ -38,7 +39,7 @@ export const ConversationSelector: React.FC = ({ {selectedIndex === -1 && ( - + Press Enter, then type an address @@ -47,7 +48,7 @@ export const ConversationSelector: React.FC = ({ {/* Divider */} {conversations.length > 0 && ( - ────────────────────────────────────── + ────────────────────────────────────── )} @@ -55,9 +56,9 @@ export const ConversationSelector: React.FC = ({ {conversations.length === 0 ? ( - 📭 No conversations yet! + 📭 No conversations yet! - + Select "New Chat" above to start chatting @@ -85,7 +86,7 @@ export const ConversationSelector: React.FC = ({ {/* Type badge */} {conv.type === "group" ? "👥" : "👤"} @@ -103,7 +104,7 @@ export const ConversationSelector: React.FC = ({ {/* Last message preview */} {conv.lastMessage && ( - + {" "} - {conv.lastMessage.slice(0, 30)} {conv.lastMessage.length > 30 ? "..." : ""} @@ -112,7 +113,7 @@ export const ConversationSelector: React.FC = ({ {/* Time */} {conv.lastMessageAt && ( - ({formatTime(conv.lastMessageAt)}) + ({formatTime(conv.lastMessageAt)}) )} {/* Unread count */} @@ -132,7 +133,7 @@ export const ConversationSelector: React.FC = ({ {/* Navigation hints */} ⌨️ Controls: - ↑↓: Navigate • Enter: Select • Esc: Back + ↑↓: Navigate • Enter: Select • Esc: Back ); diff --git a/src/components/Input.tsx b/src/components/Input.tsx index a64bd19..40196f8 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -1,7 +1,6 @@ import React from "react"; import { Box, Text } from "ink"; import TextInput from "ink-text-input"; -import { CommandSuggestions, CommandSuggestion } from "./CommandSuggestions"; const RED = "#fc4c34"; @@ -11,10 +10,6 @@ interface InputProps { onSubmit: (value: string) => void; placeholder?: string; commandMode?: boolean; - suggestions?: CommandSuggestion[]; - selectedSuggestionIndex?: number; - showSuggestions?: boolean; - onSuggestionSelect?: (suggestion: CommandSuggestion) => void; } export const Input: React.FC = ({ @@ -23,10 +18,6 @@ export const Input: React.FC = ({ onSubmit, placeholder = "Type a message...", commandMode = false, - suggestions = [], - selectedSuggestionIndex = 0, - showSuggestions = false, - onSuggestionSelect, }) => { return ( @@ -49,15 +40,7 @@ export const Input: React.FC = ({ - {/* Command suggestions */} - - - {commandMode && !showSuggestions && ( + {commandMode && ( 💡 Press Enter to execute command diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index d9d5a7d..0442d8f 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -5,7 +5,6 @@ import { ChatView } from "./ChatView.js"; import { StatusBar } from "./StatusBar.js"; import { Input } from "./Input.js"; import { CommandPalette, type Command } from "./CommandPalette.js"; -import { CommandSuggestion } from "./CommandSuggestions.js"; import type { ConversationInfo, FormattedMessage } from "../types/index.js"; interface LayoutProps { @@ -37,11 +36,6 @@ interface LayoutProps { onCommandExecute: (command: Command) => void; onCommandPaletteClose: () => void; - // Command suggestions props - suggestions?: CommandSuggestion[]; - selectedSuggestionIndex?: number; - showSuggestions?: boolean; - onSuggestionSelect?: (suggestion: CommandSuggestion) => void; } export const Layout: React.FC = ({ @@ -63,15 +57,11 @@ export const Layout: React.FC = ({ commands, onCommandExecute, onCommandPaletteClose, - suggestions = [], - selectedSuggestionIndex = 0, - showSuggestions = false, - onSuggestionSelect, }) => { return ( - + {/* Main content area */} - + {/* Sidebar */} {showSidebar && ( = ({ flexGrow={1} marginLeft={showSidebar ? 1 : 0} borderStyle="round" - borderColor="gray" + borderColor="#666666" paddingX={1} paddingY={0} + minHeight={0} > {/* Chat view */} - { - // This will be handled by the main app - }} - /> + + { + // This will be handled by the main app + }} + /> + {/* Input inside chat window border */} - + {isInputActive || currentConversation ? ( = ({ : "Enter Ethereum address or inbox ID..." } commandMode={commandMode} - suggestions={suggestions} - selectedSuggestionIndex={selectedSuggestionIndex} - showSuggestions={showSuggestions} - onSuggestionSelect={onSuggestionSelect} /> ) : ( - + {conversations.length === 0 ? "No conversations available. Press Enter to start a new chat..." : selectedConversationIndex === -1 @@ -161,6 +150,7 @@ export const Layout: React.FC = ({ conversationCount={conversations.length} error={error} statusMessage={statusMessage} + commands={commands} /> diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index e4d1efb..c863555 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -3,7 +3,7 @@ import { Box, Text } from "ink"; import type { ConversationInfo } from "../types/index.js"; const RED = "#fc4c34"; -const DIM_RED = "#cc3f2a"; +const DIM_GREY = "#666666"; interface SidebarProps { conversations: ConversationInfo[]; @@ -23,7 +23,7 @@ export const Sidebar: React.FC = ({ flexDirection="column" width={width} borderStyle="round" - borderColor="gray" + borderColor={DIM_GREY} paddingX={1} > {/* Header */} @@ -36,7 +36,7 @@ export const Sidebar: React.FC = ({ {/* Conversation List */} {conversations.length === 0 ? ( - No conversations + No conversations ) : ( conversations.map((conv, index) => { const isSelected = index === selectedIndex; @@ -46,18 +46,18 @@ export const Sidebar: React.FC = ({ return ( {/* Selection indicator */} - + {isSelected ? "▶" : " "} {/* Current conversation indicator */} - + {isCurrent ? "●" : " "} {/* Type badge */} {conv.type === "group" ? "[G]" : "[D]"} @@ -90,7 +90,7 @@ export const Sidebar: React.FC = ({ {/* Footer hints */} - ↑↓:Navigate Enter:Open + ↑↓:Navigate Enter:Open ); diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index 1b49d2b..6ebf2f3 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -1,5 +1,6 @@ import React from "react"; import { Box, Text } from "ink"; +import type { Command } from "./CommandPalette"; const RED = "#fc4c34"; @@ -9,6 +10,7 @@ interface StatusBarProps { conversationCount: number; error?: string; statusMessage?: string; + commands?: Command[]; } export const StatusBar: React.FC = ({ @@ -17,6 +19,7 @@ export const StatusBar: React.FC = ({ conversationCount, error, statusMessage, + commands = [], }) => { const statusColor = connectionStatus === "connected" @@ -64,14 +67,11 @@ export const StatusBar: React.FC = ({ ^B: - Sidebar - ^K: - Switch - ^C: - Quit + Sidebar + {/* Status message - at the very bottom */} {statusMessage && ( { const streamRef = useRef | null>(null); const isStreamingRef = useRef(false); const refreshIntervalRef = useRef(null); + const isInitializingRef = useRef(false); // Initialize agent useEffect(() => { + // Prevent multiple initializations + if (isInitializingRef.current || agent || globalAgent) { + if (globalAgent && !agent) { + setAgent(globalAgent); + setAddress(globalAgent.address || ""); + setInboxId(globalAgent.client.inboxId); + setUrl(getTestUrl(globalAgent.client) || ""); + setIsLoading(false); + } + return; + } + const initAgent = async () => { + isInitializingRef.current = true; try { onStatusChange?.("Initializing XMTP client..."); @@ -137,6 +154,9 @@ export const useXMTP = (options: UseXMTPOptions): UseXMTPReturn => { dbPath: (inboxId) => "." + `/cli-${env}-${inboxId.slice(0, 8)}.db3`, }); + // Store globally to prevent re-initialization + globalAgent = newAgent; + setAgent(newAgent); setAddress(newAgent.address || ""); setInboxId(newAgent.client.inboxId); @@ -196,18 +216,25 @@ export const useXMTP = (options: UseXMTPOptions): UseXMTPReturn => { setError(errMsg); onError?.(errMsg); setIsLoading(false); + isInitializingRef.current = false; } }; initAgent(); - // Cleanup interval on unmount + // Cleanup function return () => { if (refreshIntervalRef.current) { clearInterval(refreshIntervalRef.current); + refreshIntervalRef.current = null; + } + if (streamRef.current) { + // Stop streaming if active + isStreamingRef.current = false; + streamRef.current = null; } }; - }, [env, agentIdentifiers, onError, onStatusChange]); + }, [env, agentIdentifiers]); // Find or create conversation const findOrCreateConversationInternal = async ( diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts index dd152d2..dd5ecde 100644 --- a/src/utils/formatters.ts +++ b/src/utils/formatters.ts @@ -22,10 +22,26 @@ export const formatMessage = ( if (typeof message.content === "string") { content = message.content; } else { + // For non-string content (like JSON objects), show a summary instead of full JSON try { - content = JSON.stringify(message.content, null, 2); + const obj = message.content as Record; + if (obj && typeof obj === "object") { + // Show a summary of the object structure + const keys = Object.keys(obj); + if (keys.length === 0) { + content = "[Empty object]"; + } else if (keys.length <= 3) { + // For small objects, show key-value pairs + content = keys.map(key => `${key}: ${typeof obj[key] === 'object' ? '[Object]' : obj[key]}`).join(", "); + } else { + // For large objects, show just the keys + content = `[Object with ${keys.length} properties: ${keys.slice(0, 3).join(", ")}${keys.length > 3 ? "..." : ""}]`; + } + } else { + content = `[${typeof message.content}]`; + } } catch { - content = JSON.stringify(message.content); + content = "[Complex data]"; } }