diff --git a/packages/opencode/src/cli/ink/App.tsx b/packages/opencode/src/cli/ink/App.tsx index 5304812e77d..3c4429b7af9 100644 --- a/packages/opencode/src/cli/ink/App.tsx +++ b/packages/opencode/src/cli/ink/App.tsx @@ -17,11 +17,21 @@ export const App = (): ReactElement => { const [state, dispatch] = useReducer(appReducer, initialState) const [uiMode, setUIMode] = useState(state.ui.mode) const sessionRef = useRef(state.session) + const hasStartedRef = useRef(false) + const [showWelcome, setShowWelcome] = useState(true) useEffect(() => { sessionRef.current = state.session }, [state.session]) + // Hide welcome banner after first message - use ref to survive re-renders + useEffect(() => { + if (!hasStartedRef.current && (state.messages.length > 0 || state.streaming.text)) { + hasStartedRef.current = true + setShowWelcome(false) + } + }, [state.messages.length, state.streaming.text]) + // Initialize session on mount useEffect(() => { const initSession = async () => { @@ -85,13 +95,15 @@ export const App = (): ReactElement => { return ( - {/* Welcome message - always visible */} - - - oclite v0.1 - - - Lightweight OpenCode TUI - + {/* Welcome message - only show once */} + {showWelcome && ( + + + oclite v0.1 + + - Lightweight OpenCode TUI + + )} {/* Completed messages */} diff --git a/packages/opencode/src/cli/ink/hooks/useSDKEvents.ts b/packages/opencode/src/cli/ink/hooks/useSDKEvents.ts index affb5d81d3f..4e6a2bec61e 100644 --- a/packages/opencode/src/cli/ink/hooks/useSDKEvents.ts +++ b/packages/opencode/src/cli/ink/hooks/useSDKEvents.ts @@ -27,6 +27,7 @@ export function useSDKEvents(sessionId: string | null, dispatch: Dispatch(null) const streamingLockRef = useRef(false) + const seenTextLengthRef = useRef>(new Map()) useEffect(() => { if (!sessionId) { @@ -52,7 +53,15 @@ export function useSDKEvents(sessionId: string | null, dispatch: Dispatch void, currentAssistantMessageIdRef: { current: string | null }, streamingLockRef: { current: boolean }, + seenTextLengthRef: { current: Map }, ) { - // Debug logging for event flow - const getSessionId = () => { - const props = event.properties as Record - if (props.part && typeof props.part === "object" && props.part !== null) { - const part = props.part as Record - if (typeof part.sessionID === "string") { - return part.sessionID.slice(0, 8) - } - } - if (props.info && typeof props.info === "object" && props.info !== null) { - const info = props.info as Record - if (typeof info.sessionID === "string") { - return info.sessionID.slice(0, 8) - } - } - if (typeof props.sessionID === "string") { - return props.sessionID.slice(0, 8) - } - return "unknown" - } - console.error(`[EVENT] ${event.type}`, getSessionId()) - switch (event.type) { case "message.part.updated": { const part = event.properties.part @@ -159,8 +147,19 @@ function handleEvent( return } - // Use delta for incremental updates, fallback to full text - const content = event.properties.delta ?? part.text + // Prefer delta for incremental updates, compute delta from full text as fallback + let content = event.properties.delta + if (content === undefined && typeof part.text === "string") { + const messageKey = `${part.sessionID}:${part.messageID}` + const seenLength = seenTextLengthRef.current.get(messageKey) ?? 0 + + // Defensive check: only slice if text grew (prevent loss on truncation) + if (part.text.length >= seenLength) { + content = part.text.slice(seenLength) + seenTextLengthRef.current.set(messageKey, part.text.length) + } + } + if (typeof content === "string" && content.length > 0) { dispatch({ type: "STREAM_TEXT", @@ -234,6 +233,10 @@ function handleEvent( if (event.properties.info.sessionID !== sessionId) return setIsStreaming(false) + // Clean up text length tracking for this message (always, not just for current message) + const messageKey = `${event.properties.info.sessionID}:${event.properties.info.id}` + seenTextLengthRef.current.delete(messageKey) + // Clear the tracked message ID if this is the expected assistant message if (currentAssistantMessageIdRef.current === event.properties.info.id) { currentAssistantMessageIdRef.current = null diff --git a/packages/opencode/src/cli/ink/index.tsx b/packages/opencode/src/cli/ink/index.tsx index 69baf68f10a..1b65ed3f605 100644 --- a/packages/opencode/src/cli/ink/index.tsx +++ b/packages/opencode/src/cli/ink/index.tsx @@ -9,6 +9,7 @@ export function startInkTUI() { stdin: process.stdin, stdout: process.stdout, exitOnCtrlC: true, // Let Ink handle Ctrl+C exit + patchConsole: true, // Prevent console.log/error from breaking Ink rendering }) return instance.waitUntilExit()