Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions src/renderer/components/providers/grok/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,20 @@ const PROVIDER_KIND = providerManifest.kind;

registerProviderIcon(PROVIDER_KIND, GrokIcon);

// Grok has two models, both covered by the subscription (no per-token cost), so
// pick by fit: `grok-composer-2.5-fast` is Grok's own default — fast, ideal for
// the lightweight title/commit one-shots; the frontier `grok-4.5` takes the
// conflict resolver, which is a real code-editing session. (The old `grok-build`
// model id was retired upstream — the 0.2.x catalog is grok-4.5 +
// grok-composer-2.5-fast.)
// Grok 0.2.118 advertises grok-4.5 as its supported model. Keep utility
// defaults aligned with the live catalog so background generations do not
// fail with an unknown model id.
registerCommitGenDefaults(PROVIDER_KIND, {
label: "Grok",
hint: "Composer 2.5 Fast",
model: "grok-composer-2.5-fast",
hint: "Grok 4.5",
model: "grok-4.5",
effort: "",
});

registerTitleGenDefaults(PROVIDER_KIND, {
label: "Grok",
hint: "Composer 2.5 Fast",
model: "grok-composer-2.5-fast",
hint: "Grok 4.5",
model: "grok-4.5",
effort: "",
});

Expand All @@ -42,7 +39,7 @@ registerConflictResolverDefaults(PROVIDER_KIND, {

// Composer surface for Grok: a single Default ↔ Bypass Approvals toggle.
// Plan mode is intentionally absent — it is not driveable from launch flags
// (re-verified on grok 0.2.93). Effort needs no control here: the shared
// (re-verified on grok 0.2.118). Effort needs no control here: the shared
// model picker reads `capabilities.modelEfforts` filled by the ACP probe.
// See `supervisor/agents/grok/detection.ts` and `supervisor/agents/grok/argv.ts`.
registerComposerControls(PROVIDER_KIND, ({ capabilities, config, isDisabled, onConfigChange }) => {
Expand Down
9 changes: 3 additions & 6 deletions src/supervisor/agents/commandBuilders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,14 +454,10 @@ describe("agent command builders", () => {

it("builds a Grok one-shot command via the headless `grok -p` path", () => {
expect(
createGrokAdapter().buildOneShotCommand?.(
"grok-composer-2.5-fast",
undefined,
"Summarize this diff",
),
createGrokAdapter().buildOneShotCommand?.("grok-4.5", undefined, "Summarize this diff"),
).toEqual({
command: "grok",
args: ["-p", "Summarize this diff", "-m", "grok-composer-2.5-fast", "--always-approve"],
args: ["--no-auto-update", "-p", "Summarize this diff", "-m", "grok-4.5", "--always-approve"],
stdin: "",
});
});
Expand Down Expand Up @@ -538,6 +534,7 @@ describe("agent command builders", () => {
).toEqual({
command: "grok",
args: [
"--no-auto-update",
"-p",
"Summarize this diff",
"-m",
Expand Down
17 changes: 13 additions & 4 deletions src/supervisor/agents/grok/argv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ describe("buildGrokArgs (TUI/PTY)", () => {
});

describe("buildGrokAcpArgs (`grok agent stdio` prefix)", () => {
it("emits nothing for a bare default config", () => {
expect(buildGrokAcpArgs({} as any)).toEqual([]);
it("disables auto-update for a bare default config", () => {
expect(buildGrokAcpArgs({} as any)).toEqual(["--no-auto-update"]);
});

it("never emits --permission-mode or --no-plan", () => {
Expand All @@ -113,16 +113,25 @@ describe("buildGrokAcpArgs (`grok agent stdio` prefix)", () => {
});

it("forwards config.effort as --reasoning-effort", () => {
expect(buildGrokAcpArgs({ effort: "medium" } as any)).toEqual(["--reasoning-effort", "medium"]);
expect(buildGrokAcpArgs({ effort: "medium" } as any)).toEqual([
"--no-auto-update",
"--reasoning-effort",
"medium",
]);
});

it("adds --always-approve when approval policy bypasses permissions", () => {
expect(buildGrokAcpArgs({ approvalPolicy: "bypassPermissions" } as any)).toEqual([
"--no-auto-update",
"--always-approve",
]);
});

it("passes -m <model> when set", () => {
expect(buildGrokAcpArgs({ model: "grok-4.5" } as any)).toEqual(["-m", "grok-4.5"]);
expect(buildGrokAcpArgs({ model: "grok-4.5" } as any)).toEqual([
"--no-auto-update",
"-m",
"grok-4.5",
]);
});
});
10 changes: 5 additions & 5 deletions src/supervisor/agents/grok/argv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ThreadConfig } from "@/shared/contracts";

/**
* Flag references — verified against `grok --help`, `grok agent --help`, and
* live PTY/ACP probes on grok 0.2.93 (2026-07-09):
* live PTY/ACP probes on grok 0.2.118 (2026-08-02):
* https://docs.x.ai/build/cli/headless-scripting
* https://docs.x.ai/build/modes-and-commands
*
Expand All @@ -21,10 +21,10 @@ import type { ThreadConfig } from "@/shared/contracts";
* • `--reasoning-effort <EFFORT>` is honored at TUI launch since 0.2.x
* (verified live: the composer footer shows "Grok 4.5 (low)" and the
* session's summary.json records `reasoning_effort`). Models that don't
* advertise `supportsReasoningEffort` (grok-composer-2.5-fast) simply
* ignore it, so we forward `config.effort` whenever it is set.
* advertise `supportsReasoningEffort` simply ignore it, so we forward
* `config.effort` whenever it is set.
* • `--permission-mode <MODE>` is STILL silently ignored at launch on both
* surfaces (verified live on 0.2.93: booting the TUI with
* surfaces (verified live on 0.2.118: booting the TUI with
* `--permission-mode plan` shows no "· plan" footer chip while Shift+Tab
* does, and an ACP session created with it reports kind "build"). The
* only approval control Grok honors at launch remains `--always-approve`
Expand Down Expand Up @@ -103,7 +103,7 @@ export function buildGrokArgs(
* Argv prefix for `grok [FLAGS] agent stdio` (ACP / GUI tab).
*/
export function buildGrokAcpArgs(config: ThreadConfig): string[] {
const args: string[] = [];
const args = ["--no-auto-update"];
pushSharedFlags(args, config);
return args;
}
10 changes: 5 additions & 5 deletions src/supervisor/agents/grok/detection.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import { buildGrokProviderMetadata, mapGrokEffortCapabilities } from "./detection";

// Model `_meta` shapes as returned live by `grok agent stdio` 0.2.93
// Model `_meta` shapes as returned live by `grok agent stdio` 0.2.118
// (initialize/_meta.modelState and session/new `models.availableModels[]._meta`).
const GROK_45_META = {
totalContextTokens: 500_000,
Expand All @@ -15,7 +15,7 @@ const GROK_45_META = {
],
};

const COMPOSER_META = {
const MODEL_WITHOUT_EFFORT_META = {
totalContextTokens: 200_000,
agentType: "cursor",
};
Expand All @@ -24,7 +24,7 @@ describe("mapGrokEffortCapabilities", () => {
it("derives ascending effort tiers and the advertised default", () => {
const caps = mapGrokEffortCapabilities({
"grok-4.5": GROK_45_META,
"grok-composer-2.5-fast": COMPOSER_META,
"model-without-effort": MODEL_WITHOUT_EFFORT_META,
});
expect(caps.efforts).toEqual(["low", "medium", "high"]);
expect(caps.defaultEffort).toBe("high");
Expand All @@ -33,11 +33,11 @@ describe("mapGrokEffortCapabilities", () => {
it("gives models without tiers an explicit empty list so the picker hides effort", () => {
const caps = mapGrokEffortCapabilities({
"grok-4.5": GROK_45_META,
"grok-composer-2.5-fast": COMPOSER_META,
"model-without-effort": MODEL_WITHOUT_EFFORT_META,
});
expect(caps.modelEfforts).toEqual({
"grok-4.5": ["low", "medium", "high"],
"grok-composer-2.5-fast": [],
"model-without-effort": [],
});
});

Expand Down
15 changes: 7 additions & 8 deletions src/supervisor/agents/grok/detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { getAgentProbeCwd, resolveProbeSpawnCwd } from "../probeCwd";

// Approval policies surfaced to Poracode. Grok only honors `--always-approve`
// (bypass) at launch — `--permission-mode <MODE>` is silently ignored by both
// the TUI and `grok agent stdio` (re-verified live on 0.2.93; see argv.ts).
// the TUI and `grok agent stdio` (re-verified live on 0.2.118; see argv.ts).
// We therefore expose a single Default ↔ Bypass Approvals toggle in the
// composer.
const GROK_APPROVAL_POLICIES = [
Expand All @@ -30,7 +30,7 @@ const GROK_APPROVAL_POLICIES = [

// Plan mode is intentionally omitted from the composer surface: it cannot be
// force-activated at launch on either Grok surface — `--permission-mode plan`
// is silently ignored (verified live on 0.2.93). Plan mode is entered in the
// is silently ignored (verified live on 0.2.118). Plan mode is entered in the
// TUI via Shift+Tab or by the model calling `enter_plan_mode`, so a Plan/Work
// toggle would falsely imply we drive it.
//
Expand Down Expand Up @@ -64,7 +64,7 @@ async function probeCapabilities(
location: ProjectLocation,
executablePath?: string,
): Promise<CapabilitiesProbeResult> {
const spec = buildGrokCommand(location, ["agent", "stdio"], executablePath);
const spec = buildGrokCommand(location, ["--no-auto-update", "agent", "stdio"], executablePath);
const sessionCwd = getAgentProbeCwd(location);
const processCwd = resolveProbeSpawnCwd(location, spec.cwd);
const probe = await probeAcpCapabilities(spec.command, spec.args, sessionCwd, {
Expand All @@ -79,7 +79,7 @@ async function probeCapabilities(
});

// Extract context windows from model _meta (grok reports totalContextTokens
// per model, e.g. 500k for grok-4.5 and 200k for grok-composer-2.5-fast).
// per model, e.g. 500k for grok-4.5).
let contextCaps: Pick<AgentCapability, "contextSizes" | "modelContextSizes"> = {};
if (probe?.modelMetadata) {
const sizes = new Map<string, number>();
Expand Down Expand Up @@ -145,10 +145,9 @@ type GrokReasoningEffortMeta = { id?: unknown; default?: unknown };

/**
* Derive effort capabilities from the per-model `_meta.reasoningEfforts` the
* grok 0.2.x ACP handshake advertises (verified live on 0.2.93: grok-4.5
* exposes high/medium/low with high as default; grok-composer-2.5-fast
* advertises none). Models without tiers get an explicit empty list so the
* shared model picker hides the effort dropdown for them.
* grok 0.2.x ACP handshake advertises (verified live on 0.2.118: grok-4.5
* exposes high/medium/low with high as default). Models without tiers get an
* explicit empty list so the shared model picker hides the effort dropdown.
*/
export function mapGrokEffortCapabilities(
modelMetadata: Record<string, Record<string, unknown>> | undefined,
Expand Down
19 changes: 18 additions & 1 deletion src/supervisor/agents/grok/grok.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function oscNotify(body: string, code: 9 | 99 | 777 = 9): OscNotification {
return { code, title: "", body, payload: undefined };
}

// Observed live from grok PTY captures (0.1.218, re-verified on 0.2.93
// Observed live from grok PTY captures (0.1.218, re-verified on 0.2.118
// idle title is still plain "grok"):
// OSC 0 "grok" (idle, frequent)
// OSC 0 "⠴ - Waiting - grok" (working, braille frames ⠴ / ⠦)
Expand Down Expand Up @@ -203,6 +203,23 @@ describe("createGrokAdapter buildLaunchArgv / buildResumeArgv session flags", ()
expect(result.sessionRef?.providerSessionId).toBe(SESSION_ID);
});

it("resumes a known UUID after the project directory moves", () => {
const originalProjectDir = join(tmpdir(), "grok-original-proj");
mkdirSync(join(grokHome, "sessions", encodeURIComponent(originalProjectDir), SESSION_ID), {
recursive: true,
});

const adapter = createGrokAdapter();
const result = adapter.buildLaunchArgv(
location,
config,
"",
createKnownSessionRef(SESSION_ID),
{},
);
expect(result.args.slice(0, 2)).toEqual(["-r", SESSION_ID]);
});

it("buildResumeArgv applies the same materialization fallback", () => {
const adapter = createGrokAdapter();
const fresh = adapter.buildResumeArgv(location, config, "", createKnownSessionRef(SESSION_ID));
Expand Down
14 changes: 7 additions & 7 deletions src/supervisor/agents/grok/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export function createGrokAdapter(): AgentAdapter {
snapshotGrokPreSpawnSessions(location, cwd);

// Resolve the session ID before spawning the PTY: resume a known one
// with `-r`, otherwise pre-assign a fresh UUID with `-s` (grok 0.2.93,
// with `-r`, otherwise pre-assign a fresh UUID with `-s` (grok 0.2.118,
// works on native and WSL alike). Grok normally materializes the
// session dir within ~1s of boot; a known id whose dir never appeared
// (launch died at startup) is re-assigned via `-s`
Expand Down Expand Up @@ -189,7 +189,7 @@ export function createGrokAdapter(): AgentAdapter {
const location = detectProbeLocation(ctx);
return buildGrokCommand(
location,
["agent", "stdio"],
["--no-auto-update", "agent", "stdio"],
resolveAgentBinaryPath(location, "grok"),
);
},
Expand Down Expand Up @@ -222,7 +222,7 @@ export function createGrokAdapter(): AgentAdapter {
if (t.includes("grok build")) return true;
if (/type @|mention files|\/ commands/i.test(text)) return true;
// 0.2.x composer footer ("Shift+Tab:mode") — present on both the
// welcome screen and resumed sessions (verified live on 0.2.93).
// welcome screen and resumed sessions (verified live on 0.2.118).
if (t.includes("shift+tab")) return true;
return false;
},
Expand All @@ -247,12 +247,12 @@ export function createGrokAdapter(): AgentAdapter {
// One-shot (title / commit) generation reuses Grok's documented headless
// path: `grok -p <prompt>`. `--always-approve` keeps the non-interactive run
// from blocking on a tool-approval prompt it cannot answer (mirrors the
// launch/ACP bypass in argv.ts). The default is Grok's own default model —
// fast and subscription-covered, ideal for lightweight one-shots.
defaultOneShotModel: "grok-composer-2.5-fast",
// launch/ACP bypass in argv.ts). Grok 0.2.118 advertises grok-4.5 as its
// supported model, so utility runs use the same live catalog entry.
defaultOneShotModel: "grok-4.5",
buildOneShotCommand(model, effort, prompt) {
if (!prompt) return undefined;
const args = ["-p", prompt];
const args = ["--no-auto-update", "-p", prompt];
if (model) args.push("-m", model);
if (effort) args.push("--reasoning-effort", effort);
args.push("--always-approve");
Expand Down
13 changes: 13 additions & 0 deletions src/supervisor/agents/grok/sessionFiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ describe("grok session materialization (native)", () => {
expect(grokSessionDirMaterialized(location, projectDir, SESSION_ID)).toBe(true);
});

it("reports true after the project moves because UUID resumes are global", () => {
const originalProjectDir = join(tmpdir(), "grok-original-proj");
mkdirSync(join(grokHome, "sessions", encodeURIComponent(originalProjectDir), SESSION_ID), {
recursive: true,
});

expect(grokSessionDirMaterialized(location, projectDir, SESSION_ID)).toBe(true);
expect(resolveGrokSessionArg(location, projectDir, SESSION_ID)).toEqual({
kind: "resume",
sessionId: SESSION_ID,
});
});

it("resolveGrokSessionArg re-assigns unmaterialized ids with -s and resumes real ones with -r", () => {
expect(resolveGrokSessionArg(location, projectDir, SESSION_ID)).toEqual({
kind: "new",
Expand Down
36 changes: 27 additions & 9 deletions src/supervisor/agents/grok/sessionFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ function getGrokSessionsRoot(location: ProjectLocation): string | null {
return getNativeGrokSessionsRoot();
}

function sessionExistsUnderAnyCwd(
sessionsRoot: string,
currentCwdKey: string,
sessionId: string,
): boolean {
if (existsSync(join(sessionsRoot, currentCwdKey, sessionId))) return true;
if (!existsSync(sessionsRoot)) return false;

for (const entry of readdirSync(sessionsRoot, { withFileTypes: true })) {
if (entry.isDirectory() && existsSync(join(sessionsRoot, entry.name, sessionId))) return true;
}
return false;
}

/**
* Call this from buildLaunchArgv (and optionally buildResumeArgv) immediately
* before spawning the grok PTY. It records what sessions already exist for
Expand Down Expand Up @@ -94,15 +108,17 @@ function isUuid(s: string): boolean {
}

/**
* True when the session directory for <cwd>/<sessionId> exists on disk.
* True when the session directory exists anywhere in Grok's session store.
*
* grok 0.2.93 normally writes the session dir within ~1s of TUI boot
* grok 0.2.118 normally writes the session dir within ~1s of TUI boot
* (verified live), but a UUID we pre-assigned with `-s` can still be missing
* when the launch died at startup (spawn failure, immediate kill) or the TUI
* deferred to its welcome/resume menu. Callers use this to decide between
* `-r <id>` (resume a real session) and `-s <id>` (re-assign the same id —
* `-s` requires the session to not exist and exits 1 on a collision, so the
* choice must reflect live disk state, never a timing assumption).
* choice must reflect live disk state, never a timing assumption). Searching
* every cwd also preserves resume after a project folder moves, which Grok
* supports by locating the original session independently of the current cwd.
*
* Returns `undefined` when the check is unavailable (WSL distro home not
* cached / UNC bridge unreachable); callers should then default to resume.
Expand All @@ -115,17 +131,19 @@ export function grokSessionDirMaterialized(
if (location.kind === "wsl") {
const home = getCachedWslHomeDirectory(location.distro);
if (!home) return undefined;
const linuxPath = `${home}/.grok/sessions/${encodeCwdKey(cwd)}/${sessionId}`;
const uncPath = `\\\\wsl.localhost\\${location.distro}${linuxPath.replaceAll("/", "\\")}`;
const linuxRoot = `${home}/.grok/sessions`;
const uncRoot = `\\\\wsl.localhost\\${location.distro}${linuxRoot.replaceAll("/", "\\")}`;
try {
return existsSync(uncPath);
return sessionExistsUnderAnyCwd(uncRoot, encodeCwdKey(cwd), sessionId);
} catch {
return undefined;
}
}
const dir = getGrokCwdSessionsDir(location, cwd);
if (!dir) return undefined;
return existsSync(join(dir, sessionId));
try {
return sessionExistsUnderAnyCwd(getNativeGrokSessionsRoot(), encodeCwdKey(cwd), sessionId);
} catch {
return undefined;
}
}

/**
Expand Down