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
1 change: 1 addition & 0 deletions .agents/docs/agent-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Opening two provider folders side-by-side answers "what does this provider do di
| Gemini | auto, gemini-3.1-pro-preview, gemini-2.5-pro/flash/flash-lite, etc. | (none) | terminal | No |
| Copilot | (probed via ACP) | low, medium, high, xhigh | terminal | Yes (ACP) |
| Cursor | auto, composer-\*, GPT/Opus/Sonnet variants | (embedded in model name) | terminal | No |
| Grok | grok-build (probed via ACP) | (none) | terminal | Yes (ACP) |

## Plugin Architecture

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"fmt:check": "oxfmt --check .",
"test": "vitest run",
"test:perf:cli-hook": "vitest run src/supervisor/runtime/cliHookEventChain.perf.test.ts",
"test:integration:providers": "vitest run --config vitest.integration.config.ts",
"update-server": "node scripts/update-server.mjs",
"db:clone": "node scripts/clone-db.mjs",
"prepare": "husky"
Expand Down
6 changes: 6 additions & 0 deletions scripts/prepare-agent-plugins.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* - cursor: plugin.json, forward.mjs
* - gemini: plugin.json, forward.mjs
* - copilot: plugin.json, forward.mjs
* - grok: plugin.json, forward.mjs
* - opencode: plugin.json, lightcode-status.mjs (in-process plugin, no forward.mjs)
*
* Plus a shared forwarder runtime under `_runtime/lightcode-hook-runtime.mjs`
Expand Down Expand Up @@ -63,6 +64,11 @@ const PLUGINS = [
assets: ["plugin.json", "forward.mjs"],
srcDir: join(repoRoot, "src", "supervisor", "agents", "copilot", "plugin"),
},
{
kind: "grok",
assets: ["plugin.json", "forward.mjs"],
srcDir: join(repoRoot, "src", "supervisor", "agents", "grok", "plugin"),
},
{
kind: "opencode",
assets: ["plugin.json", "lightcode-status.mjs"],
Expand Down
79 changes: 70 additions & 9 deletions src/renderer/actions/agentLoginActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Project } from "@/shared/contracts";
import { readBridge } from "@/renderer/bridge";
import { useAppStore } from "@/renderer/state/appStore";
import { useDevTerminalStore } from "@/renderer/state/devTerminalStore";
import { useLoginTerminalStore } from "@/renderer/state/loginTerminalStore";
import { usePanelStore } from "@/renderer/state/panelStore";
import { useSharedSettings } from "@/renderer/state/sharedSettingsStore";
import { writeScriptToShell } from "@/renderer/utils/shellUtils";
Expand Down Expand Up @@ -98,16 +99,70 @@ export function runAgentLoginCommand(input: {
onCommandComplete?: (exitCode: number) => void;
project?: Project;
}): boolean {
return runAgentTerminalCommand({
label: input.label,
command: input.command,
...(input.env ? { env: input.env } : {}),
...(input.onCommandComplete ? { onCommandComplete: input.onCommandComplete } : {}),
...(input.project ? { project: input.project } : {}),
const project = input.project ?? resolveLoginProject();
if (!project) {
toast.warning("Add a project before signing in.");
return false;
}

// Replace any active login session — only one terminal panel at a time.
const previous = useLoginTerminalStore.getState().active;
if (previous) {
previous.onForceClose?.();
void readBridge()
.closeThread({ threadId: previous.shellId })
.catch(() => undefined);
}

const shellId = `login:${crypto.randomUUID()}`;
// Wipe the bash prompt + echoed script line that briefly appear before the
// TUI takes over. `clear` (POSIX) / `Clear-Host` (PowerShell) gives the
// overlay a clean canvas so the user only sees the agent's own UI.
const cleared =
project.location.kind === "windows"
? `Clear-Host; ${input.command}`
: `clear; ${input.command}`;
const command = buildTerminalCommand({
command: cleared,
env: input.env,
openUrlsInNativeBrowser: true,
tabPurpose: "login",
toastPurpose: "login",
project,
});
const completionToken = createCompletionToken();
const script = appendCompletionSignal(command, project, completionToken);

let fired = false;
const fireOnce = (exitCode: number) => {
if (fired) return;
fired = true;
input.onCommandComplete?.(exitCode);
};

const stopWatching = watchCommandCompletion(shellId, completionToken, (exitCode) => {
fireOnce(exitCode);
// Auto-dismiss the overlay shortly after the command exits so the user
// can read any final success line before it slides away.
window.setTimeout(() => useLoginTerminalStore.getState().close(), 1200);
});

useLoginTerminalStore.getState().open({
shellId,
label: input.label,
projectLocation: project.location,
onForceClose: () => {
stopWatching();
fireOnce(-1);
},
});

void readBridge()
.startShell({ shellId, projectLocation: project.location })
.catch((error) => {
toast.danger(error instanceof Error ? error.message : `Unable to open ${input.label} login.`);
useLoginTerminalStore.getState().close();
});
writeScriptToShell(shellId, script);
return true;
}

function quotePosixShellArg(value: string): string {
Expand Down Expand Up @@ -165,7 +220,7 @@ function watchCommandCompletion(
shellId: string,
token: string,
onCommandComplete: (exitCode: number) => void,
): void {
): () => void {
const marker = completionMarker(token);
let buffer = "";
let done = false;
Expand All @@ -188,4 +243,10 @@ function watchCommandCompletion(
unsubscribe();
onCommandComplete(Number(match[1]));
});
return () => {
if (done) return;
done = true;
window.clearTimeout(timeout);
unsubscribe();
};
}
10 changes: 10 additions & 0 deletions src/renderer/components/providers/grok/GrokIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createProviderIcon } from "../common/createProviderIcon";

const GROK_PATH =
"M2.30047 8.77631L12.0474 23H16.3799L6.63183 8.77631H2.30047ZM6.6285 16.6762L2.29492 23H6.63072L8.79584 19.8387L6.6285 16.6762ZM17.3709 1L9.88007 11.9308L12.0474 15.0944L21.7067 1H17.3709ZM18.1555 7.76374V23H21.7067V2.5818L18.1555 7.76374Z";

export const GrokIcon = createProviderIcon({
cssPrefix: "lightcode-grok-icon",
path: GROK_PATH,
viewBox: "0 0 24 24",
});
54 changes: 54 additions & 0 deletions src/renderer/components/providers/grok/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
export * from "./GrokIcon";

import { GrokIcon } from "./GrokIcon";
import { fullAccessToggle } from "../composerControlBuilders";
import {
registerCommitGenDefaults,
registerComposerControls,
registerConflictResolverDefaults,
registerProviderIcon,
registerProviderLabel,
registerTitleGenDefaults,
} from "../ProviderIcon";

registerProviderIcon("grok", GrokIcon);
registerProviderLabel("grok", "Grok Build");

registerCommitGenDefaults("grok", {
label: "Grok",
hint: "Build",
model: "grok-build",
effort: "",
});

registerTitleGenDefaults("grok", {
label: "Grok",
hint: "Build",
model: "grok-build",
effort: "",
});

registerConflictResolverDefaults("grok", {
label: "Grok",
hint: "Build",
model: "grok-build",
effort: "",
});

// Composer surface for Grok: a single Default ↔ Bypass Approvals toggle.
// Plan mode and effort are intentionally absent — neither is driveable from
// launch flags on Grok 0.1.218. See `supervisor/agents/grok/detection.ts`
// and `supervisor/agents/grok/argv.ts` for the rationale.
registerComposerControls("grok", ({ capabilities, config, isDisabled, onConfigChange }) => {
if (!capabilities.approvalPolicies?.length) return [];
const bypassPolicy = capabilities.bypassPermissions?.approvalPolicy ?? "bypassPermissions";
const isBypass = config.approvalPolicy === bypassPolicy;
return [
fullAccessToggle({
isFullAccess: isBypass,
isDisabled,
onChange: (isSelected) =>
onConfigChange({ approvalPolicy: isSelected ? bypassPolicy : "default" }),
}),
];
});
1 change: 1 addition & 0 deletions src/renderer/components/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from "./claude";
export * from "./copilot";
export * from "./codex";
export * from "./gemini";
export * from "./grok";
export * from "./antigravity";
export * from "./cursor";
export * from "./opencode";
Expand Down
10 changes: 7 additions & 3 deletions src/renderer/components/thread/ChatPane/ChatPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,13 @@ export function ChatPane(props: ChatPaneProps) {
const showTailLoader = isLive || completedTurnCanRenderInTail;
// The agent is not actually working while it waits for a user answer, so the
// tail loader keeps rendering but its elapsed-time counter freezes for the
// duration of the wait and resumes once the thread flips back to working.
const isTurnPaused =
status === "needs_reply" || status === "needs_approval" || hasOpenRuntimeRequest;
// duration of the wait and resumes once the user submits a response. Anchor
// on `hasOpenRuntimeRequest` (cleared optimistically by the request panel)
// rather than `thread.status`, which only flips back to `working` after the
// supervisor's round-trip — for plan approvals the agent often opens a new
// request before that round-trip completes, leaving status stuck at
// `needs_approval` even though the user has already answered.
const isTurnPaused = hasOpenRuntimeRequest;
const showEmptyHint = isEmpty && !isLive;
// The tail loader displays the most recent completed turn's frozen elapsed
// time when the thread is idle and no newer timeline row exists. Once an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ interface ThreadRuntimeRequestPanelProps {
*
* Renders two flavors based on `request.requestType`:
* - `tool_user_input` → vertical list of menu rows; click submits.
* - approval requests → primary action with chevron-dropdown for alternates,
* and negative options as ghost buttons.
* - approval requests (incl. `tool_call_approval`) → primary action with
* chevron-dropdown for alternates, and negative options as ghost buttons.
*
* Resolves through `resolveThreadServerRequest` with `method: "requestPermission"`,
* matching the existing renderer<->supervisor contract.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const APPROVAL_REQUEST_TYPES = new Set<CanonicalRequestType>([
"file_read_approval",
"file_change_approval",
"apply_patch_approval",
"tool_call_approval",
]);

export function getApprovalDenyOption(request: OpenRuntimeRequest): UserInputOption | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export function QuestionRows(props: {
isDisabled={isDisabled || selectedIds.length === 0}
size="sm"
variant="secondary"
className="text-white"
onPress={() => onSubmit(selectedIds)}
>
Submit
Expand Down
31 changes: 31 additions & 0 deletions src/renderer/state/loginTerminalStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { create } from "zustand";
import type { ProjectLocation } from "@/shared/contracts";

/**
* One-shot login terminal session. Owned by `runAgentLoginCommand`,
* rendered by `<LoginTerminalOverlay />`. The overlay slides over any
* existing overlay (e.g. Settings) so the user can complete a TUI auth
* flow without losing their place.
*
* `onForceClose` fires when the user dismisses the overlay before the
* command's own exit (X button / Escape). Callers use it to clear
* pending-state UI even when no exit code arrives.
*/
export interface LoginTerminalSession {
shellId: string;
label: string;
projectLocation: ProjectLocation;
onForceClose?: () => void;
}

interface LoginTerminalState {
active: LoginTerminalSession | null;
open: (session: LoginTerminalSession) => void;
close: () => void;
}

export const useLoginTerminalStore = create<LoginTerminalState>((set) => ({
active: null,
open: (session) => set({ active: session }),
close: () => set((state) => (state.active === null ? {} : { active: null })),
}));
Loading