refactor: rename props and update tracker output

old-agentic-v1^2
Philipp Burckhardt 2023-06-30 14:22:02 -04:00
rodzic e7b3c9aa14
commit c09c24ac66
2 zmienionych plików z 82 dodań i 63 usunięć

Wyświetl plik

@ -4,34 +4,43 @@ import readline from 'node:readline'
import { bgWhite, black, bold, cyan, gray, green, red, yellow } from 'colorette' import { bgWhite, black, bold, cyan, gray, green, red, yellow } from 'colorette'
import { SPACE } from '@/constants' import { SPACE } from '@/constants'
import { capitalize } from '@/utils'
import { TaskEvent, TaskStatus } from './event' import { TaskEvent, TaskStatus } from './event'
import { SYMBOLS } from './symbols' import { SYMBOLS } from './symbols'
const MAGIC_STRING = '__INSIDE_TRACKER__' // Define a unique "magic" string export const MAGIC_STRING = '__INSIDE_TRACKER__' // a unique "magic" string that used to identify the output of the tracker
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
const RE_ANSI_ESCAPES = /\x1b\[[0-9;]*[A-Za-z]/ // cursor movement, screen clearing, etc. const RE_ANSI_ESCAPES = /^(\x1b\[[0-9;]*[ABCDHJK]|[\r\n])+$/ // cursor movement, screen clearing, etc.
const originalStdoutWrite = process.stdout.write const originalStdoutWrite = process.stdout.write
const originalStderrWrite = process.stderr.write const originalStderrWrite = process.stderr.write
export interface TerminalTaskTrackerOptions {
spinnerInterval?: number
inactivityThreshold?: number
}
export class TerminalTaskTracker { export class TerminalTaskTracker {
protected events: Record<string, any[]> = { root: [] } protected _events: Record<string, any[]> = { root: [] }
protected interval: NodeJS.Timeout | null = null protected _interval: NodeJS.Timeout | null = null
protected inactivityTimeout: NodeJS.Timeout | null = null protected _inactivityTimeout: NodeJS.Timeout | null = null
protected truncateOutput = false protected _truncateOutput = false
protected viewMode = 'tasks' protected _viewMode = 'tasks'
protected outputs: Array<string | Uint8Array> = [] protected _outputs: Array<string | Uint8Array> = []
protected renderingPaused = false protected _renderingPaused = false
protected _spinnerInterval: number protected _spinnerInterval: number
protected _inactivityThreshold: number protected _inactivityThreshold: number
private stdoutBuffer: string[] = [] private _stdoutBuffer: string[] = []
private stderrBuffer: string[] = [] private _stderrBuffer: string[] = []
constructor({ spinnerInterval = 100, inactivityThreshold = 2000 } = {}) { constructor({
spinnerInterval = 100,
inactivityThreshold = 2_000
}: TerminalTaskTrackerOptions = {}) {
this._spinnerInterval = spinnerInterval this._spinnerInterval = spinnerInterval
this._inactivityThreshold = inactivityThreshold this._inactivityThreshold = inactivityThreshold
@ -45,7 +54,10 @@ export class TerminalTaskTracker {
buffer = Buffer.from(buffer).toString('utf-8') buffer = Buffer.from(buffer).toString('utf-8')
} }
this.stdoutBuffer.push(buffer) if (!this._renderingPaused) {
this._stdoutBuffer.push(buffer)
}
return originalStdoutWrite.call(process.stdout, buffer) return originalStdoutWrite.call(process.stdout, buffer)
} }
@ -61,10 +73,10 @@ export class TerminalTaskTracker {
buffer.replace(MAGIC_STRING, '') buffer.replace(MAGIC_STRING, '')
) )
} else { } else {
if (!RE_ANSI_ESCAPES.test(buffer)) { if (!this._renderingPaused && !RE_ANSI_ESCAPES.test(buffer)) {
// If an ANSI escape sequence is written to stderr, it will mess up the output, so we need to write it to stdout instead: // If an ANSI escape sequence is written to stderr, it will mess up the output, so we need to write it to stdout instead:
// This write is from outside the tracker, add it to stderrBuffer and write to stderr: // This write is from outside the tracker, add it to stderrBuffer and write to stderr:
this.stderrBuffer.push(buffer) this._stderrBuffer.push(buffer)
} }
return originalStderrWrite.call(process.stderr, buffer) return originalStderrWrite.call(process.stderr, buffer)
@ -93,7 +105,7 @@ export class TerminalTaskTracker {
} }
start() { start() {
this.interval = setInterval(() => { this._interval = setInterval(() => {
this.render() this.render()
}, this._spinnerInterval) }, this._spinnerInterval)
@ -107,12 +119,12 @@ export class TerminalTaskTracker {
} }
close() { close() {
if (this.interval) { if (this._interval) {
clearInterval(this.interval) clearInterval(this._interval)
} }
if (this.inactivityTimeout) { if (this._inactivityTimeout) {
clearTimeout(this.inactivityTimeout) clearTimeout(this._inactivityTimeout)
} }
process.stdin.setRawMode(false) process.stdin.setRawMode(false)
@ -129,15 +141,14 @@ export class TerminalTaskTracker {
'', '',
bgWhite(black(' Completed all tasks. ')), bgWhite(black(' Completed all tasks. ')),
'', '',
bgWhite(black(' stdout: ')),
'',
this.stdoutBuffer.join(''),
'',
'', '',
bgWhite(black(' stderr: ')), bgWhite(black(' stderr: ')),
'', '',
this.stderrBuffer.join(''), this._stderrBuffer.join(''),
'', '',
bgWhite(black(' stdout: ')),
'',
this._stdoutBuffer.join(''),
'' ''
] ]
@ -148,16 +159,17 @@ export class TerminalTaskTracker {
} }
pause() { pause() {
this.renderingPaused = true this.clearAndSetCursorPosition()
this._renderingPaused = true
} }
resume() { resume() {
this.renderingPaused = false this._renderingPaused = false
this.render() this.render()
} }
stringify(value: any) { stringify(value: any): string {
if (this.truncateOutput) { if (this._truncateOutput) {
const json = JSON.stringify(value) const json = JSON.stringify(value)
if (json.length < 40) { if (json.length < 40) {
return json return json
@ -170,14 +182,13 @@ export class TerminalTaskTracker {
} }
toggleOutputTruncation() { toggleOutputTruncation() {
this.truncateOutput = !this.truncateOutput this._truncateOutput = !this._truncateOutput
} }
startInactivityTimeout() { startInactivityTimeout() {
this.inactivityTimeout = setTimeout(() => { this._inactivityTimeout = setTimeout(() => {
// Check if all tasks are completed: const allTasksCompleted = Object.values(this._events).every((events) =>
const allTasksCompleted = Object.values(this.events).every((events) => events.every((event) => event.status === TaskStatus.COMPLETED)
events.every((event) => event.status !== TaskStatus.RUNNING)
) )
if (allTasksCompleted) { if (allTasksCompleted) {
@ -190,21 +201,21 @@ export class TerminalTaskTracker {
addEvent<TInput, TOutput>(event: TaskEvent<TInput, TOutput>) { addEvent<TInput, TOutput>(event: TaskEvent<TInput, TOutput>) {
const { parent = 'root', taskId, name, status, inputs, output } = event const { parent = 'root', taskId, name, status, inputs, output } = event
if (!this.events[parent]) { if (!this._events[parent]) {
this.events[parent] = [] this._events[parent] = []
} }
const existingEventIndex = this.events[parent].findIndex( const existingEventIndex = this._events[parent].findIndex(
(e) => e.taskId === taskId (e) => e.taskId === taskId
) )
if (existingEventIndex !== -1) { if (existingEventIndex !== -1) {
// If the event already exists, update its status and output: // If the event already exists, update its status and output:
this.events[parent][existingEventIndex].status = status this._events[parent][existingEventIndex].status = status
this.events[parent][existingEventIndex].output = output this._events[parent][existingEventIndex].output = output
} else { } else {
// If the event does not exist, add it to the array: // If the event does not exist, add it to the array:
this.events[parent].push({ taskId, name, status, inputs }) this._events[parent].push({ taskId, name, status, inputs })
} }
} }
@ -224,12 +235,12 @@ export class TerminalTaskTracker {
} }
} }
renderTree(node: string, level = 0) { renderTree(node: string, level = 0): string[] {
const indent = SPACE.repeat(level * 2) const indent = SPACE.repeat(level * 2)
let lines: string[] = [] let lines: string[] = []
if (this.events[node]) { if (this._events[node]) {
this.events[node].forEach(({ name, status, output, inputs }) => { this._events[node].forEach(({ name, status, output, inputs }) => {
const [statusSymbol, color] = this.getStatusSymbolColor(status) const [statusSymbol, color] = this.getStatusSymbolColor(status)
lines.push( lines.push(
@ -240,7 +251,7 @@ export class TerminalTaskTracker {
gray('(' + this.stringify(inputs) + ')') gray('(' + this.stringify(inputs) + ')')
) )
if (this.events[name]) { if (this._events[name]) {
lines = lines.concat( lines = lines.concat(
this.renderTree(name, level + 1).map((line, index, arr) => { this.renderTree(name, level + 1).map((line, index, arr) => {
if (index === arr.length - 1) { if (index === arr.length - 1) {
@ -253,7 +264,7 @@ export class TerminalTaskTracker {
} }
let line = '' let line = ''
if (this.events[name]) { if (this._events[name]) {
line = indent + gray(SYMBOLS.BAR_END) line = indent + gray(SYMBOLS.BAR_END)
} }
@ -308,57 +319,61 @@ export class TerminalTaskTracker {
process.stderr.write(MAGIC_STRING + output) process.stderr.write(MAGIC_STRING + output)
} }
toggleView(direction) { toggleView(direction: string) {
const viewModes = ['tasks', 'stdout', 'stderr'] const viewModes = ['tasks', 'stdout', 'stderr']
const currentIdx = viewModes.indexOf(this.viewMode) const currentIdx = viewModes.indexOf(this._viewMode)
if (direction === 'next') { if (direction === 'next') {
this.viewMode = viewModes[(currentIdx + 1) % viewModes.length] this._viewMode = viewModes[(currentIdx + 1) % viewModes.length]
} else if (direction === 'prev') { } else if (direction === 'prev') {
this.viewMode = this._viewMode =
viewModes[(currentIdx - 1 + viewModes.length) % viewModes.length] viewModes[(currentIdx - 1 + viewModes.length) % viewModes.length]
} }
this.render() this.render()
} }
getSpinnerSymbol() { getSpinnerSymbol(): string {
return SYMBOLS.SPINNER[ return SYMBOLS.SPINNER[
Math.floor(Date.now() / this._spinnerInterval) % SYMBOLS.SPINNER.length Math.floor(Date.now() / this._spinnerInterval) % SYMBOLS.SPINNER.length
] ]
} }
renderHeader() { renderHeader() {
const legend = [ const commands = [
'commands', 'ctrl+c: exit',
'ctrl+c exit',
'ctrl+e: truncate output', 'ctrl+e: truncate output',
'ctrl+left/right: switch view' 'ctrl+left/right: switch view'
].join(' | ') ].join(' | ')
const header = [` Agentic: ${this.viewMode} `, ` ${legend} `, ''].join('\n') const header = [
` Agentic - ${capitalize(this._viewMode)} View`,
' ' + commands + ' ',
'',
''
].join('\n')
this.writeWithMagicString(bgWhite(black(header))) this.writeWithMagicString(bgWhite(black(header)))
} }
render() { render() {
if (this.renderingPaused) { if (this._renderingPaused) {
return // Do not render if paused return // Do not render if paused
} }
this.clearAndSetCursorPosition() this.clearAndSetCursorPosition()
if (this.viewMode === 'tasks') { if (this._viewMode === 'tasks') {
const lines = this.renderTree('root') const lines = this.renderTree('root')
this.clearPreviousRender(lines.length + 2) this.clearPreviousRender(lines.length)
this.renderHeader() this.renderHeader()
this.writeWithMagicString(lines) this.writeWithMagicString(lines)
} else if (this.viewMode === 'stdout') { } else if (this._viewMode === 'stdout') {
this.clearPreviousRender(this.stdoutBuffer.length + 2) this.clearPreviousRender(this._stdoutBuffer.length)
this.renderHeader() this.renderHeader()
this.writeWithMagicString(this.stdoutBuffer) this.writeWithMagicString(this._stdoutBuffer)
} else if (this.viewMode === 'stderr') { } else if (this._viewMode === 'stderr') {
this.clearPreviousRender(this.stderrBuffer.length + 2) this.clearPreviousRender(this._stderrBuffer.length)
this.renderHeader() this.renderHeader()
this.writeWithMagicString(this.stderrBuffer) this.writeWithMagicString(this._stderrBuffer)
} }
} }
} }

Wyświetl plik

@ -254,3 +254,7 @@ export function isArray(value: any): value is any[] {
export function identity<T>(x: T): T { export function identity<T>(x: T): T {
return x return x
} }
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}