mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat: add Grok Build support (#1030)
* test: define Grok Build integration behavior * feat: add Grok Build agent integration * test: cover Grok permissions and resume paths * docs: add Grok Build setup guide * fix: scope Grok ACP discovery to session cwd * fix: align Grok permission UI semantics * docs: clarify Grok runner setup * test: require Grok create model and effort options * feat: add Grok create model and effort pickers * test: define Grok runtime parity behavior * feat: add Grok runtime ACP controls and discovery * fix: tighten Grok runtime controls * fix: suppress nonfatal Grok title quota errors * feat: support Grok Auto permission mode * feat: forward ACP native session titles for Grok * fix: guard Grok Windows shell arguments
This commit is contained in:
@@ -251,6 +251,7 @@ export const SpawnSessionRequestSchema = z.object({
|
||||
effort: z.string().optional(),
|
||||
modelReasoningEffort: z.string().optional(),
|
||||
yolo: z.boolean().optional(),
|
||||
permissionMode: PermissionModeSchema.optional(),
|
||||
sessionType: z.enum(['simple', 'worktree']).optional(),
|
||||
worktreeName: z.string().optional()
|
||||
})
|
||||
@@ -376,6 +377,34 @@ export type OpencodeModelsResponse = {
|
||||
|
||||
export type ListOpencodeModelsResponse = OpencodeModelsResponse
|
||||
|
||||
export type GrokModelSummary = {
|
||||
modelId: string
|
||||
name?: string
|
||||
reasoningEfforts?: GrokReasoningEffortOption[]
|
||||
}
|
||||
|
||||
export type GrokReasoningEffortOption = {
|
||||
value: string
|
||||
name?: string
|
||||
isDefault?: boolean
|
||||
}
|
||||
|
||||
export type GrokModelsResponse = {
|
||||
success: boolean
|
||||
availableModels?: GrokModelSummary[]
|
||||
currentModelId?: string | null
|
||||
autoPermissionModeSupported?: boolean
|
||||
error?: string
|
||||
}
|
||||
export type ListGrokModelsResponse = GrokModelsResponse
|
||||
|
||||
export type GrokReasoningEffortResponse = {
|
||||
success: boolean
|
||||
options?: GrokReasoningEffortOption[]
|
||||
currentValue?: string | null
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type OpencodeReasoningEffortOption = {
|
||||
value: string
|
||||
name?: string
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
isKnownFlavor,
|
||||
supportsEffort,
|
||||
supportsModelChange,
|
||||
isCodexFamilyFlavor,
|
||||
} from './flavors'
|
||||
|
||||
describe('hasCapability', () => {
|
||||
@@ -47,6 +48,11 @@ describe('hasCapability', () => {
|
||||
expect(hasCapability('kimi', Capabilities.Effort)).toBe(false)
|
||||
})
|
||||
|
||||
test('grok supports runtime model and effort switching through ACP', () => {
|
||||
expect(hasCapability('grok', Capabilities.ModelChange)).toBe(true)
|
||||
expect(hasCapability('grok', Capabilities.Effort)).toBe(true)
|
||||
})
|
||||
|
||||
test('unknown flavor returns false', () => {
|
||||
expect(hasCapability('unknown-flavor', Capabilities.ModelChange)).toBe(false)
|
||||
})
|
||||
@@ -66,6 +72,7 @@ describe('getFlavorLabel', () => {
|
||||
expect(getFlavorLabel('opencode')).toBe('OpenCode')
|
||||
expect(getFlavorLabel('pi')).toBe('Pi')
|
||||
expect(getFlavorLabel('kimi')).toBe('Kimi')
|
||||
expect(getFlavorLabel('grok')).toBe('Grok Build')
|
||||
})
|
||||
|
||||
test('unknown flavor returns Unknown', () => {
|
||||
@@ -87,6 +94,7 @@ describe('isKnownFlavor', () => {
|
||||
expect(isKnownFlavor('opencode')).toBe(true)
|
||||
expect(isKnownFlavor('pi')).toBe(true)
|
||||
expect(isKnownFlavor('kimi')).toBe(true)
|
||||
expect(isKnownFlavor('grok')).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for unknown/null/undefined', () => {
|
||||
@@ -97,6 +105,10 @@ describe('isKnownFlavor', () => {
|
||||
})
|
||||
|
||||
describe('convenience functions', () => {
|
||||
test('treats Grok as a generic ACP/Codex-family permission flow', () => {
|
||||
expect(isCodexFamilyFlavor('grok')).toBe(true)
|
||||
})
|
||||
|
||||
test('supportsModelChange matches hasCapability', () => {
|
||||
expect(supportsModelChange('claude')).toBe(true)
|
||||
expect(supportsModelChange('gemini')).toBe(true)
|
||||
@@ -113,6 +125,7 @@ describe('convenience functions', () => {
|
||||
expect(supportsEffort('codex')).toBe(false)
|
||||
expect(supportsEffort('gemini')).toBe(false)
|
||||
expect(supportsEffort('pi')).toBe(true)
|
||||
expect(supportsEffort('grok')).toBe(true)
|
||||
expect(supportsEffort('kimi')).toBe(false)
|
||||
expect(supportsEffort(null)).toBe(false)
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ const FLAVOR_CAPS: Record<AgentFlavor, ReadonlySet<Capability>> = {
|
||||
claude: new Set([Capabilities.ModelChange, Capabilities.Effort]),
|
||||
gemini: new Set([Capabilities.ModelChange]),
|
||||
kimi: new Set([Capabilities.ModelChange]),
|
||||
grok: new Set([Capabilities.ModelChange, Capabilities.Effort]),
|
||||
codex: new Set([Capabilities.ModelChange]),
|
||||
cursor: new Set([Capabilities.ModelChange]),
|
||||
opencode: new Set([Capabilities.ModelChange]),
|
||||
@@ -24,6 +25,7 @@ const FLAVOR_LABELS: Record<AgentFlavor, string> = {
|
||||
claude: 'Claude',
|
||||
gemini: 'Gemini',
|
||||
kimi: 'Kimi',
|
||||
grok: 'Grok Build',
|
||||
codex: 'Codex',
|
||||
cursor: 'Cursor',
|
||||
opencode: 'OpenCode',
|
||||
@@ -55,5 +57,9 @@ export function supportsEffort(flavor: string | null | undefined): boolean {
|
||||
}
|
||||
|
||||
export function isCodexFamilyFlavor(flavor: string | null | undefined): boolean {
|
||||
return flavor === 'codex' || flavor === 'gemini' || flavor === 'kimi' || flavor === 'opencode'
|
||||
return flavor === 'codex'
|
||||
|| flavor === 'gemini'
|
||||
|| flavor === 'grok'
|
||||
|| flavor === 'kimi'
|
||||
|| flavor === 'opencode'
|
||||
}
|
||||
|
||||
@@ -29,6 +29,15 @@ describe('Gemini CLI sunset (read-only, not creatable)', () => {
|
||||
})
|
||||
|
||||
describe('getPermissionModesForFlavor', () => {
|
||||
test("returns the conservative Grok modes", () => {
|
||||
expect(getPermissionModesForFlavor('grok')).toEqual([
|
||||
'default',
|
||||
'auto',
|
||||
'plan',
|
||||
'bypassPermissions'
|
||||
])
|
||||
})
|
||||
|
||||
test("returns [] for flavor 'pi' (RPC mode has no runtime permission switching)", () => {
|
||||
expect(getPermissionModesForFlavor('pi')).toEqual([])
|
||||
})
|
||||
@@ -54,6 +63,15 @@ describe('getPermissionModeOptionsForFlavor', () => {
|
||||
})
|
||||
|
||||
describe('isPermissionModeAllowedForFlavor', () => {
|
||||
test("allows only the supported Grok modes", () => {
|
||||
expect(isPermissionModeAllowedForFlavor('default', 'grok')).toBe(true)
|
||||
expect(isPermissionModeAllowedForFlavor('plan', 'grok')).toBe(true)
|
||||
expect(isPermissionModeAllowedForFlavor('bypassPermissions', 'grok')).toBe(true)
|
||||
expect(isPermissionModeAllowedForFlavor('acceptEdits', 'grok')).toBe(false)
|
||||
expect(isPermissionModeAllowedForFlavor('auto', 'grok')).toBe(true)
|
||||
expect(isPermissionModeAllowedForFlavor('yolo', 'grok')).toBe(false)
|
||||
})
|
||||
|
||||
test("no mode is allowed for pi", () => {
|
||||
expect(isPermissionModeAllowedForFlavor('yolo', 'pi')).toBe(false)
|
||||
expect(isPermissionModeAllowedForFlavor('default', 'pi')).toBe(false)
|
||||
|
||||
+7
-1
@@ -7,7 +7,7 @@ import { z } from 'zod'
|
||||
*/
|
||||
export const AGENT_MESSAGE_PAYLOAD_TYPE = 'codex' as const
|
||||
|
||||
export const AGENT_FLAVORS = ['claude', 'codex', 'cursor', 'gemini', 'kimi', 'opencode', 'pi'] as const
|
||||
export const AGENT_FLAVORS = ['claude', 'codex', 'cursor', 'gemini', 'grok', 'kimi', 'opencode', 'pi'] as const
|
||||
export type AgentFlavor = typeof AGENT_FLAVORS[number]
|
||||
export const AgentFlavorSchema = z.enum(AGENT_FLAVORS)
|
||||
|
||||
@@ -34,6 +34,9 @@ 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 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]
|
||||
|
||||
@@ -124,6 +127,9 @@ export function getPermissionModesForFlavor(flavor?: string | null): readonly Pe
|
||||
if (flavor === 'kimi') {
|
||||
return KIMI_PERMISSION_MODES
|
||||
}
|
||||
if (flavor === 'grok') {
|
||||
return GROK_PERMISSION_MODES
|
||||
}
|
||||
if (flavor === 'opencode') {
|
||||
return OPENCODE_PERMISSION_MODES
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ export const RPC_METHODS = {
|
||||
ListPiModels: 'listPiModels',
|
||||
ListOpencodeModels: 'listOpencodeModels',
|
||||
ListOpencodeModelsForCwd: 'listOpencodeModelsForCwd',
|
||||
ListGrokModelsForCwd: 'listGrokModelsForCwd',
|
||||
ListGrokModels: 'listGrokModels',
|
||||
ListGrokReasoningEffortOptions: 'listGrokReasoningEffortOptions',
|
||||
ListOpencodeReasoningEffortOptions: 'listOpencodeReasoningEffortOptions'
|
||||
} as const
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export const MetadataSchema = z.object({
|
||||
codexSessionId: z.string().optional(),
|
||||
geminiSessionId: z.string().optional(),
|
||||
opencodeSessionId: z.string().optional(),
|
||||
grokSessionId: z.string().optional(),
|
||||
cursorSessionId: z.string().optional(),
|
||||
cursorSessionProtocol: z.enum(['acp', 'stream-json']).optional(),
|
||||
// Drives the web `CursorMigrationBanner`:
|
||||
|
||||
@@ -65,6 +65,18 @@ describe('getPendingRequestKinds', () => {
|
||||
})
|
||||
|
||||
describe('toSessionSummary', () => {
|
||||
it('uses grokSessionId as the native resume token', () => {
|
||||
const summary = toSessionSummary(makeSession({
|
||||
metadata: {
|
||||
path: '/proj',
|
||||
host: 'local',
|
||||
flavor: 'grok',
|
||||
grokSessionId: 'grok-session-1'
|
||||
}
|
||||
}))
|
||||
expect(summary.metadata?.agentSessionId).toBe('grok-session-1')
|
||||
})
|
||||
|
||||
it('includes pending request kinds and background task count', () => {
|
||||
const summary = toSessionSummary(makeSession({
|
||||
backgroundTaskCount: 2,
|
||||
|
||||
@@ -119,6 +119,7 @@ export function toSessionSummary(session: Session): SessionSummary {
|
||||
?? session.metadata.claudeSessionId
|
||||
?? session.metadata.geminiSessionId
|
||||
?? session.metadata.opencodeSessionId
|
||||
?? session.metadata.grokSessionId
|
||||
?? session.metadata.cursorSessionId
|
||||
?? session.metadata.kimiSessionId
|
||||
?? undefined,
|
||||
|
||||
@@ -32,6 +32,14 @@ export const BUILTIN_SLASH_COMMANDS = {
|
||||
{ name: 'compress', description: 'Compress the context by replacing it with a summary', source: 'builtin' },
|
||||
{ name: 'stats', description: 'Check session stats', source: 'builtin' },
|
||||
],
|
||||
grok: [
|
||||
{ name: 'compact', description: 'Compress conversation history to save context', source: 'builtin' },
|
||||
{ name: 'context', description: 'Show context window usage and session stats', source: 'builtin' },
|
||||
{ name: 'session-info', description: 'Show Grok session model, turns, and context usage', source: 'builtin' },
|
||||
{ name: 'goal', description: 'Set, manage, or inspect an autonomous goal', source: 'builtin' },
|
||||
{ name: 'always-approve', description: 'Toggle automatic tool approval', source: 'builtin' },
|
||||
{ name: 'auto', description: 'Let Grok classify safe tool calls for automatic approval', source: 'builtin' },
|
||||
],
|
||||
opencode: [
|
||||
{ name: 'help', description: 'Show supported HAPI OpenCode slash commands', source: 'builtin' },
|
||||
{ name: 'status', description: 'Show current OpenCode session config', source: 'builtin' },
|
||||
|
||||
@@ -37,6 +37,7 @@ export type {
|
||||
CodexPermissionMode,
|
||||
CursorPermissionMode,
|
||||
GeminiPermissionMode,
|
||||
GrokPermissionMode,
|
||||
KimiPermissionMode,
|
||||
OpencodePermissionMode,
|
||||
PermissionMode,
|
||||
|
||||
Reference in New Issue
Block a user