mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
* 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>
156 lines
5.1 KiB
TypeScript
156 lines
5.1 KiB
TypeScript
import { z } from 'zod'
|
|
|
|
/**
|
|
* @description The legacy payload type identifier used for all generic agent messages.
|
|
* Changing this value will affect the communication schema between CLI, Hub, and Web.
|
|
* A migration plan is required if this literal is ever modified.
|
|
*/
|
|
export const AGENT_MESSAGE_PAYLOAD_TYPE = 'codex' as const
|
|
|
|
export const AGENT_FLAVORS = ['claude', 'codex', 'cursor', 'gemini', 'kimi', 'opencode', 'pi'] as const
|
|
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]
|
|
|
|
export const CODEX_PERMISSION_MODES = ['default', 'read-only', 'safe-yolo', 'yolo'] as const
|
|
export type CodexPermissionMode = typeof CODEX_PERMISSION_MODES[number]
|
|
|
|
export const CODEX_COLLABORATION_MODES = ['default', 'plan'] as const
|
|
export type CodexCollaborationMode = typeof CODEX_COLLABORATION_MODES[number]
|
|
|
|
export const GEMINI_PERMISSION_MODES = ['default', 'read-only', 'safe-yolo', 'yolo'] as const
|
|
export type GeminiPermissionMode = typeof GEMINI_PERMISSION_MODES[number]
|
|
|
|
export const KIMI_PERMISSION_MODES = ['default', 'read-only', 'safe-yolo', 'yolo'] as const
|
|
export type KimiPermissionMode = typeof KIMI_PERMISSION_MODES[number]
|
|
|
|
export const OPENCODE_PERMISSION_MODES = ['default', 'plan', 'yolo'] as const
|
|
export type OpencodePermissionMode = typeof OPENCODE_PERMISSION_MODES[number]
|
|
|
|
export const CURSOR_PERMISSION_MODES = ['default', 'plan', 'ask', 'debug', 'yolo'] as const
|
|
export type CursorPermissionMode = typeof CURSOR_PERMISSION_MODES[number]
|
|
|
|
export const PERMISSION_MODES = [
|
|
'default',
|
|
'acceptEdits',
|
|
'auto',
|
|
'bypassPermissions',
|
|
'plan',
|
|
'ask',
|
|
'debug',
|
|
'read-only',
|
|
'safe-yolo',
|
|
'yolo'
|
|
] as const
|
|
export type PermissionMode = typeof PERMISSION_MODES[number]
|
|
|
|
|
|
export const PERMISSION_MODE_LABELS: Record<PermissionMode, string> = {
|
|
default: 'Default',
|
|
acceptEdits: 'Accept Edits',
|
|
auto: 'Auto',
|
|
plan: 'Plan Mode',
|
|
ask: 'Ask Mode',
|
|
debug: 'Debug Mode',
|
|
bypassPermissions: 'Yolo',
|
|
'read-only': 'Read Only',
|
|
'safe-yolo': 'Safe Yolo',
|
|
yolo: 'Yolo'
|
|
}
|
|
|
|
export type PermissionModeTone = 'neutral' | 'info' | 'warning' | 'danger'
|
|
|
|
export const PERMISSION_MODE_TONES: Record<PermissionMode, PermissionModeTone> = {
|
|
default: 'neutral',
|
|
acceptEdits: 'warning',
|
|
auto: 'warning',
|
|
plan: 'info',
|
|
ask: 'info',
|
|
debug: 'info',
|
|
bypassPermissions: 'danger',
|
|
'read-only': 'warning',
|
|
'safe-yolo': 'warning',
|
|
yolo: 'danger'
|
|
}
|
|
|
|
export type PermissionModeOption = {
|
|
mode: PermissionMode
|
|
label: string
|
|
tone: PermissionModeTone
|
|
}
|
|
|
|
export type CodexCollaborationModeOption = {
|
|
mode: CodexCollaborationMode
|
|
label: string
|
|
}
|
|
|
|
export const CODEX_COLLABORATION_MODE_LABELS: Record<CodexCollaborationMode, string> = {
|
|
default: 'Default',
|
|
plan: 'Plan'
|
|
}
|
|
|
|
export function getPermissionModeLabel(mode: PermissionMode): string {
|
|
return PERMISSION_MODE_LABELS[mode]
|
|
}
|
|
|
|
export function getPermissionModeTone(mode: PermissionMode): PermissionModeTone {
|
|
return PERMISSION_MODE_TONES[mode]
|
|
}
|
|
|
|
export function getCodexCollaborationModeLabel(mode: CodexCollaborationMode): string {
|
|
return CODEX_COLLABORATION_MODE_LABELS[mode]
|
|
}
|
|
|
|
export function getPermissionModesForFlavor(flavor?: string | null): readonly PermissionMode[] {
|
|
if (flavor === 'codex') {
|
|
return CODEX_PERMISSION_MODES
|
|
}
|
|
if (flavor === 'gemini') {
|
|
return GEMINI_PERMISSION_MODES
|
|
}
|
|
if (flavor === 'kimi') {
|
|
return KIMI_PERMISSION_MODES
|
|
}
|
|
if (flavor === 'opencode') {
|
|
return OPENCODE_PERMISSION_MODES
|
|
}
|
|
if (flavor === 'cursor') {
|
|
return CURSOR_PERMISSION_MODES
|
|
}
|
|
if (flavor === 'pi') {
|
|
// Pi RPC mode has no runtime permission switching (always auto-approve);
|
|
// no permission modes are offered.
|
|
return []
|
|
}
|
|
return CLAUDE_PERMISSION_MODES
|
|
}
|
|
|
|
export function getPermissionModeOptionsForFlavor(flavor?: string | null): PermissionModeOption[] {
|
|
return getPermissionModesForFlavor(flavor).map((mode) => ({
|
|
mode,
|
|
label: getPermissionModeLabel(mode),
|
|
tone: getPermissionModeTone(mode)
|
|
}))
|
|
}
|
|
|
|
export function isPermissionModeAllowedForFlavor(mode: PermissionMode, flavor?: string | null): boolean {
|
|
return getPermissionModesForFlavor(flavor).includes(mode)
|
|
}
|
|
|
|
export function getCodexCollaborationModeOptions(): CodexCollaborationModeOption[] {
|
|
return CODEX_COLLABORATION_MODES.map((mode) => ({
|
|
mode,
|
|
label: getCodexCollaborationModeLabel(mode)
|
|
}))
|
|
}
|