Merge pull request #14145 from NousResearch/bb/tui-polish

fix(tui): input wrap, shift-tab yolo, statusline, clean boot
This commit is contained in:
brooklyn! 2026-04-22 16:48:37 -05:00 committed by GitHub
commit a1d57292af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 715 additions and 229 deletions

View file

@ -395,10 +395,7 @@ describe('topLevelSubagents', () => {
})
it('excludes children whose parent is present', () => {
const items = [
makeItem({ id: 'p', index: 0 }),
makeItem({ depth: 1, id: 'c', index: 0, parentId: 'p' })
]
const items = [makeItem({ id: 'p', index: 0 }), makeItem({ depth: 1, id: 'c', index: 0, parentId: 'p' })]
expect(topLevelSubagents(items).map(s => s.id)).toEqual(['p'])
})

View file

@ -0,0 +1,60 @@
import { describe, expect, it } from 'vitest'
import { cursorLayout, offsetFromPosition } from '../components/textInput.js'
describe('cursorLayout — char-wrap parity with wrap-ansi', () => {
it('places cursor mid-line at its column', () => {
expect(cursorLayout('hello world', 6, 40)).toEqual({ column: 6, line: 0 })
})
it('places cursor at end of a non-full line', () => {
expect(cursorLayout('hi', 2, 10)).toEqual({ column: 2, line: 0 })
})
it('wraps to next line when cursor lands exactly at the right edge', () => {
// 8 chars on an 8-col line: text fills the row exactly; the cursor's
// inverted-space cell overflows to col 0 of the next row.
expect(cursorLayout('abcdefgh', 8, 8)).toEqual({ column: 0, line: 1 })
})
it('tracks a word across a char-wrap boundary without jumping', () => {
// With wordWrap:false, "hello world" at cols=8 is "hello wo\nrld" —
// typing incremental letters doesn't reshuffle the word across lines.
expect(cursorLayout('hello wo', 8, 8)).toEqual({ column: 0, line: 1 })
expect(cursorLayout('hello wor', 9, 8)).toEqual({ column: 1, line: 1 })
expect(cursorLayout('hello worl', 10, 8)).toEqual({ column: 2, line: 1 })
})
it('honours explicit newlines', () => {
expect(cursorLayout('one\ntwo', 5, 40)).toEqual({ column: 1, line: 1 })
expect(cursorLayout('one\ntwo', 4, 40)).toEqual({ column: 0, line: 1 })
})
it('does not wrap when cursor is before the right edge', () => {
expect(cursorLayout('abcdefg', 7, 8)).toEqual({ column: 7, line: 0 })
})
})
describe('offsetFromPosition — char-wrap inverse of cursorLayout', () => {
it('returns 0 for empty input', () => {
expect(offsetFromPosition('', 0, 0, 10)).toBe(0)
})
it('maps clicks within a single line', () => {
expect(offsetFromPosition('hello', 0, 3, 40)).toBe(3)
})
it('maps clicks past end to value length', () => {
expect(offsetFromPosition('hi', 0, 10, 40)).toBe(2)
})
it('maps clicks on a wrapped second row at cols boundary', () => {
// "abcdefghij" at cols=8 wraps to "abcdefgh\nij" — click at row 1 col 0
// should land on 'i' (offset 8).
expect(offsetFromPosition('abcdefghij', 1, 0, 8)).toBe(8)
})
it('maps clicks past a \\n into the target line', () => {
expect(offsetFromPosition('one\ntwo', 1, 2, 40)).toBe(6)
})
})

View file

@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { $uiState, resetUiState } from '../app/uiStore.js'
import { applyDisplay } from '../app/useConfigSync.js'
import { applyDisplay, normalizeStatusBar } from '../app/useConfigSync.js'
describe('applyDisplay', () => {
beforeEach(() => {
@ -36,10 +36,20 @@ describe('applyDisplay', () => {
expect(s.inlineDiffs).toBe(false)
expect(s.showCost).toBe(true)
expect(s.showReasoning).toBe(true)
expect(s.statusBar).toBe(false)
expect(s.statusBar).toBe('off')
expect(s.streaming).toBe(false)
})
it('coerces legacy true + "on" alias to top', () => {
const setBell = vi.fn()
applyDisplay({ config: { display: { tui_statusbar: true as unknown as 'on' } } }, setBell)
expect($uiState.get().statusBar).toBe('top')
applyDisplay({ config: { display: { tui_statusbar: 'on' } } }, setBell)
expect($uiState.get().statusBar).toBe('top')
})
it('applies v1 parity defaults when display fields are missing', () => {
const setBell = vi.fn()
@ -50,7 +60,7 @@ describe('applyDisplay', () => {
expect(s.inlineDiffs).toBe(true)
expect(s.showCost).toBe(false)
expect(s.showReasoning).toBe(false)
expect(s.statusBar).toBe(true)
expect(s.statusBar).toBe('top')
expect(s.streaming).toBe(true)
})
@ -64,4 +74,42 @@ describe('applyDisplay', () => {
expect(s.inlineDiffs).toBe(true)
expect(s.streaming).toBe(true)
})
it('accepts the new string statusBar modes', () => {
const setBell = vi.fn()
applyDisplay({ config: { display: { tui_statusbar: 'bottom' } } }, setBell)
expect($uiState.get().statusBar).toBe('bottom')
applyDisplay({ config: { display: { tui_statusbar: 'top' } } }, setBell)
expect($uiState.get().statusBar).toBe('top')
})
})
describe('normalizeStatusBar', () => {
it('maps legacy bool + on alias to top/off', () => {
expect(normalizeStatusBar(true)).toBe('top')
expect(normalizeStatusBar(false)).toBe('off')
expect(normalizeStatusBar('on')).toBe('top')
})
it('passes through the canonical enum', () => {
expect(normalizeStatusBar('off')).toBe('off')
expect(normalizeStatusBar('top')).toBe('top')
expect(normalizeStatusBar('bottom')).toBe('bottom')
})
it('defaults missing/unknown values to top', () => {
expect(normalizeStatusBar(undefined)).toBe('top')
expect(normalizeStatusBar(null)).toBe('top')
expect(normalizeStatusBar('sideways')).toBe('top')
expect(normalizeStatusBar(42)).toBe('top')
})
it('trims whitespace and folds case', () => {
expect(normalizeStatusBar(' Bottom ')).toBe('bottom')
expect(normalizeStatusBar('TOP')).toBe('top')
expect(normalizeStatusBar(' on ')).toBe('top')
expect(normalizeStatusBar('OFF')).toBe('off')
})
})