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:
SSU-WEI HUANG
2026-06-29 11:42:41 +08:00
committed by GitHub
co-authored by HAPI Claude Opus 4.8
parent 26a24bb6ce
commit b44885ae67
28 changed files with 182 additions and 2170 deletions
+21
View File
@@ -1,5 +1,8 @@
import { describe, expect, it, test } from 'bun:test'
import {
AGENT_FLAVORS,
AgentFlavorSchema,
CREATABLE_AGENT_FLAVORS,
getPermissionModeLabel,
getPermissionModeOptionsForFlavor,
getPermissionModeTone,
@@ -7,6 +10,24 @@ import {
isPermissionModeAllowedForFlavor,
} from './modes'
describe('Gemini CLI sunset (read-only, not creatable)', () => {
test('gemini stays a valid flavor so existing stored sessions still validate/load', () => {
expect(AGENT_FLAVORS).toContain('gemini')
expect(AgentFlavorSchema.safeParse('gemini').success).toBe(true)
})
test('gemini is excluded from creatable flavors (not offered for new sessions)', () => {
expect(CREATABLE_AGENT_FLAVORS).not.toContain('gemini')
})
test('all other flavors remain creatable', () => {
for (const flavor of AGENT_FLAVORS) {
if (flavor === 'gemini') continue
expect(CREATABLE_AGENT_FLAVORS).toContain(flavor)
}
})
})
describe('getPermissionModesForFlavor', () => {
test("returns [] for flavor 'pi' (RPC mode has no runtime permission switching)", () => {
expect(getPermissionModesForFlavor('pi')).toEqual([])
+8
View File
@@ -11,6 +11,14 @@ export const AGENT_FLAVORS = ['claude', 'codex', 'cursor', 'gemini', 'kimi', 'op
export type AgentFlavor = typeof AGENT_FLAVORS[number]
export const AgentFlavorSchema = z.enum(AGENT_FLAVORS)
// Flavors offered when CREATING a new session. Gemini CLI is intentionally
// excluded: Google sunset the consumer Gemini CLI (2026-06-18) so it can no
// longer be launched. It is kept in AGENT_FLAVORS / AgentFlavorSchema above so
// existing stored Gemini sessions still validate and remain viewable.
export const CREATABLE_AGENT_FLAVORS: readonly AgentFlavor[] = AGENT_FLAVORS.filter(
(flavor) => flavor !== 'gemini'
)
export const CLAUDE_PERMISSION_MODES = ['default', 'acceptEdits', 'auto', 'bypassPermissions', 'plan'] as const
export type ClaudePermissionMode = typeof CLAUDE_PERMISSION_MODES[number]