diff --git a/ui-tui/src/app.tsx b/ui-tui/src/app.tsx index 06399ba7ad..88dcf84e64 100644 --- a/ui-tui/src/app.tsx +++ b/ui-tui/src/app.tsx @@ -709,7 +709,10 @@ export function App({ gw }: { gw: GatewayClient }) { historyDraftRef.current = '' } - if (full.startsWith('/') && slashRef.current(full)) { + if (full.startsWith('/')) { + appendMessage({ role: 'system', text: full, kind: 'slash' }) + pushHistory(full) + slashRef.current(full) clearInput() return @@ -793,7 +796,7 @@ export function App({ gw }: { gw: GatewayClient }) { send(full) }, // eslint-disable-next-line react-hooks/exhaustive-deps - [busy, enqueue, gw, listPasteIds, pastes, resolvePasteTokens, sid] + [appendMessage, busy, enqueue, gw, listPasteIds, pastes, pushHistory, resolvePasteTokens, sid] ) // ── Input handling ─────────────────────────────────────────────── diff --git a/ui-tui/src/components/messageLine.tsx b/ui-tui/src/components/messageLine.tsx index 91d1fe8c33..8b8b30894b 100644 --- a/ui-tui/src/components/messageLine.tsx +++ b/ui-tui/src/components/messageLine.tsx @@ -31,6 +31,10 @@ export const MessageLine = memo(function MessageLine({ const { body, glyph, prefix } = ROLE[msg.role](t) const content = (() => { + if (msg.kind === 'slash') { + return {msg.text} + } + if (msg.role === 'assistant') { return hasAnsi(msg.text) ? {msg.text} : } @@ -41,9 +45,11 @@ export const MessageLine = memo(function MessageLine({ return ( {head} + [long message] + {rest.join('')} ) @@ -53,7 +59,7 @@ export const MessageLine = memo(function MessageLine({ })() return ( - + {msg.thinking && ( 💭 {msg.thinking.replace(/\n/g, ' ').slice(0, 200)} diff --git a/ui-tui/src/components/thinking.tsx b/ui-tui/src/components/thinking.tsx index 5dbcfdab47..f4f5130eec 100644 --- a/ui-tui/src/components/thinking.tsx +++ b/ui-tui/src/components/thinking.tsx @@ -3,15 +3,21 @@ import { memo, useEffect, useState } from 'react' import spinners, { type BrailleSpinnerName } from 'unicode-animations' import { FACES, TOOL_VERBS, VERBS } from '../constants.js' -import { isToolTrailResultLine, lastCotTrailIndex } from '../lib/text.js' +import { + isToolTrailResultLine, + lastCotTrailIndex, + pick, + scaleHex, + THINKING_COT_FADE, + THINKING_COT_MAX, + thinkingCotTail +} from '../lib/text.js' import type { Theme } from '../theme.js' import type { ActiveTool, ActivityItem } from '../types.js' const THINK: BrailleSpinnerName[] = ['helix', 'breathe', 'orbit', 'dna', 'waverows', 'snake', 'pulse'] const TOOL: BrailleSpinnerName[] = ['cascade', 'scan', 'diagswipe', 'fillsweep', 'rain', 'columns', 'sparkle'] -const pick = (a: T[]) => a[Math.floor(Math.random() * a.length)]! - const tone = (item: ActivityItem, t: Theme) => item.tone === 'error' ? t.color.error : item.tone === 'warn' ? t.color.warn : t.color.dim @@ -128,7 +134,8 @@ export const Thinking = memo(function Thinking({ reasoning, t }: { reasoning: st return () => clearInterval(id) }, []) - const tail = reasoning.slice(-160).replace(/\n/g, ' ') + const tail = thinkingCotTail(reasoning) + const clipped = reasoning.length > THINKING_COT_MAX return ( <> @@ -138,8 +145,17 @@ export const Thinking = memo(function Thinking({ reasoning, t }: { reasoning: st {tail ? ( - - 💭 {tail} + + {clipped && + Array.from({ length: Math.min(THINKING_COT_FADE, tail.length) }, (_, i) => ( + + {tail[i]} + + ))} + + + {clipped ? tail.slice(THINKING_COT_FADE) : tail} + ) : null} diff --git a/ui-tui/src/lib/text.ts b/ui-tui/src/lib/text.ts index ddb6f9fdd4..7f835c0cd4 100644 --- a/ui-tui/src/lib/text.ts +++ b/ui-tui/src/lib/text.ts @@ -53,6 +53,23 @@ export const lastCotTrailIndex = (trail: readonly string[]) => { return -1 } +export const THINKING_COT_MAX = 160 +export const THINKING_COT_FADE = 5 + +export const thinkingCotTail = (reasoning: string) => reasoning.replace(/\n/g, ' ').slice(-THINKING_COT_MAX) + +/** Scale #RRGGBB by k ∈ [0,1] — used for left-edge fade toward terminal bg. */ +export const scaleHex = (hex: string, k: number) => { + const h = hex.replace('#', '') + + const ch = (o: number) => + Math.round(parseInt(h.slice(o, o + 2), 16) * k) + .toString(16) + .padStart(2, '0') + + return `#${ch(0)}${ch(2)}${ch(4)}` +} + export const estimateRows = (text: string, w: number, compact = false) => { let inCode = false let rows = 0 diff --git a/ui-tui/src/types.ts b/ui-tui/src/types.ts index 3254c2674a..1cfa035403 100644 --- a/ui-tui/src/types.ts +++ b/ui-tui/src/types.ts @@ -24,7 +24,7 @@ export interface ClarifyReq { export interface Msg { role: Role text: string - kind?: 'intro' + kind?: 'intro' | 'slash' info?: SessionInfo thinking?: string tools?: string[]