diff --git a/ui-tui/src/__tests__/interactionMode.test.ts b/ui-tui/src/__tests__/interactionMode.test.ts deleted file mode 100644 index 1a44519ddb..0000000000 --- a/ui-tui/src/__tests__/interactionMode.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' - -import { getInteractionMode, markScrolling, markTyping, resetInteractionMode } from '../app/interactionMode.js' -import { SCROLLING_IDLE_MS, TYPING_IDLE_MS } from '../config/timing.js' - -describe('interactionMode', () => { - afterEach(() => { - resetInteractionMode() - vi.useRealTimers() - }) - - it('holds scrolling mode briefly then returns idle', () => { - vi.useFakeTimers() - markScrolling() - expect(getInteractionMode()).toBe('scrolling') - vi.advanceTimersByTime(SCROLLING_IDLE_MS) - expect(getInteractionMode()).toBe('idle') - }) - - it('typing takes priority over scrolling', () => { - vi.useFakeTimers() - markTyping() - markScrolling() - expect(getInteractionMode()).toBe('typing') - vi.advanceTimersByTime(TYPING_IDLE_MS) - expect(getInteractionMode()).toBe('idle') - }) -}) diff --git a/ui-tui/src/app/interactionMode.ts b/ui-tui/src/app/interactionMode.ts deleted file mode 100644 index f18033f81c..0000000000 --- a/ui-tui/src/app/interactionMode.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { SCROLLING_IDLE_MS, TYPING_IDLE_MS } from '../config/timing.js' - -export type InteractionMode = 'idle' | 'scrolling' | 'typing' - -type Timer = null | ReturnType - -let mode: InteractionMode = 'idle' -let scrollingTimer: Timer = null -let typingTimer: Timer = null - -const clear = (t: Timer): null => { - if (t) { - clearTimeout(t) - } - - return null -} - -export function getInteractionMode(): InteractionMode { - return mode -} - -export function markTyping(): void { - mode = 'typing' - typingTimer = clear(typingTimer) - scrollingTimer = clear(scrollingTimer) - typingTimer = setTimeout(() => { - typingTimer = null - mode = 'idle' - }, TYPING_IDLE_MS) -} - -export function markScrolling(): void { - if (mode === 'typing') { - return - } - - mode = 'scrolling' - scrollingTimer = clear(scrollingTimer) - scrollingTimer = setTimeout(() => { - scrollingTimer = null - if (mode === 'scrolling') { - mode = 'idle' - } - }, SCROLLING_IDLE_MS) -} - -export function resetInteractionMode(): void { - scrollingTimer = clear(scrollingTimer) - typingTimer = clear(typingTimer) - mode = 'idle' -} diff --git a/ui-tui/src/app/useSubmission.ts b/ui-tui/src/app/useSubmission.ts index 70a3faf329..6a585bd61d 100644 --- a/ui-tui/src/app/useSubmission.ts +++ b/ui-tui/src/app/useSubmission.ts @@ -101,10 +101,10 @@ export function useSubmission(opts: UseSubmissionOptions) { gw.request('prompt.submit', { session_id: sid, text: submitText }).catch((e: Error) => { if (isSessionBusyError(e)) { - composerActions.enqueue(text) + composerActions.enqueue(submitText) patchUiState({ busy: true, status: 'queued for next turn' }) - return sys(`queued: "${text.slice(0, 50)}${text.length > 50 ? '…' : ''}"`) + return sys(`queued: "${submitText.slice(0, 50)}${submitText.length > 50 ? '…' : ''}"`) } sys(`error: ${e.message}`) diff --git a/ui-tui/src/config/timing.ts b/ui-tui/src/config/timing.ts index d428bacfee..e0bd611b82 100644 --- a/ui-tui/src/config/timing.ts +++ b/ui-tui/src/config/timing.ts @@ -2,5 +2,4 @@ export const STREAM_BATCH_MS = 16 export const STREAM_IDLE_BATCH_MS = 16 export const STREAM_TYPING_BATCH_MS = 80 export const TYPING_IDLE_MS = 250 -export const SCROLLING_IDLE_MS = 450 export const REASONING_PULSE_MS = 700 diff --git a/ui-tui/src/lib/viewportStore.ts b/ui-tui/src/lib/viewportStore.ts index 298d094bfb..0a52e99a87 100644 --- a/ui-tui/src/lib/viewportStore.ts +++ b/ui-tui/src/lib/viewportStore.ts @@ -1,6 +1,6 @@ import type { ScrollBoxHandle } from '@hermes/ink' import type { RefObject } from 'react' -import { useCallback, useSyncExternalStore } from 'react' +import { useCallback, useMemo, useSyncExternalStore } from 'react' export interface ViewportSnapshot { atBottom: boolean @@ -45,6 +45,19 @@ export function viewportSnapshotKey(v: ViewportSnapshot) { return `${v.atBottom ? 1 : 0}:${v.top}:${v.viewportHeight}:${v.scrollHeight}:${v.pending}` } +const snapshotFromKey = (key: string): ViewportSnapshot => { + const [atBottom = '1', top = '0', viewportHeight = '0', scrollHeight = '0', pending = '0'] = key.split(':') + + return { + atBottom: atBottom === '1', + bottom: Number(top) + Number(viewportHeight), + pending: Number(pending), + scrollHeight: Number(scrollHeight), + top: Number(top), + viewportHeight: Number(viewportHeight) + } +} + export function useViewportSnapshot(scrollRef: RefObject): ViewportSnapshot { const key = useSyncExternalStore( useCallback((cb: () => void) => scrollRef.current?.subscribe(cb) ?? (() => {}), [scrollRef]), @@ -52,7 +65,5 @@ export function useViewportSnapshot(scrollRef: RefObject () => viewportSnapshotKey(EMPTY) ) - void key - - return getViewportSnapshot(scrollRef.current) + return useMemo(() => snapshotFromKey(key), [key]) }