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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ode",
"version": "0.1.20",
"version": "0.1.21",
"description": "Coding anywhere with your coding agents connected",
"module": "packages/core/index.ts",
"type": "module",
Expand Down
81 changes: 48 additions & 33 deletions packages/ims/discord/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import {
SETTINGS_LAUNCHER_ITEMS,
type SettingsLauncherAction,
} from "@/ims/shared/settings-domain";
import {
refreshSettingsProviderData,
type SettingsProviderData,
} from "@/ims/shared/settings-provider-data";
import {
AGENT_PROVIDERS,
isAgentProviderId,
Expand All @@ -31,7 +35,6 @@ import {
getChannelModel,
getChannelBaseBranch,
resolveChannelCwd,
isAgentEnabled,
setChannelAgentProvider,
setChannelModel,
setChannelWorkingDirectory,
Expand Down Expand Up @@ -147,8 +150,16 @@ function parseProvider(value: string): AgentProviderId | null {
return isAgentProviderId(normalized) ? normalized : null;
}

function getProviderModels(provider: AgentProviderId): string[] {
return getProviderModelList(provider, getProviderModelListsFromConfig());
function getProviderModels(provider: AgentProviderId, providerData?: SettingsProviderData): string[] {
if (!providerData) {
return getProviderModelList(provider, getProviderModelListsFromConfig());
}

return getProviderModelList(provider, {
opencode: providerData.opencodeModels,
codex: providerData.codexModels,
kilo: providerData.kiloModels,
});
}

function draftKey(userId: string, channelId: string): string {
Expand All @@ -170,13 +181,15 @@ function getDraftOrInitial(userId: string, channelId: string): { provider: Agent
function buildChannelSettingsPickerPayload(params: {
channelId: string;
userId: string;
providerData?: SettingsProviderData;
}): {
content: string;
components: Array<ActionRowBuilder<StringSelectMenuBuilder> | ActionRowBuilder<ButtonBuilder>>;
} {
const { channelId, userId } = params;
const { channelId, userId, providerData } = params;
const draft = getDraftOrInitial(userId, channelId);
const providerOptions = AGENT_PROVIDERS.map((provider) => ({
const enabledProviders = providerData?.enabledProviders ?? AGENT_PROVIDERS;
const providerOptions = enabledProviders.map((provider) => ({
label: provider,
value: provider,
default: provider === draft.provider,
Expand All @@ -191,7 +204,7 @@ function buildChannelSettingsPickerPayload(params: {
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(providerSelect),
];

const models = getProviderModels(draft.provider);
const models = getProviderModels(draft.provider, providerData);
if (providerSupportsModelSelection(draft.provider)) {
const selectedModel = findMatchingModel(models, draft.model) ?? "";
const modelOptions = models.length > 0
Expand Down Expand Up @@ -418,25 +431,26 @@ function stringSelectLabel(params: {
return label;
}

function buildChannelSettingsModal(channelId: string): ModalBuilder {
function buildChannelSettingsModal(channelId: string, providerData?: SettingsProviderData): ModalBuilder {
const provider = getChannelAgentProvider(channelId);
const model = getChannelModel(channelId) || "";
const providerModels = getProviderModels(provider);
const providerModels = getProviderModels(provider, providerData);
const selectedModel = findMatchingModel(providerModels, model) ?? providerModels[0] ?? "";
const baseBranch = getChannelBaseBranch(channelId) || "main";
const workingDirectory = resolveChannelCwd(channelId).workingDirectory || "";
const systemMessage = getChannelSystemMessage(channelId) || "";
const enabledProviders = providerData?.enabledProviders ?? AGENT_PROVIDERS;

const modalLabels: LabelBuilder[] = [
stringSelectLabel({
id: "agent_provider",
label: "Agent provider",
options: AGENT_PROVIDERS.map((item) => ({
options: enabledProviders.map((item) => ({
label: item,
value: item,
default: item === provider,
})),
placeholder: AGENT_PROVIDERS.join(", "),
placeholder: enabledProviders.join(", "),
}),
];

Expand Down Expand Up @@ -595,7 +609,8 @@ async function handleLauncherButtonInteraction(interaction: any): Promise<boolea
channelId,
userId: interaction.user.id,
});
await interaction.showModal(buildChannelSettingsModal(channelId));
const providerData = await refreshSettingsProviderData(getChannelAgentProvider(channelId));
await interaction.showModal(buildChannelSettingsModal(channelId, providerData));
return true;
}

Expand Down Expand Up @@ -623,24 +638,17 @@ async function handleModalSubmitInteraction(interaction: any): Promise<boolean>
if (modalKind === DISCORD_MODAL_CHANNEL) {
const providerValue = (getModalSelectValue(interaction, "agent_provider") || getModalValue(interaction, "agent_provider")).trim();
const parsedProvider = parseProvider(providerValue);
if (!parsedProvider || !isAgentEnabled(parsedProvider)) {
const providerData = parsedProvider ? await refreshSettingsProviderData(parsedProvider) : null;
if (!parsedProvider || !providerData?.enabledProviders.includes(parsedProvider)) {
await interaction.reply({
content: `Invalid provider. Use one of: ${AGENT_PROVIDERS.join(", ")}`,
content: `Invalid provider. Use one of: ${(providerData?.enabledProviders ?? AGENT_PROVIDERS).join(", ")}`,
flags: MessageFlags.Ephemeral,
});
return true;
}

const modelInputRaw = (getModalSelectValue(interaction, "model") || getModalValue(interaction, "model")).trim();
const modelInput = modelInputRaw === PROVIDER_DEFAULT_MODEL_VALUE ? "" : modelInputRaw;
const providerModels = getProviderModels(parsedProvider);
if (providerModels.length > 0 && modelInput && !findMatchingModel(providerModels, modelInput)) {
await interaction.reply({
content: "Model is not available for the selected provider.",
flags: MessageFlags.Ephemeral,
});
return true;
}

const workingDirectory = getModalValue(interaction, "working_directory").trim();
const baseBranch = getModalValue(interaction, "base_branch").trim() || "main";
Expand All @@ -650,7 +658,11 @@ async function handleModalSubmitInteraction(interaction: any): Promise<boolean>
setChannelModel(channelId, resolveStoredModelForProvider({
provider: parsedProvider,
selectedModel: modelInput,
lists: getProviderModelListsFromConfig(),
lists: {
opencode: providerData.opencodeModels,
codex: providerData.codexModels,
kilo: providerData.kiloModels,
},
}));
setChannelWorkingDirectory(channelId, workingDirectory.length > 0 ? workingDirectory : null);
setChannelBaseBranch(channelId, baseBranch);
Expand Down Expand Up @@ -838,17 +850,18 @@ async function handleChannelSettingsComponentInteraction(interaction: any): Prom
if (action === "provider") {
const selected = interaction.values?.[0] as string | undefined;
const parsed = selected ? parseProvider(selected) : null;
if (!parsed || !isAgentEnabled(parsed)) {
const providerData = parsed ? await refreshSettingsProviderData(parsed) : null;
if (!parsed || !providerData?.enabledProviders.includes(parsed)) {
await interaction.reply({ content: "Selected provider is invalid or disabled.", flags: MessageFlags.Ephemeral });
return true;
}
const models = getProviderModels(parsed);
const models = getProviderModels(parsed, providerData);
const nextDraft = {
provider: parsed,
model: models.length > 0 ? (findMatchingModel(models, draft.model) ?? models[0]!) : "",
};
channelSettingsDrafts.set(key, nextDraft);
await interaction.update(buildChannelSettingsPickerPayload({ channelId, userId }));
await interaction.update(buildChannelSettingsPickerPayload({ channelId, userId, providerData }));
return true;
}

Expand All @@ -860,30 +873,32 @@ async function handleChannelSettingsComponentInteraction(interaction: any): Prom
model: selected === PROVIDER_DEFAULT_MODEL_VALUE ? "" : selected,
});
}
await interaction.update(buildChannelSettingsPickerPayload({ channelId, userId }));
const providerData = await refreshSettingsProviderData(draft.provider);
await interaction.update(buildChannelSettingsPickerPayload({ channelId, userId, providerData }));
return true;
}

if (action === "save") {
const models = getProviderModels(draft.provider);
if (models.length > 0 && draft.model && !findMatchingModel(models, draft.model)) {
await interaction.reply({ content: "Selected model is no longer available.", flags: MessageFlags.Ephemeral });
return true;
}
const providerData = await refreshSettingsProviderData(draft.provider);

setChannelAgentProvider(channelId, draft.provider);
setChannelModel(channelId, resolveStoredModelForProvider({
provider: draft.provider,
selectedModel: draft.model,
lists: getProviderModelListsFromConfig(),
lists: {
opencode: providerData.opencodeModels,
codex: providerData.codexModels,
kilo: providerData.kiloModels,
},
}));
channelSettingsDrafts.delete(key);
await interaction.reply({ content: "Channel provider/model updated.", flags: MessageFlags.Ephemeral });
return true;
}

if (action === "edit") {
await interaction.showModal(buildChannelSettingsModal(channelId));
const providerData = await refreshSettingsProviderData(draft.provider);
await interaction.showModal(buildChannelSettingsModal(channelId, providerData));
return true;
}

Expand Down
46 changes: 27 additions & 19 deletions packages/ims/lark/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createAgentAdapter } from "@/agents/adapter";
import type { OpenCodeMessageContext } from "@/agents/types";
import * as Lark from "@larksuiteoapi/node-sdk";
import {
getChannelAgentProvider,
getChannelSystemMessage,
getGitHubInfoForUser,
getLarkAppCredentials,
Expand Down Expand Up @@ -39,6 +40,7 @@ import {
resolveLarkSettingsCardAction,
sendLarkSettingsCard,
} from "./settings";
import { refreshSettingsProviderData } from "@/ims/shared/settings-provider-data";
import { createProcessorManager } from "@/ims/shared/processor-manager";
import {
extractFormValues,
Expand Down Expand Up @@ -958,31 +960,37 @@ async function processLarkCardAction(payload: unknown): Promise<void> {
return;
}

const detailAction = (
action === "set_general_settings"
|| action === "set_general_status_format"
|| action === "set_general_status_frequency"
|| action === "set_general_git_strategy"
|| action === "set_general_auto_update"
|| action === "set_channel_settings"
|| action === "set_github_info"
|| action === "clear_github_info"
)
? (
action === "set_channel_settings"
? "open_settings_modal"
: action === "set_github_info" || action === "clear_github_info"
? "open_github_token_modal"
: "open_general_settings_modal"
)
: action;

const providerData = detailAction === "open_settings_modal"
? await refreshSettingsProviderData(getChannelAgentProvider(channelId))
: undefined;

const card = action === "open_settings_launcher"
? null
: buildLarkSettingsDetailCard({
action: (
action === "set_general_settings"
||
action === "set_general_status_format"
|| action === "set_general_status_frequency"
|| action === "set_general_git_strategy"
|| action === "set_general_auto_update"
|| action === "set_channel_settings"
|| action === "set_github_info"
|| action === "clear_github_info"
)
? (
action === "set_channel_settings"
? "open_settings_modal"
: action === "set_github_info" || action === "clear_github_info"
? "open_github_token_modal"
: "open_general_settings_modal"
)
: action,
action: detailAction,
channelId,
threadId,
userId: userId || "",
providerData,
notice: (
action === "set_general_settings"
||
Expand Down
14 changes: 11 additions & 3 deletions packages/ims/lark/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
getProviderModelListsFromConfig,
SETTINGS_LAUNCHER_ITEMS,
} from "@/ims/shared/settings-domain";
import type { SettingsProviderData } from "@/ims/shared/settings-provider-data";

export type LarkSettingsCardAction =
| "open_settings_launcher"
Expand Down Expand Up @@ -155,8 +156,9 @@ export function buildLarkSettingsDetailCard(params: {
threadId: string;
userId: string;
notice?: string;
providerData?: SettingsProviderData;
}): Record<string, unknown> {
const { action, channelId, threadId, userId, notice } = params;
const { action, channelId, threadId, userId, notice, providerData } = params;

if (action === "open_general_settings_modal") {
const general = getUserGeneralSettings();
Expand Down Expand Up @@ -290,11 +292,17 @@ export function buildLarkSettingsDetailCard(params: {
const cwd = resolveChannelCwd(channelId).workingDirectory || "(not set)";
const baseBranch = getChannelBaseBranch(channelId);
const systemMessage = getChannelSystemMessage(channelId) || "(none)";
const providerOptions = getEnabledProvidersWithFallback().map((item) => ({
const providerOptions = (providerData?.enabledProviders ?? getEnabledProvidersWithFallback()).map((item) => ({
text: { tag: "plain_text", content: item },
value: item,
}));
const modelLists = getProviderModelListsFromConfig();
const modelLists = providerData
? {
opencode: providerData.opencodeModels,
codex: providerData.codexModels,
kilo: providerData.kiloModels,
}
: getProviderModelListsFromConfig();
const providerModels = provider === "codex"
? modelLists.codex
: provider === "kilo"
Expand Down
68 changes: 68 additions & 0 deletions packages/ims/shared/settings-provider-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { startServer as startCodexServer } from "@/agents/codex";
import {
getCodexModels,
getEnabledAgentProviders,
getKiloModels,
getOpenCodeModels,
invalidateOdeConfigCache,
} from "@/config";
import { runAgentCheck, type AgentCheckResult } from "@/core/web/agent-check";
import { AGENT_PROVIDERS, type AgentProviderId } from "@/shared/agent-provider";

export type SettingsProviderData = {
enabledProviders: AgentProviderId[];
opencodeModels: string[];
codexModels: string[];
kiloModels: string[];
};

function getSelectableProvidersFromConfig(): AgentProviderId[] {
const enabled = getEnabledAgentProviders().filter(
(provider): provider is AgentProviderId => AGENT_PROVIDERS.includes(provider as AgentProviderId)
);
if (enabled.length > 0) return enabled;
return Array.from(AGENT_PROVIDERS);
}

function getSelectableProvidersFromAgentCheck(
result: AgentCheckResult,
selectedProvider?: AgentProviderId
): AgentProviderId[] {
const enabled = AGENT_PROVIDERS.filter((provider) => {
if (provider === "claudecode") return result.claudecode;
return result[provider];
});

if (selectedProvider && !enabled.includes(selectedProvider)) {
enabled.unshift(selectedProvider);
}

if (enabled.length > 0) return enabled;
return getSelectableProvidersFromConfig();
}

export async function refreshSettingsProviderData(selectedProvider?: AgentProviderId): Promise<SettingsProviderData> {
invalidateOdeConfigCache();

let agentCheckResult: AgentCheckResult | null = null;
try {
agentCheckResult = await runAgentCheck();
} catch {
// Fall back to the config currently loaded from disk.
}

try {
await startCodexServer();
} catch {
// Fall back to models currently stored in local config.
}

return {
enabledProviders: agentCheckResult
? getSelectableProvidersFromAgentCheck(agentCheckResult, selectedProvider)
: getSelectableProvidersFromConfig(),
opencodeModels: agentCheckResult ? agentCheckResult.opencodeModels : getOpenCodeModels(),
codexModels: getCodexModels(),
kiloModels: agentCheckResult ? agentCheckResult.kiloModels : getKiloModels(),
};
}
Loading
Loading