mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(tui): show the invocation for a skill send, not the skill body
Carry a display string through the submit path so the transcript renders
what the user typed while the agent still receives the expanded skill.
Drops the '⚡ loading skill' line — the invocation bubble says it.
This commit is contained in:
parent
b700f9f253
commit
5a940180b4
7 changed files with 46 additions and 17 deletions
|
|
@ -810,8 +810,10 @@ describe('createSlashHandler', () => {
|
|||
expect(ctx.gateway.gw.request).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('falls through to command.dispatch for skill commands and sends the message', async () => {
|
||||
const skillMessage = 'Use this skill to do X.\n\n## Steps\n1. First step'
|
||||
it('falls through to command.dispatch for skill commands, sending the body but showing the invocation', async () => {
|
||||
const skillMessage =
|
||||
'[IMPORTANT: The user has invoked the "hermes-agent-dev" skill, indicating they want you to follow its instructions.\n' +
|
||||
'The full skill content is loaded below.]\n\nUse this skill to do X.\n\n## Steps\n1. First step'
|
||||
|
||||
const ctx = buildCtx({
|
||||
gateway: {
|
||||
|
|
@ -836,9 +838,12 @@ describe('createSlashHandler', () => {
|
|||
const h = createSlashHandler(ctx)
|
||||
expect(h('/hermes-agent-dev')).toBe(true)
|
||||
await vi.waitFor(() => {
|
||||
expect(ctx.transcript.sys).toHaveBeenCalledWith('⚡ loading skill: hermes-agent-dev')
|
||||
expect(ctx.transcript.send).toHaveBeenCalledWith(skillMessage, true, '/hermes-agent-dev')
|
||||
})
|
||||
expect(ctx.transcript.send).toHaveBeenCalledWith(skillMessage)
|
||||
// The expanded skill body is model-facing: no transcript line may carry it.
|
||||
for (const [line] of ctx.transcript.sys.mock.calls) {
|
||||
expect(line).not.toContain('Use this skill to do X')
|
||||
}
|
||||
})
|
||||
|
||||
it('handles command.dispatch payloads returned directly by slash.exec', async () => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { dispatchDisplayText } from '@hermes/shared/skill-scaffold'
|
||||
|
||||
import { parseSlashCommand } from '../domain/slash.js'
|
||||
import type { SlashExecResponse } from '../gatewayTypes.js'
|
||||
import { asCommandDispatch, rpcErrorMessage } from '../lib/rpc.js'
|
||||
|
|
@ -104,10 +106,19 @@ export function createSlashHandler(ctx: SlashHandlerContext): (cmd: string) => b
|
|||
return void handler(`/${d.target}${argTail}`)
|
||||
}
|
||||
|
||||
if (d.type === 'skill') {
|
||||
sys(`⚡ loading skill: ${d.name}`)
|
||||
// A skill/bundle dispatch's `message` is the expanded skill body —
|
||||
// model-facing scaffolding. The transcript shows the invocation instead;
|
||||
// an ordinary send has no projection and goes through unchanged.
|
||||
const sendDispatch = (display: string | undefined, message: string) => {
|
||||
const shown = dispatchDisplayText(display, message)
|
||||
|
||||
return d.message?.trim() ? send(d.message) : sys(`/${parsed.name}: skill payload missing message`)
|
||||
return shown ? send(message, true, shown) : send(message)
|
||||
}
|
||||
|
||||
if (d.type === 'skill') {
|
||||
return d.message?.trim()
|
||||
? sendDispatch(d.display, d.message)
|
||||
: sys(`/${parsed.name}: skill payload missing message`)
|
||||
}
|
||||
|
||||
if (d.type === 'send') {
|
||||
|
|
@ -115,7 +126,7 @@ export function createSlashHandler(ctx: SlashHandlerContext): (cmd: string) => b
|
|||
sys(d.notice)
|
||||
}
|
||||
|
||||
return d.message?.trim() ? send(d.message) : sys(`/${parsed.name}: empty message`)
|
||||
return d.message?.trim() ? sendDispatch(d.display, d.message) : sys(`/${parsed.name}: empty message`)
|
||||
}
|
||||
|
||||
if (d.type === 'prefill') {
|
||||
|
|
|
|||
|
|
@ -526,7 +526,7 @@ export interface SlashHandlerContext {
|
|||
transcript: {
|
||||
page: (text: string, title?: string) => void
|
||||
panel: (title: string, sections: PanelSection[]) => void
|
||||
send: (text: string) => void
|
||||
send: (text: string, showUserMessage?: boolean, displayText?: string) => void
|
||||
setHistoryItems: StateSetter<Msg[]>
|
||||
sys: (text: string) => void
|
||||
trimLastExchange: (items: Msg[]) => Msg[]
|
||||
|
|
|
|||
|
|
@ -42,7 +42,16 @@ export function markSubmitting(): void {
|
|||
// Submit a ready prompt (already resolved to be neither a slash command nor a
|
||||
// shell escape, with a live session). Pulled out of useSubmission so the
|
||||
// synchronous-busy invariant above is unit-testable without React test infra.
|
||||
export function submitPrompt(text: string, deps: SubmitPromptDeps, showUserMessage = true): void {
|
||||
//
|
||||
// `displayOverride` is what the transcript shows when it differs from what the
|
||||
// agent receives — a `/skill` invocation expands into the whole skill body, and
|
||||
// that scaffolding is model-facing only.
|
||||
export function submitPrompt(
|
||||
text: string,
|
||||
deps: SubmitPromptDeps,
|
||||
showUserMessage = true,
|
||||
displayOverride?: string
|
||||
): void {
|
||||
const sid = getUiState().sid
|
||||
|
||||
if (!sid) {
|
||||
|
|
@ -63,7 +72,7 @@ export function submitPrompt(text: string, deps: SubmitPromptDeps, showUserMessa
|
|||
deps.setLastUserMsg(text)
|
||||
|
||||
if (show) {
|
||||
deps.appendMessage({ role: 'user', text: displayText })
|
||||
deps.appendMessage({ role: 'user', text: displayOverride || displayText })
|
||||
}
|
||||
|
||||
patchUiState({ busy: true, status: 'running…' })
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export function useSubmission(opts: UseSubmissionOptions) {
|
|||
}, [composerState.input, composerState.inputBuf])
|
||||
|
||||
const send = useCallback(
|
||||
(text: string, showUserMessage = true) => {
|
||||
(text: string, showUserMessage = true, displayText?: string) => {
|
||||
const expand = expandSnips(composerState.pasteSnips)
|
||||
|
||||
submitPrompt(
|
||||
|
|
@ -80,7 +80,8 @@ export function useSubmission(opts: UseSubmissionOptions) {
|
|||
setLastUserMsg,
|
||||
sys
|
||||
},
|
||||
showUserMessage
|
||||
showUserMessage,
|
||||
displayText
|
||||
)
|
||||
},
|
||||
[appendMessage, composerActions, composerState.pasteSnips, gw, setLastUserMsg, sys]
|
||||
|
|
|
|||
|
|
@ -70,8 +70,8 @@ export type {
|
|||
export type CommandDispatchResponse =
|
||||
| { output?: string; type: 'exec' | 'plugin' }
|
||||
| { target: string; type: 'alias' }
|
||||
| { message?: string; name: string; type: 'skill' }
|
||||
| { message: string; notice?: string; type: 'send' }
|
||||
| { display?: string; message?: string; name: string; type: 'skill' }
|
||||
| { display?: string; message: string; notice?: string; type: 'send' }
|
||||
| { message: string; notice?: string; type: 'prefill' }
|
||||
|
||||
// ── Config ───────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -22,15 +22,18 @@ export const asCommandDispatch = (value: unknown): CommandDispatchResponse | nul
|
|||
return { type: 'alias', target: o.target }
|
||||
}
|
||||
|
||||
const str = (value: unknown) => (typeof value === 'string' ? value : undefined)
|
||||
|
||||
if (t === 'skill' && typeof o.name === 'string') {
|
||||
return { type: 'skill', name: o.name, message: typeof o.message === 'string' ? o.message : undefined }
|
||||
return { type: 'skill', name: o.name, message: str(o.message), display: str(o.display) }
|
||||
}
|
||||
|
||||
if (t === 'send' && typeof o.message === 'string') {
|
||||
return {
|
||||
type: 'send',
|
||||
message: o.message,
|
||||
notice: typeof o.notice === 'string' ? o.notice : undefined
|
||||
notice: str(o.notice),
|
||||
display: str(o.display)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue