mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
fix(tui): save live transcript from slash command
This commit is contained in:
parent
8bbeaea6c7
commit
1b8ca9254f
2 changed files with 108 additions and 0 deletions
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { existsSync, readFileSync, unlinkSync } from 'node:fs'
|
||||||
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import { createSlashHandler } from '../app/createSlashHandler.js'
|
import { createSlashHandler } from '../app/createSlashHandler.js'
|
||||||
|
|
@ -287,6 +289,58 @@ describe('createSlashHandler', () => {
|
||||||
expect(ctx.transcript.page).not.toHaveBeenCalled()
|
expect(ctx.transcript.page).not.toHaveBeenCalled()
|
||||||
expect(ctx.transcript.sys).toHaveBeenCalledWith('no conversation yet')
|
expect(ctx.transcript.sys).toHaveBeenCalledWith('no conversation yet')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('/save writes the current TUI transcript without using the slash worker', () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
vi.setSystemTime(new Date(2026, 3, 25, 15, 4, 5))
|
||||||
|
const filename = 'hermes_conversation_20260425_150405.json'
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (existsSync(filename)) {
|
||||||
|
unlinkSync(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = buildCtx({
|
||||||
|
local: {
|
||||||
|
...buildLocal(),
|
||||||
|
getHistoryItems: vi.fn(() => [
|
||||||
|
{ role: 'system', text: 'intro' },
|
||||||
|
{ role: 'user', text: 'hello' },
|
||||||
|
{ role: 'assistant', text: 'hi there', tools: ['read_file'] },
|
||||||
|
{ role: 'tool', text: 'tool output' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
createSlashHandler(ctx)('/save')
|
||||||
|
|
||||||
|
expect(ctx.gateway.gw.request).not.toHaveBeenCalled()
|
||||||
|
expect(ctx.transcript.sys).toHaveBeenCalledWith(`conversation saved to: ${filename}`)
|
||||||
|
|
||||||
|
const saved = JSON.parse(readFileSync(filename, 'utf8'))
|
||||||
|
|
||||||
|
expect(saved.messages).toEqual([
|
||||||
|
{ role: 'user', text: 'hello' },
|
||||||
|
{ role: 'assistant', text: 'hi there', tools: ['read_file'] },
|
||||||
|
{ role: 'tool', text: 'tool output' }
|
||||||
|
])
|
||||||
|
} finally {
|
||||||
|
if (existsSync(filename)) {
|
||||||
|
unlinkSync(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.useRealTimers()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('/save reports empty state without touching the slash worker', () => {
|
||||||
|
const ctx = buildCtx()
|
||||||
|
|
||||||
|
createSlashHandler(ctx)('/save')
|
||||||
|
|
||||||
|
expect(ctx.gateway.gw.request).not.toHaveBeenCalled()
|
||||||
|
expect(ctx.transcript.sys).toHaveBeenCalledWith('no conversation yet')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const buildCtx = (overrides: Partial<Ctx> = {}): Ctx => ({
|
const buildCtx = (overrides: Partial<Ctx> = {}): Ctx => ({
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { writeFileSync } from 'node:fs'
|
||||||
|
|
||||||
import { NO_CONFIRM_DESTRUCTIVE } from '../../../config/env.js'
|
import { NO_CONFIRM_DESTRUCTIVE } from '../../../config/env.js'
|
||||||
import { dailyFortune, randomFortune } from '../../../content/fortunes.js'
|
import { dailyFortune, randomFortune } from '../../../content/fortunes.js'
|
||||||
import { HOTKEYS } from '../../../content/hotkeys.js'
|
import { HOTKEYS } from '../../../content/hotkeys.js'
|
||||||
|
|
@ -46,6 +48,24 @@ const DETAILS_USAGE =
|
||||||
|
|
||||||
const DETAILS_SECTION_USAGE = 'usage: /details <section> [hidden|collapsed|expanded|reset]'
|
const DETAILS_SECTION_USAGE = 'usage: /details <section> [hidden|collapsed|expanded|reset]'
|
||||||
|
|
||||||
|
const pad2 = (n: number): string => String(n).padStart(2, '0')
|
||||||
|
|
||||||
|
const saveTimestamp = (d = new Date()): string =>
|
||||||
|
`${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}_${pad2(d.getHours())}${pad2(
|
||||||
|
d.getMinutes()
|
||||||
|
)}${pad2(d.getSeconds())}`
|
||||||
|
|
||||||
|
const serializableTranscript = (items: Msg[]) =>
|
||||||
|
items
|
||||||
|
.filter(m => m.role === 'user' || m.role === 'assistant' || m.role === 'tool')
|
||||||
|
.filter(m => m.text.trim() || m.thinking?.trim() || m.tools?.length)
|
||||||
|
.map(m => ({
|
||||||
|
role: m.role,
|
||||||
|
text: m.text,
|
||||||
|
...(m.thinking ? { thinking: m.thinking } : {}),
|
||||||
|
...(m.tools?.length ? { tools: m.tools } : {})
|
||||||
|
}))
|
||||||
|
|
||||||
export const coreCommands: SlashCommand[] = [
|
export const coreCommands: SlashCommand[] = [
|
||||||
{
|
{
|
||||||
help: 'list commands + hotkeys',
|
help: 'list commands + hotkeys',
|
||||||
|
|
@ -351,6 +371,40 @@ export const coreCommands: SlashCommand[] = [
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
help: 'save the current transcript to JSON',
|
||||||
|
name: 'save',
|
||||||
|
run: (_arg, ctx) => {
|
||||||
|
const messages = serializableTranscript(ctx.local.getHistoryItems())
|
||||||
|
|
||||||
|
if (!messages.length) {
|
||||||
|
return ctx.transcript.sys('no conversation yet')
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = `hermes_conversation_${saveTimestamp()}.json`
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeFileSync(
|
||||||
|
filename,
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
model: ctx.ui.info?.model ?? null,
|
||||||
|
saved_at: new Date().toISOString(),
|
||||||
|
session_id: ctx.sid,
|
||||||
|
messages
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}\n`,
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
ctx.transcript.sys(`conversation saved to: ${filename}`)
|
||||||
|
} catch (error) {
|
||||||
|
ctx.transcript.sys(`failed to save: ${String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
aliases: ['sb'],
|
aliases: ['sb'],
|
||||||
help: 'status bar position (on|off|top|bottom)',
|
help: 'status bar position (on|off|top|bottom)',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue