mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
fix(tui): apply ui-tui fix pass and restore type-check
- run the requested ui-tui lint+format pass and include resulting formatting updates - guard text-measure cache eviction key in hermes-ink so ui-tui type-check stays green
This commit is contained in:
@@ -53,7 +53,11 @@ export function AlternateScreen(t0: Props) {
|
||||
}
|
||||
|
||||
writeRaw(
|
||||
ENTER_ALT_SCREEN + ERASE_SCROLLBACK + ERASE_SCREEN + CURSOR_HOME + (mouseTracking ? ENABLE_MOUSE_TRACKING : DISABLE_MOUSE_TRACKING)
|
||||
ENTER_ALT_SCREEN +
|
||||
ERASE_SCROLLBACK +
|
||||
ERASE_SCREEN +
|
||||
CURSOR_HOME +
|
||||
(mouseTracking ? ENABLE_MOUSE_TRACKING : DISABLE_MOUSE_TRACKING)
|
||||
)
|
||||
ink?.setAltScreenActive(true, mouseTracking)
|
||||
|
||||
|
||||
@@ -323,27 +323,38 @@ const measureTextNode = function (
|
||||
widthMode: LayoutMeasureMode
|
||||
): { width: number; height: number } {
|
||||
const elem = node.nodeName !== '#text' ? (node as DOMElement) : node.parentNode
|
||||
|
||||
if (elem && elem.nodeName === 'ink-text') {
|
||||
let cache = elem._textMeasureCache
|
||||
|
||||
if (!cache) {
|
||||
cache = { gen: 0, entries: new Map() }
|
||||
elem._textMeasureCache = cache
|
||||
}
|
||||
|
||||
const key = `${width}|${widthMode}`
|
||||
const hit = cache.entries.get(key)
|
||||
|
||||
if (hit && hit._gen === cache.gen) {
|
||||
return hit.result
|
||||
}
|
||||
|
||||
const result = computeTextMeasure(node, width, widthMode)
|
||||
|
||||
// Enforce cap with FIFO eviction to avoid unbounded growth during
|
||||
// pathological frames where yoga probes many widths.
|
||||
if (cache.entries.size >= MEASURE_CACHE_CAP) {
|
||||
const firstKey = cache.entries.keys().next().value
|
||||
cache.entries.delete(firstKey)
|
||||
if (firstKey !== undefined) {
|
||||
cache.entries.delete(firstKey)
|
||||
}
|
||||
}
|
||||
|
||||
cache.entries.set(key, { _gen: cache.gen, result })
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
return computeTextMeasure(node, width, widthMode)
|
||||
}
|
||||
|
||||
@@ -475,6 +486,7 @@ export const clearYogaNodeReferences = (node: DOMElement | TextNode): void => {
|
||||
for (const child of node.childNodes) {
|
||||
clearYogaNodeReferences(child)
|
||||
}
|
||||
|
||||
node._textMeasureCache = undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -9,18 +9,21 @@ describe('shouldEmitClipboardSequence', () => {
|
||||
})
|
||||
|
||||
it('keeps OSC enabled for remote or plain local terminals', () => {
|
||||
expect(shouldEmitClipboardSequence({ SSH_CONNECTION: '1', TMUX: '/tmp/tmux-1/default,1,0' } as NodeJS.ProcessEnv)).toBe(
|
||||
true
|
||||
)
|
||||
expect(
|
||||
shouldEmitClipboardSequence({ SSH_CONNECTION: '1', TMUX: '/tmp/tmux-1/default,1,0' } as NodeJS.ProcessEnv)
|
||||
).toBe(true)
|
||||
expect(shouldEmitClipboardSequence({ TERM: 'xterm-256color' } as NodeJS.ProcessEnv)).toBe(true)
|
||||
})
|
||||
|
||||
it('honors explicit env override', () => {
|
||||
expect(shouldEmitClipboardSequence({ HERMES_TUI_CLIPBOARD_OSC52: '1', TMUX: '/tmp/tmux-1/default,1,0' } as NodeJS.ProcessEnv)).toBe(
|
||||
true
|
||||
)
|
||||
expect(shouldEmitClipboardSequence({ HERMES_TUI_COPY_OSC52: '0', TERM: 'xterm-256color' } as NodeJS.ProcessEnv)).toBe(
|
||||
false
|
||||
)
|
||||
expect(
|
||||
shouldEmitClipboardSequence({
|
||||
HERMES_TUI_CLIPBOARD_OSC52: '1',
|
||||
TMUX: '/tmp/tmux-1/default,1,0'
|
||||
} as NodeJS.ProcessEnv)
|
||||
).toBe(true)
|
||||
expect(
|
||||
shouldEmitClipboardSequence({ HERMES_TUI_COPY_OSC52: '0', TERM: 'xterm-256color' } as NodeJS.ProcessEnv)
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -226,7 +226,10 @@ describe('createGatewayEventHandler', () => {
|
||||
const inlineDiff = '--- a/foo.ts\n+++ b/foo.ts\n@@\n-old\n+new'
|
||||
const assistantText = 'Done. Clean swap:\n\n```diff\n-old\n+new\n```'
|
||||
|
||||
onEvent({ payload: { inline_diff: inlineDiff, summary: 'patched', tool_id: 'tool-1' }, type: 'tool.complete' } as any)
|
||||
onEvent({
|
||||
payload: { inline_diff: inlineDiff, summary: 'patched', tool_id: 'tool-1' },
|
||||
type: 'tool.complete'
|
||||
} as any)
|
||||
onEvent({ payload: { text: assistantText }, type: 'message.complete' } as any)
|
||||
|
||||
expect(appended).toHaveLength(1)
|
||||
|
||||
@@ -126,9 +126,7 @@ describe('createSlashHandler', () => {
|
||||
const ctx = buildCtx()
|
||||
createSlashHandler(ctx)('/details tools blink')
|
||||
expect(getUiState().sections.tools).toBeUndefined()
|
||||
expect(ctx.transcript.sys).toHaveBeenCalledWith(
|
||||
'usage: /details <section> [hidden|collapsed|expanded|reset]'
|
||||
)
|
||||
expect(ctx.transcript.sys).toHaveBeenCalledWith('usage: /details <section> [hidden|collapsed|expanded|reset]')
|
||||
})
|
||||
|
||||
it('shows tool enable usage when names are missing', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { isSectionName, parseDetailsMode, resolveSections, sectionMode, SECTION_NAMES } from '../domain/details.js'
|
||||
import { isSectionName, parseDetailsMode, resolveSections, SECTION_NAMES, sectionMode } from '../domain/details.js'
|
||||
|
||||
describe('parseDetailsMode', () => {
|
||||
it('accepts the canonical modes case-insensitively', () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useStore } from '@nanostores/react'
|
||||
|
||||
import { GatewayProvider } from './app/gatewayContext.js'
|
||||
import { useMainApp } from './app/useMainApp.js'
|
||||
import { $uiState } from './app/uiStore.js'
|
||||
import { useMainApp } from './app/useMainApp.js'
|
||||
import { AppLayout } from './components/appLayout.js'
|
||||
import type { GatewayClient } from './gatewayClient.js'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NO_CONFIRM_DESTRUCTIVE } from '../../../config/env.js'
|
||||
import { dailyFortune, randomFortune } from '../../../content/fortunes.js'
|
||||
import { HOTKEYS } from '../../../content/hotkeys.js'
|
||||
import { SECTION_NAMES, isSectionName, nextDetailsMode, parseDetailsMode } from '../../../domain/details.js'
|
||||
import { isSectionName, nextDetailsMode, parseDetailsMode, SECTION_NAMES } from '../../../domain/details.js'
|
||||
import type {
|
||||
ConfigGetValueResponse,
|
||||
ConfigSetResponse,
|
||||
@@ -40,8 +40,10 @@ const flagFromArg = (arg: string, current: boolean): boolean | null => {
|
||||
|
||||
const RESET_WORDS = new Set(['reset', 'clear', 'default'])
|
||||
const CYCLE_WORDS = new Set(['cycle', 'toggle'])
|
||||
|
||||
const DETAILS_USAGE =
|
||||
'usage: /details [hidden|collapsed|expanded|cycle] or /details <section> [hidden|collapsed|expanded|reset]'
|
||||
|
||||
const DETAILS_SECTION_USAGE = 'usage: /details <section> [hidden|collapsed|expanded|reset]'
|
||||
|
||||
export const coreCommands: SlashCommand[] = [
|
||||
@@ -97,9 +99,7 @@ export const coreCommands: SlashCommand[] = [
|
||||
}
|
||||
|
||||
patchUiState({ mouseTracking: next })
|
||||
ctx.gateway
|
||||
.rpc<ConfigSetResponse>('config.set', { key: 'mouse', value: next ? 'on' : 'off' })
|
||||
.catch(() => {})
|
||||
ctx.gateway.rpc<ConfigSetResponse>('config.set', { key: 'mouse', value: next ? 'on' : 'off' }).catch(() => {})
|
||||
|
||||
queueMicrotask(() => ctx.transcript.sys(`mouse tracking ${next ? 'on' : 'off'}`))
|
||||
}
|
||||
@@ -178,7 +178,9 @@ export const coreCommands: SlashCommand[] = [
|
||||
gateway
|
||||
.rpc<ConfigGetValueResponse>('config.get', { key: 'details_mode' })
|
||||
.then(r => {
|
||||
if (ctx.stale()) return
|
||||
if (ctx.stale()) {
|
||||
return
|
||||
}
|
||||
|
||||
const mode = parseDetailsMode(r?.value) ?? ui.detailsMode
|
||||
patchUiState({ detailsMode: mode })
|
||||
|
||||
@@ -300,6 +300,7 @@ class TurnController {
|
||||
|
||||
const hasDiffSegment = segments.some(msg => msg.kind === 'diff')
|
||||
const detailsBelongBeforeDiff = hasDiffSegment && (tools.length > 0 || Boolean(savedReasoning))
|
||||
|
||||
const finalMessages = detailsBelongBeforeDiff
|
||||
? insertBeforeFirstDiff(segments, {
|
||||
kind: 'trail',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { atom } from 'nanostores'
|
||||
|
||||
import { MOUSE_TRACKING } from '../config/env.js'
|
||||
import { ZERO } from '../domain/usage.js'
|
||||
import { DEFAULT_THEME } from '../theme.js'
|
||||
import { MOUSE_TRACKING } from '../config/env.js'
|
||||
|
||||
import type { UiState } from './interfaces.js'
|
||||
|
||||
|
||||
@@ -159,16 +159,14 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
||||
voice.setProcessing(false)
|
||||
}
|
||||
|
||||
gateway
|
||||
.rpc<VoiceRecordResponse>('voice.record', { action })
|
||||
.catch((e: Error) => {
|
||||
// Revert optimistic UI on failure.
|
||||
if (starting) {
|
||||
voice.setRecording(false)
|
||||
}
|
||||
gateway.rpc<VoiceRecordResponse>('voice.record', { action }).catch((e: Error) => {
|
||||
// Revert optimistic UI on failure.
|
||||
if (starting) {
|
||||
voice.setRecording(false)
|
||||
}
|
||||
|
||||
actions.sys(`voice error: ${e.message}`)
|
||||
})
|
||||
actions.sys(`voice error: ${e.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
useInput((ch, key) => {
|
||||
|
||||
@@ -640,14 +640,14 @@ export function useMainApp(gw: GatewayClient) {
|
||||
const showProgressArea = anyPanelVisible
|
||||
? Boolean(
|
||||
ui.busy ||
|
||||
turn.outcome ||
|
||||
turn.streamPendingTools.length ||
|
||||
turn.streamSegments.length ||
|
||||
turn.subagents.length ||
|
||||
turn.tools.length ||
|
||||
turn.turnTrail.length ||
|
||||
hasReasoning ||
|
||||
turn.activity.length
|
||||
turn.outcome ||
|
||||
turn.streamPendingTools.length ||
|
||||
turn.streamSegments.length ||
|
||||
turn.subagents.length ||
|
||||
turn.tools.length ||
|
||||
turn.turnTrail.length ||
|
||||
hasReasoning ||
|
||||
turn.activity.length
|
||||
)
|
||||
: turn.activity.some(item => item.tone !== 'info')
|
||||
|
||||
|
||||
@@ -218,11 +218,7 @@ export function StatusRule({
|
||||
{voiceLabel ? (
|
||||
<Text
|
||||
color={
|
||||
voiceLabel.startsWith('●')
|
||||
? t.color.error
|
||||
: voiceLabel.startsWith('◉')
|
||||
? t.color.warn
|
||||
: t.color.dim
|
||||
voiceLabel.startsWith('●') ? t.color.error : voiceLabel.startsWith('◉') ? t.color.warn : t.color.dim
|
||||
}
|
||||
>
|
||||
{' │ '}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Ansi, Box, NoSelect, Text } from '@hermes/ink'
|
||||
import { memo } from 'react'
|
||||
|
||||
import { sectionMode } from '../domain/details.js'
|
||||
import { LONG_MSG } from '../config/limits.js'
|
||||
import { sectionMode } from '../domain/details.js'
|
||||
import { userDisplay } from '../domain/messages.js'
|
||||
import { ROLE } from '../domain/roles.js'
|
||||
import { compactPreview, hasAnsi, isPasteBackedText, stripAnsi } from '../lib/text.js'
|
||||
@@ -72,8 +72,7 @@ export const MessageLine = memo(function MessageLine({
|
||||
const { body, glyph, prefix } = ROLE[msg.role](t)
|
||||
|
||||
const showDetails =
|
||||
(toolsMode !== 'hidden' && Boolean(msg.tools?.length)) ||
|
||||
(thinkingMode !== 'hidden' && Boolean(thinking))
|
||||
(toolsMode !== 'hidden' && Boolean(msg.tools?.length)) || (thinkingMode !== 'hidden' && Boolean(thinking))
|
||||
|
||||
const content = (() => {
|
||||
if (msg.kind === 'slash') {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, NoSelect, Text } from '@hermes/ink'
|
||||
import { memo, useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||
import { memo, type ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
import spinners, { type BrailleSpinnerName } from 'unicode-animations'
|
||||
|
||||
import { THINKING_COT_MAX } from '../config/limits.js'
|
||||
@@ -919,13 +919,22 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
// hidden sections stay hidden so the override is honoured.
|
||||
|
||||
const expandAll = () => {
|
||||
if (visible.thinking !== 'hidden') setOpenThinking(true)
|
||||
if (visible.tools !== 'hidden') setOpenTools(true)
|
||||
if (visible.thinking !== 'hidden') {
|
||||
setOpenThinking(true)
|
||||
}
|
||||
|
||||
if (visible.tools !== 'hidden') {
|
||||
setOpenTools(true)
|
||||
}
|
||||
|
||||
if (visible.subagents !== 'hidden') {
|
||||
setOpenSubagents(true)
|
||||
setDeepSubagents(true)
|
||||
}
|
||||
if (visible.activity !== 'hidden') setOpenMeta(true)
|
||||
|
||||
if (visible.activity !== 'hidden') {
|
||||
setOpenMeta(true)
|
||||
}
|
||||
}
|
||||
|
||||
const metaTone: 'dim' | 'error' | 'warn' = activity.some(i => i.tone === 'error')
|
||||
|
||||
@@ -43,7 +43,5 @@ export const isAction = (key: { ctrl: boolean; meta: boolean; super?: boolean },
|
||||
* accept Cmd+B (the platform action modifier) so existing macOS muscle memory
|
||||
* keeps working.
|
||||
*/
|
||||
export const isVoiceToggleKey = (
|
||||
key: { ctrl: boolean; meta: boolean; super?: boolean },
|
||||
ch: string
|
||||
): boolean => (key.ctrl || isActionMod(key)) && ch.toLowerCase() === 'b'
|
||||
export const isVoiceToggleKey = (key: { ctrl: boolean; meta: boolean; super?: boolean }, ch: string): boolean =>
|
||||
(key.ctrl || isActionMod(key)) && ch.toLowerCase() === 'b'
|
||||
|
||||
Reference in New Issue
Block a user