mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-08 07:17:39 +00:00
feat(gemini): remove launchable Gemini CLI agent, keep old sessions readable (#953)
* feat(gemini): remove launchable Gemini CLI agent, keep sessions readable Google sunset the consumer Gemini CLI (Pro/Ultra/free tiers stopped serving requests 2026-06-18). This removes the ability to launch/create Gemini CLI sessions while keeping existing stored Gemini sessions fully readable in the web UI. Removed (no longer launchable): - cli/src/gemini/ runtime (runGemini, loop, local/remote launchers, session, ACP backend, config, scanner) + GeminiDisplay ink view - `hapi gemini` command + registry entry + usage line - runner spawn branch & buildCliArgs mapping now reject gemini with a clear error; resume dispatch throws a clear "no longer supported" error - gemini dropped from the new-session agent selector via new CREATABLE_AGENT_FLAVORS, and from preferred-agent defaults Kept (read path — existing sessions still validate, load, render): - `gemini` in AGENT_FLAVORS / AgentFlavorSchema, FLAVOR_CAPS / FLAVOR_LABELS - AgentFlavorIcon badge, model-option labels, ACP message normalization, metadata.geminiSessionId, hub session dedup/resume-id Note: the Gemini *Live voice* backend is a separate feature and is untouched. Adds read-guarantee tests (stored gemini validates; excluded from creatable). typecheck + full suite green. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(gemini): reject gemini resume before handoff (#953 review) HAPI Bot [Major]: `hapi resume <active-gemini-session>` called handoffSessionToLocal() — which tells the running remote agent to exit — before reaching the gemini-unsupported throw in dispatchLocalResume, so it could stop the live/readable session and then fail locally. Move the gemini guard into resumeCommand.run before the handoff, so an active Gemini session is left running/readable instead of being stopped. Keep the dispatch-layer guard as defense-in-depth. Adds a regression test asserting handoffSessionToLocal is not called for an active gemini target. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(gemini): harden against stale gemini input (#953 review) Two [Minor] follow-ups from HAPI Bot: - newSessionFormDraft: coerce a restored browse draft's agent to a creatable flavor, so a pre-removal 'gemini' draft cannot submit agent:'gemini' even though the selector no longer offers it. - buildCliArgs: reject 'gemini' explicitly instead of silently falling through to the 'claude' command if the exported helper is reused outside the guarded spawnSession path. Updated the buildCliArgs precedence test to a creatable agent and added a test asserting buildCliArgs('gemini') throws. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(gemini): reset dependent draft fields when coercing stale agent (#953 review) Follow-up [Minor]: coercing a stale gemini draft's agent to claude left model/base/effort untouched, so a { agent:'gemini', model:'gemini-2.5-pro' } draft restored as claude *with* a Gemini model, which handleCreate() then sent to the runner. Now reset model / cursorSelectedBase / effort / modelReasoningEffort to defaults whenever the agent is coerced. Adds a regression test. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(gemini): tombstone `hapi gemini` so it errors clearly (#953 review) HAPI Bot [Major]: after removing geminiCommand from the registry, resolveCommand() treats `gemini` as an unknown subcommand and falls through to the default Claude command (forwarding "gemini" as an arg), so `hapi gemini` silently started Claude instead of reporting the sunset. Add an explicit tombstone `gemini` command that prints the sunset error and exits 1. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(web): assert AgentSelector hides the sunset Gemini agent (#953) Render regression test confirming the new-session AgentSelector offers exactly CREATABLE_AGENT_FLAVORS and never shows a Gemini radio. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: HAPI <noreply@hapi.run> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
HAPI
Claude Opus 4.8
parent
26a24bb6ce
commit
b44885ae67
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render } from '@testing-library/react'
|
||||
import { CREATABLE_AGENT_FLAVORS } from '@hapi/protocol'
|
||||
|
||||
vi.mock('@/lib/use-translation', () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}))
|
||||
|
||||
import { AgentSelector } from './AgentSelector'
|
||||
import type { AgentType } from './types'
|
||||
|
||||
function renderedAgentValues(): string[] {
|
||||
const { container } = render(
|
||||
<AgentSelector agent={'claude' as AgentType} isDisabled={false} onAgentChange={() => {}} />
|
||||
)
|
||||
return Array.from(container.querySelectorAll('input[type="radio"]'))
|
||||
.map((el) => (el as HTMLInputElement).value)
|
||||
}
|
||||
|
||||
describe('AgentSelector', () => {
|
||||
it('does not offer the sunset Gemini CLI as a new-session agent', () => {
|
||||
expect(renderedAgentValues()).not.toContain('gemini')
|
||||
})
|
||||
|
||||
it('offers exactly the creatable agent flavors', () => {
|
||||
expect(renderedAgentValues()).toEqual([...CREATABLE_AGENT_FLAVORS])
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AGENT_FLAVORS } from '@hapi/protocol'
|
||||
import { CREATABLE_AGENT_FLAVORS } from '@hapi/protocol'
|
||||
import type { AgentType } from './types'
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
|
||||
@@ -15,7 +15,7 @@ export function AgentSelector(props: {
|
||||
{t('newSession.agent')}
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-2">
|
||||
{AGENT_FLAVORS.map((agentType) => (
|
||||
{CREATABLE_AGENT_FLAVORS.map((agentType) => (
|
||||
<label
|
||||
key={agentType}
|
||||
className="flex items-center gap-1.5 cursor-pointer"
|
||||
|
||||
@@ -80,4 +80,29 @@ describe('newSessionFormDraft', () => {
|
||||
const draft = loadNewSessionFormDraft()!
|
||||
expect(newSessionDraftMatchesMachine(draft, 'machine-b')).toBe(false)
|
||||
})
|
||||
|
||||
it('coerces a stale uncreatable agent (gemini) to claude and resets dependent fields', () => {
|
||||
saveNewSessionFormDraft({
|
||||
agent: 'gemini',
|
||||
model: 'gemini-2.5-pro',
|
||||
cursorSelectedBase: 'composer-2.5',
|
||||
machineId: 'machine-1',
|
||||
effort: 'high',
|
||||
modelReasoningEffort: 'high',
|
||||
yoloMode: true,
|
||||
sessionType: 'simple',
|
||||
worktreeName: ''
|
||||
})
|
||||
|
||||
const loaded = loadNewSessionFormDraft()!
|
||||
expect(loaded.agent).toBe('claude')
|
||||
// agent-dependent fields reset so a Gemini model isn't carried into Claude
|
||||
expect(loaded.model).toBe('auto')
|
||||
expect(loaded.cursorSelectedBase).toBe('auto')
|
||||
expect(loaded.effort).toBe('auto')
|
||||
expect(loaded.modelReasoningEffort).toBe('default')
|
||||
// agent-independent fields preserved
|
||||
expect(loaded.yoloMode).toBe(true)
|
||||
expect(loaded.machineId).toBe('machine-1')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CREATABLE_AGENT_FLAVORS } from '@hapi/protocol'
|
||||
import type { AgentType, ClaudeEffort, CodexReasoningEffort, SessionType } from './types'
|
||||
|
||||
const DRAFT_STORAGE_KEY = 'hapi:new-session-form-draft'
|
||||
@@ -32,13 +33,25 @@ export function loadNewSessionFormDraft(): NewSessionFormDraft | null {
|
||||
if (typeof parsed.agent !== 'string' || typeof parsed.model !== 'string') {
|
||||
return null
|
||||
}
|
||||
// Coerce a stale/uncreatable agent (e.g. a pre-removal 'gemini' draft)
|
||||
// back to a launchable default. When the agent is coerced, also drop the
|
||||
// agent-dependent fields (model / cursor base / effort) so a Gemini
|
||||
// draft does not carry a Gemini model into the Claude fallback.
|
||||
const restoredAgent: AgentType = (CREATABLE_AGENT_FLAVORS as readonly string[]).includes(parsed.agent)
|
||||
? (parsed.agent as AgentType)
|
||||
: 'claude'
|
||||
const agentPreserved = restoredAgent === parsed.agent
|
||||
return {
|
||||
agent: parsed.agent as AgentType,
|
||||
model: parsed.model,
|
||||
cursorSelectedBase: typeof parsed.cursorSelectedBase === 'string' ? parsed.cursorSelectedBase : 'auto',
|
||||
agent: restoredAgent,
|
||||
model: agentPreserved ? parsed.model : 'auto',
|
||||
cursorSelectedBase: agentPreserved && typeof parsed.cursorSelectedBase === 'string'
|
||||
? parsed.cursorSelectedBase
|
||||
: 'auto',
|
||||
machineId: typeof parsed.machineId === 'string' ? parsed.machineId : null,
|
||||
effort: (parsed.effort as ClaudeEffort | undefined) ?? 'auto',
|
||||
modelReasoningEffort: (parsed.modelReasoningEffort as CodexReasoningEffort | undefined) ?? 'default',
|
||||
effort: agentPreserved ? ((parsed.effort as ClaudeEffort | undefined) ?? 'auto') : 'auto',
|
||||
modelReasoningEffort: agentPreserved
|
||||
? ((parsed.modelReasoningEffort as CodexReasoningEffort | undefined) ?? 'default')
|
||||
: 'default',
|
||||
yoloMode: Boolean(parsed.yoloMode),
|
||||
sessionType: (parsed.sessionType as SessionType | undefined) ?? 'simple',
|
||||
worktreeName: typeof parsed.worktreeName === 'string' ? parsed.worktreeName : ''
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { AGENT_FLAVORS } from '@hapi/protocol'
|
||||
import { CREATABLE_AGENT_FLAVORS } from '@hapi/protocol'
|
||||
import type { AgentType } from './types'
|
||||
|
||||
const AGENT_STORAGE_KEY = 'hapi:newSession:agent'
|
||||
const YOLO_STORAGE_KEY = 'hapi:newSession:yolo'
|
||||
|
||||
const VALID_AGENTS = AGENT_FLAVORS
|
||||
// Only launchable flavors are valid defaults; a stale 'gemini' preference
|
||||
// (no longer creatable) falls back to 'claude'.
|
||||
const VALID_AGENTS = CREATABLE_AGENT_FLAVORS
|
||||
|
||||
export function loadPreferredAgent(): AgentType {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user