Files
hapi/shared/src/modes.ts
T

248 lines
8.6 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 = ['agy', 'claude', 'codex', 'copilot', 'cursor', 'dsh', 'gemini', 'grok', '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 AGY_PERMISSION_MODES = ['request-review', 'always-proceed'] as const
export type AgyPermissionMode = typeof AGY_PERMISSION_MODES[number]
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 COPILOT_PERMISSION_MODES = ['default', 'read-only', 'safe-yolo', 'yolo'] as const
export type CopilotPermissionMode = typeof COPILOT_PERMISSION_MODES[number]
export const GROK_PERMISSION_MODES = ['default', 'auto', 'plan', 'bypassPermissions'] as const
export type GrokPermissionMode = typeof GROK_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', 'autoReview', 'yolo'] as const
export type CursorPermissionMode = typeof CURSOR_PERMISSION_MODES[number]
export const DSH_PERMISSION_MODES = ['default', 'read-only', 'workspace-write', 'danger-full-access'] as const
export type DshPermissionMode = typeof DSH_PERMISSION_MODES[number]
export const PERMISSION_MODES = [
'default',
'acceptEdits',
'auto',
'bypassPermissions',
'plan',
'ask',
'debug',
'autoReview',
'read-only',
'workspace-write',
'danger-full-access',
'safe-yolo',
'yolo',
'request-review',
'always-proceed'
] 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',
autoReview: 'Auto-review',
bypassPermissions: 'Yolo',
'read-only': 'Read Only',
'workspace-write': 'Workspace Write',
'danger-full-access': 'Full Access',
'safe-yolo': 'Safe Yolo',
yolo: 'Yolo',
'request-review': 'Request Review',
'always-proceed': 'Always Proceed'
}
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',
autoReview: 'warning',
bypassPermissions: 'danger',
'read-only': 'warning',
'workspace-write': 'warning',
'danger-full-access': 'danger',
'safe-yolo': 'warning',
yolo: 'danger',
'request-review': 'neutral',
'always-proceed': '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 === 'copilot') {
return COPILOT_PERMISSION_MODES
}
if (flavor === 'grok') {
return GROK_PERMISSION_MODES
}
if (flavor === 'opencode') {
return OPENCODE_PERMISSION_MODES
}
if (flavor === 'agy') {
return AGY_PERMISSION_MODES
}
if (flavor === 'cursor') {
return CURSOR_PERMISSION_MODES
}
if (flavor === 'dsh') {
return DSH_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
}
/** New Codex sessions use the native shared runtime, without HAPI Safe Yolo. */
export function getLaunchPermissionModesForFlavor(flavor?: string | null): readonly PermissionMode[] {
return getPermissionModesForFlavor(flavor).filter(mode => flavor !== 'codex' || mode !== 'safe-yolo')
}
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)
}))
}
/**
* Flavors that can deliver a queued message into an active turn on demand
* (per-message "Steer" from the waiting queue), without waiting for full-turn end.
*
* Steer = soft mid-turn delivery (same idea as Cursor GUI default "Send"):
* - Pi: native queued-message steer
* - Codex: app-server `turn/steer` (true mid-turn inject)
* - Cursor ACP: concurrent `session/prompt` soft-send (no cancel). Legacy
* stream-json Cursor sessions are NOT steerable — gate with
* {@link isSteeringSupportedForSession}.
* - DeepSeek Harness: native queued-message steer
*
* Claude / others: not supported (no reachable soft-steer path) — UI hides Steer.
*/
export const STEERING_SUPPORTED_FLAVORS = ['pi', 'codex', 'cursor', 'dsh'] as const
export function isSteeringSupportedForFlavor(flavor?: string | null): boolean {
return (STEERING_SUPPORTED_FLAVORS as readonly string[]).includes(flavor ?? '')
}
/**
* Session-aware steer gate. Prefer this over {@link isSteeringSupportedForFlavor}
* when metadata is available: legacy Cursor stream-json sessions advertise
* flavor `cursor` but cannot steer.
*
* Matches CLI legacy detection: explicit `stream-json`, or a pre-ACP session
* that has `cursorSessionId` without `cursorSessionProtocol: 'acp'`.
*/
export function isSteeringSupportedForSession(metadata?: {
flavor?: string | null
cursorSessionId?: string | null
cursorSessionProtocol?: 'acp' | 'stream-json' | null
} | null): boolean {
if (metadata?.flavor === 'pi' || metadata?.flavor === 'codex' || metadata?.flavor === 'dsh') {
return true
}
if (metadata?.flavor !== 'cursor') {
return false
}
if (metadata.cursorSessionProtocol === 'stream-json') {
return false
}
if (!metadata.cursorSessionProtocol && metadata.cursorSessionId) {
return false
}
return true
}