mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
fix(desktop): keep free-text slash arguments editable (#72768)
Some checks are pending
CI / Detect affected areas (push) Waiting to run
CI / Python tests (push) Blocked by required conditions
CI / Python lints (push) Blocked by required conditions
CI / JS & TS checks (push) Blocked by required conditions
CI / Desktop E2E (push) Blocked by required conditions
CI / Docs Site (push) Blocked by required conditions
CI / Deny unrelated histories (push) Blocked by required conditions
CI / Check contributors (push) Blocked by required conditions
CI / Check uv.lock (push) Blocked by required conditions
CI / Check no committed infographics (push) Blocked by required conditions
CI / package-lock.json diff (push) Blocked by required conditions
CI / Lint Docker scripts (push) Blocked by required conditions
CI / Build&Test Docker image (push) Blocked by required conditions
CI / Supply-chain scan (push) Blocked by required conditions
CI / Review label gate (push) Blocked by required conditions
CI / OSV scan (push) Waiting to run
CI / CI review comment (live) (push) Blocked by required conditions
CI / All required checks pass (push) Blocked by required conditions
CI / CI timing report (push) Blocked by required conditions
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Waiting to run
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Blocked by required conditions
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
Docker Build, Test, and Publish / merge (push) Blocked by required conditions
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Waiting to run
auto-fix lint issues & formatting / Apply patch (push) Blocked by required conditions
Some checks are pending
CI / Detect affected areas (push) Waiting to run
CI / Python tests (push) Blocked by required conditions
CI / Python lints (push) Blocked by required conditions
CI / JS & TS checks (push) Blocked by required conditions
CI / Desktop E2E (push) Blocked by required conditions
CI / Docs Site (push) Blocked by required conditions
CI / Deny unrelated histories (push) Blocked by required conditions
CI / Check contributors (push) Blocked by required conditions
CI / Check uv.lock (push) Blocked by required conditions
CI / Check no committed infographics (push) Blocked by required conditions
CI / package-lock.json diff (push) Blocked by required conditions
CI / Lint Docker scripts (push) Blocked by required conditions
CI / Build&Test Docker image (push) Blocked by required conditions
CI / Supply-chain scan (push) Blocked by required conditions
CI / Review label gate (push) Blocked by required conditions
CI / OSV scan (push) Waiting to run
CI / CI review comment (live) (push) Blocked by required conditions
CI / All required checks pass (push) Blocked by required conditions
CI / CI timing report (push) Blocked by required conditions
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Waiting to run
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Blocked by required conditions
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
Docker Build, Test, and Publish / merge (push) Blocked by required conditions
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Waiting to run
auto-fix lint issues & formatting / Apply patch (push) Blocked by required conditions
This commit is contained in:
parent
2b0fb72aca
commit
c2e45b555f
5 changed files with 156 additions and 51 deletions
|
|
@ -121,3 +121,44 @@ describe('useComposerTrigger — slash anywhere in the prompt', () => {
|
|||
expect(hook.result.current.trigger).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('useComposerTrigger — free-text slash arguments', () => {
|
||||
it('keeps a picked /goal command as editable text while retaining subcommand completion', () => {
|
||||
const editor = mountEditor('/go')
|
||||
const goal = item('/goal', 'Commands')
|
||||
const { hook } = mountTrigger(editor, [goal])
|
||||
|
||||
act(() => hook.result.current.refreshTrigger())
|
||||
act(() => hook.result.current.replaceTriggerWithChip(goal))
|
||||
|
||||
expect(composerPlainText(editor)).toBe('/goal ')
|
||||
expect(editor.querySelector('[data-slash-kind]')).toBeNull()
|
||||
expect(hook.result.current.trigger).not.toBeNull()
|
||||
})
|
||||
|
||||
it('does not seal a multi-word /goal into a chip when the option list runs empty', () => {
|
||||
const editor = mountEditor('/goal finish the full prompt')
|
||||
const { hook } = mountTrigger(editor, [])
|
||||
|
||||
act(() => hook.result.current.refreshTrigger())
|
||||
|
||||
expect(hook.result.current.slashFreeTextArgStage).toBe(true)
|
||||
expect(hook.result.current.commitTypedSlashDirective()).toBe(false)
|
||||
expect(composerPlainText(editor)).toBe('/goal finish the full prompt')
|
||||
expect(editor.querySelector('[data-slash-kind]')).toBeNull()
|
||||
})
|
||||
|
||||
it('still commits a fully typed finite option as one directive chip', () => {
|
||||
const editor = mountEditor('/personality creative')
|
||||
const { hook } = mountTrigger(editor, [])
|
||||
|
||||
act(() => hook.result.current.refreshTrigger())
|
||||
|
||||
expect(hook.result.current.slashFreeTextArgStage).toBe(false)
|
||||
act(() => {
|
||||
expect(hook.result.current.commitTypedSlashDirective()).toBe(true)
|
||||
})
|
||||
expect(composerPlainText(editor)).toBe('/personality creative ')
|
||||
expect(editor.querySelector('[data-slash-kind]')?.getAttribute('data-ref-text')).toBe('/personality creative')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { Unstable_TriggerAdapter, Unstable_TriggerItem } from '@assistant-u
|
|||
import { type MutableRefObject, type RefObject, useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { hermesDirectiveFormatter } from '@/components/assistant-ui/directive-text'
|
||||
import { desktopSlashCommandTakesArgs } from '@/lib/desktop-slash-commands'
|
||||
import { desktopSlashCommandArgumentMode } from '@/lib/desktop-slash-commands'
|
||||
|
||||
import {
|
||||
COMPLETION_ACTIONS,
|
||||
|
|
@ -89,11 +89,16 @@ export function useComposerTrigger({
|
|||
const before = textBeforeCaret(editor)
|
||||
const found = detectTrigger(before ?? composerPlainText(editor))
|
||||
|
||||
// The arg-stage popover is only useful for commands with an options screen.
|
||||
// For a no-arg command it would dead-end on "No matches", so drop it — the
|
||||
// directive is already complete.
|
||||
// A text-only command has no completion screen once its prose begins. Mixed
|
||||
// commands such as /goal stay live so their finite subcommands can still be
|
||||
// suggested, while arbitrary goal text remains valid.
|
||||
const argumentMode =
|
||||
found?.kind === '/' && slashArgStage(found.query)
|
||||
? desktopSlashCommandArgumentMode(slashCommandToken(found.query))
|
||||
: null
|
||||
|
||||
const detected =
|
||||
found?.kind === '/' && slashArgStage(found.query) && !desktopSlashCommandTakesArgs(slashCommandToken(found.query))
|
||||
found?.kind === '/' && slashArgStage(found.query) && argumentMode !== 'options' && argumentMode !== 'mixed'
|
||||
? null
|
||||
: found
|
||||
|
||||
|
|
@ -134,6 +139,11 @@ export function useComposerTrigger({
|
|||
// Space/Tab — neither should dead-end on a popover.
|
||||
const argStageEmpty = trigger?.kind === '/' && slashArgStage(trigger.query) && !triggerLoading && !triggerItems.length
|
||||
|
||||
const slashFreeTextArgStage =
|
||||
trigger?.kind === '/' &&
|
||||
slashArgStage(trigger.query) &&
|
||||
['mixed', 'text'].includes(desktopSlashCommandArgumentMode(slashCommandToken(trigger.query)) ?? '')
|
||||
|
||||
const closeTrigger = () => {
|
||||
setTrigger(null)
|
||||
setTriggerItems([])
|
||||
|
|
@ -148,9 +158,16 @@ export function useComposerTrigger({
|
|||
// the completion list is empty because the arg is already fully typed (the
|
||||
// backend completer drops exact matches). Reuses the chip path via a
|
||||
// synthetic item whose serialized form is the verbatim text.
|
||||
const commitTypedSlashDirective = () => {
|
||||
const commitTypedSlashDirective = (): boolean => {
|
||||
if (trigger?.kind !== '/') {
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
// Free prose must stay ordinary contentEditable text. This guard also
|
||||
// protects against a stale completion result reaching the keydown path
|
||||
// before refreshTrigger has caught up with the latest DOM input.
|
||||
if (desktopSlashCommandArgumentMode(slashCommandToken(trigger.query)) !== 'options') {
|
||||
return false
|
||||
}
|
||||
|
||||
const text = `/${trigger.query.trimEnd()}`
|
||||
|
|
@ -168,6 +185,8 @@ export function useComposerTrigger({
|
|||
rawText: text
|
||||
}
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const replaceTriggerWithChip = (item: Unstable_TriggerItem) => {
|
||||
|
|
@ -208,15 +227,15 @@ export function useComposerTrigger({
|
|||
// there's no command invocation for the args to belong to.
|
||||
const command = (item.metadata as { command?: string } | undefined)?.command ?? ''
|
||||
|
||||
const expandsToArgs =
|
||||
trigger.kind === '/' && !trigger.inline && !serialized.includes(' ') && desktopSlashCommandTakesArgs(command)
|
||||
const argumentMode = desktopSlashCommandArgumentMode(command)
|
||||
const expandsToArgs = trigger.kind === '/' && !trigger.inline && !serialized.includes(' ') && argumentMode !== null
|
||||
|
||||
const text = starter || serialized.endsWith(' ') ? serialized : `${serialized} `
|
||||
const directive = !starter && serialized.match(/^@([^:]+):(.+)$/)
|
||||
// No pill while expanding — the bare command stays plain text until an arg
|
||||
// is picked, at which point a single pill is emitted for the full command.
|
||||
const slashKind = !expandsToArgs && trigger.kind === '/' ? slashChipKindForItem(item) : null
|
||||
const keepTriggerOpen = starter || expandsToArgs
|
||||
const keepTriggerOpen = starter || (expandsToArgs && argumentMode !== 'text')
|
||||
|
||||
const finish = () => {
|
||||
draftRef.current = composerPlainText(editor)
|
||||
|
|
@ -288,6 +307,7 @@ export function useComposerTrigger({
|
|||
refreshTrigger,
|
||||
replaceTriggerWithChip,
|
||||
setTriggerActive,
|
||||
slashFreeTextArgStage,
|
||||
trigger,
|
||||
triggerActive,
|
||||
triggerItems,
|
||||
|
|
|
|||
|
|
@ -304,6 +304,7 @@ export function ChatBar({
|
|||
refreshTrigger,
|
||||
replaceTriggerWithChip,
|
||||
setTriggerActive,
|
||||
slashFreeTextArgStage,
|
||||
trigger,
|
||||
triggerActive,
|
||||
triggerItems,
|
||||
|
|
@ -544,7 +545,9 @@ export function ChatBar({
|
|||
// options step, and an arg option commits the full `/cmd arg` chip. Space
|
||||
// is slash-only (an `@` mention takes a literal space) and gated to a
|
||||
// non-empty query so a bare `/ ` still types a space.
|
||||
const acceptOnSpace = event.key === ' ' && trigger.kind === '/' && Boolean(trigger.query.trim())
|
||||
const acceptOnSpace =
|
||||
event.key === ' ' && trigger.kind === '/' && Boolean(trigger.query.trim()) && !slashFreeTextArgStage
|
||||
|
||||
const accept = event.key === 'Enter' || event.key === 'Tab' || acceptOnSpace
|
||||
|
||||
if (accept) {
|
||||
|
|
@ -579,11 +582,12 @@ export function ChatBar({
|
|||
slashArgStage(trigger.query) &&
|
||||
trigger.query.trim()
|
||||
) {
|
||||
event.preventDefault()
|
||||
triggerKeyConsumedRef.current = true
|
||||
commitTypedSlashDirective()
|
||||
if (commitTypedSlashDirective()) {
|
||||
event.preventDefault()
|
||||
triggerKeyConsumedRef.current = true
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ArrowUp/ArrowDown navigate, in priority order: the queue (edit entries in
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
|||
|
||||
import {
|
||||
desktopSkinSlashCompletions,
|
||||
desktopSlashCommandArgumentMode,
|
||||
desktopSlashDescription,
|
||||
desktopSlashUnavailableMessage,
|
||||
filterDesktopCommandsCatalog,
|
||||
|
|
@ -65,7 +66,7 @@ describe('desktop slash command curation', () => {
|
|||
|
||||
it('routes /pet through the desktop action handler and drops /pets', () => {
|
||||
expect(resolveDesktopCommand('/pet')?.surface).toEqual({ kind: 'action', action: 'pet' })
|
||||
expect(resolveDesktopCommand('/pet')?.args).toBe(true)
|
||||
expect(desktopSlashCommandArgumentMode('/pet')).toBe('options')
|
||||
expect(isDesktopSlashSuggestion('/pet')).toBe(true)
|
||||
expect(isDesktopSlashCommand('/pet')).toBe(true)
|
||||
expect(resolveDesktopCommand('/pets')?.surface).toEqual({ kind: 'unavailable', reason: 'settings' })
|
||||
|
|
@ -81,14 +82,14 @@ describe('desktop slash command curation', () => {
|
|||
expect(desktopSlashUnavailableMessage('/browser')).toBeNull()
|
||||
expect(resolveDesktopCommand('/browser')?.surface).toEqual({ kind: 'action', action: 'browser' })
|
||||
// Bare /browser expands to its sub-action options in the popover.
|
||||
expect(resolveDesktopCommand('/browser')?.args).toBe(true)
|
||||
expect(desktopSlashCommandArgumentMode('/browser')).toBe('options')
|
||||
})
|
||||
|
||||
it('routes /compress through the session-compression action', () => {
|
||||
// /compress must be an action (session.compress RPC), not exec: the slash
|
||||
// worker route times out on large sessions (#44456).
|
||||
expect(resolveDesktopCommand('/compress')?.surface).toEqual({ kind: 'action', action: 'compress' })
|
||||
expect(resolveDesktopCommand('/compress')?.args).toBe(true)
|
||||
expect(desktopSlashCommandArgumentMode('/compress')).toBe('text')
|
||||
expect(isDesktopSlashCommand('/compress')).toBe(true)
|
||||
expect(isDesktopSlashSuggestion('/compress')).toBe(true)
|
||||
expect(desktopSlashUnavailableMessage('/compress')).toBeNull()
|
||||
|
|
@ -144,12 +145,13 @@ describe('desktop slash command curation', () => {
|
|||
}
|
||||
})
|
||||
|
||||
it('keeps /goal arg text editable instead of sealing it into a chip', () => {
|
||||
// /goal takes free prose (the goal itself) plus subcommands. Without
|
||||
// args:true, Space after the command name committed a sealed directive
|
||||
// chip and the goal text rendered awkwardly after a pill.
|
||||
expect(resolveDesktopCommand('/goal')?.surface).toEqual({ kind: 'exec' })
|
||||
expect(resolveDesktopCommand('/goal')?.args).toBe(true)
|
||||
it('distinguishes free prose from finite slash option lists', () => {
|
||||
expect(desktopSlashCommandArgumentMode('/goal')).toBe('mixed')
|
||||
expect(desktopSlashCommandArgumentMode('/steer')).toBe('text')
|
||||
expect(desktopSlashCommandArgumentMode('/queue')).toBe('text')
|
||||
expect(desktopSlashCommandArgumentMode('/personality')).toBe('options')
|
||||
expect(desktopSlashCommandArgumentMode('/handoff')).toBe('options')
|
||||
expect(desktopSlashCommandArgumentMode('/version')).toBeNull()
|
||||
})
|
||||
|
||||
it('routes /journey (and aliases) to the memory graph overlay action', () => {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,16 @@ export interface SlashCommandBuildCtx {
|
|||
sessionId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* How arguments behave in the Desktop composer.
|
||||
*
|
||||
* - `options` → a finite completion list; picking or fully typing an option may
|
||||
* commit the complete directive as a chip.
|
||||
* - `text` → arbitrary prose; the command and its argument stay editable.
|
||||
* - `mixed` → offers subcommand completions but also accepts arbitrary prose.
|
||||
*/
|
||||
export type DesktopSlashArgumentMode = 'mixed' | 'options' | 'text'
|
||||
|
||||
export interface DesktopCommandSpec {
|
||||
/** Canonical command, leading slash included (e.g. `/resume`). */
|
||||
name: string
|
||||
|
|
@ -104,12 +114,8 @@ export interface DesktopCommandSpec {
|
|||
* the status bar), so the popover doesn't dead-end on inline completion.
|
||||
*/
|
||||
hidden?: boolean
|
||||
/**
|
||||
* The command has an inline options "screen" (theme / personality / session /
|
||||
* platform / toolset list). Picking the bare command in the popover expands to
|
||||
* that argument step instead of committing — mirroring typing `/<cmd> ` by hand.
|
||||
*/
|
||||
args?: boolean
|
||||
/** Composer behavior for text following the command token. */
|
||||
argumentMode?: DesktopSlashArgumentMode
|
||||
}
|
||||
|
||||
const exec = (): DesktopCommandSurface => ({ kind: 'exec' })
|
||||
|
|
@ -151,17 +157,22 @@ const DESKTOP_COMMAND_SPECS: readonly DesktopCommandSpec[] = [
|
|||
name: '/handoff',
|
||||
description: 'Hand off this session to a messaging platform',
|
||||
surface: action('handoff'),
|
||||
args: true
|
||||
argumentMode: 'options'
|
||||
},
|
||||
{ name: '/profile', description: 'Switch the active Hermes profile', surface: action('profile') },
|
||||
{ name: '/skin', description: 'Switch desktop theme or cycle to the next one', surface: action('skin'), args: true },
|
||||
{ name: '/title', description: 'Rename the current session', surface: action('title') },
|
||||
{
|
||||
name: '/skin',
|
||||
description: 'Switch desktop theme or cycle to the next one',
|
||||
surface: action('skin'),
|
||||
argumentMode: 'options'
|
||||
},
|
||||
{ name: '/title', description: 'Rename the current session', surface: action('title'), argumentMode: 'text' },
|
||||
{ name: '/help', description: 'Show desktop slash commands', aliases: ['/commands'], surface: action('help') },
|
||||
{
|
||||
name: '/browser',
|
||||
description: 'Manage browser CDP connection [connect|disconnect|status] (local gateway only)',
|
||||
surface: action('browser'),
|
||||
args: true
|
||||
argumentMode: 'options'
|
||||
},
|
||||
{
|
||||
name: '/journey',
|
||||
|
|
@ -177,7 +188,7 @@ const DESKTOP_COMMAND_SPECS: readonly DesktopCommandSpec[] = [
|
|||
description: 'Resume a saved session',
|
||||
aliases: ['/sessions', '/switch'],
|
||||
surface: picker('session'),
|
||||
args: true
|
||||
argumentMode: 'options'
|
||||
},
|
||||
|
||||
// Backend-executed commands that render useful inline output.
|
||||
|
|
@ -194,7 +205,7 @@ const DESKTOP_COMMAND_SPECS: readonly DesktopCommandSpec[] = [
|
|||
name: '/approvals',
|
||||
description: 'Show or set approval mode [manual|smart|off]',
|
||||
surface: exec(),
|
||||
args: true
|
||||
argumentMode: 'options'
|
||||
},
|
||||
{
|
||||
name: '/agents',
|
||||
|
|
@ -202,7 +213,13 @@ const DESKTOP_COMMAND_SPECS: readonly DesktopCommandSpec[] = [
|
|||
aliases: ['/tasks'],
|
||||
surface: exec()
|
||||
},
|
||||
{ name: '/background', description: 'Run a prompt in the background', aliases: ['/bg', '/btw'], surface: exec() },
|
||||
{
|
||||
name: '/background',
|
||||
description: 'Run a prompt in the background',
|
||||
aliases: ['/bg', '/btw'],
|
||||
surface: exec(),
|
||||
argumentMode: 'text'
|
||||
},
|
||||
// /compress must be an action (session.compress RPC), not exec: the slash
|
||||
// worker route times out on large sessions (30s WS / 45s pipe) before the
|
||||
// LLM summarise call finishes, then command.dispatch surfaces a bogus
|
||||
|
|
@ -212,16 +229,26 @@ const DESKTOP_COMMAND_SPECS: readonly DesktopCommandSpec[] = [
|
|||
description: 'Compress this conversation context',
|
||||
aliases: ['/compact'],
|
||||
surface: action('compress'),
|
||||
args: true
|
||||
argumentMode: 'text'
|
||||
},
|
||||
{ name: '/debug', description: 'Create a debug report', surface: exec() },
|
||||
{ name: '/goal', description: 'Manage the standing goal for this session', surface: exec(), args: true },
|
||||
{ name: '/personality', description: 'Switch personality for this session', surface: exec(), args: true },
|
||||
{
|
||||
name: '/goal',
|
||||
description: 'Manage the standing goal for this session',
|
||||
surface: exec(),
|
||||
argumentMode: 'mixed'
|
||||
},
|
||||
{
|
||||
name: '/personality',
|
||||
description: 'Switch personality for this session',
|
||||
surface: exec(),
|
||||
argumentMode: 'options'
|
||||
},
|
||||
{
|
||||
name: '/pet',
|
||||
description: 'Toggle or adopt a petdex mascot (/pet, /pet list, /pet boba)',
|
||||
surface: action('pet'),
|
||||
args: true
|
||||
argumentMode: 'options'
|
||||
},
|
||||
{
|
||||
name: '/hatch',
|
||||
|
|
@ -229,7 +256,13 @@ const DESKTOP_COMMAND_SPECS: readonly DesktopCommandSpec[] = [
|
|||
aliases: ['/generate-pet'],
|
||||
surface: action('hatch')
|
||||
},
|
||||
{ name: '/queue', description: 'Queue a prompt for the next turn', aliases: ['/q'], surface: exec() },
|
||||
{
|
||||
name: '/queue',
|
||||
description: 'Queue a prompt for the next turn',
|
||||
aliases: ['/q'],
|
||||
surface: exec(),
|
||||
argumentMode: 'text'
|
||||
},
|
||||
{ name: '/retry', description: 'Retry the last user message', surface: exec() },
|
||||
{ name: '/rollback', description: 'List or restore filesystem checkpoints', surface: exec() },
|
||||
{
|
||||
|
|
@ -242,9 +275,19 @@ const DESKTOP_COMMAND_SPECS: readonly DesktopCommandSpec[] = [
|
|||
description: 'Show current session status',
|
||||
surface: rpc('session.status', ctx => ({ session_id: ctx.sessionId }))
|
||||
},
|
||||
{ name: '/steer', description: 'Steer the current run after the next tool call', surface: exec(), args: true },
|
||||
{
|
||||
name: '/steer',
|
||||
description: 'Steer the current run after the next tool call',
|
||||
surface: exec(),
|
||||
argumentMode: 'text'
|
||||
},
|
||||
{ name: '/stop', description: 'Stop running background processes', surface: exec() },
|
||||
{ name: '/tools', description: 'List or toggle tools available to the agent', surface: exec(), args: true },
|
||||
{
|
||||
name: '/tools',
|
||||
description: 'List or toggle tools available to the agent',
|
||||
surface: exec(),
|
||||
argumentMode: 'options'
|
||||
},
|
||||
{ name: '/undo', description: 'Remove the last user/assistant exchange', surface: exec() },
|
||||
{ name: '/usage', description: 'Show token usage for this session', surface: exec() },
|
||||
{ name: '/version', description: 'Show Hermes Agent version', surface: exec() },
|
||||
|
|
@ -433,13 +476,8 @@ export function desktopSlashDescription(command: string, fallback = ''): string
|
|||
return SPEC_BY_NAME.get(canonicalDesktopSlashCommand(command))?.description || fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* True when picking the bare command should expand to its inline argument
|
||||
* options (theme / personality / session / platform / toolset) rather than
|
||||
* committing immediately. Lets the popover act as a two-step picker.
|
||||
*/
|
||||
export function desktopSlashCommandTakesArgs(command: string): boolean {
|
||||
return resolveDesktopCommand(command)?.args ?? false
|
||||
export function desktopSlashCommandArgumentMode(command: string): DesktopSlashArgumentMode | null {
|
||||
return resolveDesktopCommand(command)?.argumentMode ?? null
|
||||
}
|
||||
|
||||
export function desktopSkinSlashCompletions(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue