mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 00:03:01 +03:00
refactor(tui): modernize active status animation
This commit is contained in:
+17
-2
@@ -1030,8 +1030,22 @@ describe("NanobotTui layout", () => {
|
||||
await setup.renderOnce()
|
||||
let frame = setup.captureCharFrame()
|
||||
|
||||
expect(frame).toMatch(/[◐◓◑◒] Thinking\s+0s/u)
|
||||
expect(frame).toMatch(/Thinking\s+0s/u)
|
||||
expect(frame).not.toMatch(/[◐◓◑◒⠋⠙⠹⠸]/u)
|
||||
expect(frame).not.toContain("hidden reasoning")
|
||||
const status = (app as unknown as {
|
||||
status: {
|
||||
content: { chunks: Array<{ fg?: { toInts(): number[] } }> }
|
||||
plainText: string
|
||||
}
|
||||
}).status
|
||||
expect(status.plainText).toMatch(/^Thinking\s+0s/u)
|
||||
const shimmerColors = new Set(
|
||||
status.content.chunks
|
||||
.slice(0, "Thinking".length)
|
||||
.map((chunk) => chunk.fg?.toInts().join(",")),
|
||||
)
|
||||
expect(shimmerColors.size).toBeGreaterThan(1)
|
||||
|
||||
app.accept({
|
||||
event: "message",
|
||||
@@ -1044,7 +1058,8 @@ describe("NanobotTui layout", () => {
|
||||
await setup.renderOnce()
|
||||
frame = setup.captureCharFrame()
|
||||
|
||||
expect(frame).toMatch(/[◐◓◑◒] Working\s+0s/u)
|
||||
expect(frame).toMatch(/Working\s+0s/u)
|
||||
expect(frame).not.toMatch(/[◐◓◑◒⠋⠙⠹⠸]/u)
|
||||
expect(frame).toContain("› exec")
|
||||
app.accept({ event: "turn_end", chat_id: "chat" })
|
||||
})
|
||||
|
||||
+43
-4
@@ -2,6 +2,7 @@ import {
|
||||
BoxRenderable,
|
||||
CliRenderEvents,
|
||||
RGBA,
|
||||
StyledText,
|
||||
SyntaxStyle,
|
||||
TextareaRenderable,
|
||||
TextRenderable,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
type CliRenderer,
|
||||
type KeyEvent,
|
||||
type PasteEvent,
|
||||
type TextChunk,
|
||||
type ThemeMode,
|
||||
type TreeSitterClient,
|
||||
} from "@opentui/core"
|
||||
@@ -122,6 +124,9 @@ const LIGHT: Palette = {
|
||||
}
|
||||
|
||||
const COMPOSER_PLACEHOLDER = "Ask nanobot anything"
|
||||
const SHIMMER_PAUSE = 16
|
||||
const SHIMMER_BAND = 4
|
||||
const SHIMMER_INTERVAL_MS = 80
|
||||
const LOCAL_COMMANDS: TuiCommand[] = [
|
||||
{
|
||||
command: "/sessions",
|
||||
@@ -219,6 +224,37 @@ function diffViewerTheme(palette: Palette, backgroundKnown: boolean): DiffViewer
|
||||
}
|
||||
}
|
||||
|
||||
function shimmerStatus(
|
||||
label: string,
|
||||
suffix: string,
|
||||
frame: number,
|
||||
palette: Palette,
|
||||
): StyledText {
|
||||
const chars = Array.from(label)
|
||||
const base = RGBA.fromHex(palette.muted).toInts()
|
||||
const highlight = RGBA.fromHex(palette.text).toInts()
|
||||
// Sweep immediately, then leave a quiet pause before repeating. The text
|
||||
// keeps a constant width throughout, so the footer never jitters.
|
||||
const position = frame % (chars.length + SHIMMER_PAUSE)
|
||||
const chunks: TextChunk[] = chars.map((text, index) => {
|
||||
const distance = Math.abs(index - position)
|
||||
const intensity = distance > SHIMMER_BAND
|
||||
? 0
|
||||
: (1 + Math.cos(Math.PI * distance / SHIMMER_BAND)) / 2
|
||||
return {
|
||||
__isChunk: true,
|
||||
text,
|
||||
fg: RGBA.fromInts(
|
||||
Math.round(base[0] + (highlight[0] - base[0]) * intensity),
|
||||
Math.round(base[1] + (highlight[1] - base[1]) * intensity),
|
||||
Math.round(base[2] + (highlight[2] - base[2]) * intensity),
|
||||
),
|
||||
}
|
||||
})
|
||||
chunks.push({ __isChunk: true, text: suffix, fg: RGBA.fromHex(palette.muted) })
|
||||
return new StyledText(chunks)
|
||||
}
|
||||
|
||||
function formatElapsed(milliseconds: number): string {
|
||||
const seconds = Math.max(0, Math.floor(milliseconds / 1000))
|
||||
if (seconds < 60) return `${seconds}s`
|
||||
@@ -800,7 +836,7 @@ export class NanobotTui {
|
||||
this.shimmerTimer = setInterval(() => {
|
||||
this.shimmerFrame += 1
|
||||
this.renderActiveStatus()
|
||||
}, 120)
|
||||
}, SHIMMER_INTERVAL_MS)
|
||||
return
|
||||
}
|
||||
if (this.shimmerTimer) clearInterval(this.shimmerTimer)
|
||||
@@ -810,14 +846,17 @@ export class NanobotTui {
|
||||
}
|
||||
|
||||
private renderActiveStatus(): void {
|
||||
const frames = ["◐", "◓", "◑", "◒"]
|
||||
const frame = frames[this.shimmerFrame % frames.length]
|
||||
const elapsed = formatElapsed(Date.now() - this.activeStartedAt)
|
||||
const progress = this.lastProgress
|
||||
? ` · ${this.lastProgress.replace(/^\s*[·›✓×]\s*/u, "")}`
|
||||
: ""
|
||||
const navigation = this.transcriptNavigation.awayFromBottom ? " · Ctrl+End latest" : ""
|
||||
this.status.content = `${frame} ${this.activeLabel} ${elapsed}${progress}${navigation}`
|
||||
this.status.content = shimmerStatus(
|
||||
this.activeLabel,
|
||||
` ${elapsed}${progress}${navigation}`,
|
||||
this.shimmerFrame,
|
||||
this.palette,
|
||||
)
|
||||
}
|
||||
|
||||
private readyStatus(detail = ""): string {
|
||||
|
||||
Reference in New Issue
Block a user