feat: add inline token count etc and fix venv
This commit is contained in:
@@ -1,84 +1,19 @@
|
||||
import { AlternateScreen, Box, NoSelect, ScrollBox, type ScrollBoxHandle, Text } from '@hermes/ink'
|
||||
import { AlternateScreen, Box, NoSelect, ScrollBox, Text } from '@hermes/ink'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import type { RefObject } from 'react'
|
||||
|
||||
import { PLACEHOLDER } from '../app/constants.js'
|
||||
import type { CompletionItem, TranscriptRow, VirtualHistoryState } from '../app/interfaces.js'
|
||||
import type { AppLayoutProps } from '../app/interfaces.js'
|
||||
import { $isBlocked } from '../app/overlayStore.js'
|
||||
import { $uiState } from '../app/uiStore.js'
|
||||
import type { ActiveTool, ActivityItem, Msg } from '../types.js'
|
||||
|
||||
import { StatusRule, StickyPromptTracker, TranscriptScrollbar } from './appChrome.js'
|
||||
import { AppOverlays } from './appOverlays.js'
|
||||
import { Banner, Panel, SessionPanel } from './branding.js'
|
||||
import { MessageLine } from './messageLine.js'
|
||||
import { QueuedMessages } from './queuedMessages.js'
|
||||
import type { PasteEvent } from './textInput.js'
|
||||
import { TextInput } from './textInput.js'
|
||||
import { ToolTrail } from './thinking.js'
|
||||
|
||||
export interface AppLayoutActions {
|
||||
answerApproval: (choice: string) => void
|
||||
answerClarify: (answer: string) => void
|
||||
answerSecret: (value: string) => void
|
||||
answerSudo: (pw: string) => void
|
||||
onModelSelect: (value: string) => void
|
||||
resumeById: (id: string) => void
|
||||
setStickyPrompt: (value: string) => void
|
||||
}
|
||||
|
||||
export interface AppLayoutComposerProps {
|
||||
cols: number
|
||||
compIdx: number
|
||||
completions: CompletionItem[]
|
||||
empty: boolean
|
||||
handleTextPaste: (event: PasteEvent) => { cursor: number; value: string } | null
|
||||
input: string
|
||||
inputBuf: string[]
|
||||
pagerPageSize: number
|
||||
queueEditIdx: number | null
|
||||
queuedDisplay: string[]
|
||||
submit: (value: string) => void
|
||||
updateInput: (next: string) => void
|
||||
}
|
||||
|
||||
export interface AppLayoutProgressProps {
|
||||
activity: ActivityItem[]
|
||||
reasoning: string
|
||||
reasoningActive: boolean
|
||||
reasoningStreaming: boolean
|
||||
showProgressArea: boolean
|
||||
showStreamingArea: boolean
|
||||
streaming: string
|
||||
tools: ActiveTool[]
|
||||
turnTrail: string[]
|
||||
}
|
||||
|
||||
export interface AppLayoutStatusProps {
|
||||
cwdLabel: string
|
||||
durationLabel: string
|
||||
showStickyPrompt: boolean
|
||||
statusColor: string
|
||||
stickyPrompt: string
|
||||
voiceLabel: string
|
||||
}
|
||||
|
||||
export interface AppLayoutTranscriptProps {
|
||||
historyItems: Msg[]
|
||||
scrollRef: RefObject<ScrollBoxHandle | null>
|
||||
virtualHistory: VirtualHistoryState
|
||||
virtualRows: TranscriptRow[]
|
||||
}
|
||||
|
||||
export interface AppLayoutProps {
|
||||
actions: AppLayoutActions
|
||||
composer: AppLayoutComposerProps
|
||||
mouseTracking: boolean
|
||||
progress: AppLayoutProgressProps
|
||||
status: AppLayoutStatusProps
|
||||
transcript: AppLayoutTranscriptProps
|
||||
}
|
||||
|
||||
export function AppLayout({ actions, composer, mouseTracking, progress, status, transcript }: AppLayoutProps) {
|
||||
const ui = useStore($uiState)
|
||||
const isBlocked = useStore($isBlocked)
|
||||
@@ -125,8 +60,10 @@ export function AppLayout({ actions, composer, mouseTracking, progress, status,
|
||||
reasoning={progress.reasoning}
|
||||
reasoningActive={progress.reasoningActive}
|
||||
reasoningStreaming={progress.reasoningStreaming}
|
||||
reasoningTokens={progress.reasoningTokens}
|
||||
t={ui.theme}
|
||||
tools={progress.tools}
|
||||
toolTokens={progress.toolTokens}
|
||||
trail={progress.turnTrail}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Box, Text } from '@hermes/ink'
|
||||
import { useStore } from '@nanostores/react'
|
||||
|
||||
import { useGateway } from '../app/gatewayContext.js'
|
||||
import type { CompletionItem } from '../app/interfaces.js'
|
||||
import type { AppOverlaysProps } from '../app/interfaces.js'
|
||||
import { $overlayState, patchOverlayState } from '../app/overlayStore.js'
|
||||
import { $uiState } from '../app/uiStore.js'
|
||||
|
||||
@@ -12,19 +12,6 @@ import { ModelPicker } from './modelPicker.js'
|
||||
import { ApprovalPrompt, ClarifyPrompt } from './prompts.js'
|
||||
import { SessionPicker } from './sessionPicker.js'
|
||||
|
||||
export interface AppOverlaysProps {
|
||||
cols: number
|
||||
compIdx: number
|
||||
completions: CompletionItem[]
|
||||
onApprovalChoice: (choice: string) => void
|
||||
onClarifyAnswer: (value: string) => void
|
||||
onModelSelect: (value: string) => void
|
||||
onPickerSelect: (sessionId: string) => void
|
||||
onSecretSubmit: (value: string) => void
|
||||
onSudoSubmit: (pw: string) => void
|
||||
pagerPageSize: number
|
||||
}
|
||||
|
||||
export function AppOverlays({
|
||||
cols,
|
||||
compIdx,
|
||||
|
||||
@@ -85,7 +85,14 @@ export const MessageLine = memo(function MessageLine({
|
||||
>
|
||||
{showDetails && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<ToolTrail detailsMode={detailsMode} reasoning={thinking} t={t} trail={msg.tools} />
|
||||
<ToolTrail
|
||||
detailsMode={detailsMode}
|
||||
reasoning={thinking}
|
||||
reasoningTokens={msg.thinkingTokens}
|
||||
t={t}
|
||||
toolTokens={msg.toolTokens}
|
||||
trail={msg.tools}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Box, Text } from '@hermes/ink'
|
||||
import { memo, type ReactNode, useEffect, useState } from 'react'
|
||||
import { memo, type ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
import spinners, { type BrailleSpinnerName } from 'unicode-animations'
|
||||
|
||||
import { FACES, VERBS } from '../constants.js'
|
||||
import {
|
||||
estimateTokensRough,
|
||||
fmtK,
|
||||
formatToolCall,
|
||||
parseToolTrailResultLine,
|
||||
pick,
|
||||
@@ -89,6 +90,7 @@ function Chevron({
|
||||
count,
|
||||
onClick,
|
||||
open,
|
||||
suffix,
|
||||
t,
|
||||
title,
|
||||
tone = 'dim'
|
||||
@@ -96,6 +98,7 @@ function Chevron({
|
||||
count?: number
|
||||
onClick: () => void
|
||||
open: boolean
|
||||
suffix?: string
|
||||
t: Theme
|
||||
title: string
|
||||
tone?: 'dim' | 'error' | 'warn'
|
||||
@@ -108,6 +111,12 @@ function Chevron({
|
||||
<Text color={t.color.amber}>{open ? '▾ ' : '▸ '}</Text>
|
||||
{title}
|
||||
{typeof count === 'number' ? ` (${count})` : ''}
|
||||
{suffix ? (
|
||||
<Text color={t.color.statusFg} dimColor>
|
||||
{' '}
|
||||
{suffix}
|
||||
</Text>
|
||||
) : null}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
@@ -128,29 +137,35 @@ export const Thinking = memo(function Thinking({
|
||||
streaming?: boolean
|
||||
t: Theme
|
||||
}) {
|
||||
const [tick, setTick] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setTick(v => v + 1), 1100)
|
||||
|
||||
return () => clearInterval(id)
|
||||
}, [])
|
||||
|
||||
const preview = thinkingPreview(reasoning, mode, THINKING_COT_MAX)
|
||||
const lines = useMemo(() => preview.split('\n').map(line => line.replace(/\t/g, ' ')), [preview])
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color={t.color.dim}>
|
||||
<Spinner color={t.color.dim} /> {FACES[tick % FACES.length] ?? '(•_•)'}{' '}
|
||||
{VERBS[tick % VERBS.length] ?? 'thinking'}…
|
||||
</Text>
|
||||
|
||||
{preview ? (
|
||||
<Text color={t.color.dim} dimColor wrap={mode === 'full' ? 'wrap-trim' : 'truncate-end'}>
|
||||
<Text dimColor>└ </Text>
|
||||
{preview}
|
||||
<StreamCursor color={t.color.dim} dimColor streaming={streaming} visible={active} />
|
||||
</Text>
|
||||
mode === 'full' ? (
|
||||
<Box flexDirection="row">
|
||||
<Text color={t.color.dim} dimColor>
|
||||
└{' '}
|
||||
</Text>
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
{lines.map((line, index) => (
|
||||
<Text color={t.color.dim} dimColor key={index} wrap="wrap-trim">
|
||||
{line || ' '}
|
||||
{index === lines.length - 1 ? (
|
||||
<StreamCursor color={t.color.dim} dimColor streaming={streaming} visible={active} />
|
||||
) : null}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Text color={t.color.dim} dimColor wrap="truncate-end">
|
||||
<Text dimColor>└ </Text>
|
||||
{preview}
|
||||
<StreamCursor color={t.color.dim} dimColor streaming={streaming} visible={active} />
|
||||
</Text>
|
||||
)
|
||||
) : active ? (
|
||||
<Text color={t.color.dim} dimColor>
|
||||
<Text dimColor>└ </Text>
|
||||
@@ -175,9 +190,11 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
detailsMode = 'collapsed',
|
||||
reasoningActive = false,
|
||||
reasoning = '',
|
||||
reasoningTokens,
|
||||
reasoningStreaming = false,
|
||||
t,
|
||||
tools = [],
|
||||
toolTokens,
|
||||
trail = [],
|
||||
activity = []
|
||||
}: {
|
||||
@@ -185,9 +202,11 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
detailsMode?: DetailsMode
|
||||
reasoningActive?: boolean
|
||||
reasoning?: string
|
||||
reasoningTokens?: number
|
||||
reasoningStreaming?: boolean
|
||||
t: Theme
|
||||
tools?: ActiveTool[]
|
||||
toolTokens?: number
|
||||
trail?: string[]
|
||||
activity?: ActivityItem[]
|
||||
}) {
|
||||
@@ -311,6 +330,17 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
const hasTools = groups.length > 0
|
||||
const hasMeta = meta.length > 0
|
||||
const hasThinking = !!cot || reasoningActive || (busy && !hasTools)
|
||||
const thinkingLive = reasoningActive || reasoningStreaming
|
||||
|
||||
const tokenCount = reasoningTokens !== undefined ? reasoningTokens : reasoning ? estimateTokensRough(reasoning) : 0
|
||||
|
||||
const toolTokenCount = toolTokens ?? 0
|
||||
const totalTokenCount = tokenCount + toolTokenCount
|
||||
const thinkingTokensLabel = tokenCount > 0 ? `~${fmtK(tokenCount)} tokens` : null
|
||||
|
||||
const toolTokensLabel = toolTokens !== undefined && toolTokens > 0 ? `~${fmtK(toolTokens)} tokens` : undefined
|
||||
|
||||
const totalTokensLabel = tokenCount > 0 && toolTokenCount > 0 ? `~${fmtK(totalTokenCount)} total` : null
|
||||
|
||||
// ── Hidden: errors/warnings only ──────────────────────────────
|
||||
|
||||
@@ -368,6 +398,13 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
))
|
||||
: null
|
||||
|
||||
const totalBlock = totalTokensLabel ? (
|
||||
<Text color={t.color.statusFg} dimColor>
|
||||
<Text color={t.color.amber}>Σ </Text>
|
||||
{totalTokensLabel}
|
||||
</Text>
|
||||
) : null
|
||||
|
||||
// ── Expanded: flat, no accordions ──────────────────────────────
|
||||
|
||||
if (detailsMode === 'expanded') {
|
||||
@@ -376,6 +413,7 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
{thinkingBlock}
|
||||
{toolBlock}
|
||||
{metaBlock}
|
||||
{totalBlock}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -392,7 +430,20 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
<Box flexDirection="column">
|
||||
{hasThinking && (
|
||||
<>
|
||||
<Chevron onClick={() => setOpenThinking(v => !v)} open={openThinking} t={t} title="Thinking" />
|
||||
<Box onClick={() => setOpenThinking(v => !v)}>
|
||||
<Text color={t.color.dim} dimColor={!thinkingLive}>
|
||||
<Text color={t.color.amber}>{openThinking ? '▾ ' : '▸ '}</Text>
|
||||
<Text bold={thinkingLive} color={thinkingLive ? t.color.cornsilk : t.color.dim} dimColor={!thinkingLive}>
|
||||
Thinking
|
||||
</Text>
|
||||
{thinkingTokensLabel ? (
|
||||
<Text color={t.color.statusFg} dimColor>
|
||||
{' '}
|
||||
{thinkingTokensLabel}
|
||||
</Text>
|
||||
) : null}
|
||||
</Text>
|
||||
</Box>
|
||||
{openThinking && thinkingBlock}
|
||||
</>
|
||||
)}
|
||||
@@ -403,6 +454,7 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
count={groups.length}
|
||||
onClick={() => setOpenTools(v => !v)}
|
||||
open={openTools}
|
||||
suffix={toolTokensLabel}
|
||||
t={t}
|
||||
title="Tool calls"
|
||||
/>
|
||||
@@ -423,6 +475,8 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
{openMeta && metaBlock}
|
||||
</>
|
||||
)}
|
||||
|
||||
{totalBlock}
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user