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:
SSU-WEI HUANG
2026-07-13 08:41:30 +08:00
committed by GitHub
parent de07643828
commit b9eed7c071
92 changed files with 3796 additions and 63 deletions
+29
View File
@@ -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
+13
View File
@@ -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)
})
+7 -1
View File
@@ -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'
}
+18
View File
@@ -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
View File
@@ -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
}
+3
View File
@@ -30,6 +30,9 @@ export const RPC_METHODS = {
ListPiModels: 'listPiModels',
ListOpencodeModels: 'listOpencodeModels',
ListOpencodeModelsForCwd: 'listOpencodeModelsForCwd',
ListGrokModelsForCwd: 'listGrokModelsForCwd',
ListGrokModels: 'listGrokModels',
ListGrokReasoningEffortOptions: 'listGrokReasoningEffortOptions',
ListOpencodeReasoningEffortOptions: 'listOpencodeReasoningEffortOptions'
} as const
+1
View File
@@ -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`:
+12
View File
@@ -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,
+1
View File
@@ -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,
+8
View File
@@ -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' },
+1
View File
@@ -37,6 +37,7 @@ export type {
CodexPermissionMode,
CursorPermissionMode,
GeminiPermissionMode,
GrokPermissionMode,
KimiPermissionMode,
OpencodePermissionMode,
PermissionMode,