mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Merge branch 'feat/ink-refactor' of github.com:NousResearch/hermes-agent into feat/ink-refactor
This commit is contained in:
commit
690d62a6d1
4 changed files with 107 additions and 44 deletions
|
|
@ -898,6 +898,34 @@ class SlashCommandCompleter(Completer):
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _personality_completions(sub_text: str, sub_lower: str):
|
||||
"""Yield completions for /personality from configured personalities."""
|
||||
try:
|
||||
from hermes_cli.config import load_config
|
||||
personalities = load_config().get("agent", {}).get("personalities", {})
|
||||
if "none".startswith(sub_lower) and "none" != sub_lower:
|
||||
yield Completion(
|
||||
"none",
|
||||
start_position=-len(sub_text),
|
||||
display="none",
|
||||
display_meta="clear personality overlay",
|
||||
)
|
||||
for name, prompt in personalities.items():
|
||||
if name.startswith(sub_lower) and name != sub_lower:
|
||||
if isinstance(prompt, dict):
|
||||
meta = prompt.get("description") or prompt.get("system_prompt", "")[:50]
|
||||
else:
|
||||
meta = str(prompt)[:50]
|
||||
yield Completion(
|
||||
name,
|
||||
start_position=-len(sub_text),
|
||||
display=name,
|
||||
display_meta=meta,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _model_completions(self, sub_text: str, sub_lower: str):
|
||||
"""Yield completions for /model from config aliases + built-in aliases."""
|
||||
seen = set()
|
||||
|
|
@ -960,6 +988,9 @@ class SlashCommandCompleter(Completer):
|
|||
if base_cmd == "/skin":
|
||||
yield from self._skin_completions(sub_text, sub_lower)
|
||||
return
|
||||
if base_cmd == "/personality":
|
||||
yield from self._personality_completions(sub_text, sub_lower)
|
||||
return
|
||||
|
||||
# Static subcommand completions
|
||||
if " " not in sub_text and base_cmd in SUBCOMMANDS and self._command_allowed(base_cmd):
|
||||
|
|
|
|||
|
|
@ -502,10 +502,35 @@ def _wire_callbacks(sid: str):
|
|||
set_secret_capture_callback(secret_cb)
|
||||
|
||||
|
||||
def _resolve_personality_prompt(cfg: dict) -> str:
|
||||
"""Resolve the active personality into a system prompt string."""
|
||||
name = (cfg.get("display", {}).get("personality", "") or "").strip().lower()
|
||||
if not name or name in ("default", "none", "neutral"):
|
||||
return ""
|
||||
try:
|
||||
from hermes_cli.config import load_config as _load_full_cfg
|
||||
personalities = _load_full_cfg().get("agent", {}).get("personalities", {})
|
||||
except Exception:
|
||||
personalities = cfg.get("agent", {}).get("personalities", {})
|
||||
pval = personalities.get(name)
|
||||
if pval is None:
|
||||
return ""
|
||||
if isinstance(pval, dict):
|
||||
parts = [pval.get("system_prompt", "")]
|
||||
if pval.get("tone"):
|
||||
parts.append(f'Tone: {pval["tone"]}')
|
||||
if pval.get("style"):
|
||||
parts.append(f'Style: {pval["style"]}')
|
||||
return "\n".join(p for p in parts if p)
|
||||
return str(pval)
|
||||
|
||||
|
||||
def _make_agent(sid: str, key: str, session_id: str | None = None):
|
||||
from run_agent import AIAgent
|
||||
cfg = _load_cfg()
|
||||
system_prompt = cfg.get("agent", {}).get("system_prompt", "") or ""
|
||||
if not system_prompt:
|
||||
system_prompt = _resolve_personality_prompt(cfg)
|
||||
return AIAgent(
|
||||
model=_resolve_model(),
|
||||
quiet_mode=True,
|
||||
|
|
@ -1218,16 +1243,36 @@ def _(rid, params: dict) -> dict:
|
|||
else:
|
||||
cfg["custom_prompt"] = value
|
||||
nv = value
|
||||
_save_cfg(cfg)
|
||||
elif key == "personality":
|
||||
cfg.setdefault("display", {})["personality"] = value if value not in ("none", "default", "neutral") else ""
|
||||
pname = value if value not in ("none", "default", "neutral") else ""
|
||||
_write_config_key("display.personality", pname)
|
||||
cfg = _load_cfg()
|
||||
new_prompt = _resolve_personality_prompt(cfg)
|
||||
_write_config_key("agent.system_prompt", new_prompt)
|
||||
nv = value
|
||||
sid_key = params.get("session_id", "")
|
||||
if session:
|
||||
try:
|
||||
new_agent = _make_agent(sid_key, session["session_key"], session_id=session["session_key"])
|
||||
session["agent"] = new_agent
|
||||
with session["history_lock"]:
|
||||
session["history"] = []
|
||||
session["history_version"] = int(session.get("history_version", 0)) + 1
|
||||
except Exception:
|
||||
if session.get("agent"):
|
||||
agent = session["agent"]
|
||||
agent.ephemeral_system_prompt = new_prompt or None
|
||||
agent._cached_system_prompt = None
|
||||
else:
|
||||
cfg.setdefault("display", {})[key] = value
|
||||
_write_config_key(f"display.{key}", value)
|
||||
nv = value
|
||||
_save_cfg(cfg)
|
||||
if key == "skin":
|
||||
_emit("skin.changed", "", resolve_skin())
|
||||
return _ok(rid, {"key": key, "value": nv})
|
||||
if key == "skin":
|
||||
_emit("skin.changed", "", resolve_skin())
|
||||
resp = {"key": key, "value": nv}
|
||||
if key == "personality":
|
||||
resp["cleared"] = True
|
||||
return _ok(rid, resp)
|
||||
except Exception as e:
|
||||
return _err(rid, 5001, str(e))
|
||||
|
||||
|
|
@ -1255,6 +1300,8 @@ def _(rid, params: dict) -> dict:
|
|||
return _ok(rid, {"prompt": _load_cfg().get("custom_prompt", "")})
|
||||
if key == "skin":
|
||||
return _ok(rid, {"value": _load_cfg().get("display", {}).get("skin", "default")})
|
||||
if key == "personality":
|
||||
return _ok(rid, {"value": _load_cfg().get("display", {}).get("personality", "default")})
|
||||
if key == "mtime":
|
||||
cfg_path = _hermes_home / "config.yaml"
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1037,9 +1037,13 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
return
|
||||
}
|
||||
|
||||
if (completions.length && input && historyIdx === null && (key.upArrow || key.downArrow)) {
|
||||
if (completions.length && input && (key.upArrow || key.downArrow)) {
|
||||
setCompIdx(i => (key.upArrow ? (i - 1 + completions.length) % completions.length : (i + 1) % completions.length))
|
||||
|
||||
if (historyIdx !== null) {
|
||||
setHistoryIdx(null)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1839,13 +1843,16 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
|
||||
case 'personality':
|
||||
if (arg) {
|
||||
rpc('config.set', { key: 'personality', value: arg }).then((r: any) =>
|
||||
sys(`personality: ${r.value || 'default'}`)
|
||||
)
|
||||
rpc('config.set', { session_id: sid, key: 'personality', value: arg }).then((r: any) => {
|
||||
if (r?.cleared) {
|
||||
setMessages([])
|
||||
setHistoryItems([])
|
||||
}
|
||||
|
||||
sys(`personality → ${r?.value}`)
|
||||
})
|
||||
} else {
|
||||
gw.request('slash.exec', { command: 'personality', session_id: sid })
|
||||
.then((r: any) => panel('Personality', [{ text: r?.output || '(no output)' }]))
|
||||
.catch(() => sys('personality command failed'))
|
||||
rpc('config.get', { key: 'personality' }).then((r: any) => sys(`personality: ${r?.value || 'default'}`))
|
||||
}
|
||||
|
||||
return true
|
||||
|
|
@ -2273,6 +2280,11 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
|
||||
const submit = useCallback(
|
||||
(value: string) => {
|
||||
if (completions.length && completions[compIdx]) {
|
||||
value = value.slice(0, compReplace) + completions[compIdx].text
|
||||
setInput(value)
|
||||
}
|
||||
|
||||
if (!value.trim() && !inputBuf.length) {
|
||||
const now = Date.now()
|
||||
const dbl = now - lastEmptyAt.current < 450
|
||||
|
|
@ -2330,7 +2342,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
|
||||
dispatchSubmission([...inputBuf, value].join('\n'))
|
||||
},
|
||||
[dequeue, dispatchSubmission, inputBuf, sid]
|
||||
[compIdx, compReplace, completions, dequeue, dispatchSubmission, inputBuf, sid]
|
||||
)
|
||||
|
||||
// ── Derived ──────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ type InkExt = typeof Ink & {
|
|||
}
|
||||
|
||||
const ink = Ink as unknown as InkExt
|
||||
const { Box, Text, useStdin, useInput, stringWidth, useDeclaredCursor, useTerminalFocus } = ink
|
||||
const { Box, Text, useInput, stringWidth, useDeclaredCursor, useTerminalFocus } = ink
|
||||
|
||||
// ── ANSI escapes ─────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -18,7 +18,6 @@ const INV = `${ESC}[7m`
|
|||
const INV_OFF = `${ESC}[27m`
|
||||
const DIM = `${ESC}[2m`
|
||||
const DIM_OFF = `${ESC}[22m`
|
||||
const FWD_DEL_RE = new RegExp(`${ESC}\\[3(?:[~$^]|;)`)
|
||||
const PRINTABLE = /^[ -~\u00a0-\uffff]+$/
|
||||
const BRACKET_PASTE = new RegExp(`${ESC}?\\[20[01]~`, 'g')
|
||||
|
||||
|
|
@ -122,31 +121,6 @@ function renderWithCursor(value: string, cursor: number) {
|
|||
return done ? out : out + invert(' ')
|
||||
}
|
||||
|
||||
// ── Forward-delete detection hook ────────────────────────────────────
|
||||
|
||||
function useFwdDelete(active: boolean) {
|
||||
const ref = useRef(false)
|
||||
const { inputEmitter: ee } = useStdin()
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
return
|
||||
}
|
||||
|
||||
const h = (d: string) => {
|
||||
ref.current = FWD_DEL_RE.test(d)
|
||||
}
|
||||
|
||||
ee.prependListener('input', h)
|
||||
|
||||
return () => {
|
||||
ee.removeListener('input', h)
|
||||
}
|
||||
}, [active, ee])
|
||||
|
||||
return ref
|
||||
}
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface PasteEvent {
|
||||
|
|
@ -171,7 +145,6 @@ interface Props {
|
|||
|
||||
export function TextInput({ columns = 80, value, onChange, onPaste, onSubmit, placeholder = '', focus = true }: Props) {
|
||||
const [cur, setCur] = useState(value.length)
|
||||
const fwdDel = useFwdDelete(focus)
|
||||
const termFocus = useTerminalFocus()
|
||||
|
||||
const curRef = useRef(cur)
|
||||
|
|
@ -364,7 +337,7 @@ export function TextInput({ columns = 80, value, onChange, onPaste, onSubmit, pl
|
|||
}
|
||||
|
||||
// Deletion
|
||||
else if ((k.backspace || k.delete) && !fwdDel.current && c > 0) {
|
||||
else if (k.backspace && c > 0) {
|
||||
if (mod) {
|
||||
const t = wordLeft(v, c)
|
||||
v = v.slice(0, t) + v.slice(c)
|
||||
|
|
@ -373,7 +346,7 @@ export function TextInput({ columns = 80, value, onChange, onPaste, onSubmit, pl
|
|||
v = v.slice(0, c - 1) + v.slice(c)
|
||||
c--
|
||||
}
|
||||
} else if (k.delete && fwdDel.current && c < v.length) {
|
||||
} else if (k.delete && c < v.length) {
|
||||
if (mod) {
|
||||
const t = wordRight(v, c)
|
||||
v = v.slice(0, c) + v.slice(t)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue