mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
* fix(pr-comments): let users mark comment authors as bots for the Humans/Bots filter Some review bots post from regular user accounts that defeat both provider bot metadata and login heuristics, so their comments were misclassified as human. Adds a persisted prBotAuthorOverrides setting with a "Mark author as bot" comment action, applied consistently across desktop and mobile. Fixes #7597 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(pr-comments): address review feedback on bot-author overrides - Cap sanitized prBotAuthorOverrides at 500 entries so malformed payloads can't bloat GlobalSettings or slow comment classification - Reuse the shared normalizePRCommentAuthorLogin in isBotPRComment on desktop and mobile instead of duplicating the normalization inline - Pass botAuthorOverrides from CommentRow to CommentMoreMenu instead of re-subscribing per menu instance - Re-fetch mobile bot-author overrides alongside each PR refetch so they don't stay a stale one-shot snapshot for the whole session Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(pr-comments): harden bot author override sync * fix(pr-comments): bound and recover override updates * fix(pr-comments): merge overrides from canonical settings * fix(pr-comments): make bot override updates atomic * fix(pr-comments): surface rejected bot overrides * fix(i18n): translate bot override warning * fix(i18n): translate bot author actions --------- Co-authored-by: Dzmitry Bachko <dbachko@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
229 lines
9.4 KiB
TypeScript
229 lines
9.4 KiB
TypeScript
import { BrowserWindow, ipcMain, nativeTheme } from 'electron'
|
|
import type { Store } from '../persistence'
|
|
import type { GlobalSettings, PersistedState } from '../../shared/types'
|
|
import { listSystemFontFamilies } from '../system-fonts'
|
|
import { previewGhosttyImport } from '../ghostty/index'
|
|
import { previewWarpThemeImport } from '../warp-themes'
|
|
import { setMainUiLanguage } from '../i18n/main-i18n'
|
|
import { rebuildAppMenu } from '../menu/register-app-menu'
|
|
import { track } from '../telemetry/client'
|
|
import { SETTINGS_CHANGED_WHITELIST, type SettingsChangedKey } from '../../shared/telemetry-events'
|
|
import type { AgentAwakeService } from '../agent-awake-service'
|
|
import { sanitizeFloatingWorkspaceDirectorySetting } from './floating-workspace-directory'
|
|
import { applyAgentStatusHooksEnabled } from '../agent-hooks/managed-agent-hook-controls'
|
|
import { applyElectronProxySettings } from '../network/proxy-settings'
|
|
import { normalizeProxyBypassRules, normalizeProxyUrl } from '../../shared/network-proxy'
|
|
import { normalizeAppIconId } from '../../shared/app-icon'
|
|
import { normalizeUiLanguage } from '../../shared/ui-language'
|
|
import { applyAppIcon } from '../app-icon'
|
|
import { normalizeTerminalCustomThemes } from '../../shared/terminal-custom-themes'
|
|
import { normalizeDesktopTerminalScrollbackRows } from '../../shared/terminal-scrollback-policy'
|
|
import { normalizeTerminalLineHeight } from '../../shared/terminal-line-height-settings'
|
|
import { prepareLocalWorktreeRootsForRepos } from '../worktree-root-preparation'
|
|
import { scheduleCurrentWorktreeBaseDirectoryWatcherSync } from './worktree-base-directory-watcher'
|
|
import { applyPRBotAuthorOverride } from '../../shared/pr-bot-author-overrides'
|
|
|
|
// Why: the whitelist is the source-of-truth for which keys we emit on. Casting
|
|
// to a Set once at module load lets the IPC handler's per-key membership
|
|
// check stay O(1) without re-coercing the readonly tuple on every call.
|
|
const SETTINGS_CHANGED_WHITELIST_SET = new Set<string>(SETTINGS_CHANGED_WHITELIST)
|
|
|
|
type LegacyTerminalScrollbackSettingsUpdate = Partial<GlobalSettings> & {
|
|
terminalScrollbackBytes?: unknown
|
|
}
|
|
|
|
function sanitizeRendererSettingsUpdate(args: Partial<GlobalSettings>): Partial<GlobalSettings> {
|
|
const { terminalScrollbackBytes: _legacyScrollbackBytes, ...sanitizedArgs } =
|
|
args as LegacyTerminalScrollbackSettingsUpdate
|
|
void _legacyScrollbackBytes
|
|
return sanitizedArgs
|
|
}
|
|
|
|
// Why: fields that appear in the View > Appearance submenu need the menu
|
|
// rebuilt after any update so the checkbox `checked` state stays in sync
|
|
// with the persisted value. Electron doesn't reactively re-render menu
|
|
// items when the backing state changes.
|
|
const APPEARANCE_MENU_KEYS: readonly (keyof GlobalSettings)[] = [
|
|
'showTasksButton',
|
|
'showAutomationsButton',
|
|
'showMobileButton',
|
|
'showTitlebarAppName'
|
|
]
|
|
|
|
export function registerSettingsHandlers(
|
|
store: Store,
|
|
agentAwakeService?: AgentAwakeService
|
|
): void {
|
|
store.onSettingsChanged((updates, _settings, originWebContentsId) => {
|
|
for (const window of BrowserWindow.getAllWindows()) {
|
|
const isOrigin =
|
|
originWebContentsId !== undefined && window.webContents.id === originWebContentsId
|
|
if (!window.isDestroyed() && !isOrigin) {
|
|
window.webContents.send('settings:changed', updates)
|
|
}
|
|
}
|
|
})
|
|
|
|
ipcMain.handle('settings:get', () => {
|
|
return store.getSettings()
|
|
})
|
|
|
|
ipcMain.handle(
|
|
'settings:update-pr-bot-author-override',
|
|
(event, args: { author: string; isBot: boolean }) => {
|
|
const current = store.getSettings().prBotAuthorOverrides
|
|
const next = applyPRBotAuthorOverride(current, args.author, args.isBot)
|
|
store.updateSettings(
|
|
{ prBotAuthorOverrides: next },
|
|
{ notifyListeners: true, originWebContentsId: event.sender.id }
|
|
)
|
|
return store.getSettings()
|
|
}
|
|
)
|
|
|
|
// Why: terminal panes can bind PTYs before async settings hydration
|
|
// completes. The side-effect authority kill switch is consulted once at
|
|
// transport creation, so the renderer needs the persisted value
|
|
// synchronously or pre-hydration bindings would always pick main authority
|
|
// (terminal-side-effect-authority.md, migration switch).
|
|
ipcMain.on('settings:get-sync', (event) => {
|
|
event.returnValue = store.getSettings()
|
|
})
|
|
|
|
ipcMain.handle('settings:set', async (event, args: Partial<GlobalSettings>) => {
|
|
const sanitizedArgs = sanitizeRendererSettingsUpdate(args)
|
|
// Why: Floating Workspace grants are trusted only when written by the
|
|
// main-process directory picker, never by renderer-provided settings IPC.
|
|
delete sanitizedArgs.floatingTerminalTrustedCwds
|
|
if (typeof args.floatingTerminalCwd === 'string') {
|
|
sanitizedArgs.floatingTerminalCwd = await sanitizeFloatingWorkspaceDirectorySetting(
|
|
store,
|
|
args.floatingTerminalCwd
|
|
)
|
|
}
|
|
if ('httpProxyUrl' in args) {
|
|
const proxyUrl = normalizeProxyUrl(args.httpProxyUrl)
|
|
sanitizedArgs.httpProxyUrl = proxyUrl.ok ? proxyUrl.value : ''
|
|
}
|
|
if ('httpProxyBypassRules' in args) {
|
|
sanitizedArgs.httpProxyBypassRules = normalizeProxyBypassRules(args.httpProxyBypassRules)
|
|
}
|
|
if ('appIcon' in args) {
|
|
sanitizedArgs.appIcon = normalizeAppIconId(args.appIcon)
|
|
}
|
|
if ('terminalCustomThemes' in args) {
|
|
sanitizedArgs.terminalCustomThemes = normalizeTerminalCustomThemes(args.terminalCustomThemes)
|
|
}
|
|
if ('terminalScrollbackRows' in args) {
|
|
sanitizedArgs.terminalScrollbackRows = normalizeDesktopTerminalScrollbackRows(
|
|
args.terminalScrollbackRows
|
|
)
|
|
}
|
|
if ('terminalLineHeight' in args) {
|
|
sanitizedArgs.terminalLineHeight = normalizeTerminalLineHeight(args.terminalLineHeight)
|
|
}
|
|
if ('uiLanguage' in args) {
|
|
sanitizedArgs.uiLanguage = normalizeUiLanguage(args.uiLanguage)
|
|
}
|
|
if (args.theme) {
|
|
nativeTheme.themeSource = args.theme
|
|
}
|
|
// Why: capture the pre-update value so we only emit when the value
|
|
// actually changes. The settings UI sometimes re-saves the same value
|
|
// (e.g. blur after a no-op edit), and a `settings_changed` event for a
|
|
// no-op flip would inflate the experimental-feature-adoption signal.
|
|
const before = store.getSettings()
|
|
const result = store.updateSettings(sanitizedArgs, {
|
|
notifyListeners: true,
|
|
originWebContentsId: event.sender.id
|
|
})
|
|
if ('keepComputerAwakeWhileAgentsRun' in sanitizedArgs) {
|
|
agentAwakeService?.setEnabled(result.keepComputerAwakeWhileAgentsRun)
|
|
}
|
|
if (
|
|
'agentStatusHooksEnabled' in sanitizedArgs &&
|
|
before.agentStatusHooksEnabled !== result.agentStatusHooksEnabled
|
|
) {
|
|
try {
|
|
applyAgentStatusHooksEnabled(result.agentStatusHooksEnabled)
|
|
} catch (error) {
|
|
console.warn('[settings] failed to apply agentStatusHooksEnabled:', error)
|
|
}
|
|
}
|
|
if ('uiLanguage' in sanitizedArgs && before.uiLanguage !== result.uiLanguage) {
|
|
await setMainUiLanguage(result.uiLanguage)
|
|
rebuildAppMenu()
|
|
}
|
|
if (
|
|
('workspaceDir' in sanitizedArgs && before.workspaceDir !== result.workspaceDir) ||
|
|
('nestWorkspaces' in sanitizedArgs && before.nestWorkspaces !== result.nestWorkspaces)
|
|
) {
|
|
void prepareLocalWorktreeRootsForRepos(store)
|
|
scheduleCurrentWorktreeBaseDirectoryWatcherSync()
|
|
}
|
|
if (APPEARANCE_MENU_KEYS.some((key) => key in sanitizedArgs)) {
|
|
rebuildAppMenu()
|
|
}
|
|
if ('httpProxyUrl' in sanitizedArgs || 'httpProxyBypassRules' in sanitizedArgs) {
|
|
try {
|
|
await applyElectronProxySettings(result)
|
|
} catch {
|
|
console.warn('[settings] failed to apply network proxy settings')
|
|
}
|
|
}
|
|
if ('appIcon' in sanitizedArgs && before.appIcon !== result.appIcon) {
|
|
applyAppIcon(result.appIcon)
|
|
}
|
|
|
|
// Why: telemetry-plan.md§Settings — fire `settings_changed` only for
|
|
// whitelisted keys, with `value_kind` distinguishing booleans from
|
|
// string-enum settings. We deliberately do NOT send the raw value for
|
|
// non-enum settings; the whitelist is currently scoped to experimental
|
|
// toggles, all of which are booleans, so `value_kind === 'bool'` is
|
|
// the path the v1 enum has a slot for. If a non-bool whitelisted
|
|
// setting is ever added, extend the discriminator here at the same
|
|
// time the schema's `value_kind` enum gains the new value.
|
|
for (const key of Object.keys(sanitizedArgs)) {
|
|
if (!SETTINGS_CHANGED_WHITELIST_SET.has(key)) {
|
|
continue
|
|
}
|
|
const beforeValue = (before as Record<string, unknown>)[key]
|
|
const afterValue = (result as Record<string, unknown>)[key]
|
|
if (beforeValue === afterValue) {
|
|
continue
|
|
}
|
|
if (typeof afterValue !== 'boolean') {
|
|
// No non-bool whitelist entries today; skip rather than guess.
|
|
continue
|
|
}
|
|
track('settings_changed', {
|
|
setting_key: key as SettingsChangedKey,
|
|
value_kind: 'bool'
|
|
})
|
|
}
|
|
|
|
return result
|
|
})
|
|
|
|
ipcMain.handle('settings:listFonts', () => {
|
|
return listSystemFontFamilies()
|
|
})
|
|
|
|
ipcMain.handle('settings:previewGhosttyImport', () => {
|
|
return previewGhosttyImport(store)
|
|
})
|
|
|
|
ipcMain.handle('settings:previewWarpThemeImport', (event, args?: unknown) => {
|
|
const source = args === undefined ? { kind: 'auto' } : args
|
|
return previewWarpThemeImport(store, source, event.sender)
|
|
})
|
|
|
|
ipcMain.handle('cache:getGitHub', () => {
|
|
return store.getGitHubCache()
|
|
})
|
|
|
|
ipcMain.handle('cache:setGitHub', (_event, args: { cache: PersistedState['githubCache'] }) => {
|
|
store.setGitHubCache(args.cache)
|
|
})
|
|
}
|