refactor: simplify assistant approvals
This commit is contained in:
1 parent
ef6d350b4d
commit
2f7d150d01
3 files changed
+27
-91
No files matched your search
@@ -41,32 +41,18 @@ host gateway address.
|
||||
- Ask from `Cmd/Ctrl + K`.
|
||||
- Add the [Assistant widget](../widgets/assistant) to a board.
|
||||
|
||||
On desktop, the full conversation opens as a side panel so the current dashboard remains visible. On mobile, it uses
|
||||
the full screen. Homarr keeps one active conversation surface: while the side panel is open, the board widget becomes
|
||||
a compact return control, and a visible board widget replaces the floating activity bar. If a board contains multiple
|
||||
Assistant widgets, use a compact widget's action to move the active conversation there. Minimize returns the
|
||||
conversation to its widget or activity state; Close also dismisses completed activity.
|
||||
|
||||
The assistant can work with boards, apps, integrations, widgets, Custom Widgets, and connected services. Use `@` to
|
||||
add Homarr items as context, `/` for shortcuts, and the paperclip for supported files.
|
||||
|
||||
In supported browsers, use the response action menu to read an answer aloud.
|
||||
|
||||
Suggested prompts fill the composer first so you can review or edit them before sending. Questions and approval requests
|
||||
stay next to the composer without hiding the transcript.
|
||||
|
||||
Use **Conversations** to search and switch threads. Open a conversation's action menu to rename it, export it as
|
||||
Markdown, or delete it after confirmation.
|
||||
Use **Conversations** to search, switch, rename, export, or delete threads.
|
||||
|
||||
Each request includes the user's name, current page and board, home board, server time, browser time zone, and a small
|
||||
summary of accessible Homarr resources. Live status is fetched only when needed.
|
||||
|
||||
## Permissions and privacy
|
||||
|
||||
- Read-only actions can run automatically. Changes require approval by default.
|
||||
- **Automatic approvals** can prepare proposed form details and approve protected changes for the current conversation.
|
||||
It turns off when you switch conversations and still respects Homarr permissions.
|
||||
- Questions always wait for your answer, even when automatic approvals are enabled.
|
||||
- Read-only actions can run automatically. Changes require approval by default. Automatic approvals apply only to the
|
||||
current conversation, turn off when you switch conversations, and never answer questions for you.
|
||||
- Homarr permissions apply to every tool call and mentioned item.
|
||||
- API keys and custom headers are encrypted and never returned to the browser.
|
||||
- Conversations are stored per user and can be exported or deleted.
|
||||
|
||||
@@ -36,70 +36,41 @@ export const createAssistantAutoApprovalTracker = (maximumAttempts = Number.POSI
|
||||
interface AssistantAutoApprovalContextValue {
|
||||
enabled: boolean;
|
||||
setEnabled: (enabled: boolean) => void;
|
||||
preparationEnabled: boolean;
|
||||
approvalEnabled: boolean;
|
||||
setPreparationEnabled: (enabled: boolean) => void;
|
||||
setApprovalEnabled: (enabled: boolean) => void;
|
||||
requestAction: (toolCallId: string, confirm: () => void, mode?: AssistantAutomaticActionMode) => boolean;
|
||||
requestAction: (toolCallId: string, confirm: () => void) => boolean;
|
||||
completeAction: (toolCallId: string) => void;
|
||||
retryRevision: number;
|
||||
}
|
||||
|
||||
export type AssistantAutomaticActionMode = "preparation" | "approval";
|
||||
|
||||
const AssistantAutoApprovalContext = createContext<AssistantAutoApprovalContextValue | null>(null);
|
||||
|
||||
export const AssistantAutoApprovalProvider = ({
|
||||
children,
|
||||
conversationId,
|
||||
}: PropsWithChildren<{ conversationId: string }>) => {
|
||||
const [preparationEnabled, setPreparationEnabledState] = useState(false);
|
||||
const [approvalEnabled, setApprovalEnabledState] = useState(false);
|
||||
const [enabled, setEnabledState] = useState(false);
|
||||
const [retryRevision, setRetryRevision] = useState(0);
|
||||
const trackerRef = useRef(createAssistantAutoApprovalTracker(AUTO_APPROVAL_MAX_ATTEMPTS));
|
||||
const retryTimersRef = useRef(new Map<string, { timer: number; mode: AssistantAutomaticActionMode }>());
|
||||
const retryTimersRef = useRef(new Map<string, number>());
|
||||
const previousConversationIdRef = useRef(conversationId);
|
||||
const conversationMatches = previousConversationIdRef.current === conversationId;
|
||||
const effectivePreparationEnabled = conversationMatches && preparationEnabled;
|
||||
const effectiveApprovalEnabled = conversationMatches && approvalEnabled;
|
||||
const effectiveEnabled = conversationMatches && enabled;
|
||||
|
||||
const clearRetryTimer = useCallback((toolCallId: string) => {
|
||||
const retry = retryTimersRef.current.get(toolCallId);
|
||||
if (retry !== undefined) window.clearTimeout(retry.timer);
|
||||
const timer = retryTimersRef.current.get(toolCallId);
|
||||
if (timer !== undefined) window.clearTimeout(timer);
|
||||
retryTimersRef.current.delete(toolCallId);
|
||||
}, []);
|
||||
|
||||
const clearAutomaticActions = useCallback((mode?: AssistantAutomaticActionMode) => {
|
||||
for (const [toolCallId, retry] of retryTimersRef.current) {
|
||||
if (mode !== undefined && retry.mode !== mode) continue;
|
||||
window.clearTimeout(retry.timer);
|
||||
retryTimersRef.current.delete(toolCallId);
|
||||
trackerRef.current.complete(toolCallId);
|
||||
}
|
||||
if (mode === undefined) trackerRef.current.clear();
|
||||
const clearAutomaticActions = useCallback(() => {
|
||||
for (const timer of retryTimersRef.current.values()) window.clearTimeout(timer);
|
||||
retryTimersRef.current.clear();
|
||||
trackerRef.current.clear();
|
||||
}, []);
|
||||
|
||||
const setEnabled = useCallback(
|
||||
(nextEnabled: boolean) => {
|
||||
if (!nextEnabled) clearAutomaticActions();
|
||||
setPreparationEnabledState(nextEnabled);
|
||||
setApprovalEnabledState(nextEnabled);
|
||||
},
|
||||
[clearAutomaticActions],
|
||||
);
|
||||
|
||||
const setPreparationEnabled = useCallback(
|
||||
(nextEnabled: boolean) => {
|
||||
if (!nextEnabled) clearAutomaticActions("preparation");
|
||||
setPreparationEnabledState(nextEnabled);
|
||||
},
|
||||
[clearAutomaticActions],
|
||||
);
|
||||
|
||||
const setApprovalEnabled = useCallback(
|
||||
(nextEnabled: boolean) => {
|
||||
if (!nextEnabled) clearAutomaticActions("approval");
|
||||
setApprovalEnabledState(nextEnabled);
|
||||
setEnabledState(nextEnabled);
|
||||
},
|
||||
[clearAutomaticActions],
|
||||
);
|
||||
@@ -110,35 +81,33 @@ export const AssistantAutoApprovalProvider = ({
|
||||
|
||||
if (previousConversationId === conversationId) return;
|
||||
clearAutomaticActions();
|
||||
setPreparationEnabledState(false);
|
||||
setApprovalEnabledState(false);
|
||||
setEnabledState(false);
|
||||
}, [clearAutomaticActions, conversationId]);
|
||||
|
||||
useEffect(() => clearAutomaticActions, [clearAutomaticActions]);
|
||||
|
||||
const requestAction = useCallback(
|
||||
(toolCallId: string, confirm: () => void, mode: AssistantAutomaticActionMode = "preparation") => {
|
||||
const modeEnabled = mode === "approval" ? effectiveApprovalEnabled : effectivePreparationEnabled;
|
||||
if (!modeEnabled || !trackerRef.current.claim(toolCallId)) return false;
|
||||
(toolCallId: string, confirm: () => void) => {
|
||||
if (!effectiveEnabled || !trackerRef.current.claim(toolCallId)) return false;
|
||||
|
||||
try {
|
||||
confirm();
|
||||
clearRetryTimer(toolCallId);
|
||||
retryTimersRef.current.set(toolCallId, {
|
||||
mode,
|
||||
timer: window.setTimeout(() => {
|
||||
retryTimersRef.current.set(
|
||||
toolCallId,
|
||||
window.setTimeout(() => {
|
||||
retryTimersRef.current.delete(toolCallId);
|
||||
trackerRef.current.release(toolCallId);
|
||||
setRetryRevision((revision) => revision + 1);
|
||||
}, AUTO_APPROVAL_RETRY_TIMEOUT_MS),
|
||||
});
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
trackerRef.current.release(toolCallId);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[clearRetryTimer, effectiveApprovalEnabled, effectivePreparationEnabled],
|
||||
[clearRetryTimer, effectiveEnabled],
|
||||
);
|
||||
|
||||
const completeAction = useCallback(
|
||||
@@ -150,28 +119,14 @@ export const AssistantAutoApprovalProvider = ({
|
||||
);
|
||||
|
||||
const value = useMemo(() => {
|
||||
const enabled = effectivePreparationEnabled || effectiveApprovalEnabled;
|
||||
return {
|
||||
enabled,
|
||||
enabled: effectiveEnabled,
|
||||
setEnabled,
|
||||
preparationEnabled: effectivePreparationEnabled,
|
||||
approvalEnabled: effectiveApprovalEnabled,
|
||||
setPreparationEnabled,
|
||||
setApprovalEnabled,
|
||||
requestAction,
|
||||
completeAction,
|
||||
retryRevision,
|
||||
};
|
||||
}, [
|
||||
completeAction,
|
||||
effectiveApprovalEnabled,
|
||||
effectivePreparationEnabled,
|
||||
requestAction,
|
||||
retryRevision,
|
||||
setApprovalEnabled,
|
||||
setEnabled,
|
||||
setPreparationEnabled,
|
||||
]);
|
||||
}, [completeAction, effectiveEnabled, requestAction, retryRevision, setEnabled]);
|
||||
|
||||
return <AssistantAutoApprovalContext.Provider value={value}>{children}</AssistantAutoApprovalContext.Provider>;
|
||||
};
|
||||
@@ -187,20 +142,16 @@ export const useAssistantAutomaticAction = ({
|
||||
ready,
|
||||
completed,
|
||||
confirm,
|
||||
mode = "preparation",
|
||||
}: {
|
||||
toolCallId: string;
|
||||
ready: boolean;
|
||||
completed: boolean;
|
||||
confirm: () => void;
|
||||
mode?: AssistantAutomaticActionMode;
|
||||
}) => {
|
||||
const { approvalEnabled, completeAction, preparationEnabled, requestAction, retryRevision } =
|
||||
useAssistantAutoApproval();
|
||||
const { completeAction, enabled, requestAction, retryRevision } = useAssistantAutoApproval();
|
||||
const [inProgress, setInProgress] = useState(false);
|
||||
const confirmRef = useRef(confirm);
|
||||
confirmRef.current = confirm;
|
||||
const enabled = mode === "approval" ? approvalEnabled : preparationEnabled;
|
||||
|
||||
useEffect(() => {
|
||||
if (!ready || completed || !enabled) {
|
||||
@@ -209,9 +160,9 @@ export const useAssistantAutomaticAction = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const requested = requestAction(toolCallId, () => confirmRef.current(), mode);
|
||||
const requested = requestAction(toolCallId, () => confirmRef.current());
|
||||
setInProgress(requested);
|
||||
}, [completeAction, completed, enabled, mode, ready, requestAction, retryRevision, toolCallId]);
|
||||
}, [completeAction, completed, enabled, ready, requestAction, retryRevision, toolCallId]);
|
||||
|
||||
return inProgress;
|
||||
};
|
||||
@@ -897,7 +897,6 @@ const ToolPart = ({
|
||||
toolCallId,
|
||||
ready: awaitingApproval,
|
||||
completed: !awaitingApproval,
|
||||
mode: "approval",
|
||||
confirm: () => {
|
||||
respondToApproval({ approved: true, reason: "Approved automatically by the user." });
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user