From 7fdf24381abf09f2e43a38c21288a6046177ed8b Mon Sep 17 00:00:00 2001 From: LIU9293 Date: Sun, 8 Feb 2026 14:53:03 +0000 Subject: [PATCH] feat: add dedicated general settings flow Split channel and general setting launchers so users can update status format and git strategy separately, and align local settings UI labels with the new General section. --- packages/config/index.ts | 3 + packages/config/local/ode.ts | 28 ++++ packages/ims/slack/client.ts | 46 +++++- packages/ims/slack/commands.ts | 144 ++++++++++++++++++ packages/ims/slack/message-router.ts | 11 +- .../src/routes/local-setting/+layout.svelte | 8 +- .../src/routes/local-setting/+page.svelte | 2 +- 7 files changed, 229 insertions(+), 13 deletions(-) diff --git a/packages/config/index.ts b/packages/config/index.ts index 4cc19a2..b3b0528 100644 --- a/packages/config/index.ts +++ b/packages/config/index.ts @@ -22,6 +22,7 @@ export { getSlackTargetChannels, getDefaultCwd, getGitHubInfoForUser, + getUserGeneralSettings, resolveChannelCwd, getChannelSystemMessage, getChannelBaseBranch, @@ -30,6 +31,7 @@ export { setChannelBaseBranch, setChannelSystemMessage, setGitHubInfoForUser, + setUserGeneralSettings, clearGitHubInfoForUser, setChannelModel, setChannelAgentProvider, @@ -41,6 +43,7 @@ export { type UpdateConfig, type ChannelDetail, type UserConfig, + type UserGeneralSettings, } from "./local/ode"; export { diff --git a/packages/config/local/ode.ts b/packages/config/local/ode.ts index e63b4ea..a4bc282 100644 --- a/packages/config/local/ode.ts +++ b/packages/config/local/ode.ts @@ -446,6 +446,11 @@ export type GitHubInfo = { gitEmail?: string; }; +export type UserGeneralSettings = { + defaultStatusMessageFormat: "minimum" | "medium" | "aggressive"; + gitStrategy: "default" | "worktree"; +}; + export function getGitHubInfoForUser(userId: string): GitHubInfo | null { const info = loadOdeConfig().githubInfos?.[userId]; if (!info) return null; @@ -456,6 +461,29 @@ export function getGitHubInfoForUser(userId: string): GitHubInfo | null { return { token, gitName, gitEmail }; } +export function getUserGeneralSettings(): UserGeneralSettings { + const user = loadOdeConfig().user; + return { + defaultStatusMessageFormat: + user.defaultStatusMessageFormat === "minimum" || user.defaultStatusMessageFormat === "aggressive" + ? user.defaultStatusMessageFormat + : "medium", + gitStrategy: user.gitStrategy === "default" ? "default" : "worktree", + }; +} + +export function setUserGeneralSettings(settings: UserGeneralSettings): void { + const config = loadOdeConfig(); + saveOdeConfig({ + ...config, + user: { + ...config.user, + defaultStatusMessageFormat: settings.defaultStatusMessageFormat, + gitStrategy: settings.gitStrategy, + }, + }); +} + export function setGitHubInfoForUser(userId: string, info: GitHubInfo): void { const config = loadOdeConfig(); const githubInfos = { ...(config.githubInfos ?? {}) }; diff --git a/packages/ims/slack/client.ts b/packages/ims/slack/client.ts index 0fd6eb9..d659d08 100644 --- a/packages/ims/slack/client.ts +++ b/packages/ims/slack/client.ts @@ -233,7 +233,11 @@ function describeSettingsIssues(channelId: string): string[] { return issues; } -function isSettingsCommand(text: string): boolean { +function isChannelSettingsCommand(text: string): boolean { + return /^\/channel\b/i.test(text.trim()); +} + +function isGeneralSettingsCommand(text: string): boolean { return /^\/setting\b/i.test(text.trim()); } @@ -241,7 +245,7 @@ function isGitHubCommand(text: string): boolean { return /^\/gh\b/i.test(text.trim()); } -async function postSettingsLauncher( +async function postChannelSettingsLauncher( channelId: string, userId: string, client: WebClient @@ -273,6 +277,38 @@ async function postSettingsLauncher( }); } +async function postGeneralSettingsLauncher( + channelId: string, + userId: string, + client: WebClient +): Promise { + await client.chat.postEphemeral({ + channel: channelId, + user: userId, + text: "Open general settings", + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: "Open general settings for status message format and git strategy.", + }, + }, + { + type: "actions", + elements: [ + { + type: "button", + action_id: "open_general_settings_modal", + text: { type: "plain_text", text: "Open settings" }, + value: channelId, + }, + ], + }, + ], + }); +} + async function postGitHubLauncher( channelId: string, userId: string, @@ -554,9 +590,11 @@ export function setupMessageHandlers(): void { isThreadActive, markThreadActive, isGitHubCommand, - isSettingsCommand, + isChannelSettingsCommand, + isGeneralSettingsCommand, postGitHubLauncher, - postSettingsLauncher, + postChannelSettingsLauncher, + postGeneralSettingsLauncher, describeSettingsIssues, getChannelAgentProvider, handleStopCommand: (channelId, threadId) => coreRuntime.handleStopCommand(channelId, threadId), diff --git a/packages/ims/slack/commands.ts b/packages/ims/slack/commands.ts index 30566d1..196fc55 100644 --- a/packages/ims/slack/commands.ts +++ b/packages/ims/slack/commands.ts @@ -16,12 +16,16 @@ import { getChannelSystemMessage, setChannelSystemMessage, setGitHubInfoForUser, + getUserGeneralSettings, + setUserGeneralSettings, } from "@/config"; import { startServer as startOpenCodeServer } from "@/agents/opencode"; import { startServer as startCodexServer } from "@/agents/codex"; const SETTINGS_LAUNCH_ACTION = "open_settings_modal"; const SETTINGS_MODAL_ID = "settings_modal"; +const GENERAL_SETTINGS_LAUNCH_ACTION = "open_general_settings_modal"; +const GENERAL_SETTINGS_MODAL_ID = "general_settings_modal"; const GITHUB_LAUNCH_ACTION = "open_github_token_modal"; const GITHUB_MODAL_ID = "github_token_modal"; const GITHUB_TOKEN_BLOCK = "github_token"; @@ -40,8 +44,14 @@ const BASE_BRANCH_BLOCK = "base_branch"; const BASE_BRANCH_ACTION = "base_branch_input"; const CHANNEL_SYSTEM_MESSAGE_BLOCK = "channel_system_message"; const CHANNEL_SYSTEM_MESSAGE_ACTION = "channel_system_message_input"; +const GENERAL_STATUS_MESSAGE_FORMAT_BLOCK = "general_status_message_format"; +const GENERAL_STATUS_MESSAGE_FORMAT_ACTION = "general_status_message_format_select"; +const GENERAL_GIT_STRATEGY_BLOCK = "general_git_strategy"; +const GENERAL_GIT_STRATEGY_ACTION = "general_git_strategy_select"; type AgentProvider = "opencode" | "claudecode" | "codex" | "kimi" | "kiro" | "qwen"; +type StatusMessageFormat = "aggressive" | "medium" | "minimum"; +type GitStrategy = "default" | "worktree"; const AGENT_PROVIDERS: AgentProvider[] = ["opencode", "claudecode", "codex", "kimi", "kiro", "qwen"]; @@ -54,6 +64,17 @@ const AGENT_PROVIDER_LABELS: Record = { qwen: "Qwen Code", }; +const STATUS_MESSAGE_FORMAT_OPTIONS: Array<{ label: string; value: StatusMessageFormat }> = [ + { label: "Aggressive", value: "aggressive" }, + { label: "Medium", value: "medium" }, + { label: "Minimum", value: "minimum" }, +]; + +const GIT_STRATEGY_OPTIONS: Array<{ label: string; value: GitStrategy }> = [ + { label: "Worktree", value: "worktree" }, + { label: "Default", value: "default" }, +]; + function parseAgentProvider(value: unknown): AgentProvider { if (typeof value !== "string") return "opencode"; return AGENT_PROVIDERS.includes(value as AgentProvider) ? value as AgentProvider : "opencode"; @@ -284,6 +305,66 @@ function buildGitHubTokenModal(params: { }; } +function buildGeneralSettingsModal(params: { + channelId: string; + statusMessageFormat: StatusMessageFormat; + gitStrategy: GitStrategy; +}) { + const { channelId, statusMessageFormat, gitStrategy } = params; + const statusMessageFormatOptions = STATUS_MESSAGE_FORMAT_OPTIONS.map((option) => ({ + text: { type: "plain_text" as const, text: option.label }, + value: option.value, + })); + const gitStrategyOptions = GIT_STRATEGY_OPTIONS.map((option) => ({ + text: { type: "plain_text" as const, text: option.label }, + value: option.value, + })); + + return { + type: "modal" as const, + callback_id: GENERAL_SETTINGS_MODAL_ID, + private_metadata: channelId, + title: { type: "plain_text" as const, text: "General Settings" }, + submit: { type: "plain_text" as const, text: "Save" }, + close: { type: "plain_text" as const, text: "Cancel" }, + blocks: [ + { + type: "section" as const, + text: { + type: "mrkdwn" as const, + text: "Configure default status message format and git strategy.", + }, + }, + { + type: "input" as const, + block_id: GENERAL_STATUS_MESSAGE_FORMAT_BLOCK, + label: { type: "plain_text" as const, text: "Status Message Format" }, + element: { + type: "static_select" as const, + action_id: GENERAL_STATUS_MESSAGE_FORMAT_ACTION, + options: statusMessageFormatOptions, + initial_option: + statusMessageFormatOptions.find((option) => option.value === statusMessageFormat) + ?? statusMessageFormatOptions[1], + }, + }, + { + type: "input" as const, + block_id: GENERAL_GIT_STRATEGY_BLOCK, + label: { type: "plain_text" as const, text: "Git Strategy" }, + element: { + type: "static_select" as const, + action_id: GENERAL_GIT_STRATEGY_ACTION, + options: gitStrategyOptions, + initial_option: + gitStrategyOptions.find((option) => option.value === gitStrategy) + ?? gitStrategyOptions[0], + }, + }, + ], + }; +} + export function setupInteractiveHandlers(): void { for (const slackApp of getApps()) { slackApp.action(SETTINGS_LAUNCH_ACTION, async ({ ack, body, client }) => { @@ -347,6 +428,25 @@ export function setupInteractiveHandlers(): void { }); }); + slackApp.action(GENERAL_SETTINGS_LAUNCH_ACTION, async ({ ack, body, client }) => { + await ack(); + + const channelId = (body as any).actions?.[0]?.value + ?? (body as any).channel?.id + ?? ""; + const generalSettings = getUserGeneralSettings(); + const view = buildGeneralSettingsModal({ + channelId, + statusMessageFormat: generalSettings.defaultStatusMessageFormat, + gitStrategy: generalSettings.gitStrategy, + }); + + await client.views.open({ + trigger_id: (body as any).trigger_id, + view, + }); + }); + slackApp.action(PROVIDER_ACTION, async ({ ack, body, client }) => { await ack(); @@ -528,6 +628,50 @@ export function setupInteractiveHandlers(): void { }); }); + slackApp.view(GENERAL_SETTINGS_MODAL_ID, async ({ ack, view, body, client }) => { + const values = view.state.values; + const selectedStatusMessageFormat = values?.[GENERAL_STATUS_MESSAGE_FORMAT_BLOCK]?.[GENERAL_STATUS_MESSAGE_FORMAT_ACTION]?.selected_option?.value; + const selectedGitStrategy = values?.[GENERAL_GIT_STRATEGY_BLOCK]?.[GENERAL_GIT_STRATEGY_ACTION]?.selected_option?.value; + + const statusMessageFormat: StatusMessageFormat = + selectedStatusMessageFormat === "aggressive" + || selectedStatusMessageFormat === "minimum" + || selectedStatusMessageFormat === "medium" + ? selectedStatusMessageFormat + : "medium"; + const gitStrategy: GitStrategy = selectedGitStrategy === "default" ? "default" : "worktree"; + + await ack(); + + try { + setUserGeneralSettings({ + defaultStatusMessageFormat: statusMessageFormat, + gitStrategy, + }); + } catch (err) { + const userId = (body as any).user?.id; + const channelId = view.private_metadata || (body as any).channel?.id; + if (userId && channelId) { + await client.chat.postEphemeral({ + channel: channelId, + user: userId, + text: `Failed to update general settings: ${err instanceof Error ? err.message : String(err)}`, + }); + } + return; + } + + const userId = (body as any).user?.id; + const channelId = view.private_metadata || (body as any).channel?.id; + if (userId && channelId) { + await client.chat.postEphemeral({ + channel: channelId, + user: userId, + text: "General settings updated.", + }); + } + }); + // Handle user choice button clicks (from Ode ask_user actions) slackApp.action(/^user_choice_\d+$/, async ({ ack, body, client }) => { await ack(); diff --git a/packages/ims/slack/message-router.ts b/packages/ims/slack/message-router.ts index 4fc1279..1051671 100644 --- a/packages/ims/slack/message-router.ts +++ b/packages/ims/slack/message-router.ts @@ -16,9 +16,11 @@ type RouterDeps = { isThreadActive: (channelId: string, threadId: string) => boolean; markThreadActive: (channelId: string, threadId: string) => void; isGitHubCommand: (text: string) => boolean; - isSettingsCommand: (text: string) => boolean; + isChannelSettingsCommand: (text: string) => boolean; + isGeneralSettingsCommand: (text: string) => boolean; postGitHubLauncher: (channelId: string, userId: string, client: any) => Promise; - postSettingsLauncher: (channelId: string, userId: string, client: any) => Promise; + postChannelSettingsLauncher: (channelId: string, userId: string, client: any) => Promise; + postGeneralSettingsLauncher: (channelId: string, userId: string, client: any) => Promise; describeSettingsIssues: (channelId: string) => string[]; getChannelAgentProvider: (channelId: string) => "opencode" | "claudecode" | "codex" | "kimi" | "kiro" | "qwen"; handleStopCommand: (channelId: string, threadId: string) => Promise; @@ -113,7 +115,7 @@ async function maybeNotifySettingsIssues( text: `Channel settings need attention:\n- ${settingsIssues.join("\n- ")}`, thread_ts: threadId, }); - await deps.postSettingsLauncher(channelId, userId, client); + await deps.postChannelSettingsLauncher(channelId, userId, client); return true; } @@ -132,7 +134,8 @@ async function maybeHandleLauncherCommand(params: { launch: (channelId: string, userId: string, client: any) => Promise; }> = [ { matches: deps.isGitHubCommand, launch: deps.postGitHubLauncher }, - { matches: deps.isSettingsCommand, launch: deps.postSettingsLauncher }, + { matches: deps.isChannelSettingsCommand, launch: deps.postChannelSettingsLauncher }, + { matches: deps.isGeneralSettingsCommand, launch: deps.postGeneralSettingsLauncher }, ]; const handler = commandHandlers.find((entry) => entry.matches(cleanText)); diff --git a/packages/web-ui/src/routes/local-setting/+layout.svelte b/packages/web-ui/src/routes/local-setting/+layout.svelte index 89ad76b..80b41fe 100644 --- a/packages/web-ui/src/routes/local-setting/+layout.svelte +++ b/packages/web-ui/src/routes/local-setting/+layout.svelte @@ -8,7 +8,7 @@ let pathname = "/local-setting"; let normalizedPathname = pathname; - let activeSection: "profile" | "agents" | "slack" = "profile"; + let activeSection: "general" | "agents" | "slack" = "general"; let pendingSlackAppToken = ""; let pendingSlackBotToken = ""; @@ -19,7 +19,7 @@ ? "agents" : normalizedPathname.startsWith("/local-setting/slack-bot") ? "slack" - : "profile"; + : "general"; $: selectedWorkspace = getSelectedWorkspace($page.params.workspaceName ?? "", $localSettingStore.config.workspaces); @@ -91,8 +91,8 @@