fix(tui): address latest review feedback
This commit is contained in:
@@ -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')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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<typeof setTimeout>
|
|
||||||
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
@@ -101,10 +101,10 @@ export function useSubmission(opts: UseSubmissionOptions) {
|
|||||||
|
|
||||||
gw.request<PromptSubmitResponse>('prompt.submit', { session_id: sid, text: submitText }).catch((e: Error) => {
|
gw.request<PromptSubmitResponse>('prompt.submit', { session_id: sid, text: submitText }).catch((e: Error) => {
|
||||||
if (isSessionBusyError(e)) {
|
if (isSessionBusyError(e)) {
|
||||||
composerActions.enqueue(text)
|
composerActions.enqueue(submitText)
|
||||||
patchUiState({ busy: true, status: 'queued for next turn' })
|
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}`)
|
sys(`error: ${e.message}`)
|
||||||
|
|||||||
@@ -2,5 +2,4 @@ export const STREAM_BATCH_MS = 16
|
|||||||
export const STREAM_IDLE_BATCH_MS = 16
|
export const STREAM_IDLE_BATCH_MS = 16
|
||||||
export const STREAM_TYPING_BATCH_MS = 80
|
export const STREAM_TYPING_BATCH_MS = 80
|
||||||
export const TYPING_IDLE_MS = 250
|
export const TYPING_IDLE_MS = 250
|
||||||
export const SCROLLING_IDLE_MS = 450
|
|
||||||
export const REASONING_PULSE_MS = 700
|
export const REASONING_PULSE_MS = 700
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ScrollBoxHandle } from '@hermes/ink'
|
import type { ScrollBoxHandle } from '@hermes/ink'
|
||||||
import type { RefObject } from 'react'
|
import type { RefObject } from 'react'
|
||||||
import { useCallback, useSyncExternalStore } from 'react'
|
import { useCallback, useMemo, useSyncExternalStore } from 'react'
|
||||||
|
|
||||||
export interface ViewportSnapshot {
|
export interface ViewportSnapshot {
|
||||||
atBottom: boolean
|
atBottom: boolean
|
||||||
@@ -45,6 +45,19 @@ export function viewportSnapshotKey(v: ViewportSnapshot) {
|
|||||||
return `${v.atBottom ? 1 : 0}:${v.top}:${v.viewportHeight}:${v.scrollHeight}:${v.pending}`
|
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<ScrollBoxHandle | null>): ViewportSnapshot {
|
export function useViewportSnapshot(scrollRef: RefObject<ScrollBoxHandle | null>): ViewportSnapshot {
|
||||||
const key = useSyncExternalStore(
|
const key = useSyncExternalStore(
|
||||||
useCallback((cb: () => void) => scrollRef.current?.subscribe(cb) ?? (() => {}), [scrollRef]),
|
useCallback((cb: () => void) => scrollRef.current?.subscribe(cb) ?? (() => {}), [scrollRef]),
|
||||||
@@ -52,7 +65,5 @@ export function useViewportSnapshot(scrollRef: RefObject<ScrollBoxHandle | null>
|
|||||||
() => viewportSnapshotKey(EMPTY)
|
() => viewportSnapshotKey(EMPTY)
|
||||||
)
|
)
|
||||||
|
|
||||||
void key
|
return useMemo(() => snapshotFromKey(key), [key])
|
||||||
|
|
||||||
return getViewportSnapshot(scrollRef.current)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user