mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 12:16:54 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
64ca26e611
commit
f426460358
6 files changed
+67
-10
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -66,6 +66,7 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
|
||||
const [denyUserIds, setDenyUserIds] = useState<string[]>(initial.deny_user_ids ?? []);
|
||||
const [allowChannels, setAllowChannels] = useState<string[]>(initial.allow_channels ?? []);
|
||||
const [denyChannels, setDenyChannels] = useState<string[]>(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)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notifications */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium">Notifications</h3>
|
||||
<div className="rounded-lg border p-4">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={progressNotifications}
|
||||
onChange={(e) => setProgressNotifications(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-input"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-sm font-medium">Progress notifications</span>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Send periodic "Your team is working on it..." messages to chat during async delegations.
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save button */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={handleSave} disabled={saving} className="gap-2">
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface TeamAccessSettings {
|
||||
deny_user_ids?: string[];
|
||||
allow_channels?: string[];
|
||||
deny_channels?: string[];
|
||||
progress_notifications?: boolean;
|
||||
}
|
||||
|
||||
export interface TeamData {
|
||||
|
||||
Reference in new issue
Block a user