From de0a10d6137bcb9e0f97fb043215ad7088e3fdc0 Mon Sep 17 00:00:00 2001 From: yf Date: Mon, 20 Apr 2026 22:51:40 +1000 Subject: [PATCH 1/2] Add execution control UI and inline messages - PipelineControl component: Stop button during agent execution - ChatInput: route messages during execution as inline messages (interjection) - Wire pause/resume/stop/sendInlineMessage/executionState through all layers - package.json: use local connectonion-ts for development --- app/[address]/[sessionId]/page.tsx | 9 ++ components/chat/chat-input.tsx | 14 +++- components/chat/chat.tsx | 2 + components/chat/index.ts | 1 + components/chat/mode-indicator.tsx | 121 +++++++++++++++------------ components/chat/pipeline-control.tsx | 35 ++++++++ components/chat/types.ts | 4 + components/chat/use-agent-sdk.ts | 20 +++++ package.json | 2 +- 9 files changed, 151 insertions(+), 57 deletions(-) create mode 100644 components/chat/pipeline-control.tsx diff --git a/app/[address]/[sessionId]/page.tsx b/app/[address]/[sessionId]/page.tsx index 9e79270..7ec47cc 100644 --- a/app/[address]/[sessionId]/page.tsx +++ b/app/[address]/[sessionId]/page.tsx @@ -115,6 +115,11 @@ export default function ChatSessionPage() { setMode, checkSessionStatus, reconnect, + pause, + resume, + stopExecution, + sendInlineMessage, + executionState, } = useAgentSDK({ agentAddress: address, sessionId, @@ -214,6 +219,7 @@ export default function ChatSessionPage() { handleSend(lastMessage) : undefined} onReconnect={handleReconnect} + executionState={executionState} + isProcessing={isLoading} + onStopExecution={stopExecution} /> } connectionError={connectionError} diff --git a/components/chat/chat-input.tsx b/components/chat/chat-input.tsx index c5704bb..3524de5 100644 --- a/components/chat/chat-input.tsx +++ b/components/chat/chat-input.tsx @@ -14,6 +14,7 @@ import type { ChatInputProps, FileAttachment } from './types' export function ChatInput({ onSend, + onInlineMessage, isLoading = false, placeholder = 'Message...', statusBar, @@ -48,7 +49,16 @@ export function ChatInput({ const handleSubmit = useCallback(() => { const trimmed = value.trim() - if ((!trimmed && images.length === 0 && files.length === 0) || isLoading) return + if (!trimmed && images.length === 0 && files.length === 0) return + + // During execution, route text-only messages as inline messages + if (isLoading && onInlineMessage && trimmed) { + onInlineMessage(trimmed) + setValue('') + return + } + + if (isLoading) return onSend( trimmed, @@ -59,7 +69,7 @@ export function ChatInput({ setImages([]) setFiles([]) // Height resets automatically via useEffect when value changes - }, [value, images, files, isLoading, onSend]) + }, [value, images, files, isLoading, onSend, onInlineMessage]) const handleFileSelect = useCallback((e: ChangeEvent) => { const selected = e.target.files diff --git a/components/chat/chat.tsx b/components/chat/chat.tsx index ec1a0f6..2b45168 100644 --- a/components/chat/chat.tsx +++ b/components/chat/chat.tsx @@ -14,6 +14,7 @@ import type { ChatProps, ThinkingUI, UserUI } from './types' export function Chat({ ui = [], onSend, + onInlineMessage, isLoading = false, placeholder = 'Send a message...', elapsedTime = 0, @@ -111,6 +112,7 @@ export function Chat({ return ( void onReconnect?: () => void + executionState?: 'running' | 'paused' | 'stopped' | null + isProcessing?: boolean + onStopExecution?: () => void } const BASE_MODES: ApprovalMode[] = ['safe', 'plan', 'accept_edits', 'ulw'] @@ -98,7 +102,7 @@ export function ModeIndicator({ mode, onModeChange, disabled }: ModeIndicatorPro } /** Left-right split status bar: connection on left, mode cycle on right */ -export function ModeStatusBar({ mode, onModeChange, disabled, sessionState, connectionError, onRetry, onReconnect }: ModeStatusBarProps) { +export function ModeStatusBar({ mode, onModeChange, disabled, sessionState, connectionError, onRetry, onReconnect, executionState, isProcessing, onStopExecution }: ModeStatusBarProps) { const currentMode = MODE_CONFIG[mode] || MODE_CONFIG.safe const cycleMode = useCallback(() => { @@ -125,60 +129,69 @@ export function ModeStatusBar({ mode, onModeChange, disabled, sessionState, conn const showConnection = sessionState === 'active' || sessionState === 'connected' || sessionState === 'disconnected' || sessionState === 'reconnecting' || !!connectionError return ( -
- {/* Left: Connection status */} -
- {showConnection && ( - connectionError ? ( -
- - error - {onRetry && ( - - )} -
- ) : sessionState === 'disconnected' ? ( -
- - disconnected - {onReconnect && ( - - )} -
- ) : sessionState === 'active' ? ( -
- - live -
- ) : sessionState === 'connected' ? ( -
- - connected -
- ) : null - )} -
- - {/* Right: Mode cycle */} - + )} +
+ ) : sessionState === 'disconnected' ? ( +
+ + disconnected + {onReconnect && ( + + )} +
+ ) : sessionState === 'active' ? ( +
+ + live +
+ ) : sessionState === 'connected' ? ( +
+ + connected +
+ ) : null + )} + + + {/* Right: Mode cycle */} + + ))} + + ) } diff --git a/components/chat/pipeline-control.tsx b/components/chat/pipeline-control.tsx new file mode 100644 index 0000000..68dceef --- /dev/null +++ b/components/chat/pipeline-control.tsx @@ -0,0 +1,35 @@ +'use client' + +import { HiOutlineStop } from 'react-icons/hi' + +interface PipelineControlProps { + executionState: 'running' | 'paused' | 'stopped' | null + isProcessing: boolean + stopExecution: () => void +} + +export function PipelineControl({ executionState, isProcessing, stopExecution }: PipelineControlProps) { + // Only show when agent is actively working + if (!isProcessing) return null + + const isStopped = (executionState ?? 'running') === 'stopped' + + return ( +
+
+ + {isStopped ? ( + Stopped + ) : ( + + )} +
+ ) +} diff --git a/components/chat/types.ts b/components/chat/types.ts index 824292b..49cbcd1 100644 --- a/components/chat/types.ts +++ b/components/chat/types.ts @@ -282,6 +282,8 @@ export interface SkillInfo { export interface ChatProps { ui?: UI[] onSend: (message: string, images?: string[], files?: FileAttachment[]) => void + /** Send an inline message to the agent during execution */ + onInlineMessage?: (content: string) => void isLoading?: boolean placeholder?: string className?: string @@ -331,6 +333,8 @@ export interface ChatMessageProps { export interface ChatInputProps { onSend: (message: string, images?: string[], files?: FileAttachment[]) => void + /** Send an inline message to the agent during execution (like Claude Code interjections) */ + onInlineMessage?: (content: string) => void isLoading?: boolean placeholder?: string className?: string diff --git a/components/chat/use-agent-sdk.ts b/components/chat/use-agent-sdk.ts index 71078d0..9a3b0f7 100644 --- a/components/chat/use-agent-sdk.ts +++ b/components/chat/use-agent-sdk.ts @@ -58,6 +58,16 @@ interface UseAgentSDKReturn { /** Reconnect to existing session to receive pending output */ reconnect: () => void clear: () => void + /** Pause agent execution */ + pause: () => void + /** Resume paused agent */ + resume: () => void + /** Stop agent execution */ + stopExecution: () => void + /** Send inline message during execution */ + sendInlineMessage: (content: string) => void + /** Current execution state */ + executionState: 'running' | 'paused' | 'stopped' | null } /** @@ -145,6 +155,11 @@ export function useAgentSDK(options: UseAgentSDKOptions): UseAgentSDKReturn { signOnboard, setMode: sdkSetMode, reconnect: sdkReconnect, + pause: sdkPause, + resume: sdkResume, + stopExecution: sdkStopExecution, + sendInlineMessage: sdkSendInlineMessage, + executionState, } = useAgentForHuman(agentAddress, sessionId) // Timer effect for elapsed time display @@ -314,5 +329,10 @@ export function useAgentSDK(options: UseAgentSDKOptions): UseAgentSDKReturn { checkSessionStatus, reconnect: sdkReconnect, clear, + pause: sdkPause, + resume: sdkResume, + stopExecution: sdkStopExecution, + sendInlineMessage: sdkSendInlineMessage, + executionState, } } diff --git a/package.json b/package.json index 3f1e731..a8d4bc6 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@types/react-syntax-highlighter": "^15.5.13", "bip39": "^3.1.0", "clsx": "^2.1.1", - "connectonion": "^0.1.0", + "connectonion": "file:../openonion/connectonion-ts", "next": "16.0.10", "prism-react-renderer": "^2.4.1", "react": "19.2.1", From 86640bc1dc8117060c56de414ee964fc29018c79 Mon Sep 17 00:00:00 2001 From: yf Date: Mon, 20 Apr 2026 23:18:28 +1000 Subject: [PATCH 2/2] Revert package.json: use npm connectonion, not local file link --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a8d4bc6..3f1e731 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@types/react-syntax-highlighter": "^15.5.13", "bip39": "^3.1.0", "clsx": "^2.1.1", - "connectonion": "file:../openonion/connectonion-ts", + "connectonion": "^0.1.0", "next": "16.0.10", "prism-react-renderer": "^2.4.1", "react": "19.2.1",