From f42646035829f6b2bf6a697a3d6058438b2226bb Mon Sep 17 00:00:00 2001 From: viettranx Date: Thu, 5 Mar 2026 14:20:29 +0700 Subject: [PATCH] feat(teams): per-team progress notifications setting with UI toggle Added progress_notifications to agent_teams.settings JSONB so teams can opt-in to periodic "Your team is working on it..." chat messages during async delegations. Global default is OFF. Backend: progressEnabled resolved per-delegation from team settings, falling back to global default. Frontend: checkbox toggle in team settings page. Co-Authored-By: Claude Opus 4.6 --- internal/gateway/methods/teams_crud.go | 9 ++++--- internal/tools/delegate.go | 9 ++++++- internal/tools/delegate_policy.go | 23 ++++++++++++++--- internal/tools/delegate_prep.go | 9 +++++++ ui/web/src/pages/teams/team-settings-tab.tsx | 26 +++++++++++++++++++- ui/web/src/types/team.ts | 1 + 6 files changed, 67 insertions(+), 10 deletions(-) diff --git a/internal/gateway/methods/teams_crud.go b/internal/gateway/methods/teams_crud.go index edfb12cb..8b829e56 100644 --- a/internal/gateway/methods/teams_crud.go +++ b/internal/gateway/methods/teams_crud.go @@ -204,10 +204,11 @@ func (m *TeamsMethods) handleUpdate(_ context.Context, client *gateway.Client, r // Validate settings against teamAccessSettings schema (strip unknown fields) type teamAccessSettings struct { - AllowUserIDs []string `json:"allow_user_ids"` - DenyUserIDs []string `json:"deny_user_ids"` - AllowChannels []string `json:"allow_channels"` - DenyChannels []string `json:"deny_channels"` + AllowUserIDs []string `json:"allow_user_ids"` + DenyUserIDs []string `json:"deny_user_ids"` + AllowChannels []string `json:"allow_channels"` + DenyChannels []string `json:"deny_channels"` + ProgressNotifications *bool `json:"progress_notifications,omitempty"` } raw, _ := json.Marshal(params.Settings) var access teamAccessSettings diff --git a/internal/tools/delegate.go b/internal/tools/delegate.go index da5e1806..74b015cb 100644 --- a/internal/tools/delegate.go +++ b/internal/tools/delegate.go @@ -46,7 +46,8 @@ type DelegationTask struct { TeamID uuid.UUID `json:"-"` // from link.TeamID (for delegation history) TeamTaskID uuid.UUID `json:"-"` - cancelFunc context.CancelFunc `json:"-"` + cancelFunc context.CancelFunc `json:"-"` + progressEnabled bool `json:"-"` // resolved from team settings or global default } // DelegateOpts configures a single delegation call. @@ -143,6 +144,7 @@ type DelegateManager struct { active sync.Map // delegationID → *DelegationTask pendingArtifacts sync.Map // sourceAgentID string → *DelegateArtifacts progressSent sync.Map // "sourceAgentID:chatID" → true (dedup grouped notifications) + progressEnabled bool // send "Your team is working on it..." to chat (default: false/off) completedMu sync.Mutex completedSessions []string // session keys pending cleanup } @@ -177,3 +179,8 @@ func (dm *DelegateManager) SetHookEngine(engine *hooks.Engine) { dm.hookEngine = engine } +// SetProgressEnabled toggles "Your team is working on it..." chat notifications. +func (dm *DelegateManager) SetProgressEnabled(enabled bool) { + dm.progressEnabled = enabled +} + diff --git a/internal/tools/delegate_policy.go b/internal/tools/delegate_policy.go index 2dd4554c..4c5eba86 100644 --- a/internal/tools/delegate_policy.go +++ b/internal/tools/delegate_policy.go @@ -40,10 +40,11 @@ func checkUserPermission(settings json.RawMessage, userID string) error { // teamAccessSettings defines access control rules stored in agent_teams.settings JSONB. // Empty/nil lists mean "no restriction". Deny lists take precedence over allow lists. type teamAccessSettings struct { - AllowUserIDs []string `json:"allow_user_ids"` - DenyUserIDs []string `json:"deny_user_ids"` - AllowChannels []string `json:"allow_channels"` - DenyChannels []string `json:"deny_channels"` + AllowUserIDs []string `json:"allow_user_ids"` + DenyUserIDs []string `json:"deny_user_ids"` + AllowChannels []string `json:"allow_channels"` + DenyChannels []string `json:"deny_channels"` + ProgressNotifications *bool `json:"progress_notifications,omitempty"` } // checkTeamAccess validates whether a user/channel combination is authorized @@ -135,6 +136,20 @@ func parseQualityGates(otherConfig json.RawMessage) []hooks.HookConfig { return cfg.QualityGates } +func parseProgressNotifications(settings json.RawMessage, globalDefault bool) bool { + if len(settings) == 0 { + return globalDefault + } + var s teamAccessSettings + if json.Unmarshal(settings, &s) != nil { + return globalDefault + } + if s.ProgressNotifications != nil { + return *s.ProgressNotifications + } + return globalDefault +} + // applyQualityGates evaluates quality gates on a delegation result. // Returns the (possibly revised) result. If a blocking gate fails after all retries, // returns the last result anyway with a logged warning (does not hard-fail the delegation). diff --git a/internal/tools/delegate_prep.go b/internal/tools/delegate_prep.go index a7ea72b5..4484a93c 100644 --- a/internal/tools/delegate_prep.go +++ b/internal/tools/delegate_prep.go @@ -184,6 +184,12 @@ func (dm *DelegateManager) prepareDelegation(ctx context.Context, opts DelegateO task.TeamID = *link.TeamID } + // Resolve progress notifications: per-team setting overrides global default. + task.progressEnabled = dm.progressEnabled + if team != nil { + task.progressEnabled = parseProgressNotifications(team.Settings, dm.progressEnabled) + } + return task, link, nil } @@ -232,6 +238,9 @@ func (dm *DelegateManager) injectDependencyResults(ctx context.Context, opts *De // active delegations from the same source agent. Uses progressSent to dedup — // concurrent tickers only send one notification per cycle, then release for next tick. func (dm *DelegateManager) sendProgressNotification(task *DelegationTask) { + if !task.progressEnabled { + return + } // Skip internal/delegate channels — only notify on real user-facing channels. if dm.msgBus == nil || task.OriginChannel == "" || task.OriginChatID == "" || task.OriginChannel == "delegate" || task.OriginChannel == "system" { diff --git a/ui/web/src/pages/teams/team-settings-tab.tsx b/ui/web/src/pages/teams/team-settings-tab.tsx index ba40ca55..bb825e97 100644 --- a/ui/web/src/pages/teams/team-settings-tab.tsx +++ b/ui/web/src/pages/teams/team-settings-tab.tsx @@ -66,6 +66,7 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps) const [denyUserIds, setDenyUserIds] = useState(initial.deny_user_ids ?? []); const [allowChannels, setAllowChannels] = useState(initial.allow_channels ?? []); const [denyChannels, setDenyChannels] = useState(initial.deny_channels ?? []); + const [progressNotifications, setProgressNotifications] = useState(initial.progress_notifications ?? false); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); @@ -83,6 +84,7 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps) setDenyUserIds(s.deny_user_ids ?? []); setAllowChannels(s.allow_channels ?? []); setDenyChannels(s.deny_channels ?? []); + setProgressNotifications(s.progress_notifications ?? false); setSaved(false); setError(null); }, [team]); @@ -97,6 +99,7 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps) if (denyUserIds.length > 0) settings.deny_user_ids = denyUserIds; if (allowChannels.length > 0) settings.allow_channels = allowChannels; if (denyChannels.length > 0) settings.deny_channels = denyChannels; + if (progressNotifications) settings.progress_notifications = true; await updateTeamSettings(teamId, settings); setSaved(true); onSaved(); @@ -106,7 +109,7 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps) } finally { setSaving(false); } - }, [teamId, allowUserIds, denyUserIds, allowChannels, denyChannels, updateTeamSettings, onSaved]); + }, [teamId, allowUserIds, denyUserIds, allowChannels, denyChannels, progressNotifications, updateTeamSettings, onSaved]); const userOptions = knownUsers.map((u) => ({ value: u, label: u })); const channelOptions = CHANNEL_TYPES.map((c) => ({ value: c.value, label: c.label })); @@ -175,6 +178,27 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps) + {/* Notifications */} +
+

Notifications

+
+ +
+
+ {/* Save button */}