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.0.77",
"version": "0.0.78",
"description": "Coding anywhere with your coding agents connected",
"module": "packages/core/index.ts",
"type": "module",
Expand Down
11 changes: 11 additions & 0 deletions packages/config/dashboard-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export type DashboardConfig = {
defaultMessageFrequency?: "aggressive" | "medium" | "minimum";
statusMessageFrequencyMs?: 2000 | 5000 | 10000;
};
updates: {
autoUpgrade: boolean;
};
agents: {
opencode: {
enabled: boolean;
Expand Down Expand Up @@ -90,6 +93,9 @@ export const defaultDashboardConfig: DashboardConfig = {
defaultStatusMessageFormat: "medium",
statusMessageFrequencyMs: 2000,
},
updates: {
autoUpgrade: true,
},
agents: {
opencode: { enabled: true, models: [] },
claudecode: { enabled: true },
Expand Down Expand Up @@ -289,6 +295,11 @@ export const sanitizeDashboardConfig = (config: unknown): DashboardConfig => {
),
statusMessageFrequencyMs: asStatusMessageFrequencyMs(user.statusMessageFrequencyMs),
},
updates: {
autoUpgrade: record.updates && typeof record.updates === "object"
? (record.updates as Record<string, unknown>).autoUpgrade !== false
: true,
},
agents: {
opencode: {
enabled: opencodeRecord.enabled !== false,
Expand Down
26 changes: 25 additions & 1 deletion packages/config/local/ode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,9 @@ function toDashboardConfig(config: OdeConfig): DashboardConfig {
? config.user.IM_MESSAGE_UPDATE_INTERVAL_MS
: 2000,
},
updates: {
autoUpgrade: config.updates.autoUpgrade,
},
agents: structuredClone(config.agents),
workspaces: structuredClone(config.workspaces),
};
Expand Down Expand Up @@ -407,6 +410,10 @@ function mergeDashboardConfig(config: OdeConfig, dashboardConfig: DashboardConfi
? statusMessageFrequencyMs
: 2000,
},
updates: {
...config.updates,
autoUpgrade: dashboardConfig.updates.autoUpgrade !== false,
},
agents: structuredClone(dashboardConfig.agents),
workspaces,
};
Expand Down Expand Up @@ -623,6 +630,8 @@ export type GitHubInfo = {
export type UserGeneralSettings = {
defaultStatusMessageFormat: "minimum" | "medium" | "aggressive";
gitStrategy: "default" | "worktree";
statusMessageFrequencyMs: 2000 | 5000 | 10000;
autoUpdate: boolean;
};

export function getGitHubInfoForUser(userId: string): GitHubInfo | null {
Expand All @@ -640,13 +649,20 @@ export function getGitHubInfoForUser(userId: string): GitHubInfo | null {
}

export function getUserGeneralSettings(): UserGeneralSettings {
const user = loadOdeConfig().user;
const odeConfig = loadOdeConfig();
const user = odeConfig.user;
const updates = odeConfig.updates;
return {
defaultStatusMessageFormat:
user.defaultStatusMessageFormat === "minimum" || user.defaultStatusMessageFormat === "aggressive"
? user.defaultStatusMessageFormat
: "medium",
gitStrategy: user.gitStrategy === "default" ? "default" : "worktree",
statusMessageFrequencyMs:
user.IM_MESSAGE_UPDATE_INTERVAL_MS === 5000 || user.IM_MESSAGE_UPDATE_INTERVAL_MS === 10000
? user.IM_MESSAGE_UPDATE_INTERVAL_MS
: 2000,
autoUpdate: updates.autoUpgrade !== false,
};
}

Expand All @@ -657,6 +673,14 @@ export function setUserGeneralSettings(settings: UserGeneralSettings): void {
...config.user,
defaultStatusMessageFormat: settings.defaultStatusMessageFormat,
gitStrategy: settings.gitStrategy,
IM_MESSAGE_UPDATE_INTERVAL_MS:
settings.statusMessageFrequencyMs === 5000 || settings.statusMessageFrequencyMs === 10000
? settings.statusMessageFrequencyMs
: 2000,
},
updates: {
...config.updates,
autoUpgrade: settings.autoUpdate !== false,
},
}));
}
Expand Down
42 changes: 39 additions & 3 deletions packages/core/web/server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { existsSync } from "fs";
import { existsSync, readFileSync } from "fs";
import { join, resolve, sep } from "path";
import { EMBEDDED_ASSETS, HAS_EMBEDDED_ASSETS } from "./embedded-assets";
import {
Expand Down Expand Up @@ -37,11 +37,47 @@ const DEFAULT_WEB_BUILD_DIR = join(process.cwd(), "packages", "web-ui", "build")
const DEFAULT_SESSION_EVENTS_LIMIT = 2000;
const MAX_SESSION_EVENTS_LIMIT = 10000;

function resolveAppVersion(): string {
try {
const proc = Bun.spawnSync({
cmd: ["ode", "version"],
stdout: "pipe",
stderr: "pipe",
});
if (proc.exitCode === 0) {
const output = Buffer.from(proc.stdout).toString("utf-8").trim();
const match = output.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/);
if (match?.[0]) {
return match[0];
}
if (output.length > 0) {
return output;
}
}
} catch {
// Ignore and try file fallback.
}

try {
const raw = readFileSync(join(process.cwd(), "package.json"), "utf-8");
const parsed = JSON.parse(raw) as { version?: unknown };
if (typeof parsed.version === "string" && parsed.version.trim().length > 0) {
return parsed.version.trim();
}
} catch {
// Ignore and use fallback.
}
return "unknown";
}

const APP_VERSION = resolveAppVersion();

let webServer: ReturnType<typeof Bun.serve> | null = null;

type JsonResponse = {
ok: boolean;
error?: string;
version?: string;
config?: typeof defaultDashboardConfig;
workspace?: (typeof defaultDashboardConfig)["workspaces"][number];
agentCheck?: {
Expand Down Expand Up @@ -497,7 +533,7 @@ async function handleRequest(request: Request): Promise<Response> {
if (pathname === "/api/config") {
if (request.method === "GET") {
const config = await readLocalSettings();
return jsonResponse(200, { ok: true, config });
return jsonResponse(200, { ok: true, config, version: APP_VERSION });
}
if (request.method === "PUT") {
try {
Expand All @@ -508,7 +544,7 @@ async function handleRequest(request: Request): Promise<Response> {
return jsonResponse(400, { ok: false, error: validationError });
}
await writeLocalSettings(sanitized);
return jsonResponse(200, { ok: true, config: sanitized });
return jsonResponse(200, { ok: true, config: sanitized, version: APP_VERSION });
} catch (error) {
const message = error instanceof Error ? error.message : "Invalid payload";
return jsonResponse(400, { ok: false, error: message });
Expand Down
96 changes: 95 additions & 1 deletion packages/ims/discord/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ const DISCORD_THREAD_RENAME_LIMIT = 90;
const DISCORD_MODAL_CHANNEL = "ode:modal:channel_details";
const DISCORD_MODAL_GITHUB = "ode:modal:github";
const STATUS_FORMAT_OPTIONS = ["aggressive", "medium", "minimum"] as const;
const STATUS_FREQUENCY_OPTIONS = ["2000", "5000", "10000"] as const;
const GIT_STRATEGY_OPTIONS = ["worktree", "default"] as const;
const AUTO_UPDATE_OPTIONS = ["on", "off"] as const;
const PROVIDERS = ["opencode", "claudecode", "codex", "kimi", "kiro", "kilo", "qwen", "goose", "gemini"] as const;
const DISCORD_LAUNCHER_COMMANDS = [
{
Expand All @@ -62,7 +64,9 @@ const statusMessageThreadMap = new Map<string, string>();
const channelSettingsDrafts = new Map<string, { provider: typeof PROVIDERS[number]; model: string }>();
const generalSettingsDrafts = new Map<string, {
statusFormat: typeof STATUS_FORMAT_OPTIONS[number];
statusFrequencyMs: typeof STATUS_FREQUENCY_OPTIONS[number];
gitStrategy: typeof GIT_STRATEGY_OPTIONS[number];
autoUpdate: typeof AUTO_UPDATE_OPTIONS[number];
}>();

function splitForDiscord(text: string): string[] {
Expand Down Expand Up @@ -429,20 +433,40 @@ function parseGitStrategy(value: string): "default" | "worktree" | null {
return null;
}

function parseStatusFrequency(value: string): "2000" | "5000" | "10000" | null {
const normalized = value.trim();
if (normalized === "2000" || normalized === "5000" || normalized === "10000") {
return normalized;
}
return null;
}

function parseAutoUpdate(value: string): "on" | "off" | null {
const normalized = value.trim().toLowerCase();
if (normalized === "on" || normalized === "off") return normalized;
return null;
}

function getInitialGeneralDraft(): {
statusFormat: typeof STATUS_FORMAT_OPTIONS[number];
statusFrequencyMs: typeof STATUS_FREQUENCY_OPTIONS[number];
gitStrategy: typeof GIT_STRATEGY_OPTIONS[number];
autoUpdate: typeof AUTO_UPDATE_OPTIONS[number];
} {
const settings = getUserGeneralSettings();
return {
statusFormat: settings.defaultStatusMessageFormat,
statusFrequencyMs: String(settings.statusMessageFrequencyMs) as typeof STATUS_FREQUENCY_OPTIONS[number],
gitStrategy: settings.gitStrategy,
autoUpdate: settings.autoUpdate ? "on" : "off",
};
}

function getGeneralDraftOrInitial(userId: string, channelId: string): {
statusFormat: typeof STATUS_FORMAT_OPTIONS[number];
statusFrequencyMs: typeof STATUS_FREQUENCY_OPTIONS[number];
gitStrategy: typeof GIT_STRATEGY_OPTIONS[number];
autoUpdate: typeof AUTO_UPDATE_OPTIONS[number];
} {
return generalSettingsDrafts.get(draftKey(userId, channelId)) ?? getInitialGeneralDraft();
}
Expand Down Expand Up @@ -478,11 +502,35 @@ function buildGeneralSettingsPickerPayload(params: {
}))
);

const statusFrequencySelect = new StringSelectMenuBuilder()
.setCustomId(`ode:general:frequency:${params.channelId}`)
.setPlaceholder("Status frequency")
.addOptions(
STATUS_FREQUENCY_OPTIONS.map((value) => ({
label: `${Number(value) / 1000} seconds`,
value,
default: value === draft.statusFrequencyMs,
}))
);

const autoUpdateSelect = new StringSelectMenuBuilder()
.setCustomId(`ode:general:auto_update:${params.channelId}`)
.setPlaceholder("Auto update")
.addOptions(
AUTO_UPDATE_OPTIONS.map((value) => ({
label: value === "on" ? "On" : "Off",
value,
default: value === draft.autoUpdate,
}))
);

return {
content: `General settings (draft)\nStatus: ${draft.statusFormat}\nGit strategy: ${draft.gitStrategy}`,
content: `General settings (draft)\nStatus: ${draft.statusFormat}\nStatus frequency: ${Number(draft.statusFrequencyMs) / 1000} seconds\nGit strategy: ${draft.gitStrategy}\nAuto update: ${draft.autoUpdate}`,
components: [
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(statusSelect),
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(statusFrequencySelect),
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(gitSelect),
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(autoUpdateSelect),
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`ode:general:save:${params.channelId}`)
Expand Down Expand Up @@ -657,7 +705,27 @@ async function handleGeneralSettingsComponentInteraction(interaction: any): Prom
}
generalSettingsDrafts.set(key, {
statusFormat: parsed,
statusFrequencyMs: draft.statusFrequencyMs,
gitStrategy: draft.gitStrategy,
autoUpdate: draft.autoUpdate,
});
const payload = buildGeneralSettingsPickerPayload({ channelId, userId });
await interaction.update(payload);
return true;
}

if (action === "frequency") {
const selected = interaction.values?.[0] as string | undefined;
const parsed = selected ? parseStatusFrequency(selected) : null;
if (!parsed) {
await interaction.reply({ content: "Invalid status message frequency.", flags: MessageFlags.Ephemeral });
return true;
}
generalSettingsDrafts.set(key, {
statusFormat: draft.statusFormat,
statusFrequencyMs: parsed,
gitStrategy: draft.gitStrategy,
autoUpdate: draft.autoUpdate,
});
const payload = buildGeneralSettingsPickerPayload({ channelId, userId });
await interaction.update(payload);
Expand All @@ -673,7 +741,27 @@ async function handleGeneralSettingsComponentInteraction(interaction: any): Prom
}
generalSettingsDrafts.set(key, {
statusFormat: draft.statusFormat,
statusFrequencyMs: draft.statusFrequencyMs,
gitStrategy: parsed,
autoUpdate: draft.autoUpdate,
});
const payload = buildGeneralSettingsPickerPayload({ channelId, userId });
await interaction.update(payload);
return true;
}

if (action === "auto_update") {
const selected = interaction.values?.[0] as string | undefined;
const parsed = selected ? parseAutoUpdate(selected) : null;
if (!parsed) {
await interaction.reply({ content: "Invalid auto update setting.", flags: MessageFlags.Ephemeral });
return true;
}
generalSettingsDrafts.set(key, {
statusFormat: draft.statusFormat,
statusFrequencyMs: draft.statusFrequencyMs,
gitStrategy: draft.gitStrategy,
autoUpdate: parsed,
});
const payload = buildGeneralSettingsPickerPayload({ channelId, userId });
await interaction.update(payload);
Expand All @@ -684,6 +772,12 @@ async function handleGeneralSettingsComponentInteraction(interaction: any): Prom
setUserGeneralSettings({
defaultStatusMessageFormat: draft.statusFormat,
gitStrategy: draft.gitStrategy,
statusMessageFrequencyMs: draft.statusFrequencyMs === "5000"
? 5000
: draft.statusFrequencyMs === "10000"
? 10000
: 2000,
autoUpdate: draft.autoUpdate !== "off",
});
generalSettingsDrafts.delete(key);
await interaction.reply({ content: "General settings updated.", flags: MessageFlags.Ephemeral });
Expand Down
Loading