feat: better bg tasks
This commit is contained in:
parent
af0f4a52fe
commit
b597123489
5383
ui-tui/package-lock.json
generated
5383
ui-tui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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 }
|
||||
)
|
||||
|
||||
@ -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)
|
||||
}, [])
|
||||
|
||||
@ -39,9 +39,7 @@ export const HOTKEYS: [string, string][] = [
|
||||
]
|
||||
|
||||
export const INTERPOLATION_RE = /\{!(.+?)\}/g
|
||||
|
||||
export const LONG_MSG = 300
|
||||
export const MAX_CTX = 128_000
|
||||
|
||||
export const PLACEHOLDERS = [
|
||||
'Ask me anything…',
|
||||
|
||||
@ -62,7 +62,7 @@ export const DEFAULT_THEME: Theme = {
|
||||
diffAdded: 'rgb(220,255,220)',
|
||||
diffRemoved: 'rgb(255,220,220)',
|
||||
diffAddedWord: 'rgb(36,138,61)',
|
||||
diffRemovedWord: 'rgb(207,34,46)',
|
||||
diffRemovedWord: 'rgb(207,34,46)'
|
||||
},
|
||||
|
||||
brand: {
|
||||
@ -110,7 +110,7 @@ export function fromSkin(
|
||||
diffAdded: d.color.diffAdded,
|
||||
diffRemoved: d.color.diffRemoved,
|
||||
diffAddedWord: d.color.diffAddedWord,
|
||||
diffRemovedWord: d.color.diffRemovedWord,
|
||||
diffRemovedWord: d.color.diffRemovedWord
|
||||
},
|
||||
|
||||
brand: {
|
||||
|
||||
@ -24,9 +24,8 @@ export interface ClarifyReq {
|
||||
export interface Msg {
|
||||
role: Role
|
||||
text: string
|
||||
kind?: 'intro' | 'tool-active'
|
||||
kind?: 'intro'
|
||||
info?: SessionInfo
|
||||
toolId?: string
|
||||
}
|
||||
|
||||
export type Role = 'assistant' | 'system' | 'tool' | 'user'
|
||||
@ -52,7 +51,6 @@ export interface Usage {
|
||||
export interface SudoReq {
|
||||
requestId: string
|
||||
}
|
||||
|
||||
export interface SecretReq {
|
||||
envVar: string
|
||||
prompt: string
|
||||
@ -72,7 +70,6 @@ export interface PendingPaste {
|
||||
text: string
|
||||
}
|
||||
|
||||
/** From `commands.catalog` — mirrors hermes_cli.commands COMMANDS + SUBCOMMANDS + skills. */
|
||||
export interface SlashCatalog {
|
||||
canon: Record<string, string>
|
||||
pairs: [string, string][]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user