From ad1b9492625736f87d523189032ac7a5ceefe7b5 Mon Sep 17 00:00:00 2001 From: Gagancreates Date: Tue, 19 May 2026 22:43:52 +0530 Subject: [PATCH] feat: add code mode settings tab with agent install/auth checks --- apps/x/apps/main/src/ipc.ts | 17 ++ .../components/chat-input-with-mentions.tsx | 24 ++- .../src/components/settings-dialog.tsx | 199 +++++++++++++++++- .../src/application/assistant/instructions.ts | 37 ++-- apps/x/packages/core/src/code-mode/index.ts | 3 + apps/x/packages/core/src/code-mode/repo.ts | 42 ++++ apps/x/packages/core/src/code-mode/status.ts | 157 ++++++++++++++ apps/x/packages/core/src/code-mode/types.ts | 18 ++ apps/x/packages/core/src/di/container.ts | 2 + apps/x/packages/shared/src/ipc.ts | 21 ++ 10 files changed, 497 insertions(+), 23 deletions(-) create mode 100644 apps/x/packages/core/src/code-mode/index.ts create mode 100644 apps/x/packages/core/src/code-mode/repo.ts create mode 100644 apps/x/packages/core/src/code-mode/status.ts create mode 100644 apps/x/packages/core/src/code-mode/types.ts diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index f04d820d..70521a24 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -30,6 +30,9 @@ import { listGatewayModels } from '@x/core/dist/models/gateway.js'; import type { IModelConfigRepo } from '@x/core/dist/models/repo.js'; import type { IOAuthRepo } from '@x/core/dist/auth/repo.js'; import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js'; +import { ICodeModeConfigRepo } from '@x/core/dist/code-mode/repo.js'; +import { checkCodeModeAgentStatus } from '@x/core/dist/code-mode/status.js'; +import { invalidateCopilotInstructionsCache } from '@x/core/dist/application/assistant/instructions.js'; import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js'; import { ISlackConfigRepo } from '@x/core/dist/slack/repo.js'; import { isOnboardingComplete, markOnboardingComplete } from '@x/core/dist/config/note_creation_config.js'; @@ -600,6 +603,20 @@ export function setupIpcHandlers() { const config = await repo.getConfig(); return { enabled: config.enabled }; }, + 'codeMode:getConfig': async () => { + const repo = container.resolve('codeModeConfigRepo'); + const config = await repo.getConfig(); + return { enabled: config.enabled }; + }, + 'codeMode:setConfig': async (_event, args) => { + const repo = container.resolve('codeModeConfigRepo'); + await repo.setConfig({ enabled: args.enabled }); + invalidateCopilotInstructionsCache(); + return { success: true }; + }, + 'codeMode:checkAgentStatus': async () => { + return await checkCodeModeAgentStatus(); + }, 'granola:setConfig': async (_event, args) => { const repo = container.resolve('granolaConfigRepo'); await repo.setConfig({ enabled: args.enabled }); diff --git a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx index 0299849b..81e25b4e 100644 --- a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx +++ b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx @@ -177,6 +177,7 @@ function ChatInputInner({ const [workDir, setWorkDir] = useState(null) const [codingAgent, setCodingAgent] = useState<'claude' | 'codex'>('claude') const [codeModeEnabled, setCodeModeEnabled] = useState(false) + const [codeModeFeatureEnabled, setCodeModeFeatureEnabled] = useState(false) // When a run exists, freeze the dropdown to the run's resolved model+provider. useEffect(() => { @@ -259,6 +260,25 @@ function ChatInputInner({ return () => window.removeEventListener('models-config-changed', handler) }, [loadModelConfig]) + // Load the global code-mode feature flag (from settings) and stay in sync. + useEffect(() => { + const load = () => { + window.ipc.invoke('codeMode:getConfig', null) + .then((r) => setCodeModeFeatureEnabled(r.enabled)) + .catch(() => setCodeModeFeatureEnabled(false)) + } + load() + window.addEventListener('code-mode-config-changed', load) + return () => window.removeEventListener('code-mode-config-changed', load) + }, []) + + // If the feature is turned off in settings, also turn off any per-conversation chip. + useEffect(() => { + if (!codeModeFeatureEnabled && codeModeEnabled) { + setCodeModeEnabled(false) + } + }, [codeModeFeatureEnabled, codeModeEnabled]) + // Listen for coding-agent runs that were triggered without the explicit code-mode // toggle. App.tsx dispatches this when it sees an acpx executeCommand fire. We // flip the pill on with the detected agent so the UI reflects what's happening. @@ -712,7 +732,7 @@ function ChatInputInner({ ) )} - {codeModeEnabled ? ( + {codeModeFeatureEnabled && (codeModeEnabled ? (
@@ -757,7 +777,7 @@ function ChatInputInner({ Use a coding agent (Claude Code or Codex) - )} + ))}
{lockedModel ? ( + +
+
{name}
+
+ + {status?.installed ? : } + Installed + + + {status?.signedIn ? : } + Signed in + +
+
+ {ready ? ( + + Ready + + ) : ( + + Install & sign in + + )} +
+ ) +} + +function CodeModeSettings({ dialogOpen }: { dialogOpen: boolean }) { + const [enabled, setEnabled] = useState(false) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [status, setStatus] = useState(null) + const [statusLoading, setStatusLoading] = useState(false) + + const loadStatus = useCallback(async () => { + setStatusLoading(true) + try { + const result = await window.ipc.invoke("codeMode:checkAgentStatus", null) + setStatus(result) + } catch { + setStatus(null) + } finally { + setStatusLoading(false) + } + }, []) + + useEffect(() => { + if (!dialogOpen) return + let cancelled = false + async function load() { + setLoading(true) + try { + const result = await window.ipc.invoke("codeMode:getConfig", null) + if (!cancelled) setEnabled(result.enabled) + } catch { + if (!cancelled) setEnabled(false) + } finally { + if (!cancelled) setLoading(false) + } + } + load() + loadStatus() + return () => { cancelled = true } + }, [dialogOpen, loadStatus]) + + const handleToggle = useCallback(async (next: boolean) => { + setSaving(true) + setEnabled(next) + try { + await window.ipc.invoke("codeMode:setConfig", { enabled: next }) + window.dispatchEvent(new Event("code-mode-config-changed")) + toast.success(next ? "Code mode enabled" : "Code mode disabled") + } catch { + setEnabled(!next) + toast.error("Failed to update code mode") + } finally { + setSaving(false) + } + }, []) + + const anyReady = status?.claude.installed && status?.claude.signedIn + || status?.codex.installed && status?.codex.signedIn + + if (loading) { + return ( +
+ + Loading... +
+ ) + } + + return ( +
+
+

+ Code mode lets the assistant delegate coding tasks + to Claude Code or Codex running + on your machine. Pick the agent inline from the composer; the assistant calls it via + acpx + and streams results back into chat. +

+

+ Requires an active Claude Code subscription or + a ChatGPT/Codex subscription. You can have one or both. +

+
+ +
+
+ Agent status + +
+
+ + +
+
+ +
+
+
Enable code mode
+
+ Shows the code mode chip in the composer and lets the assistant delegate to your installed agents. +
+
+ +
+ + {enabled && status && !anyReady && ( +
+ +
+ Neither Claude Code nor Codex is ready. Install at least one and sign in with a subscription + account, then click Re-check. +
+
+ )} +
+ ) +} + // --- Main Settings Dialog --- export function SettingsDialog({ children, defaultTab = "account", open: controlledOpen, onOpenChange }: SettingsDialogProps) { @@ -1695,7 +1884,7 @@ export function SettingsDialog({ children, defaultTab = "account", open: control } const loadConfig = useCallback(async (tab: ConfigTab) => { - if (tab === "appearance" || tab === "models" || tab === "note-tagging" || tab === "account" || tab === "connections" || tab === "help") return + if (tab === "appearance" || tab === "models" || tab === "note-tagging" || tab === "account" || tab === "connections" || tab === "help" || tab === "code-mode") return const tabConfig = tabs.find((t) => t.id === tab)! if (!tabConfig.path) return setLoading(true) @@ -1803,7 +1992,7 @@ export function SettingsDialog({ children, defaultTab = "account", open: control
{/* Content */} -
+
{activeTab === "account" ? ( ) : activeTab === "connections" ? ( @@ -1828,6 +2017,8 @@ export function SettingsDialog({ children, defaultTab = "account", open: control ) : activeTab === "help" ? ( + ) : activeTab === "code-mode" ? ( + ) : loading ? (
Loading... diff --git a/apps/x/packages/core/src/application/assistant/instructions.ts b/apps/x/packages/core/src/application/assistant/instructions.ts index cafa00f9..b3611fe4 100644 --- a/apps/x/packages/core/src/application/assistant/instructions.ts +++ b/apps/x/packages/core/src/application/assistant/instructions.ts @@ -3,6 +3,8 @@ import { getRuntimeContext, getRuntimeContextPrompt } from "./runtime-context.js import { composioAccountsRepo } from "../../composio/repo.js"; import { isConfigured as isComposioConfigured } from "../../composio/client.js"; import { CURATED_TOOLKITS } from "@x/shared/dist/composio.js"; +import container from "../../di/container.js"; +import type { ICodeModeConfigRepo } from "../../code-mode/repo.js"; const runtimeContextPrompt = getRuntimeContextPrompt(getRuntimeContext()); @@ -29,7 +31,7 @@ Load the \`composio-integration\` skill when the user asks to interact with any `; } -function buildStaticInstructions(composioEnabled: boolean, catalog: string): string { +function buildStaticInstructions(composioEnabled: boolean, catalog: string, codeModeEnabled: boolean = true): string { // Conditionally include Composio-related instruction sections const emailDraftSuffix = composioEnabled ? ` Do NOT load this skill for reading, fetching, or checking emails — use the \`composio-integration\` skill for that instead.` @@ -80,7 +82,9 @@ ${thirdPartyBlock}**Meeting Prep:** When users ask you to prepare for a meeting, **Document Collaboration:** When users ask you to work on a document, collaborate on writing, create a new document, edit/refine existing notes, or say things like "let's work on [X]", "help me write [X]", "create a doc for [X]", or "let's draft [X]", you MUST load the \`doc-collab\` skill first. **This applies even for small one-off edits** — the skill carries the canonical *terse-and-scannable* writing style for the knowledge base, and that style applies whether you're authoring a fresh note or fixing a single section. Load it before writing anything into a note. -**Code with Agents:** When users ask you to write code, build a project, create a script, fix a bug, or do any software development task — **including simple things like "create a .c file" or "write a hello-world in Python"** — your FIRST action MUST be \`loadSkill('code-with-agents')\`. Do NOT reach for \`executeCommand\` (PowerShell / bash / shell) or any workspace file tool to do code work yourself before loading this skill. The skill decides whether to delegate to Claude Code / Codex (via acpx) or hand control back to you, and it presents the user a one-click choice when needed. Paths outside the Rowboat workspace root (e.g. \`G:/...\`, \`~/projects/...\`) are NORMAL for coding tasks — do NOT raise "outside workspace" concerns or fall back to your own tools. +${codeModeEnabled + ? `**Code with Agents:** When users ask you to write code, build a project, create a script, fix a bug, or do any software development task — **including simple things like "create a .c file" or "write a hello-world in Python"** — your FIRST action MUST be \`loadSkill('code-with-agents')\`. Do NOT reach for \`executeCommand\` (PowerShell / bash / shell) or any workspace file tool to do code work yourself before loading this skill. The skill decides whether to delegate to Claude Code / Codex (via acpx) or hand control back to you, and it presents the user a one-click choice when needed. Paths outside the Rowboat workspace root (e.g. \`G:/...\`, \`~/projects/...\`) are NORMAL for coding tasks — do NOT raise "outside workspace" concerns or fall back to your own tools.` + : `**Code with Agents (disabled):** Code mode is currently OFF in the user's settings. Do NOT load \`code-with-agents\` and do NOT call acpx. Handle coding requests yourself with your normal tools if you can. After answering, add a final line letting the user know they can delegate coding to Claude Code or Codex by enabling Code Mode in Settings → Code Mode.`} **App Control:** When users ask you to open notes, show the bases or graph view, filter or search notes, or manage saved views, load the \`app-navigation\` skill first. It provides structured guidance for navigating the app UI and controlling the knowledge base view. @@ -312,30 +316,29 @@ Never output raw file paths in plain text when they could be wrapped in a filepa /** Keep backward-compatible export for any external consumers */ export const CopilotInstructions = buildStaticInstructions(true, skillCatalog); -/** - * Cached Composio instructions. Invalidated by calling invalidateCopilotInstructionsCache(). - */ let cachedInstructions: string | null = null; -/** - * Invalidate the cached instructions so the next buildCopilotInstructions() call - * regenerates the Composio section. Call this after connecting/disconnecting a toolkit. - */ export function invalidateCopilotInstructionsCache(): void { cachedInstructions = null; } -/** - * Build full copilot instructions with dynamic Composio tools section. - * Results are cached and reused until invalidated via invalidateCopilotInstructionsCache(). - */ export async function buildCopilotInstructions(): Promise { if (cachedInstructions !== null) return cachedInstructions; const composioEnabled = await isComposioConfigured(); - const catalog = composioEnabled - ? skillCatalog - : buildSkillCatalog({ excludeIds: ['composio-integration'] }); - const baseInstructions = buildStaticInstructions(composioEnabled, catalog); + let codeModeEnabled = false; + try { + const repo = container.resolve('codeModeConfigRepo'); + codeModeEnabled = (await repo.getConfig()).enabled; + } catch { + // repo unavailable — default to disabled + } + const excludeIds: string[] = []; + if (!composioEnabled) excludeIds.push('composio-integration'); + if (!codeModeEnabled) excludeIds.push('code-with-agents'); + const catalog = excludeIds.length > 0 + ? buildSkillCatalog({ excludeIds }) + : skillCatalog; + const baseInstructions = buildStaticInstructions(composioEnabled, catalog, codeModeEnabled); const composioPrompt = await getComposioToolsPrompt(); cachedInstructions = composioPrompt ? baseInstructions + '\n' + composioPrompt diff --git a/apps/x/packages/core/src/code-mode/index.ts b/apps/x/packages/core/src/code-mode/index.ts new file mode 100644 index 00000000..bdf2eecb --- /dev/null +++ b/apps/x/packages/core/src/code-mode/index.ts @@ -0,0 +1,3 @@ +export { CodeModeConfig, CodeModeAgentStatus, AgentStatus } from './types.js'; +export { FSCodeModeConfigRepo, type ICodeModeConfigRepo } from './repo.js'; +export { checkCodeModeAgentStatus } from './status.js'; diff --git a/apps/x/packages/core/src/code-mode/repo.ts b/apps/x/packages/core/src/code-mode/repo.ts new file mode 100644 index 00000000..dd318b34 --- /dev/null +++ b/apps/x/packages/core/src/code-mode/repo.ts @@ -0,0 +1,42 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { WorkDir } from '../config/config.js'; +import { CodeModeConfig } from './types.js'; + +export interface ICodeModeConfigRepo { + getConfig(): Promise; + setConfig(config: CodeModeConfig): Promise; +} + +export class FSCodeModeConfigRepo implements ICodeModeConfigRepo { + private readonly configPath = path.join(WorkDir, 'config', 'code-mode.json'); + private readonly defaultConfig: CodeModeConfig = { enabled: false }; + + constructor() { + this.ensureConfigFile(); + } + + private async ensureConfigFile(): Promise { + try { + await fs.access(this.configPath); + } catch { + await fs.mkdir(path.dirname(this.configPath), { recursive: true }); + await fs.writeFile(this.configPath, JSON.stringify(this.defaultConfig, null, 2)); + } + } + + async getConfig(): Promise { + try { + const content = await fs.readFile(this.configPath, 'utf8'); + return CodeModeConfig.parse(JSON.parse(content)); + } catch { + return this.defaultConfig; + } + } + + async setConfig(config: CodeModeConfig): Promise { + const validated = CodeModeConfig.parse(config); + await fs.mkdir(path.dirname(this.configPath), { recursive: true }); + await fs.writeFile(this.configPath, JSON.stringify(validated, null, 2)); + } +} diff --git a/apps/x/packages/core/src/code-mode/status.ts b/apps/x/packages/core/src/code-mode/status.ts new file mode 100644 index 00000000..78e71fd2 --- /dev/null +++ b/apps/x/packages/core/src/code-mode/status.ts @@ -0,0 +1,157 @@ +import { exec } from 'child_process'; +import { promisify } from 'util'; +import os from 'os'; +import path from 'path'; +import fs from 'fs/promises'; +import { existsSync } from 'fs'; +import { CodeModeAgentStatus } from './types.js'; + +const execAsync = promisify(exec); + +// Where claude.cmd / codex.cmd typically live when installed via npm/pnpm/yarn. +// We scan these directly because Electron's spawned shell sometimes doesn't +// inherit the user's full PATH (especially on macOS GUI launches, and even on +// Windows when global npm prefix isn't propagated to system PATH). +function commonInstallPaths(binary: string): string[] { + const home = os.homedir(); + if (process.platform === 'win32') { + const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming'); + const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'); + const programFiles = process.env.ProgramFiles || 'C:\\Program Files'; + return [ + path.join(appData, 'npm', `${binary}.cmd`), + path.join(appData, 'npm', `${binary}.exe`), + path.join(localAppData, 'npm', `${binary}.cmd`), + path.join(localAppData, 'pnpm', `${binary}.cmd`), + path.join(home, 'AppData', 'Roaming', 'pnpm', `${binary}.cmd`), + path.join(programFiles, 'nodejs', `${binary}.cmd`), + path.join(home, '.volta', 'bin', `${binary}.cmd`), + ]; + } + return [ + '/usr/local/bin', + '/opt/homebrew/bin', // Apple Silicon Homebrew + '/usr/bin', + path.join(home, '.npm-global', 'bin'), + path.join(home, '.local', 'bin'), + path.join(home, '.volta', 'bin'), + path.join(home, '.nvm', 'versions', 'node'), // partial; nvm has versioned subdirs + path.join(home, 'bin'), + ].map(dir => path.join(dir, binary)); +} + +async function probeShell(binary: string): Promise { + try { + if (process.platform === 'win32') { + const { stdout } = await execAsync(`where ${binary}`, { timeout: 5000 }); + return stdout.trim().length > 0; + } + // Login shell so ~/.zprofile / ~/.bashrc PATH additions are visible — + // essential for Homebrew, nvm, asdf, volta installs on macOS GUI launches. + const { stdout } = await execAsync(`/bin/sh -lc 'command -v ${binary}'`, { timeout: 5000 }); + return stdout.trim().length > 0; + } catch { + return false; + } +} + +async function isInstalled(binary: string): Promise { + if (await probeShell(binary)) return true; + // Fallback: scan well-known install locations directly. + for (const candidate of commonInstallPaths(binary)) { + if (existsSync(candidate)) return true; + } + return false; +} + +function decodeJwtPayload(token: string): Record | null { + try { + const parts = token.split('.'); + if (parts.length < 2) return null; + const padded = parts[1].replace(/-/g, '+').replace(/_/g, '/'); + const pad = padded.length % 4 === 0 ? '' : '='.repeat(4 - (padded.length % 4)); + const json = Buffer.from(padded + pad, 'base64').toString('utf-8'); + const parsed = JSON.parse(json); + return typeof parsed === 'object' && parsed !== null ? parsed as Record : null; + } catch { + return null; + } +} + +// Validates Claude Code auth: ~/.claude/.credentials.json (or ~/.config fallback). +// Considered signed in if any of: valid API key, unexpired access token, or +// presence of a refresh token (which can mint a new access token transparently). +async function checkClaudeSignedIn(): Promise { + const home = os.homedir(); + const candidates = [ + path.join(home, '.claude', '.credentials.json'), + path.join(home, '.config', 'claude', '.credentials.json'), + ]; + for (const full of candidates) { + try { + const raw = await fs.readFile(full, 'utf-8'); + const parsed = JSON.parse(raw) as Record; + + const oauth = parsed.claudeAiOauth as Record | undefined; + if (oauth) { + const access = typeof oauth.accessToken === 'string' ? oauth.accessToken : ''; + const refresh = typeof oauth.refreshToken === 'string' ? oauth.refreshToken : ''; + if (refresh.length > 0) return true; + if (access.length > 0) { + if (typeof oauth.expiresAt === 'number' && oauth.expiresAt > 0 && oauth.expiresAt < Date.now()) { + return false; + } + return true; + } + } + + if (typeof parsed.apiKey === 'string' && parsed.apiKey.length > 10) return true; + if (typeof parsed.accessToken === 'string' && parsed.accessToken.length > 10) return true; + } catch { + // try next candidate + } + } + return false; +} + +// Validates Codex auth at ~/.codex/auth.json on all platforms. +// Considered signed in if API key set, or a refresh_token / access_token +// exists. id_token expiry is intentionally NOT used as a rejection signal — +// id_tokens are short-lived (~1h) but refresh_tokens persist for weeks. +async function checkCodexSignedIn(): Promise { + const home = os.homedir(); + const full = path.join(home, '.codex', 'auth.json'); + try { + const raw = await fs.readFile(full, 'utf-8'); + const parsed = JSON.parse(raw) as Record; + + if (typeof parsed.OPENAI_API_KEY === 'string' && parsed.OPENAI_API_KEY.length > 10) return true; + + const tokens = parsed.tokens as Record | undefined; + if (tokens) { + const refresh = typeof tokens.refresh_token === 'string' ? tokens.refresh_token : ''; + const access = typeof tokens.access_token === 'string' ? tokens.access_token : ''; + const id = typeof tokens.id_token === 'string' ? tokens.id_token : ''; + if (refresh.length > 0 || access.length > 0 || id.length > 0) return true; + } + } catch { + // file missing or unreadable + } + return false; +} + +// Exported for diagnostics — silenced unused-var warning by re-export only. +export { decodeJwtPayload }; + +export async function checkCodeModeAgentStatus(): Promise { + const [claudeInstalled, codexInstalled, claudeSignedIn, codexSignedIn] = await Promise.all([ + isInstalled('claude'), + isInstalled('codex'), + checkClaudeSignedIn(), + checkCodexSignedIn(), + ]); + return { + claude: { installed: claudeInstalled, signedIn: claudeSignedIn }, + codex: { installed: codexInstalled, signedIn: codexSignedIn }, + }; +} diff --git a/apps/x/packages/core/src/code-mode/types.ts b/apps/x/packages/core/src/code-mode/types.ts new file mode 100644 index 00000000..57a3158f --- /dev/null +++ b/apps/x/packages/core/src/code-mode/types.ts @@ -0,0 +1,18 @@ +import z from "zod"; + +export const CodeModeConfig = z.object({ + enabled: z.boolean(), +}); +export type CodeModeConfig = z.infer; + +export const AgentStatus = z.object({ + installed: z.boolean(), + signedIn: z.boolean(), +}); +export type AgentStatus = z.infer; + +export const CodeModeAgentStatus = z.object({ + claude: AgentStatus, + codex: AgentStatus, +}); +export type CodeModeAgentStatus = z.infer; diff --git a/apps/x/packages/core/src/di/container.ts b/apps/x/packages/core/src/di/container.ts index 9382de8b..f452105a 100644 --- a/apps/x/packages/core/src/di/container.ts +++ b/apps/x/packages/core/src/di/container.ts @@ -11,6 +11,7 @@ import { IAgentRuntime, AgentRuntime } from "../agents/runtime.js"; import { FSOAuthRepo, IOAuthRepo } from "../auth/repo.js"; import { FSClientRegistrationRepo, IClientRegistrationRepo } from "../auth/client-repo.js"; import { FSGranolaConfigRepo, IGranolaConfigRepo } from "../knowledge/granola/repo.js"; +import { FSCodeModeConfigRepo, ICodeModeConfigRepo } from "../code-mode/repo.js"; import { IAbortRegistry, InMemoryAbortRegistry } from "../runs/abort-registry.js"; import { FSAgentScheduleRepo, IAgentScheduleRepo } from "../agent-schedule/repo.js"; import { FSAgentScheduleStateRepo, IAgentScheduleStateRepo } from "../agent-schedule/state-repo.js"; @@ -38,6 +39,7 @@ container.register({ oauthRepo: asClass(FSOAuthRepo).singleton(), clientRegistrationRepo: asClass(FSClientRegistrationRepo).singleton(), granolaConfigRepo: asClass(FSGranolaConfigRepo).singleton(), + codeModeConfigRepo: asClass(FSCodeModeConfigRepo).singleton(), agentScheduleRepo: asClass(FSAgentScheduleRepo).singleton(), agentScheduleStateRepo: asClass(FSAgentScheduleStateRepo).singleton(), slackConfigRepo: asClass(FSSlackConfigRepo).singleton(), diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index ec0d2e4c..9b1c4834 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -416,6 +416,27 @@ const ipcSchemas = { enabled: z.boolean(), }), }, + 'codeMode:getConfig': { + req: z.null(), + res: z.object({ + enabled: z.boolean(), + }), + }, + 'codeMode:setConfig': { + req: z.object({ + enabled: z.boolean(), + }), + res: z.object({ + success: z.literal(true), + }), + }, + 'codeMode:checkAgentStatus': { + req: z.null(), + res: z.object({ + claude: z.object({ installed: z.boolean(), signedIn: z.boolean() }), + codex: z.object({ installed: z.boolean(), signedIn: z.boolean() }), + }), + }, 'granola:setConfig': { req: z.object({ enabled: z.boolean(),