Skip to content

Commit 2938c94

Browse files
committed
refactor: split workspace settings by platform and centralize provider logic
1 parent e3d028b commit 2938c94

File tree

15 files changed

+908
-770
lines changed

15 files changed

+908
-770
lines changed

packages/config/dashboard-config.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
parseStatusMessageFrequencyMs,
44
type StatusMessageFrequencyMs,
55
} from "./status-message-frequency";
6+
import { isAgentProviderId, type AgentProviderId } from "@/shared/agent-provider";
67

78
export type DashboardConfig = {
89
completeOnboarding: boolean;
@@ -69,7 +70,7 @@ export type DashboardConfig = {
6970
channelDetails: {
7071
id: string;
7172
name: string;
72-
agentProvider?: "opencode" | "claudecode" | "codex" | "kimi" | "kiro" | "kilo" | "qwen" | "goose" | "gemini";
73+
agentProvider?: AgentProviderId;
7374
model: string;
7475
workingDirectory: string;
7576
baseBranch: string;
@@ -152,16 +153,10 @@ const asGitStrategy = (
152153
const asStatus = (value: unknown): DashboardConfig["workspaces"][number]["status"] =>
153154
value === "paused" ? "paused" : "active";
154155

155-
const KNOWN_AGENT_PROVIDERS = new Set<NonNullable<
156-
DashboardConfig["workspaces"][number]["channelDetails"][number]["agentProvider"]
157-
>>(["opencode", "claudecode", "codex", "kimi", "kiro", "kilo", "qwen", "goose", "gemini"]);
158-
159156
function isKnownAgentProvider(
160157
value: string
161158
): value is NonNullable<DashboardConfig["workspaces"][number]["channelDetails"][number]["agentProvider"]> {
162-
return KNOWN_AGENT_PROVIDERS.has(value as NonNullable<
163-
DashboardConfig["workspaces"][number]["channelDetails"][number]["agentProvider"]
164-
>);
159+
return isAgentProviderId(value);
165160
}
166161

167162
const asAgentProvider = (
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { z } from "zod";
2+
import { AGENT_PROVIDERS } from "@/shared/agent-provider";
3+
import { DEFAULT_STATUS_MESSAGE_FREQUENCY_MS } from "../status-message-frequency";
4+
5+
const DEFAULT_UPDATE_INTERVAL_MS = 60 * 60 * 1000;
6+
7+
const userSchema = z.object({
8+
name: z.string().optional().default(""),
9+
email: z.string().optional().default(""),
10+
initials: z.string().optional().default(""),
11+
avatar: z.string().optional().default(""),
12+
gitStrategy: z.enum(["default", "worktree"]).optional().default("worktree"),
13+
defaultStatusMessageFormat: z.enum([
14+
"minimum",
15+
"medium",
16+
"aggressive",
17+
"low",
18+
"high",
19+
]).optional().default("medium"),
20+
defaultMessageFrequency: z.enum([
21+
"minimum",
22+
"medium",
23+
"aggressive",
24+
"low",
25+
"high",
26+
]).optional(),
27+
messageUpdateIntervalMs: z.number().optional(),
28+
IM_MESSAGE_UPDATE_INTERVAL_MS: z.number().optional().default(DEFAULT_STATUS_MESSAGE_FREQUENCY_MS),
29+
});
30+
31+
export const agentProviderSchema = z.enum(AGENT_PROVIDERS);
32+
33+
const agentsSchema = z.object({
34+
opencode: z.object({
35+
enabled: z.boolean().optional().default(true),
36+
models: z.array(z.string()).optional().default([]),
37+
}).optional().default({ enabled: true, models: [] }),
38+
claudecode: z.object({
39+
enabled: z.boolean().optional().default(true),
40+
}).optional().default({ enabled: true }),
41+
codex: z.object({
42+
enabled: z.boolean().optional().default(true),
43+
models: z.array(z.string()).optional().default([]),
44+
}).optional().default({ enabled: true, models: [] }),
45+
kimi: z.object({
46+
enabled: z.boolean().optional().default(true),
47+
}).optional().default({ enabled: true }),
48+
kiro: z.object({
49+
enabled: z.boolean().optional().default(true),
50+
}).optional().default({ enabled: true }),
51+
kilo: z.object({
52+
enabled: z.boolean().optional().default(true),
53+
models: z.array(z.string()).optional().default([]),
54+
}).optional().default({ enabled: true, models: [] }),
55+
qwen: z.object({
56+
enabled: z.boolean().optional().default(true),
57+
}).optional().default({ enabled: true }),
58+
goose: z.object({
59+
enabled: z.boolean().optional().default(true),
60+
}).optional().default({ enabled: true }),
61+
gemini: z.object({
62+
enabled: z.boolean().optional().default(true),
63+
}).optional().default({ enabled: true }),
64+
}).optional().default({
65+
opencode: { enabled: true, models: [] },
66+
claudecode: { enabled: true },
67+
codex: { enabled: true, models: [] },
68+
kimi: { enabled: true },
69+
kiro: { enabled: true },
70+
kilo: { enabled: true, models: [] },
71+
qwen: { enabled: true },
72+
goose: { enabled: true },
73+
gemini: { enabled: true },
74+
});
75+
76+
const channelDetailSchema = z.object({
77+
id: z.string(),
78+
name: z.string(),
79+
agentProvider: z.preprocess(
80+
(value) => (value === "claude" ? "claudecode" : value),
81+
agentProviderSchema.optional().default("opencode")
82+
),
83+
model: z.string().optional().default(""),
84+
workingDirectory: z.string().optional().default(""),
85+
baseBranch: z.string().optional().default("main"),
86+
channelSystemMessage: z.string().optional().default(""),
87+
});
88+
89+
const updateSchema = z.object({
90+
autoUpgrade: z.boolean().optional().default(true),
91+
checkIntervalMs: z.number().optional().default(DEFAULT_UPDATE_INTERVAL_MS),
92+
});
93+
94+
const workspaceSchema = z.object({
95+
id: z.string(),
96+
type: z.enum(["slack", "discord", "lark"]).optional().default("slack"),
97+
name: z.string().optional().default(""),
98+
domain: z.string().optional().default(""),
99+
status: z.enum(["active", "paused"]).optional().default("active"),
100+
channels: z.number().optional().default(0),
101+
members: z.number().optional().default(0),
102+
lastSync: z.string().optional().default(""),
103+
slackAppToken: z.string().optional().default(""),
104+
slackBotToken: z.string().optional().default(""),
105+
discordBotToken: z.string().optional().default(""),
106+
larkAppKey: z.string().optional().default(""),
107+
larkAppId: z.string().optional().default(""),
108+
larkAppSecret: z.string().optional().default(""),
109+
channelDetails: z.array(channelDetailSchema).optional().default([]),
110+
});
111+
112+
export const odeConfigSchema = z.object({
113+
user: userSchema,
114+
githubInfos: z
115+
.record(
116+
z.string(),
117+
z.object({
118+
token: z.string().optional().default(""),
119+
gitName: z.string().optional().default(""),
120+
gitEmail: z.string().optional().default(""),
121+
})
122+
)
123+
.optional()
124+
.default({}),
125+
agents: agentsSchema,
126+
completeOnboarding: z.boolean().optional().default(false),
127+
workspaces: z.array(workspaceSchema),
128+
updates: updateSchema.optional().default({
129+
autoUpgrade: true,
130+
checkIntervalMs: DEFAULT_UPDATE_INTERVAL_MS,
131+
}),
132+
});
133+
134+
export type ChannelDetail = z.infer<typeof channelDetailSchema>;
135+
export type WorkspaceConfig = z.infer<typeof workspaceSchema>;
136+
export type AgentProvider = z.infer<typeof agentProviderSchema>;
137+
export type AgentsConfig = z.infer<typeof agentsSchema>;
138+
export type UpdateConfig = z.infer<typeof updateSchema>;
139+
export type OdeConfig = z.infer<typeof odeConfigSchema>;
140+
export type UserConfig = z.infer<typeof userSchema>;

packages/config/local/ode.ts

Lines changed: 22 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as fs from "fs";
22
import * as os from "os";
33
import * as path from "path";
4-
import { z } from "zod";
54
import { normalizeCwd } from "../paths";
65
import {
76
sanitizeDashboardConfig,
@@ -12,6 +11,27 @@ import {
1211
parseStatusMessageFrequencyMs,
1312
type StatusMessageFrequencyMs,
1413
} from "../status-message-frequency";
14+
import {
15+
odeConfigSchema,
16+
type ChannelDetail,
17+
type WorkspaceConfig,
18+
type AgentProvider,
19+
type AgentsConfig,
20+
type UpdateConfig,
21+
type OdeConfig,
22+
type UserConfig,
23+
} from "./ode-schema";
24+
import { isAgentProviderId } from "@/shared/agent-provider";
25+
26+
export type {
27+
ChannelDetail,
28+
WorkspaceConfig,
29+
AgentProvider,
30+
AgentsConfig,
31+
UpdateConfig,
32+
OdeConfig,
33+
UserConfig,
34+
} from "./ode-schema";
1535

1636
const existsSync = fs.existsSync;
1737
const mkdirSync = fs.mkdirSync;
@@ -24,147 +44,12 @@ const XDG_CONFIG_HOME = join(homedir(), ".config");
2444
const ODE_CONFIG_DIR = join(XDG_CONFIG_HOME, "ode");
2545
export const ODE_CONFIG_FILE = join(ODE_CONFIG_DIR, "ode.json");
2646

27-
const userSchema = z.object({
28-
name: z.string().optional().default(""),
29-
email: z.string().optional().default(""),
30-
initials: z.string().optional().default(""),
31-
avatar: z.string().optional().default(""),
32-
gitStrategy: z.enum(["default", "worktree"]).optional().default("worktree"),
33-
defaultStatusMessageFormat: z.enum([
34-
"minimum",
35-
"medium",
36-
"aggressive",
37-
"low",
38-
"high",
39-
]).optional().default("medium"),
40-
defaultMessageFrequency: z.enum([
41-
"minimum",
42-
"medium",
43-
"aggressive",
44-
"low",
45-
"high",
46-
]).optional(),
47-
messageUpdateIntervalMs: z.number().optional(),
48-
IM_MESSAGE_UPDATE_INTERVAL_MS: z.number().optional().default(DEFAULT_STATUS_MESSAGE_FREQUENCY_MS),
49-
});
50-
51-
const agentProviderSchema = z.enum(["opencode", "claudecode", "codex", "kimi", "kiro", "kilo", "qwen", "goose", "gemini"]);
52-
53-
const agentsSchema = z.object({
54-
opencode: z.object({
55-
enabled: z.boolean().optional().default(true),
56-
models: z.array(z.string()).optional().default([]),
57-
}).optional().default({ enabled: true, models: [] }),
58-
claudecode: z.object({
59-
enabled: z.boolean().optional().default(true),
60-
}).optional().default({ enabled: true }),
61-
codex: z.object({
62-
enabled: z.boolean().optional().default(true),
63-
models: z.array(z.string()).optional().default([]),
64-
}).optional().default({ enabled: true, models: [] }),
65-
kimi: z.object({
66-
enabled: z.boolean().optional().default(true),
67-
}).optional().default({ enabled: true }),
68-
kiro: z.object({
69-
enabled: z.boolean().optional().default(true),
70-
}).optional().default({ enabled: true }),
71-
kilo: z.object({
72-
enabled: z.boolean().optional().default(true),
73-
models: z.array(z.string()).optional().default([]),
74-
}).optional().default({ enabled: true, models: [] }),
75-
qwen: z.object({
76-
enabled: z.boolean().optional().default(true),
77-
}).optional().default({ enabled: true }),
78-
goose: z.object({
79-
enabled: z.boolean().optional().default(true),
80-
}).optional().default({ enabled: true }),
81-
gemini: z.object({
82-
enabled: z.boolean().optional().default(true),
83-
}).optional().default({ enabled: true }),
84-
}).optional().default({
85-
opencode: { enabled: true, models: [] },
86-
claudecode: { enabled: true },
87-
codex: { enabled: true, models: [] },
88-
kimi: { enabled: true },
89-
kiro: { enabled: true },
90-
kilo: { enabled: true, models: [] },
91-
qwen: { enabled: true },
92-
goose: { enabled: true },
93-
gemini: { enabled: true },
94-
});
95-
96-
const channelDetailSchema = z.object({
97-
id: z.string(),
98-
name: z.string(),
99-
agentProvider: z.preprocess(
100-
(value) => (value === "claude" ? "claudecode" : value),
101-
agentProviderSchema.optional().default("opencode")
102-
),
103-
model: z.string().optional().default(""),
104-
workingDirectory: z.string().optional().default(""),
105-
baseBranch: z.string().optional().default("main"),
106-
channelSystemMessage: z.string().optional().default(""),
107-
});
108-
10947
const DEFAULT_UPDATE_INTERVAL_MS = 60 * 60 * 1000;
11048
const MIN_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
11149
const DEFAULT_MESSAGE_UPDATE_INTERVAL_MS = DEFAULT_STATUS_MESSAGE_FREQUENCY_MS;
11250
const MIN_MESSAGE_UPDATE_INTERVAL_MS = 250;
11351
export const DEFAULT_CODEX_MODEL = "gpt-5.3-codex";
11452

115-
const updateSchema = z.object({
116-
autoUpgrade: z.boolean().optional().default(true),
117-
checkIntervalMs: z.number().optional().default(DEFAULT_UPDATE_INTERVAL_MS),
118-
});
119-
120-
const workspaceSchema = z.object({
121-
id: z.string(),
122-
type: z.enum(["slack", "discord", "lark"]).optional().default("slack"),
123-
name: z.string().optional().default(""),
124-
domain: z.string().optional().default(""),
125-
status: z.enum(["active", "paused"]).optional().default("active"),
126-
channels: z.number().optional().default(0),
127-
members: z.number().optional().default(0),
128-
lastSync: z.string().optional().default(""),
129-
slackAppToken: z.string().optional().default(""),
130-
slackBotToken: z.string().optional().default(""),
131-
discordBotToken: z.string().optional().default(""),
132-
larkAppKey: z.string().optional().default(""),
133-
larkAppId: z.string().optional().default(""),
134-
larkAppSecret: z.string().optional().default(""),
135-
channelDetails: z.array(channelDetailSchema).optional().default([]),
136-
});
137-
138-
const odeConfigSchema = z.object({
139-
user: userSchema,
140-
githubInfos: z
141-
.record(
142-
z.string(),
143-
z.object({
144-
token: z.string().optional().default(""),
145-
gitName: z.string().optional().default(""),
146-
gitEmail: z.string().optional().default(""),
147-
})
148-
)
149-
.optional()
150-
.default({}),
151-
agents: agentsSchema,
152-
completeOnboarding: z.boolean().optional().default(false),
153-
workspaces: z.array(workspaceSchema),
154-
updates: updateSchema.optional().default({
155-
autoUpgrade: true,
156-
checkIntervalMs: DEFAULT_UPDATE_INTERVAL_MS,
157-
}),
158-
});
159-
160-
export type ChannelDetail = z.infer<typeof channelDetailSchema>;
161-
export type WorkspaceConfig = z.infer<typeof workspaceSchema>;
162-
export type AgentProvider = z.infer<typeof agentProviderSchema>;
163-
export type AgentsConfig = z.infer<typeof agentsSchema>;
164-
export type UpdateConfig = z.infer<typeof updateSchema>;
165-
export type OdeConfig = z.infer<typeof odeConfigSchema>;
166-
export type UserConfig = z.infer<typeof userSchema>;
167-
16853
let cachedConfig: OdeConfig | null = null;
16954

17055
const EMPTY_TEMPLATE: OdeConfig = {
@@ -771,8 +656,7 @@ export function getChannelModel(channelId: string): string | null {
771656

772657
export function getChannelAgentProvider(channelId: string): AgentProvider {
773658
const provider = getChannelDetails(channelId)?.agentProvider;
774-
if (provider === "claudecode" || provider === "codex" || provider === "kimi" || provider === "kiro" || provider === "kilo" || provider === "qwen" || provider === "goose" || provider === "gemini") return provider;
775-
return "opencode";
659+
return isAgentProviderId(provider) ? provider : "opencode";
776660
}
777661

778662
export function setChannelModel(channelId: string, model: string): void {

packages/core/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
OpenCodeSessionInfo,
66
} from "@/agents";
77
import type { StatusMessageFormat } from "@/config/status-message-format";
8+
import type { AgentProviderId } from "@/shared/agent-provider";
89
import type { SessionMessageState } from "@/utils/session-inspector";
910

1011
export type CoreMessageContext = {
@@ -68,7 +69,7 @@ export interface IMAdapter {
6869

6970
export interface AgentAdapter {
7071
supportsEventStream: boolean;
71-
getProviderForSession(sessionId: string): "opencode" | "claudecode" | "codex" | "kimi" | "kiro" | "kilo" | "qwen" | "goose" | "gemini";
72+
getProviderForSession(sessionId: string): AgentProviderId;
7273
getDisplayNameForSession(sessionId: string): string;
7374
getOrCreateSession(
7475
channelId: string,

0 commit comments

Comments
 (0)