Skip to content

Commit 8c558a8

Browse files
authored
Merge pull request #67 from odefun/feat/general-settings-modal-17705608
feat: add dedicated general settings flow
2 parents 388fd65 + 7fdf243 commit 8c558a8

File tree

7 files changed

+229
-13
lines changed

7 files changed

+229
-13
lines changed

packages/config/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export {
2222
getSlackTargetChannels,
2323
getDefaultCwd,
2424
getGitHubInfoForUser,
25+
getUserGeneralSettings,
2526
resolveChannelCwd,
2627
getChannelSystemMessage,
2728
getChannelBaseBranch,
@@ -30,6 +31,7 @@ export {
3031
setChannelBaseBranch,
3132
setChannelSystemMessage,
3233
setGitHubInfoForUser,
34+
setUserGeneralSettings,
3335
clearGitHubInfoForUser,
3436
setChannelModel,
3537
setChannelAgentProvider,
@@ -41,6 +43,7 @@ export {
4143
type UpdateConfig,
4244
type ChannelDetail,
4345
type UserConfig,
46+
type UserGeneralSettings,
4447
} from "./local/ode";
4548

4649
export {

packages/config/local/ode.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,11 @@ export type GitHubInfo = {
446446
gitEmail?: string;
447447
};
448448

449+
export type UserGeneralSettings = {
450+
defaultStatusMessageFormat: "minimum" | "medium" | "aggressive";
451+
gitStrategy: "default" | "worktree";
452+
};
453+
449454
export function getGitHubInfoForUser(userId: string): GitHubInfo | null {
450455
const info = loadOdeConfig().githubInfos?.[userId];
451456
if (!info) return null;
@@ -456,6 +461,29 @@ export function getGitHubInfoForUser(userId: string): GitHubInfo | null {
456461
return { token, gitName, gitEmail };
457462
}
458463

464+
export function getUserGeneralSettings(): UserGeneralSettings {
465+
const user = loadOdeConfig().user;
466+
return {
467+
defaultStatusMessageFormat:
468+
user.defaultStatusMessageFormat === "minimum" || user.defaultStatusMessageFormat === "aggressive"
469+
? user.defaultStatusMessageFormat
470+
: "medium",
471+
gitStrategy: user.gitStrategy === "default" ? "default" : "worktree",
472+
};
473+
}
474+
475+
export function setUserGeneralSettings(settings: UserGeneralSettings): void {
476+
const config = loadOdeConfig();
477+
saveOdeConfig({
478+
...config,
479+
user: {
480+
...config.user,
481+
defaultStatusMessageFormat: settings.defaultStatusMessageFormat,
482+
gitStrategy: settings.gitStrategy,
483+
},
484+
});
485+
}
486+
459487
export function setGitHubInfoForUser(userId: string, info: GitHubInfo): void {
460488
const config = loadOdeConfig();
461489
const githubInfos = { ...(config.githubInfos ?? {}) };

packages/ims/slack/client.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,15 +233,19 @@ function describeSettingsIssues(channelId: string): string[] {
233233
return issues;
234234
}
235235

236-
function isSettingsCommand(text: string): boolean {
236+
function isChannelSettingsCommand(text: string): boolean {
237+
return /^\/channel\b/i.test(text.trim());
238+
}
239+
240+
function isGeneralSettingsCommand(text: string): boolean {
237241
return /^\/setting\b/i.test(text.trim());
238242
}
239243

240244
function isGitHubCommand(text: string): boolean {
241245
return /^\/gh\b/i.test(text.trim());
242246
}
243247

244-
async function postSettingsLauncher(
248+
async function postChannelSettingsLauncher(
245249
channelId: string,
246250
userId: string,
247251
client: WebClient
@@ -273,6 +277,38 @@ async function postSettingsLauncher(
273277
});
274278
}
275279

280+
async function postGeneralSettingsLauncher(
281+
channelId: string,
282+
userId: string,
283+
client: WebClient
284+
): Promise<void> {
285+
await client.chat.postEphemeral({
286+
channel: channelId,
287+
user: userId,
288+
text: "Open general settings",
289+
blocks: [
290+
{
291+
type: "section",
292+
text: {
293+
type: "mrkdwn",
294+
text: "Open general settings for status message format and git strategy.",
295+
},
296+
},
297+
{
298+
type: "actions",
299+
elements: [
300+
{
301+
type: "button",
302+
action_id: "open_general_settings_modal",
303+
text: { type: "plain_text", text: "Open settings" },
304+
value: channelId,
305+
},
306+
],
307+
},
308+
],
309+
});
310+
}
311+
276312
async function postGitHubLauncher(
277313
channelId: string,
278314
userId: string,
@@ -554,9 +590,11 @@ export function setupMessageHandlers(): void {
554590
isThreadActive,
555591
markThreadActive,
556592
isGitHubCommand,
557-
isSettingsCommand,
593+
isChannelSettingsCommand,
594+
isGeneralSettingsCommand,
558595
postGitHubLauncher,
559-
postSettingsLauncher,
596+
postChannelSettingsLauncher,
597+
postGeneralSettingsLauncher,
560598
describeSettingsIssues,
561599
getChannelAgentProvider,
562600
handleStopCommand: (channelId, threadId) => coreRuntime.handleStopCommand(channelId, threadId),

packages/ims/slack/commands.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@ import {
1616
getChannelSystemMessage,
1717
setChannelSystemMessage,
1818
setGitHubInfoForUser,
19+
getUserGeneralSettings,
20+
setUserGeneralSettings,
1921
} from "@/config";
2022
import { startServer as startOpenCodeServer } from "@/agents/opencode";
2123
import { startServer as startCodexServer } from "@/agents/codex";
2224

2325
const SETTINGS_LAUNCH_ACTION = "open_settings_modal";
2426
const SETTINGS_MODAL_ID = "settings_modal";
27+
const GENERAL_SETTINGS_LAUNCH_ACTION = "open_general_settings_modal";
28+
const GENERAL_SETTINGS_MODAL_ID = "general_settings_modal";
2529
const GITHUB_LAUNCH_ACTION = "open_github_token_modal";
2630
const GITHUB_MODAL_ID = "github_token_modal";
2731
const GITHUB_TOKEN_BLOCK = "github_token";
@@ -40,8 +44,14 @@ const BASE_BRANCH_BLOCK = "base_branch";
4044
const BASE_BRANCH_ACTION = "base_branch_input";
4145
const CHANNEL_SYSTEM_MESSAGE_BLOCK = "channel_system_message";
4246
const CHANNEL_SYSTEM_MESSAGE_ACTION = "channel_system_message_input";
47+
const GENERAL_STATUS_MESSAGE_FORMAT_BLOCK = "general_status_message_format";
48+
const GENERAL_STATUS_MESSAGE_FORMAT_ACTION = "general_status_message_format_select";
49+
const GENERAL_GIT_STRATEGY_BLOCK = "general_git_strategy";
50+
const GENERAL_GIT_STRATEGY_ACTION = "general_git_strategy_select";
4351

4452
type AgentProvider = "opencode" | "claudecode" | "codex" | "kimi" | "kiro" | "qwen";
53+
type StatusMessageFormat = "aggressive" | "medium" | "minimum";
54+
type GitStrategy = "default" | "worktree";
4555

4656
const AGENT_PROVIDERS: AgentProvider[] = ["opencode", "claudecode", "codex", "kimi", "kiro", "qwen"];
4757

@@ -54,6 +64,17 @@ const AGENT_PROVIDER_LABELS: Record<AgentProvider, string> = {
5464
qwen: "Qwen Code",
5565
};
5666

67+
const STATUS_MESSAGE_FORMAT_OPTIONS: Array<{ label: string; value: StatusMessageFormat }> = [
68+
{ label: "Aggressive", value: "aggressive" },
69+
{ label: "Medium", value: "medium" },
70+
{ label: "Minimum", value: "minimum" },
71+
];
72+
73+
const GIT_STRATEGY_OPTIONS: Array<{ label: string; value: GitStrategy }> = [
74+
{ label: "Worktree", value: "worktree" },
75+
{ label: "Default", value: "default" },
76+
];
77+
5778
function parseAgentProvider(value: unknown): AgentProvider {
5879
if (typeof value !== "string") return "opencode";
5980
return AGENT_PROVIDERS.includes(value as AgentProvider) ? value as AgentProvider : "opencode";
@@ -284,6 +305,66 @@ function buildGitHubTokenModal(params: {
284305
};
285306
}
286307

308+
function buildGeneralSettingsModal(params: {
309+
channelId: string;
310+
statusMessageFormat: StatusMessageFormat;
311+
gitStrategy: GitStrategy;
312+
}) {
313+
const { channelId, statusMessageFormat, gitStrategy } = params;
314+
const statusMessageFormatOptions = STATUS_MESSAGE_FORMAT_OPTIONS.map((option) => ({
315+
text: { type: "plain_text" as const, text: option.label },
316+
value: option.value,
317+
}));
318+
const gitStrategyOptions = GIT_STRATEGY_OPTIONS.map((option) => ({
319+
text: { type: "plain_text" as const, text: option.label },
320+
value: option.value,
321+
}));
322+
323+
return {
324+
type: "modal" as const,
325+
callback_id: GENERAL_SETTINGS_MODAL_ID,
326+
private_metadata: channelId,
327+
title: { type: "plain_text" as const, text: "General Settings" },
328+
submit: { type: "plain_text" as const, text: "Save" },
329+
close: { type: "plain_text" as const, text: "Cancel" },
330+
blocks: [
331+
{
332+
type: "section" as const,
333+
text: {
334+
type: "mrkdwn" as const,
335+
text: "Configure default status message format and git strategy.",
336+
},
337+
},
338+
{
339+
type: "input" as const,
340+
block_id: GENERAL_STATUS_MESSAGE_FORMAT_BLOCK,
341+
label: { type: "plain_text" as const, text: "Status Message Format" },
342+
element: {
343+
type: "static_select" as const,
344+
action_id: GENERAL_STATUS_MESSAGE_FORMAT_ACTION,
345+
options: statusMessageFormatOptions,
346+
initial_option:
347+
statusMessageFormatOptions.find((option) => option.value === statusMessageFormat)
348+
?? statusMessageFormatOptions[1],
349+
},
350+
},
351+
{
352+
type: "input" as const,
353+
block_id: GENERAL_GIT_STRATEGY_BLOCK,
354+
label: { type: "plain_text" as const, text: "Git Strategy" },
355+
element: {
356+
type: "static_select" as const,
357+
action_id: GENERAL_GIT_STRATEGY_ACTION,
358+
options: gitStrategyOptions,
359+
initial_option:
360+
gitStrategyOptions.find((option) => option.value === gitStrategy)
361+
?? gitStrategyOptions[0],
362+
},
363+
},
364+
],
365+
};
366+
}
367+
287368
export function setupInteractiveHandlers(): void {
288369
for (const slackApp of getApps()) {
289370
slackApp.action(SETTINGS_LAUNCH_ACTION, async ({ ack, body, client }) => {
@@ -347,6 +428,25 @@ export function setupInteractiveHandlers(): void {
347428
});
348429
});
349430

431+
slackApp.action(GENERAL_SETTINGS_LAUNCH_ACTION, async ({ ack, body, client }) => {
432+
await ack();
433+
434+
const channelId = (body as any).actions?.[0]?.value
435+
?? (body as any).channel?.id
436+
?? "";
437+
const generalSettings = getUserGeneralSettings();
438+
const view = buildGeneralSettingsModal({
439+
channelId,
440+
statusMessageFormat: generalSettings.defaultStatusMessageFormat,
441+
gitStrategy: generalSettings.gitStrategy,
442+
});
443+
444+
await client.views.open({
445+
trigger_id: (body as any).trigger_id,
446+
view,
447+
});
448+
});
449+
350450
slackApp.action(PROVIDER_ACTION, async ({ ack, body, client }) => {
351451
await ack();
352452

@@ -528,6 +628,50 @@ export function setupInteractiveHandlers(): void {
528628
});
529629
});
530630

631+
slackApp.view(GENERAL_SETTINGS_MODAL_ID, async ({ ack, view, body, client }) => {
632+
const values = view.state.values;
633+
const selectedStatusMessageFormat = values?.[GENERAL_STATUS_MESSAGE_FORMAT_BLOCK]?.[GENERAL_STATUS_MESSAGE_FORMAT_ACTION]?.selected_option?.value;
634+
const selectedGitStrategy = values?.[GENERAL_GIT_STRATEGY_BLOCK]?.[GENERAL_GIT_STRATEGY_ACTION]?.selected_option?.value;
635+
636+
const statusMessageFormat: StatusMessageFormat =
637+
selectedStatusMessageFormat === "aggressive"
638+
|| selectedStatusMessageFormat === "minimum"
639+
|| selectedStatusMessageFormat === "medium"
640+
? selectedStatusMessageFormat
641+
: "medium";
642+
const gitStrategy: GitStrategy = selectedGitStrategy === "default" ? "default" : "worktree";
643+
644+
await ack();
645+
646+
try {
647+
setUserGeneralSettings({
648+
defaultStatusMessageFormat: statusMessageFormat,
649+
gitStrategy,
650+
});
651+
} catch (err) {
652+
const userId = (body as any).user?.id;
653+
const channelId = view.private_metadata || (body as any).channel?.id;
654+
if (userId && channelId) {
655+
await client.chat.postEphemeral({
656+
channel: channelId,
657+
user: userId,
658+
text: `Failed to update general settings: ${err instanceof Error ? err.message : String(err)}`,
659+
});
660+
}
661+
return;
662+
}
663+
664+
const userId = (body as any).user?.id;
665+
const channelId = view.private_metadata || (body as any).channel?.id;
666+
if (userId && channelId) {
667+
await client.chat.postEphemeral({
668+
channel: channelId,
669+
user: userId,
670+
text: "General settings updated.",
671+
});
672+
}
673+
});
674+
531675
// Handle user choice button clicks (from Ode ask_user actions)
532676
slackApp.action(/^user_choice_\d+$/, async ({ ack, body, client }) => {
533677
await ack();

packages/ims/slack/message-router.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ type RouterDeps = {
1616
isThreadActive: (channelId: string, threadId: string) => boolean;
1717
markThreadActive: (channelId: string, threadId: string) => void;
1818
isGitHubCommand: (text: string) => boolean;
19-
isSettingsCommand: (text: string) => boolean;
19+
isChannelSettingsCommand: (text: string) => boolean;
20+
isGeneralSettingsCommand: (text: string) => boolean;
2021
postGitHubLauncher: (channelId: string, userId: string, client: any) => Promise<void>;
21-
postSettingsLauncher: (channelId: string, userId: string, client: any) => Promise<void>;
22+
postChannelSettingsLauncher: (channelId: string, userId: string, client: any) => Promise<void>;
23+
postGeneralSettingsLauncher: (channelId: string, userId: string, client: any) => Promise<void>;
2224
describeSettingsIssues: (channelId: string) => string[];
2325
getChannelAgentProvider: (channelId: string) => "opencode" | "claudecode" | "codex" | "kimi" | "kiro" | "qwen";
2426
handleStopCommand: (channelId: string, threadId: string) => Promise<boolean>;
@@ -113,7 +115,7 @@ async function maybeNotifySettingsIssues(
113115
text: `Channel settings need attention:\n- ${settingsIssues.join("\n- ")}`,
114116
thread_ts: threadId,
115117
});
116-
await deps.postSettingsLauncher(channelId, userId, client);
118+
await deps.postChannelSettingsLauncher(channelId, userId, client);
117119
return true;
118120
}
119121

@@ -132,7 +134,8 @@ async function maybeHandleLauncherCommand(params: {
132134
launch: (channelId: string, userId: string, client: any) => Promise<void>;
133135
}> = [
134136
{ matches: deps.isGitHubCommand, launch: deps.postGitHubLauncher },
135-
{ matches: deps.isSettingsCommand, launch: deps.postSettingsLauncher },
137+
{ matches: deps.isChannelSettingsCommand, launch: deps.postChannelSettingsLauncher },
138+
{ matches: deps.isGeneralSettingsCommand, launch: deps.postGeneralSettingsLauncher },
136139
];
137140

138141
const handler = commandHandlers.find((entry) => entry.matches(cleanText));

packages/web-ui/src/routes/local-setting/+layout.svelte

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
99
let pathname = "/local-setting";
1010
let normalizedPathname = pathname;
11-
let activeSection: "profile" | "agents" | "slack" = "profile";
11+
let activeSection: "general" | "agents" | "slack" = "general";
1212
let pendingSlackAppToken = "";
1313
let pendingSlackBotToken = "";
1414
@@ -19,7 +19,7 @@
1919
? "agents"
2020
: normalizedPathname.startsWith("/local-setting/slack-bot")
2121
? "slack"
22-
: "profile";
22+
: "general";
2323
2424
$: selectedWorkspace = getSelectedWorkspace($page.params.workspaceName ?? "", $localSettingStore.config.workspaces);
2525
@@ -91,8 +91,8 @@
9191

9292
<div class="layout">
9393
<aside class="sidebar card">
94-
<button class="nav-item {activeSection === 'profile' ? 'active' : ''}" on:click={() => goto('/local-setting')}>
95-
Profile
94+
<button class="nav-item {activeSection === 'general' ? 'active' : ''}" on:click={() => goto('/local-setting')}>
95+
General
9696
</button>
9797
<button class="nav-item {activeSection === 'agents' ? 'active' : ''}" on:click={() => goto('/local-setting/agents')}>
9898
Agents

0 commit comments

Comments
 (0)