feat: better bg tasks

This commit is contained in:
Brooklyn Nicholson
2026-04-08 14:18:37 -05:00
parent af0f4a52fe
commit b597123489
9 changed files with 364 additions and 5670 deletions

View File

@@ -3,24 +3,21 @@ import { Box, Text } from 'ink'
import type { Theme } from '../theme.js'
import type { ActivityItem } from '../types.js'
const toneColor = (item: ActivityItem, t: Theme) =>
item.tone === 'error' ? t.color.error : item.tone === 'warn' ? t.color.warn : t.color.dim
export function ActivityLane({ items, t }: { items: ActivityItem[]; t: Theme }) {
if (!items.length) {
return null
}
const visible = items.slice(-4)
return (
<Box flexDirection="column" marginTop={1}>
{visible.map(item => {
const color = item.tone === 'error' ? t.color.error : item.tone === 'warn' ? t.color.warn : t.color.dim
return (
<Text color={color} dimColor={item.tone === 'info'} key={item.id}>
{t.brand.tool} {item.text}
</Text>
)
})}
{items.slice(-4).map(item => (
<Text color={toneColor(item, t)} dimColor={item.tone === 'info'} key={item.id}>
{t.brand.tool} {item.text}
</Text>
))}
</Box>
)
}

View File

@@ -20,7 +20,6 @@ export const MessageLine = memo(function MessageLine({
t: Theme
}) {
const { body, glyph, prefix } = ROLE[msg.role](t)
const contentWidth = Math.max(20, cols - 5)
if (msg.role === 'tool') {
return (
@@ -61,7 +60,7 @@ export const MessageLine = memo(function MessageLine({
</Text>
</Box>
<Box width={contentWidth}>{content}</Box>
<Box width={Math.max(20, cols - 5)}>{content}</Box>
</Box>
</Box>
)

View File

@@ -1,7 +1,7 @@
import { Text, useInput } from 'ink'
import { useEffect, useRef, useState } from 'react'
function wl(s: string, p: number) {
function wordLeft(s: string, p: number) {
let i = p - 1
while (i > 0 && /\s/.test(s[i]!)) {
@@ -15,7 +15,7 @@ function wl(s: string, p: number) {
return Math.max(0, i)
}
function wr(s: string, p: number) {
function wordRight(s: string, p: number) {
let i = p
while (i < s.length && !/\s/.test(s[i]!)) {
@@ -29,7 +29,7 @@ function wr(s: string, p: number) {
return i
}
const ESC = String.fromCharCode(0x1b)
const ESC = '\x1b'
const INV = ESC + '[7m'
const INV_OFF = ESC + '[27m'
const DIM = ESC + '[2m'
@@ -63,6 +63,16 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
}
}, [value])
const commit = (v: string, c: number) => {
c = Math.max(0, Math.min(c, v.length))
setCur(c)
if (v !== value) {
selfChange.current = true
onChange(v)
}
}
const flushPaste = () => {
const pasted = pasteBuf.current
const at = pastePos.current
@@ -85,9 +95,8 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
}
if (pasted.length && PRINTABLE.test(pasted)) {
const nv = v.slice(0, at) + pasted + v.slice(at)
selfChange.current = true
onChange(nv)
onChange(v.slice(0, at) + pasted + v.slice(at))
setCur(at + pasted.length)
}
}
@@ -113,9 +122,8 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
return
}
let c = cur,
v = value
let c = cur
let v = value
const mod = k.ctrl || k.meta
if (k.home || (k.ctrl && inp === 'a')) {
@@ -123,12 +131,12 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
} else if (k.end || (k.ctrl && inp === 'e')) {
c = v.length
} else if (k.leftArrow) {
c = mod ? wl(v, c) : Math.max(0, c - 1)
c = mod ? wordLeft(v, c) : Math.max(0, c - 1)
} else if (k.rightArrow) {
c = mod ? wr(v, c) : Math.min(v.length, c + 1)
c = mod ? wordRight(v, c) : Math.min(v.length, c + 1)
} else if ((k.backspace || k.delete) && c > 0) {
if (mod) {
const t = wl(v, c)
const t = wordLeft(v, c)
v = v.slice(0, t) + v.slice(c)
c = t
} else {
@@ -136,7 +144,7 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
c--
}
} else if (k.ctrl && inp === 'w' && c > 0) {
const t = wl(v, c)
const t = wordLeft(v, c)
v = v.slice(0, t) + v.slice(c)
c = t
} else if (k.ctrl && inp === 'u') {
@@ -145,9 +153,9 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
} else if (k.ctrl && inp === 'k') {
v = v.slice(0, c)
} else if (k.meta && inp === 'b') {
c = wl(v, c)
c = wordLeft(v, c)
} else if (k.meta && inp === 'f') {
c = wr(v, c)
c = wordRight(v, c)
} else if (inp.length > 0) {
const raw = inp.replace(BRACKET_PASTE, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n')
@@ -155,9 +163,7 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
return
}
const isMultiChar = raw.length > 1 || raw.includes('\n')
if (isMultiChar) {
if (raw.length > 1 || raw.includes('\n')) {
if (!pasteBuf.current) {
pastePos.current = c
}
@@ -183,13 +189,7 @@ export function TextInput({ value, onChange, onPaste, onSubmit, placeholder = ''
return
}
c = Math.max(0, Math.min(c, v.length))
setCur(c)
if (v !== value) {
selfChange.current = true
onChange(v)
}
commit(v, c)
},
{ isActive: focus }
)

View File

@@ -9,19 +9,19 @@ import type { ActiveTool } from '../types.js'
const THINK_POOL: BrailleSpinnerName[] = ['helix', 'breathe', 'orbit', 'dna', 'waverows', 'snake', 'pulse']
const TOOL_POOL: BrailleSpinnerName[] = ['cascade', 'scan', 'diagswipe', 'fillsweep', 'rain', 'columns', 'sparkle']
const pick = <T,>(arr: T[]) => arr[Math.floor(Math.random() * arr.length)]!
const pick = <T,>(a: T[]) => a[Math.floor(Math.random() * a.length)]!
function Spinner({ color, variant = 'think' }: { color: string; variant?: 'think' | 'tool' }) {
const [spin] = useState(() => spinners[pick(variant === 'tool' ? TOOL_POOL : THINK_POOL)])
const [i, setI] = useState(0)
const [frame, setFrame] = useState(0)
useEffect(() => {
const id = setInterval(() => setI(p => (p + 1) % spin.frames.length), spin.interval)
const id = setInterval(() => setFrame(f => (f + 1) % spin.frames.length), spin.interval)
return () => clearInterval(id)
}, [spin])
return <Text color={color}>{spin.frames[i]}</Text>
return <Text color={color}>{spin.frames[frame]}</Text>
}
export const Thinking = memo(function Thinking({
@@ -36,9 +36,7 @@ export const Thinking = memo(function Thinking({
const [tick, setTick] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setTick(v => v + 1)
}, 1100)
const id = setInterval(() => setTick(v => v + 1), 1100)
return () => clearInterval(id)
}, [])