mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
feat(tui): add inline provider disconnect via 'd' keybind in /model picker
- New model.disconnect RPC method: clears API key env vars from .env and OAuth/credential pool state via clear_provider_auth() - Press 'd' on an authenticated provider opens confirmation prompt - y/Enter confirms disconnect, n/Esc cancels - Provider flips to unauthenticated state in-place (re-selectable to re-auth by pressing Enter again)
This commit is contained in:
parent
26f7f68507
commit
f4c761c6a0
2 changed files with 129 additions and 3 deletions
|
|
@ -4868,6 +4868,49 @@ def _(rid, params: dict) -> dict:
|
||||||
return _err(rid, 5034, str(e))
|
return _err(rid, 5034, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@method("model.disconnect")
|
||||||
|
def _(rid, params: dict) -> dict:
|
||||||
|
"""Remove credentials for a provider.
|
||||||
|
|
||||||
|
Params:
|
||||||
|
slug: provider slug (e.g. "deepseek", "xai")
|
||||||
|
|
||||||
|
Returns success status and the provider's slug.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from hermes_cli.auth import PROVIDER_REGISTRY, clear_provider_auth
|
||||||
|
from hermes_cli.config import remove_env_value
|
||||||
|
|
||||||
|
slug = (params.get("slug") or "").strip()
|
||||||
|
if not slug:
|
||||||
|
return _err(rid, 4001, "slug is required")
|
||||||
|
|
||||||
|
pconfig = PROVIDER_REGISTRY.get(slug)
|
||||||
|
cleared_env = False
|
||||||
|
cleared_auth = False
|
||||||
|
|
||||||
|
# Remove API key env vars from .env and process
|
||||||
|
if pconfig and pconfig.api_key_env_vars:
|
||||||
|
for ev in pconfig.api_key_env_vars:
|
||||||
|
if remove_env_value(ev):
|
||||||
|
cleared_env = True
|
||||||
|
|
||||||
|
# Clear OAuth / credential pool state
|
||||||
|
cleared_auth = clear_provider_auth(slug)
|
||||||
|
|
||||||
|
if not cleared_env and not cleared_auth:
|
||||||
|
return _err(rid, 4005, f"no credentials found for {slug}")
|
||||||
|
|
||||||
|
provider_name = pconfig.name if pconfig else slug
|
||||||
|
return _ok(rid, {
|
||||||
|
"slug": slug,
|
||||||
|
"name": provider_name,
|
||||||
|
"disconnected": True,
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return _err(rid, 5035, str(e))
|
||||||
|
|
||||||
|
|
||||||
# ── Methods: slash.exec ──────────────────────────────────────────────
|
# ── Methods: slash.exec ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ const VISIBLE = 12
|
||||||
const MIN_WIDTH = 40
|
const MIN_WIDTH = 40
|
||||||
const MAX_WIDTH = 90
|
const MAX_WIDTH = 90
|
||||||
|
|
||||||
type Stage = 'provider' | 'key' | 'model'
|
type Stage = 'provider' | 'key' | 'model' | 'disconnect'
|
||||||
|
|
||||||
export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPickerProps) {
|
export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPickerProps) {
|
||||||
const [providers, setProviders] = useState<ModelOptionProvider[]>([])
|
const [providers, setProviders] = useState<ModelOptionProvider[]>([])
|
||||||
|
|
@ -73,7 +73,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
||||||
const names = useMemo(() => providerDisplayNames(providers), [providers])
|
const names = useMemo(() => providerDisplayNames(providers), [providers])
|
||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
if (stage === 'model' || stage === 'key') {
|
if (stage === 'model' || stage === 'key' || stage === 'disconnect') {
|
||||||
setStage('provider')
|
setStage('provider')
|
||||||
setModelIdx(0)
|
setModelIdx(0)
|
||||||
setKeyInput('')
|
setKeyInput('')
|
||||||
|
|
@ -153,6 +153,53 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disconnect confirmation stage
|
||||||
|
if (stage === 'disconnect') {
|
||||||
|
if (ch.toLowerCase() === 'y' || key.return) {
|
||||||
|
if (!provider) {
|
||||||
|
setStage('provider')
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setKeySaving(true)
|
||||||
|
gw.request<{ disconnected?: boolean }>('model.disconnect', {
|
||||||
|
slug: provider.slug,
|
||||||
|
...(sessionId ? { session_id: sessionId } : {}),
|
||||||
|
})
|
||||||
|
.then(raw => {
|
||||||
|
const r = asRpcResult<{ disconnected?: boolean }>(raw)
|
||||||
|
|
||||||
|
if (r?.disconnected) {
|
||||||
|
// Mark provider as unauthenticated in local state
|
||||||
|
setProviders(prev =>
|
||||||
|
prev.map(p => p.slug === provider.slug
|
||||||
|
? { ...p, authenticated: false, models: [], total_models: 0, warning: p.key_env ? `paste ${p.key_env} to activate` : 'run `hermes model` to configure' }
|
||||||
|
: p
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setKeySaving(false)
|
||||||
|
setStage('provider')
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setKeySaving(false)
|
||||||
|
setStage('provider')
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch.toLowerCase() === 'n' || key.escape) {
|
||||||
|
setStage('provider')
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const count = stage === 'provider' ? providers.length : models.length
|
const count = stage === 'provider' ? providers.length : models.length
|
||||||
const sel = stage === 'provider' ? providerIdx : modelIdx
|
const sel = stage === 'provider' ? providerIdx : modelIdx
|
||||||
const setSel = stage === 'provider' ? setProviderIdx : setModelIdx
|
const setSel = stage === 'provider' ? setProviderIdx : setModelIdx
|
||||||
|
|
@ -210,6 +257,13 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disconnect: only in provider stage, only for authenticated providers
|
||||||
|
if (ch.toLowerCase() === 'd' && stage === 'provider' && provider?.authenticated !== false) {
|
||||||
|
setStage('disconnect')
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const n = ch === '0' ? 10 : parseInt(ch, 10)
|
const n = ch === '0' ? 10 : parseInt(ch, 10)
|
||||||
|
|
||||||
if (!Number.isNaN(n) && n >= 1 && n <= Math.min(10, count)) {
|
if (!Number.isNaN(n) && n >= 1 && n <= Math.min(10, count)) {
|
||||||
|
|
@ -294,6 +348,35 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Disconnect confirmation stage ─────────────────────────────────────
|
||||||
|
if (stage === 'disconnect' && provider) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" width={width}>
|
||||||
|
<Text bold color={t.color.accent} wrap="truncate-end">
|
||||||
|
Disconnect {provider.name}?
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text color={t.color.muted} wrap="truncate-end"> </Text>
|
||||||
|
|
||||||
|
<Text color={t.color.muted} wrap="truncate-end">
|
||||||
|
This removes saved credentials for {provider.name}.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text color={t.color.muted} wrap="truncate-end">
|
||||||
|
You can re-authenticate later by selecting it again.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text color={t.color.muted} wrap="truncate-end"> </Text>
|
||||||
|
|
||||||
|
{keySaving ? (
|
||||||
|
<Text color={t.color.muted} wrap="truncate-end">disconnecting…</Text>
|
||||||
|
) : (
|
||||||
|
<OverlayHint t={t}>y/Enter confirm · n/Esc cancel</OverlayHint>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Provider selection stage ─────────────────────────────────────────
|
// ── Provider selection stage ─────────────────────────────────────────
|
||||||
if (stage === 'provider') {
|
if (stage === 'provider') {
|
||||||
const rows = providers.map(
|
const rows = providers.map(
|
||||||
|
|
@ -359,7 +442,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
||||||
<Text color={t.color.muted} wrap="truncate-end">
|
<Text color={t.color.muted} wrap="truncate-end">
|
||||||
persist: {persistGlobal ? 'global' : 'session'} · g toggle
|
persist: {persistGlobal ? 'global' : 'session'} · g toggle
|
||||||
</Text>
|
</Text>
|
||||||
<OverlayHint t={t}>↑/↓ select · Enter choose · Esc/q cancel</OverlayHint>
|
<OverlayHint t={t}>↑/↓ select · Enter choose · d disconnect · Esc/q cancel</OverlayHint>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue