fix(sync): scope auto-approve to session tree and restore request UX

This commit is contained in:
Bohdan Triapitsyn
2026-04-16 19:16:32 +03:00
parent ddc1039d1c
commit 758c0dc4b8
4 changed files with 388 additions and 139 deletions
@@ -2,86 +2,43 @@ import type { Session } from "@opencode-ai/sdk/v2/client";
export type PermissionAutoAcceptMap = Record<string, boolean>;
const DIRECTORY_WILDCARD = "*";
const encodeBase64 = (value: string): string => {
try {
const bytes = new TextEncoder().encode(value);
let binary = "";
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}
return btoa(binary);
} catch {
return btoa(value);
}
const buildSessionMap = (sessions: Session[]): Map<string, Session> => {
const map = new Map<string, Session>();
for (const session of sessions) {
map.set(session.id, session);
}
return map;
};
export const normalizeDirectory = (value: string | null | undefined): string | null => {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
if (!trimmed) {
return null;
}
const normalized = trimmed.replace(/\\/g, "/");
if (normalized === "/") {
return "/";
}
return normalized.length > 1 ? normalized.replace(/\/+$/g, "") : normalized;
};
export const directoryAcceptKey = (directory: string): string => `${encodeBase64(directory)}/${DIRECTORY_WILDCARD}`;
export const sessionAcceptKey = (sessionID: string, directory: string): string => `${encodeBase64(directory)}/${sessionID}`;
const resolveLineage = (sessionID: string, sessions: Session[]): string[] => {
const map = new Map<string, Session>();
for (const session of sessions) {
map.set(session.id, session);
}
const map = buildSessionMap(sessions);
const result: string[] = [];
const seen = new Set<string>();
let current: string | undefined = sessionID;
const result: string[] = [];
const seen = new Set<string>();
let current: string | undefined = sessionID;
while (current && !seen.has(current)) {
seen.add(current);
result.push(current);
current = map.get(current)?.parentID;
}
return result;
while (current && !seen.has(current)) {
seen.add(current);
result.push(current);
current = map.get(current)?.parentID;
}
return result;
};
export const autoRespondsPermission = (input: {
autoAccept: PermissionAutoAcceptMap;
sessions: Session[];
sessionID: string;
directory: string;
autoAccept: PermissionAutoAcceptMap;
sessions: Session[];
sessionID: string;
}): boolean => {
const { autoAccept, sessions, sessionID, directory } = input;
const { autoAccept, sessions, sessionID } = input;
const lineage = resolveLineage(sessionID, sessions);
for (const id of resolveLineage(sessionID, sessions)) {
const key = sessionAcceptKey(id, directory);
if (key in autoAccept) {
return autoAccept[key] === true;
}
// Legacy fallback for pre-directory keys.
if (id in autoAccept) {
return autoAccept[id] === true;
}
for (const id of lineage) {
if (!Object.prototype.hasOwnProperty.call(autoAccept, id)) {
continue;
}
return autoAccept[id] === true;
}
const directoryKey = directoryAcceptKey(directory);
if (directoryKey in autoAccept) {
return autoAccept[directoryKey] === true;
}
return false;
};
export const isDirectoryAutoAccepting = (autoAccept: PermissionAutoAcceptMap, directory: string): boolean => {
const key = directoryAcceptKey(directory);
return autoAccept[key] === true;
return false;
};