chore: uptick
This commit is contained in:
@@ -144,6 +144,52 @@ export function Md({ compact, t, text }: { compact?: boolean; t: Theme; text: st
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.match(/^>\s?/)) {
|
||||
const quoteLines: string[] = []
|
||||
while (i < lines.length && lines[i]!.match(/^>\s?/)) {
|
||||
quoteLines.push(lines[i]!.replace(/^>\s?/, ''))
|
||||
i++
|
||||
}
|
||||
nodes.push(
|
||||
<Box flexDirection="column" key={key}>
|
||||
{quoteLines.map((ql, qi) => (
|
||||
<Text color={t.color.dim} key={qi}>
|
||||
{' │ '}<MdInline t={t} text={ql} />
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.includes('|') && line.trim().startsWith('|')) {
|
||||
const tableRows: string[][] = []
|
||||
while (i < lines.length && lines[i]!.trim().startsWith('|')) {
|
||||
const row = lines[i]!.trim()
|
||||
if (!/^[|\s:-]+$/.test(row)) {
|
||||
tableRows.push(
|
||||
row.split('|').filter(Boolean).map(c => c.trim())
|
||||
)
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (tableRows.length) {
|
||||
const widths = tableRows[0]!.map((_, ci) =>
|
||||
Math.max(...tableRows.map(r => (r[ci] ?? '').length))
|
||||
)
|
||||
nodes.push(
|
||||
<Box flexDirection="column" key={key} paddingLeft={2}>
|
||||
{tableRows.map((row, ri) => (
|
||||
<Text color={ri === 0 ? t.color.amber : t.color.cornsilk} key={ri}>
|
||||
{row.map((cell, ci) => cell.padEnd(widths[ci] ?? 0)).join(' ')}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
nodes.push(<MdInline key={key} t={t} text={line} />)
|
||||
i++
|
||||
}
|
||||
|
||||
29
ui-tui/src/components/maskedPrompt.tsx
Normal file
29
ui-tui/src/components/maskedPrompt.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Box, Text } from 'ink'
|
||||
import TextInput from 'ink-text-input'
|
||||
import { useState } from 'react'
|
||||
|
||||
import type { Theme } from '../theme.js'
|
||||
|
||||
export function MaskedPrompt({
|
||||
icon, label, onSubmit, sub, t
|
||||
}: {
|
||||
icon: string
|
||||
label: string
|
||||
onSubmit: (v: string) => void
|
||||
sub?: string
|
||||
t: Theme
|
||||
}) {
|
||||
const [value, setValue] = useState('')
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={t.color.warn}>{icon} {label}</Text>
|
||||
{sub && <Text color={t.color.dim}> {sub}</Text>}
|
||||
|
||||
<Box>
|
||||
<Text color={t.color.label}>{'> '}</Text>
|
||||
<TextInput mask="*" onChange={setValue} onSubmit={onSubmit} value={value} />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
94
ui-tui/src/components/sessionPicker.tsx
Normal file
94
ui-tui/src/components/sessionPicker.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Box, Text, useInput } from 'ink'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import type { GatewayClient } from '../gatewayClient.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
|
||||
interface SessionItem {
|
||||
id: string
|
||||
title: string
|
||||
preview: string
|
||||
started_at: number
|
||||
message_count: number
|
||||
}
|
||||
|
||||
function age(ts: number): string {
|
||||
const d = (Date.now() / 1000 - ts) / 86400
|
||||
if (d < 1) return 'today'
|
||||
if (d < 2) return 'yesterday'
|
||||
return `${Math.floor(d)}d ago`
|
||||
}
|
||||
|
||||
const VISIBLE = 15
|
||||
|
||||
export function SessionPicker({
|
||||
gw,
|
||||
onCancel,
|
||||
onSelect,
|
||||
t
|
||||
}: {
|
||||
gw: GatewayClient
|
||||
onCancel: () => void
|
||||
onSelect: (id: string) => void
|
||||
t: Theme
|
||||
}) {
|
||||
const [items, setItems] = useState<SessionItem[]>([])
|
||||
const [sel, setSel] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
gw.request('session.list', { limit: 20 })
|
||||
.then((r: any) => {
|
||||
setItems(r.sessions ?? [])
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(() => setLoading(false))
|
||||
}, [gw])
|
||||
|
||||
useInput((ch, key) => {
|
||||
if (key.escape) return onCancel()
|
||||
if (key.upArrow && sel > 0) setSel(s => s - 1)
|
||||
if (key.downArrow && sel < items.length - 1) setSel(s => s + 1)
|
||||
if (key.return && items[sel]) onSelect(items[sel]!.id)
|
||||
|
||||
const n = parseInt(ch)
|
||||
if (n >= 1 && n <= Math.min(9, items.length)) onSelect(items[n - 1]!.id)
|
||||
})
|
||||
|
||||
if (loading) return <Text color={t.color.dim}>loading sessions…</Text>
|
||||
|
||||
if (!items.length) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color={t.color.dim}>no previous sessions</Text>
|
||||
<Text color={t.color.dim}>Esc to cancel</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const off = Math.max(0, Math.min(sel - Math.floor(VISIBLE / 2), items.length - VISIBLE))
|
||||
const visible = items.slice(off, off + VISIBLE)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={t.color.amber}>Resume Session</Text>
|
||||
{off > 0 && <Text color={t.color.dim}> ↑ {off} more</Text>}
|
||||
{visible.map((s, vi) => {
|
||||
const i = off + vi
|
||||
return (
|
||||
<Text key={s.id}>
|
||||
<Text color={sel === i ? t.color.label : t.color.dim}>{sel === i ? '▸ ' : ' '}</Text>
|
||||
<Text color={sel === i ? t.color.cornsilk : t.color.dim}>
|
||||
{i + 1}. {s.title || s.preview || s.id.slice(0, 8)}
|
||||
</Text>
|
||||
<Text color={t.color.dim}>
|
||||
{' '}({s.message_count} msgs, {age(s.started_at)})
|
||||
</Text>
|
||||
</Text>
|
||||
)
|
||||
})}
|
||||
{off + VISIBLE < items.length && <Text color={t.color.dim}> ↓ {items.length - off - VISIBLE} more</Text>}
|
||||
<Text color={t.color.dim}>↑/↓ select · Enter resume · 1-9 quick · Esc cancel</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { pick } from '../lib/text.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
import type { ActiveTool } from '../types.js'
|
||||
|
||||
export function Thinking({ reasoning, t, tools }: { reasoning: string; t: Theme; tools: ActiveTool[] }) {
|
||||
export function Thinking({ reasoning, t, thinking, tools }: { reasoning: string; t: Theme; thinking?: string; tools: ActiveTool[] }) {
|
||||
const [frame, setFrame] = useState(0)
|
||||
const [verb] = useState(() => pick(VERBS))
|
||||
const [face] = useState(() => pick(FACES))
|
||||
@@ -30,10 +30,10 @@ export function Thinking({ reasoning, t, tools }: { reasoning: string; t: Theme;
|
||||
{SPINNER[frame]} {face} {verb}…
|
||||
</Text>
|
||||
)}
|
||||
{reasoning && (
|
||||
{(reasoning || thinking) && (
|
||||
<Text color={t.color.dim} dimColor wrap="truncate-end">
|
||||
{' 💭 '}
|
||||
{reasoning.slice(-120).replace(/\n/g, ' ')}
|
||||
{(reasoning || thinking || '').slice(-120).replace(/\n/g, ' ')}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user