mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
chore: cleanup
This commit is contained in:
parent
cc15b55bb9
commit
9931d1d814
13 changed files with 250 additions and 111 deletions
24
AGENTS.md
24
AGENTS.md
|
|
@ -60,9 +60,10 @@ hermes-agent/
|
|||
│ ├── src/entry.tsx # TTY gate + render()
|
||||
│ ├── src/app.tsx # Main state machine and UI
|
||||
│ ├── src/gatewayClient.ts # Child process + JSON-RPC bridge
|
||||
│ ├── src/components/ # Ink components (branding, markdown, prompts, etc.)
|
||||
│ ├── src/hooks/ # useCompletion, useInputHistory, useQueue
|
||||
│ └── src/lib/ # Pure helpers (history, osc52, text)
|
||||
│ ├── src/app/ # Decomposed app logic (event handler, slash handler, stores, hooks)
|
||||
│ ├── src/components/ # Ink components (branding, markdown, prompts, pickers, etc.)
|
||||
│ ├── src/hooks/ # useCompletion, useInputHistory, useQueue, useVirtualHistory
|
||||
│ └── src/lib/ # Pure helpers (history, osc52, text, rpc, messages)
|
||||
├── tui_gateway/ # Python JSON-RPC backend for Ink TUI
|
||||
│ ├── entry.py # stdio entrypoint
|
||||
│ ├── server.py # RPC handlers and session logic
|
||||
|
|
@ -215,7 +216,7 @@ Newline-delimited JSON-RPC over stdio. Requests from Ink, events from Python. Se
|
|||
| Surface | Ink component | Gateway method |
|
||||
|---------|---------------|----------------|
|
||||
| Chat streaming | `app.tsx` + `messageLine.tsx` | `prompt.submit` → `message.delta/complete` |
|
||||
| Tool activity | `activityLane.tsx` | `tool.start/progress/complete` |
|
||||
| Tool activity | `thinking.tsx` | `tool.start/progress/complete` |
|
||||
| Approvals | `prompts.tsx` | `approval.respond` ← `approval.request` |
|
||||
| Clarify/sudo/secret | `prompts.tsx`, `maskedPrompt.tsx` | `clarify/sudo/secret.respond` |
|
||||
| Session picker | `sessionPicker.tsx` | `session.list/resume` |
|
||||
|
|
@ -232,13 +233,14 @@ Newline-delimited JSON-RPC over stdio. Requests from Ink, events from Python. Se
|
|||
|
||||
```bash
|
||||
cd ui-tui
|
||||
npm install # first time
|
||||
npm run dev # watch mode
|
||||
npm start # production
|
||||
npm run build # typecheck
|
||||
npm run lint # eslint
|
||||
npm run fmt # prettier
|
||||
npm test # vitest
|
||||
npm install # first time
|
||||
npm run dev # watch mode (rebuilds hermes-ink + tsx --watch)
|
||||
npm start # production
|
||||
npm run build # full build (hermes-ink + tsc)
|
||||
npm run type-check # typecheck only (tsc --noEmit)
|
||||
npm run lint # eslint
|
||||
npm run fmt # prettier
|
||||
npm test # vitest
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
103
ui-tui/README.md
103
ui-tui/README.md
|
|
@ -59,11 +59,29 @@ npm run fmt
|
|||
npm run fix
|
||||
```
|
||||
|
||||
There is no package-local test script today.
|
||||
Tests use vitest:
|
||||
|
||||
```bash
|
||||
npm test # single run
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
## App model
|
||||
|
||||
`src/app.tsx` is the center of the UI. It holds:
|
||||
`src/app.tsx` is the center of the UI. Heavy logic is split into `src/app/`:
|
||||
|
||||
- `createGatewayEventHandler.ts` — maps gateway events to state updates
|
||||
- `createSlashHandler.ts` — local slash command dispatch
|
||||
- `useComposerState.ts` — draft, multiline buffer, queue editing
|
||||
- `useInputHandlers.ts` — keypress routing
|
||||
- `useTurnState.ts` — agent turn lifecycle
|
||||
- `overlayStore.ts` / `uiStore.ts` — nanostores for overlay and UI state
|
||||
- `gatewayContext.tsx` — React context for the gateway client
|
||||
- `constants.ts`, `helpers.ts`, `interfaces.ts`
|
||||
|
||||
The top-level `app.tsx` composes these into the Ink tree with `Static` transcript output, a live streaming assistant row, prompt overlays, queue preview, status rule, input line, and completion list.
|
||||
|
||||
State managed at the top level includes:
|
||||
|
||||
- transcript and streaming state
|
||||
- queued messages and input history
|
||||
|
|
@ -260,30 +278,62 @@ Current color overrides:
|
|||
## File map
|
||||
|
||||
```text
|
||||
ui-tui/src/
|
||||
entry.tsx TTY gate + render()
|
||||
app.tsx main state machine and UI
|
||||
gatewayClient.ts child process + JSON-RPC bridge
|
||||
theme.ts default palette + skin merge
|
||||
constants.ts display constants, hotkeys, tool labels
|
||||
types.ts shared client-side types
|
||||
banner.ts ASCII art data
|
||||
ui-tui/
|
||||
packages/hermes-ink/ forked Ink renderer (local dep)
|
||||
src/
|
||||
entry.tsx TTY gate + render()
|
||||
app.tsx top-level Ink tree, composes src/app/*
|
||||
gatewayClient.ts child process + JSON-RPC bridge
|
||||
theme.ts default palette + skin merge
|
||||
constants.ts display constants, hotkeys, tool labels
|
||||
types.ts shared client-side types
|
||||
banner.ts ASCII art data
|
||||
|
||||
components/
|
||||
branding.tsx banner + session summary
|
||||
markdown.tsx Markdown-to-Ink renderer
|
||||
maskedPrompt.tsx masked input for sudo / secrets
|
||||
messageLine.tsx transcript rows
|
||||
prompts.tsx approval + clarify flows
|
||||
queuedMessages.tsx queued input preview
|
||||
sessionPicker.tsx session resume picker
|
||||
textInput.tsx custom line editor
|
||||
thinking.tsx spinner, reasoning, tool activity
|
||||
app/
|
||||
createGatewayEventHandler.ts event → state mapping
|
||||
createSlashHandler.ts local slash dispatch
|
||||
useComposerState.ts draft + multiline + queue editing
|
||||
useInputHandlers.ts keypress routing
|
||||
useTurnState.ts agent turn lifecycle
|
||||
overlayStore.ts nanostores for overlays
|
||||
uiStore.ts nanostores for UI flags
|
||||
gatewayContext.tsx React context for gateway client
|
||||
constants.ts app-level constants
|
||||
helpers.ts pure helpers
|
||||
interfaces.ts internal interfaces
|
||||
|
||||
lib/
|
||||
history.ts persistent input history
|
||||
osc52.ts OSC 52 clipboard copy
|
||||
text.ts text helpers, ANSI detection, previews
|
||||
components/
|
||||
appChrome.tsx status bar, input row, completions
|
||||
appLayout.tsx top-level layout composition
|
||||
appOverlays.tsx overlay routing (pickers, prompts)
|
||||
branding.tsx banner + session summary
|
||||
markdown.tsx Markdown-to-Ink renderer
|
||||
maskedPrompt.tsx masked input for sudo / secrets
|
||||
messageLine.tsx transcript rows
|
||||
modelPicker.tsx model switch picker
|
||||
prompts.tsx approval + clarify flows
|
||||
queuedMessages.tsx queued input preview
|
||||
sessionPicker.tsx session resume picker
|
||||
textInput.tsx custom line editor
|
||||
thinking.tsx spinner, reasoning, tool activity
|
||||
|
||||
hooks/
|
||||
useCompletion.ts tab completion (slash + path)
|
||||
useInputHistory.ts persistent history navigation
|
||||
useQueue.ts queued message management
|
||||
useVirtualHistory.ts in-memory history for pickers
|
||||
|
||||
lib/
|
||||
history.ts persistent input history
|
||||
messages.ts message formatting helpers
|
||||
osc52.ts OSC 52 clipboard copy
|
||||
rpc.ts JSON-RPC type helpers
|
||||
text.ts text helpers, ANSI detection, previews
|
||||
|
||||
types/
|
||||
hermes-ink.d.ts type declarations for @hermes/ink
|
||||
|
||||
__tests__/ vitest suite
|
||||
```
|
||||
|
||||
Related Python side:
|
||||
|
|
@ -293,8 +343,5 @@ tui_gateway/
|
|||
entry.py stdio entrypoint
|
||||
server.py RPC handlers and session logic
|
||||
render.py optional rich/ANSI bridge
|
||||
slash_worker.py persistent HermesCLI subprocess for slash commands
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- No dead code: `main.tsx`, `altScreen.tsx`, `commandPalette.tsx`, and `lib/slash.ts` have been removed.
|
||||
|
|
|
|||
|
|
@ -173,21 +173,6 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
[selection]
|
||||
)
|
||||
|
||||
// ── Resize RPC ───────────────────────────────────────────────────
|
||||
|
||||
useEffect(() => {
|
||||
if (!ui.sid || !stdout) {
|
||||
return
|
||||
}
|
||||
|
||||
const onResize = () => rpc('terminal.resize', { session_id: ui.sid, cols: stdout.columns ?? 80 })
|
||||
stdout.on('resize', onResize)
|
||||
|
||||
return () => {
|
||||
stdout.off('resize', onResize)
|
||||
}
|
||||
}, [stdout, ui.sid]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setClockNow(Date.now()), 1000)
|
||||
|
||||
|
|
@ -256,6 +241,21 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
|
||||
const gateway = useMemo(() => ({ gw, rpc }), [gw, rpc])
|
||||
|
||||
// ── Resize RPC ───────────────────────────────────────────────────
|
||||
|
||||
useEffect(() => {
|
||||
if (!ui.sid || !stdout) {
|
||||
return
|
||||
}
|
||||
|
||||
const onResize = () => rpc('terminal.resize', { session_id: ui.sid, cols: stdout.columns ?? 80 })
|
||||
stdout.on('resize', onResize)
|
||||
|
||||
return () => {
|
||||
stdout.off('resize', onResize)
|
||||
}
|
||||
}, [rpc, stdout, ui.sid])
|
||||
|
||||
const answerClarify = useCallback(
|
||||
(answer: string) => {
|
||||
const clarify = overlay.clarify
|
||||
|
|
@ -729,8 +729,8 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
return
|
||||
}
|
||||
|
||||
composerActions.clearIn()
|
||||
const editIdx = composerRefs.queueEditRef.current
|
||||
composerActions.clearIn()
|
||||
|
||||
if (editIdx !== null) {
|
||||
composerActions.replaceQueue(editIdx, full)
|
||||
|
|
@ -769,8 +769,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
|
||||
send(full)
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[appendMessage, composerActions, composerRefs]
|
||||
[appendMessage, composerActions, composerRefs, interpolate, send, sendQueued, shellExec, sys]
|
||||
)
|
||||
|
||||
// ── Input handling ───────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'
|
|||
import { join } from 'node:path'
|
||||
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
import type { PasteEvent } from '../components/textInput.js'
|
||||
import { useCompletion } from '../hooks/useCompletion.js'
|
||||
|
|
@ -104,8 +104,8 @@ export function useComposerState({ gw, onClipboardPaste, submitRef }: UseCompose
|
|||
}
|
||||
}, [input, inputBuf, submitRef])
|
||||
|
||||
return {
|
||||
actions: {
|
||||
const actions = useMemo(
|
||||
() => ({
|
||||
clearIn,
|
||||
dequeue,
|
||||
enqueue,
|
||||
|
|
@ -120,15 +120,35 @@ export function useComposerState({ gw, onClipboardPaste, submitRef }: UseCompose
|
|||
setPasteSnips,
|
||||
setQueueEdit,
|
||||
syncQueue
|
||||
},
|
||||
refs: {
|
||||
}),
|
||||
[
|
||||
clearIn,
|
||||
dequeue,
|
||||
enqueue,
|
||||
handleTextPaste,
|
||||
openEditor,
|
||||
pushHistory,
|
||||
replaceQ,
|
||||
setCompIdx,
|
||||
setHistoryIdx,
|
||||
setQueueEdit,
|
||||
syncQueue
|
||||
]
|
||||
)
|
||||
|
||||
const refs = useMemo(
|
||||
() => ({
|
||||
historyDraftRef,
|
||||
historyRef,
|
||||
queueEditRef,
|
||||
queueRef,
|
||||
submitRef
|
||||
},
|
||||
state: {
|
||||
}),
|
||||
[historyDraftRef, historyRef, queueEditRef, queueRef, submitRef]
|
||||
)
|
||||
|
||||
const state = useMemo(
|
||||
() => ({
|
||||
compIdx,
|
||||
compReplace,
|
||||
completions,
|
||||
|
|
@ -138,6 +158,13 @@ export function useComposerState({ gw, onClipboardPaste, submitRef }: UseCompose
|
|||
pasteSnips,
|
||||
queueEditIdx,
|
||||
queuedDisplay
|
||||
}
|
||||
}),
|
||||
[compIdx, compReplace, completions, historyIdx, input, inputBuf, pasteSnips, queueEditIdx, queuedDisplay]
|
||||
)
|
||||
|
||||
return {
|
||||
actions,
|
||||
refs,
|
||||
state
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { isTransientTrailLine, sameToolTrailGroup } from '../lib/text.js'
|
||||
import type { ActiveTool, ActivityItem } from '../types.js'
|
||||
|
|
@ -191,8 +191,8 @@ export function useTurnState(): UseTurnStateResult {
|
|||
[clearReasoning, idle, streaming]
|
||||
)
|
||||
|
||||
return {
|
||||
actions: {
|
||||
const actions = useMemo(
|
||||
() => ({
|
||||
clearReasoning,
|
||||
endReasoningPhase,
|
||||
idle,
|
||||
|
|
@ -212,8 +212,23 @@ export function useTurnState(): UseTurnStateResult {
|
|||
setStreaming,
|
||||
setTools,
|
||||
setTurnTrail
|
||||
},
|
||||
refs: {
|
||||
}),
|
||||
[
|
||||
clearReasoning,
|
||||
endReasoningPhase,
|
||||
idle,
|
||||
interruptTurn,
|
||||
pruneTransient,
|
||||
pulseReasoningStreaming,
|
||||
pushActivity,
|
||||
pushTrail,
|
||||
scheduleReasoning,
|
||||
scheduleStreaming
|
||||
]
|
||||
)
|
||||
|
||||
const refs = useMemo(
|
||||
() => ({
|
||||
activeToolsRef,
|
||||
bufRef,
|
||||
interruptedRef,
|
||||
|
|
@ -228,8 +243,12 @@ export function useTurnState(): UseTurnStateResult {
|
|||
toolTokenAccRef,
|
||||
toolCompleteRibbonRef,
|
||||
turnToolsRef
|
||||
},
|
||||
state: {
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const state = useMemo(
|
||||
() => ({
|
||||
activity,
|
||||
reasoning,
|
||||
reasoningTokens,
|
||||
|
|
@ -239,6 +258,13 @@ export function useTurnState(): UseTurnStateResult {
|
|||
streaming,
|
||||
tools,
|
||||
turnTrail
|
||||
}
|
||||
}),
|
||||
[activity, reasoning, reasoningTokens, reasoningActive, toolTokens, reasoningStreaming, streaming, tools, turnTrail]
|
||||
)
|
||||
|
||||
return {
|
||||
actions,
|
||||
refs,
|
||||
state
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ export function AppOverlays({
|
|||
<Box
|
||||
backgroundColor={active ? (ui.theme.color.completionCurrentBg as any) : undefined}
|
||||
flexDirection="row"
|
||||
key={item.text}
|
||||
key={`${start + i}:${item.text}:${item.display}:${item.meta ?? ''}`}
|
||||
width="100%"
|
||||
>
|
||||
<Text bold={active} color={ui.theme.color.bronze as any}>
|
||||
|
|
|
|||
|
|
@ -52,15 +52,17 @@ export function SessionPanel({ info, sid, t }: { info: SessionInfo; sid?: string
|
|||
|
||||
const truncLine = (pfx: string, items: string[]) => {
|
||||
let line = ''
|
||||
let shown = 0
|
||||
|
||||
for (const item of items.sort()) {
|
||||
for (const item of [...items].sort()) {
|
||||
const next = line ? `${line}, ${item}` : item
|
||||
|
||||
if (pfx.length + next.length > lineBudget) {
|
||||
return line ? `${line}, …+${items.length - line.split(', ').length}` : `${item}, …`
|
||||
return line ? `${line}, …+${items.length - shown}` : `${item}, …`
|
||||
}
|
||||
|
||||
line = next
|
||||
shown++
|
||||
}
|
||||
|
||||
return line
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export function ClarifyPrompt({
|
|||
return
|
||||
}
|
||||
|
||||
if (typing) {
|
||||
if (typing || !choices.length) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -117,6 +117,8 @@ export function ClarifyPrompt({
|
|||
})
|
||||
|
||||
if (typing || !choices.length) {
|
||||
const hint = choices.length ? 'Enter send · Esc back · Ctrl+C cancel' : 'Enter send · Esc cancel · Ctrl+C cancel'
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{heading}
|
||||
|
|
@ -126,7 +128,7 @@ export function ClarifyPrompt({
|
|||
<TextInput columns={Math.max(20, cols - 6)} onChange={setCustom} onSubmit={onAnswer} value={custom} />
|
||||
</Box>
|
||||
|
||||
<Text color={t.color.dim}>Enter send · Esc back · Ctrl+C cancel</Text>
|
||||
<Text color={t.color.dim}>{hint}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,10 +74,16 @@ function StreamCursor({
|
|||
const [on, setOn] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || !streaming) {
|
||||
setOn(true)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const id = setInterval(() => setOn(v => !v), 420)
|
||||
|
||||
return () => clearInterval(id)
|
||||
}, [])
|
||||
}, [streaming, visible])
|
||||
|
||||
return visible ? (
|
||||
<Text color={color} dimColor={dimColor}>
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ export class GatewayClient extends EventEmitter {
|
|||
const pyPath = (env.PYTHONPATH ?? '').trim()
|
||||
env.PYTHONPATH = pyPath ? `${root}${delimiter}${pyPath}` : root
|
||||
this.ready = false
|
||||
this.bufferedEvents = []
|
||||
this.pendingExit = undefined
|
||||
this.stdoutRl?.close()
|
||||
this.stderrRl?.close()
|
||||
|
|
|
|||
|
|
@ -1,33 +1,39 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import type { CompletionItem } from '../app/interfaces.js'
|
||||
import type { GatewayClient } from '../gatewayClient.js'
|
||||
|
||||
const TAB_PATH_RE = /((?:["']?(?:[A-Za-z]:[\\/]|\.{1,2}\/|~\/|\/|@|[^"'`\s]+\/))[^\s]*)$/
|
||||
|
||||
interface CompletionResult {
|
||||
items?: CompletionItem[]
|
||||
replace_from?: number
|
||||
}
|
||||
|
||||
export function useCompletion(input: string, blocked: boolean, gw: GatewayClient) {
|
||||
const [completions, setCompletions] = useState<{ text: string; display: string; meta: string }[]>([])
|
||||
const [completions, setCompletions] = useState<CompletionItem[]>([])
|
||||
const [compIdx, setCompIdx] = useState(0)
|
||||
const [compReplace, setCompReplace] = useState(0)
|
||||
const ref = useRef('')
|
||||
|
||||
useEffect(() => {
|
||||
const clear = () => {
|
||||
if (!completions.length) {
|
||||
return
|
||||
}
|
||||
|
||||
setCompletions([])
|
||||
setCompIdx(0)
|
||||
setCompletions(prev => (prev.length ? [] : prev))
|
||||
setCompIdx(prev => (prev ? 0 : prev))
|
||||
setCompReplace(prev => (prev ? 0 : prev))
|
||||
}
|
||||
|
||||
if (blocked || input === ref.current) {
|
||||
if (blocked) {
|
||||
clear()
|
||||
}
|
||||
if (blocked) {
|
||||
ref.current = ''
|
||||
clear()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (input === ref.current) {
|
||||
return
|
||||
}
|
||||
|
||||
ref.current = input
|
||||
|
||||
const isSlash = input.startsWith('/')
|
||||
|
|
@ -49,7 +55,9 @@ export function useCompletion(input: string, blocked: boolean, gw: GatewayClient
|
|||
: gw.request('complete.path', { word: pathWord })
|
||||
|
||||
req
|
||||
.then((r: any) => {
|
||||
.then(raw => {
|
||||
const r = raw as CompletionResult | null | undefined
|
||||
|
||||
if (ref.current !== input) {
|
||||
return
|
||||
}
|
||||
|
|
@ -76,7 +84,7 @@ export function useCompletion(input: string, blocked: boolean, gw: GatewayClient
|
|||
}, 60)
|
||||
|
||||
return () => clearTimeout(t)
|
||||
}, [input, blocked, gw]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [blocked, gw, input])
|
||||
|
||||
return { completions, compIdx, setCompIdx, compReplace }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useRef, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
|
||||
import * as inputHistory from '../lib/history.js'
|
||||
|
||||
|
|
@ -7,7 +7,9 @@ export function useInputHistory() {
|
|||
const [historyIdx, setHistoryIdx] = useState<number | null>(null)
|
||||
const historyDraftRef = useRef('')
|
||||
|
||||
const pushHistory = (text: string) => inputHistory.append(text)
|
||||
const pushHistory = useCallback((text: string) => {
|
||||
inputHistory.append(text)
|
||||
}, [])
|
||||
|
||||
return { historyRef, historyIdx, setHistoryIdx, historyDraftRef, pushHistory }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useRef, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
|
||||
export function useQueue() {
|
||||
const queueRef = useRef<string[]>([])
|
||||
|
|
@ -6,30 +6,47 @@ export function useQueue() {
|
|||
const queueEditRef = useRef<number | null>(null)
|
||||
const [queueEditIdx, setQueueEditIdx] = useState<number | null>(null)
|
||||
|
||||
const syncQueue = () => setQueuedDisplay([...queueRef.current])
|
||||
const syncQueue = useCallback(() => {
|
||||
setQueuedDisplay([...queueRef.current])
|
||||
}, [])
|
||||
|
||||
const setQueueEdit = (idx: number | null) => {
|
||||
const setQueueEdit = useCallback((idx: number | null) => {
|
||||
queueEditRef.current = idx
|
||||
setQueueEditIdx(idx)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const enqueue = (text: string) => {
|
||||
queueRef.current.push(text)
|
||||
syncQueue()
|
||||
}
|
||||
const enqueue = useCallback(
|
||||
(text: string) => {
|
||||
queueRef.current.push(text)
|
||||
syncQueue()
|
||||
},
|
||||
[syncQueue]
|
||||
)
|
||||
|
||||
const dequeue = () => {
|
||||
const [head, ...rest] = queueRef.current
|
||||
queueRef.current = rest
|
||||
const dequeue = useCallback(() => {
|
||||
const head = queueRef.current.shift()
|
||||
syncQueue()
|
||||
|
||||
return head
|
||||
}
|
||||
}, [syncQueue])
|
||||
|
||||
const replaceQ = (i: number, text: string) => {
|
||||
queueRef.current[i] = text
|
||||
syncQueue()
|
||||
}
|
||||
const replaceQ = useCallback(
|
||||
(i: number, text: string) => {
|
||||
queueRef.current[i] = text
|
||||
syncQueue()
|
||||
},
|
||||
[syncQueue]
|
||||
)
|
||||
|
||||
return { queueRef, queueEditRef, queuedDisplay, queueEditIdx, enqueue, dequeue, replaceQ, setQueueEdit, syncQueue }
|
||||
return {
|
||||
queueRef,
|
||||
queueEditRef,
|
||||
queuedDisplay,
|
||||
queueEditIdx,
|
||||
enqueue,
|
||||
dequeue,
|
||||
replaceQ,
|
||||
setQueueEdit,
|
||||
syncQueue
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue