UI: add discord action toggles

This commit is contained in:
Shadow
2026-01-02 17:52:52 -06:00
committed by Peter Steinberger
parent 0c38f2df2a
commit 98a1deb129
7 changed files with 323 additions and 9 deletions

View File

@@ -334,6 +334,105 @@ struct ConnectionsSettings: View {
}
}
Divider().padding(.vertical, 2)
Text("Tool actions")
.font(.caption)
.foregroundStyle(.secondary)
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
GridRow {
self.gridLabel("Reactions")
Toggle("", isOn: self.$store.discordActionReactions)
.labelsHidden()
.toggleStyle(.checkbox)
}
GridRow {
self.gridLabel("Stickers")
Toggle("", isOn: self.$store.discordActionStickers)
.labelsHidden()
.toggleStyle(.checkbox)
}
GridRow {
self.gridLabel("Polls")
Toggle("", isOn: self.$store.discordActionPolls)
.labelsHidden()
.toggleStyle(.checkbox)
}
GridRow {
self.gridLabel("Permissions")
Toggle("", isOn: self.$store.discordActionPermissions)
.labelsHidden()
.toggleStyle(.checkbox)
}
GridRow {
self.gridLabel("Messages")
Toggle("", isOn: self.$store.discordActionMessages)
.labelsHidden()
.toggleStyle(.checkbox)
}
GridRow {
self.gridLabel("Threads")
Toggle("", isOn: self.$store.discordActionThreads)
.labelsHidden()
.toggleStyle(.checkbox)
}
GridRow {
self.gridLabel("Pins")
Toggle("", isOn: self.$store.discordActionPins)
.labelsHidden()
.toggleStyle(.checkbox)
}
GridRow {
self.gridLabel("Search")
Toggle("", isOn: self.$store.discordActionSearch)
.labelsHidden()
.toggleStyle(.checkbox)
}
GridRow {
self.gridLabel("Member info")
Toggle("", isOn: self.$store.discordActionMemberInfo)
.labelsHidden()
.toggleStyle(.checkbox)
}
GridRow {
self.gridLabel("Role info")
Toggle("", isOn: self.$store.discordActionRoleInfo)
.labelsHidden()
.toggleStyle(.checkbox)
}
GridRow {
self.gridLabel("Channel info")
Toggle("", isOn: self.$store.discordActionChannelInfo)
.labelsHidden()
.toggleStyle(.checkbox)
}
GridRow {
self.gridLabel("Voice status")
Toggle("", isOn: self.$store.discordActionVoiceStatus)
.labelsHidden()
.toggleStyle(.checkbox)
}
GridRow {
self.gridLabel("Events")
Toggle("", isOn: self.$store.discordActionEvents)
.labelsHidden()
.toggleStyle(.checkbox)
}
GridRow {
self.gridLabel("Role changes")
Toggle("", isOn: self.$store.discordActionRoles)
.labelsHidden()
.toggleStyle(.checkbox)
}
GridRow {
self.gridLabel("Moderation")
Toggle("", isOn: self.$store.discordActionModeration)
.labelsHidden()
.toggleStyle(.checkbox)
}
}
if self.isDiscordTokenLocked {
Text("Token set via DISCORD_BOT_TOKEN env; config edits wont override it.")
.font(.caption)

View File

@@ -174,6 +174,21 @@ final class ConnectionsStore {
var discordGroupChannels: String = ""
var discordMediaMaxMb: String = ""
var discordHistoryLimit: String = ""
var discordActionReactions = true
var discordActionStickers = true
var discordActionPolls = true
var discordActionPermissions = true
var discordActionMessages = true
var discordActionThreads = true
var discordActionPins = true
var discordActionSearch = true
var discordActionMemberInfo = true
var discordActionRoleInfo = true
var discordActionChannelInfo = true
var discordActionVoiceStatus = true
var discordActionEvents = true
var discordActionRoles = false
var discordActionModeration = false
var discordSlashEnabled = false
var discordSlashName: String = ""
var discordSlashSessionPrefix: String = ""
@@ -419,6 +434,22 @@ final class ConnectionsStore {
} else {
self.discordHistoryLimit = ""
}
let discordActions = discord?["actions"]?.dictionaryValue
self.discordActionReactions = discordActions?["reactions"]?.boolValue ?? true
self.discordActionStickers = discordActions?["stickers"]?.boolValue ?? true
self.discordActionPolls = discordActions?["polls"]?.boolValue ?? true
self.discordActionPermissions = discordActions?["permissions"]?.boolValue ?? true
self.discordActionMessages = discordActions?["messages"]?.boolValue ?? true
self.discordActionThreads = discordActions?["threads"]?.boolValue ?? true
self.discordActionPins = discordActions?["pins"]?.boolValue ?? true
self.discordActionSearch = discordActions?["search"]?.boolValue ?? true
self.discordActionMemberInfo = discordActions?["memberInfo"]?.boolValue ?? true
self.discordActionRoleInfo = discordActions?["roleInfo"]?.boolValue ?? true
self.discordActionChannelInfo = discordActions?["channelInfo"]?.boolValue ?? true
self.discordActionVoiceStatus = discordActions?["voiceStatus"]?.boolValue ?? true
self.discordActionEvents = discordActions?["events"]?.boolValue ?? true
self.discordActionRoles = discordActions?["roles"]?.boolValue ?? false
self.discordActionModeration = discordActions?["moderation"]?.boolValue ?? false
let slash = discord?["slashCommand"]?.dictionaryValue
self.discordSlashEnabled = slash?["enabled"]?.boolValue ?? false
self.discordSlashName = slash?["name"]?.stringValue ?? ""
@@ -642,6 +673,35 @@ final class ConnectionsStore {
discord.removeValue(forKey: "historyLimit")
}
var actions: [String: Any] = (discord["actions"] as? [String: Any]) ?? [:]
func setAction(_ key: String, value: Bool, defaultValue: Bool) {
if value == defaultValue {
actions.removeValue(forKey: key)
} else {
actions[key] = value
}
}
setAction("reactions", value: self.discordActionReactions, defaultValue: true)
setAction("stickers", value: self.discordActionStickers, defaultValue: true)
setAction("polls", value: self.discordActionPolls, defaultValue: true)
setAction("permissions", value: self.discordActionPermissions, defaultValue: true)
setAction("messages", value: self.discordActionMessages, defaultValue: true)
setAction("threads", value: self.discordActionThreads, defaultValue: true)
setAction("pins", value: self.discordActionPins, defaultValue: true)
setAction("search", value: self.discordActionSearch, defaultValue: true)
setAction("memberInfo", value: self.discordActionMemberInfo, defaultValue: true)
setAction("roleInfo", value: self.discordActionRoleInfo, defaultValue: true)
setAction("channelInfo", value: self.discordActionChannelInfo, defaultValue: true)
setAction("voiceStatus", value: self.discordActionVoiceStatus, defaultValue: true)
setAction("events", value: self.discordActionEvents, defaultValue: true)
setAction("roles", value: self.discordActionRoles, defaultValue: false)
setAction("moderation", value: self.discordActionModeration, defaultValue: false)
if actions.isEmpty {
discord.removeValue(forKey: "actions")
} else {
discord["actions"] = actions
}
var slash: [String: Any] = (discord["slashCommand"] as? [String: Any]) ?? [:]
if self.discordSlashEnabled {
slash["enabled"] = true

View File

@@ -26,12 +26,13 @@ import type {
SkillStatusReport,
StatusSummary,
} from "./types";
import type {
CronFormState,
DiscordForm,
IMessageForm,
SignalForm,
TelegramForm,
import {
defaultDiscordActions,
type CronFormState,
type DiscordForm,
type IMessageForm,
type SignalForm,
type TelegramForm,
} from "./ui-types";
import { loadChatHistory, sendChat, handleChatEvent } from "./controllers/chat";
import { loadNodes } from "./controllers/nodes";
@@ -143,6 +144,7 @@ export class ClawdisApp extends LitElement {
groupChannels: "",
mediaMaxMb: "",
historyLimit: "",
actions: { ...defaultDiscordActions },
slashEnabled: false,
slashName: "",
slashSessionPrefix: "",

View File

@@ -1,6 +1,13 @@
import type { GatewayBrowserClient } from "../gateway";
import type { ConfigSnapshot } from "../types";
import type { DiscordForm, IMessageForm, SignalForm, TelegramForm } from "../ui-types";
import {
defaultDiscordActions,
type DiscordActionForm,
type DiscordForm,
type IMessageForm,
type SignalForm,
type TelegramForm,
} from "../ui-types";
export type ConfigState = {
client: GatewayBrowserClient | null;
@@ -88,6 +95,11 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
const discordDm = (discord.dm ?? {}) as Record<string, unknown>;
const slash = (discord.slashCommand ?? {}) as Record<string, unknown>;
const discordActions = (discord.actions ?? {}) as Record<string, unknown>;
const readAction = (key: keyof DiscordActionForm) =>
typeof discordActions[key] === "boolean"
? (discordActions[key] as boolean)
: defaultDiscordActions[key];
state.discordForm = {
enabled: typeof discord.enabled === "boolean" ? discord.enabled : true,
token: typeof discord.token === "string" ? discord.token : "",
@@ -99,6 +111,23 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
typeof discord.mediaMaxMb === "number" ? String(discord.mediaMaxMb) : "",
historyLimit:
typeof discord.historyLimit === "number" ? String(discord.historyLimit) : "",
actions: {
reactions: readAction("reactions"),
stickers: readAction("stickers"),
polls: readAction("polls"),
permissions: readAction("permissions"),
messages: readAction("messages"),
threads: readAction("threads"),
pins: readAction("pins"),
search: readAction("search"),
memberInfo: readAction("memberInfo"),
roleInfo: readAction("roleInfo"),
channelInfo: readAction("channelInfo"),
voiceStatus: readAction("voiceStatus"),
events: readAction("events"),
roles: readAction("roles"),
moderation: readAction("moderation"),
},
slashEnabled: typeof slash.enabled === "boolean" ? slash.enabled : false,
slashName: typeof slash.name === "string" ? slash.name : "",
slashSessionPrefix:

View File

@@ -1,7 +1,14 @@
import type { GatewayBrowserClient } from "../gateway";
import { parseList } from "../format";
import type { ConfigSnapshot, ProvidersStatusSnapshot } from "../types";
import type { DiscordForm, IMessageForm, SignalForm, TelegramForm } from "../ui-types";
import {
defaultDiscordActions,
type DiscordActionForm,
type DiscordForm,
type IMessageForm,
type SignalForm,
type TelegramForm,
} from "../ui-types";
export type ConnectionsState = {
client: GatewayBrowserClient | null;
@@ -116,6 +123,14 @@ export function updateDiscordForm(
state: ConnectionsState,
patch: Partial<DiscordForm>,
) {
if (patch.actions) {
state.discordForm = {
...state.discordForm,
...patch,
actions: { ...state.discordForm.actions, ...patch.actions },
};
return;
}
state.discordForm = { ...state.discordForm, ...patch };
}
@@ -246,6 +261,32 @@ export async function saveDiscordConfig(state: ConnectionsState) {
}
}
const actions: Partial<DiscordActionForm> = {};
const applyAction = (key: keyof DiscordActionForm) => {
const value = form.actions[key];
if (value !== defaultDiscordActions[key]) actions[key] = value;
};
applyAction("reactions");
applyAction("stickers");
applyAction("polls");
applyAction("permissions");
applyAction("messages");
applyAction("threads");
applyAction("pins");
applyAction("search");
applyAction("memberInfo");
applyAction("roleInfo");
applyAction("channelInfo");
applyAction("voiceStatus");
applyAction("events");
applyAction("roles");
applyAction("moderation");
if (Object.keys(actions).length > 0) {
discord.actions = actions;
} else {
delete discord.actions;
}
const slash = { ...(discord.slashCommand ?? {}) } as Record<string, unknown>;
if (form.slashEnabled) {
slash.enabled = true;

View File

@@ -16,12 +16,49 @@ export type DiscordForm = {
groupChannels: string;
mediaMaxMb: string;
historyLimit: string;
actions: DiscordActionForm;
slashEnabled: boolean;
slashName: string;
slashSessionPrefix: string;
slashEphemeral: boolean;
};
export type DiscordActionForm = {
reactions: boolean;
stickers: boolean;
polls: boolean;
permissions: boolean;
messages: boolean;
threads: boolean;
pins: boolean;
search: boolean;
memberInfo: boolean;
roleInfo: boolean;
channelInfo: boolean;
voiceStatus: boolean;
events: boolean;
roles: boolean;
moderation: boolean;
};
export const defaultDiscordActions: DiscordActionForm = {
reactions: true,
stickers: true,
polls: true,
permissions: true,
messages: true,
threads: true,
pins: true,
search: true,
memberInfo: true,
roleInfo: true,
channelInfo: true,
voiceStatus: true,
events: true,
roles: false,
moderation: false,
};
export type SignalForm = {
enabled: boolean;
account: string;

View File

@@ -2,7 +2,13 @@ import { html, nothing } from "lit";
import { formatAgo } from "../format";
import type { ProvidersStatusSnapshot } from "../types";
import type { DiscordForm, IMessageForm, SignalForm, TelegramForm } from "../ui-types";
import type {
DiscordActionForm,
DiscordForm,
IMessageForm,
SignalForm,
TelegramForm,
} from "../ui-types";
export type ConnectionsProps = {
connected: boolean;
@@ -48,6 +54,24 @@ export function renderConnections(props: ConnectionsProps) {
const discord = props.snapshot?.discord ?? null;
const signal = props.snapshot?.signal ?? null;
const imessage = props.snapshot?.imessage ?? null;
const discordActionOptions: Array<{ key: keyof DiscordActionForm; label: string }> =
[
{ key: "reactions", label: "Reactions" },
{ key: "stickers", label: "Stickers" },
{ key: "polls", label: "Polls" },
{ key: "permissions", label: "Permissions" },
{ key: "messages", label: "Messages" },
{ key: "threads", label: "Threads" },
{ key: "pins", label: "Pins" },
{ key: "search", label: "Search" },
{ key: "memberInfo", label: "Member info" },
{ key: "roleInfo", label: "Role info" },
{ key: "channelInfo", label: "Channel info" },
{ key: "voiceStatus", label: "Voice status" },
{ key: "events", label: "Events" },
{ key: "roles", label: "Role changes" },
{ key: "moderation", label: "Moderation" },
];
const providerOrder: ProviderKey[] = [
"whatsapp",
"telegram",
@@ -572,6 +596,28 @@ function renderProvider(
</label>
</div>
<div class="card-sub" style="margin-top: 16px;">Tool actions</div>
<div class="form-grid" style="margin-top: 8px;">
${discordActionOptions.map(
(action) => html`<label class="field">
<span>${action.label}</span>
<select
.value=${props.discordForm.actions[action.key] ? "yes" : "no"}
@change=${(e: Event) =>
props.onDiscordChange({
actions: {
...props.discordForm.actions,
[action.key]: (e.target as HTMLSelectElement).value === "yes",
},
})}
>
<option value="yes">Enabled</option>
<option value="no">Disabled</option>
</select>
</label>`,
)}
</div>
${props.discordTokenLocked
? html`<div class="callout" style="margin-top: 12px;">
DISCORD_BOT_TOKEN is set in the environment. Config edits will not override it.