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
+34
View File
@@ -0,0 +1,34 @@
import chalk from 'chalk'
import { authAndSetupMachineIfNeeded } from '@/ui/auth'
import { initializeToken } from '@/ui/tokenInit'
import { maybeAutoStartServer } from '@/utils/autoStartServer'
import type { CommandDefinition } from './types'
import { GROK_PERMISSION_MODES } from '@hapi/protocol/modes'
import { parseRemoteAgentCommandOptions } from './agentCommandOptions'
export const grokCommand: CommandDefinition = {
name: 'grok',
requiresRuntimeAssets: true,
run: async ({ commandArgs }) => {
try {
const hasExplicitPermissionMode = commandArgs.includes('--permission-mode')
const normalizedArgs = hasExplicitPermissionMode
? commandArgs
: commandArgs.flatMap((arg) => arg === '--yolo'
? ['--permission-mode', 'bypassPermissions']
: [arg])
const options = parseRemoteAgentCommandOptions(normalizedArgs, GROK_PERMISSION_MODES)
await initializeToken()
await maybeAutoStartServer()
await authAndSetupMachineIfNeeded()
const { runGrok } = await import('@/grok/runGrok')
await runGrok(options)
} catch (error) {
console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error')
if (process.env.DEBUG) console.error(error)
process.exit(1)
}
}
}
+2
View File
@@ -8,6 +8,7 @@ import { runnerCommand } from './runner'
import { resumeCommand } from './resume'
import { doctorCommand } from './doctor'
import { kimiCommand } from './kimi'
import { grokCommand } from './grok'
import { opencodeCommand } from './opencode'
import { piCommand } from './pi'
import { hookForwarderCommand } from './hookForwarder'
@@ -38,6 +39,7 @@ const COMMANDS: CommandDefinition[] = [
codexCommand,
cursorCommand,
removedGeminiCommand,
grokCommand,
kimiCommand,
opencodeCommand,
piCommand,
+33
View File
@@ -10,6 +10,7 @@ const {
renderMock,
runCodexMock,
runClaudeMock,
runGrokMock,
runPiMock,
assertCodexLocalSupportedMock,
existsSyncMock
@@ -23,6 +24,7 @@ const {
renderMock: vi.fn(),
runCodexMock: vi.fn(async () => {}),
runClaudeMock: vi.fn(async () => {}),
runGrokMock: vi.fn(async () => {}),
runPiMock: vi.fn(async () => {}),
assertCodexLocalSupportedMock: vi.fn(),
existsSyncMock: vi.fn(() => true)
@@ -46,6 +48,7 @@ vi.mock('@/ui/ink/ResumeSessionPicker', () => ({
}))
vi.mock('@/codex/runCodex', () => ({ runCodex: runCodexMock }))
vi.mock('@/claude/runClaude', () => ({ runClaude: runClaudeMock }))
vi.mock('@/grok/runGrok', () => ({ runGrok: runGrokMock }))
vi.mock('@/pi/runPi', () => ({ runPi: runPiMock }))
vi.mock('@/codex/utils/codexVersion', () => ({ assertCodexLocalSupported: assertCodexLocalSupportedMock }))
vi.mock('node:fs', () => ({ existsSync: existsSyncMock }))
@@ -75,6 +78,7 @@ describe('resumeCommand', () => {
})
runCodexMock.mockClear()
runClaudeMock.mockClear()
runGrokMock.mockClear()
runPiMock.mockClear()
assertCodexLocalSupportedMock.mockClear()
existsSyncMock.mockReturnValue(true)
@@ -142,6 +146,35 @@ describe('resumeCommand', () => {
})
})
it('resumes a Grok target in the native local TUI', async () => {
getLocalResumeTargetMock.mockResolvedValue({
sessionId: 'hapi-session-grok',
flavor: 'grok',
directory: '/tmp/project',
machineId: 'machine-1',
active: false,
thinking: false,
controlledByUser: false,
agentSessionId: 'grok-session-1',
model: 'grok-4.5',
effort: 'low',
permissionMode: 'plan'
})
await resumeCommand.run(createContext(['hapi-session-grok']))
expect(runGrokMock).toHaveBeenCalledWith({
existingSessionId: 'hapi-session-grok',
workingDirectory: '/tmp/project',
resumeSessionId: 'grok-session-1',
startedBy: 'terminal',
permissionMode: 'plan',
startingMode: 'local',
model: 'grok-4.5',
effort: 'low'
})
})
it('rejects an active Gemini target before handoff (no longer supported, leaves running session alone)', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => {
+16
View File
@@ -7,6 +7,7 @@ import type {
ClaudePermissionMode,
CodexPermissionMode,
CursorPermissionMode,
GrokPermissionMode,
KimiPermissionMode,
OpencodePermissionMode
} from '@hapi/protocol/types'
@@ -120,6 +121,21 @@ async function dispatchLocalResume(target: LocalResumeTarget): Promise<void> {
return
}
if (target.flavor === 'grok') {
const { runGrok } = await import('@/grok/runGrok')
await runGrok({
existingSessionId: base.existingSessionId,
workingDirectory: base.workingDirectory,
resumeSessionId: base.resumeSessionId,
startedBy: base.startedBy,
permissionMode: base.permissionMode as GrokPermissionMode | undefined,
startingMode: 'local',
model: target.model ?? undefined,
effort: target.effort ?? undefined
})
return
}
if (target.flavor === 'kimi') {
const { runKimi } = await import('@/kimi/runKimi')
await runKimi({