Compare commits

...
Author SHA1 Message Date
Brendonovich a53d9de58f feat(desktop): keep running session tabs awake 2026-09-04 14:32:09 +00:00
17 changed files with 518 additions and 1 deletions
+2
View File
@@ -16,6 +16,7 @@ import { LanguageProvider, UiI18nBridge, type Locale } from "@/runtime/i18n/lang
import { ServerConnection, ServersProvider } from "@/runtime/server/registry"
import { SettingsProvider } from "@/settings/model"
import { TabsProvider } from "@/shell/tabs/tabs"
import { KeepAwake } from "@/shell/keep-awake"
import { WslServersProvider } from "@/servers/wsl/context"
import { ErrorPage } from "@/shell/errors/error"
import { AppRoutes, File, preloadRoute } from "@/shell/routes/routes"
@@ -106,6 +107,7 @@ export function AppInterface(props: {
const Root = (rootProps: ParentProps) => (
<TabsProvider>
<GlobalProvider>
<KeepAwake />
<BodyTypography />
<CommandProvider>
<DesktopCommands />
+4
View File
@@ -1073,6 +1073,10 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.description":
"Show edit, write, and patch tool parts expanded by default in the timeline",
"settings.general.row.pinchZoom.title": "Pinch to zoom",
"settings.general.row.keepAwake.title": "Keep computer awake",
"settings.general.row.keepAwake.description":
"Prevent sleep while open tabs have running sessions, even in the background. The screen can still turn off.",
"settings.general.row.keepAwake.error": "Could not access the keep-awake setting. Try again.",
"settings.general.row.pinchZoom.description": "Allow trackpad pinch and Ctrl-scroll gestures to zoom",
"settings.general.row.wayland.title": "Use native Wayland",
@@ -98,6 +98,15 @@ type PlatformBase = {
/** Allow native pinch/Ctrl-scroll zoom gestures (desktop only) */
setPinchZoomEnabled?(enabled: boolean): Promise<void> | void
/** Get the application-wide preference for keeping running session tabs awake. */
getKeepAwakeEnabled?(this: void): Promise<boolean>
/** Opt in to preventing idle system sleep while any window has a running session tab. */
setKeepAwakeEnabled?(this: void, enabled: boolean): Promise<void>
/** Report whether this window has running session tabs, independent of the preference. */
setKeepAwakeActive?(this: void, active: boolean): Promise<void>
/** Run a desktop-only menu action from the app chrome */
runDesktopMenuAction?(action: DesktopMenuAction): Promise<void> | void
@@ -27,6 +27,7 @@ import {
} from "./controllers"
import "@/settings/settings.css"
import { ServerConnection } from "@/runtime/server/registry"
import { KeepAwakeSetting } from "./keep-awake"
const schemeOptions: ("system" | "light" | "dark")[] = ["system", "light", "dark"]
const fontSettings = {
@@ -332,6 +333,10 @@ export const SettingsGeneral: Component<{
<TerminalPlacementSetting />
<FollowUpBehaviorSetting />
<Show when={desktop()}>
<KeepAwakeSetting />
</Show>
<SettingsRow
title={language.t("session.review.wrapLines")}
description={language.t("settings.general.row.mobileDiffWrap.description")}
@@ -0,0 +1,60 @@
import { createResource } from "solid-js"
import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { Switch } from "@opencode-ai/ui/switch"
import { useLanguage } from "@/runtime/i18n/language"
import { usePlatform } from "@/runtime/platform/platform"
import { SettingsRow } from "@/settings/row"
import { showToast } from "@/shell/notifications/toast"
export function KeepAwakeSetting() {
const platform = usePlatform()
const language = useLanguage()
const get = platform.getKeepAwakeEnabled
const set = platform.setKeepAwakeEnabled
if (!get || !set) return null
const [state, setState] = createStore({ saving: false })
const failed = () => {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: language.t("settings.general.row.keepAwake.error"),
})
}
const [enabled, { mutate, refetch }] = createResource(() => get().catch(failed))
makeEventListener(window, "focus", () => {
if (!state.saving) void refetch()
})
const change = (checked: boolean) => {
const previous = enabled.latest
setState("saving", true)
mutate(checked)
void set(checked)
.catch(() => {
mutate(previous)
failed()
})
.finally(() => setState("saving", false))
}
return (
<SettingsRow
title={language.t("settings.general.row.keepAwake.title")}
description={language.t("settings.general.row.keepAwake.description")}
>
<div data-action="settings-keep-awake">
<Switch
aria-label={language.t("settings.general.row.keepAwake.title")}
checked={enabled.latest === true}
disabled={enabled.loading || enabled.latest === undefined || state.saving}
onChange={change}
hideLabel
>
{language.t("settings.general.row.keepAwake.title")}
</Switch>
</div>
</SettingsRow>
)
}
+43
View File
@@ -0,0 +1,43 @@
import { createEffect, createMemo, onCleanup, type Accessor } from "solid-js"
import { usePlatform } from "@/runtime/platform/platform"
import { ServerConnection } from "@/runtime/server/registry"
import { useGlobal } from "@/runtime/server/runtime"
import { useTabs, type Tab } from "@/shell/tabs/tabs"
export function createOpenTabsKeepAwake(input: {
tabs: Accessor<readonly Tab[]>
running: (server: ServerConnection.Key, sessionID: string) => boolean
setActive: (active: boolean) => Promise<void>
}) {
const active = createMemo(() =>
input
.tabs()
.some(
(tab) =>
tab.type === "session" &&
(input.running(tab.server, tab.sessionId) ||
(!!tab.routeSessionId && input.running(tab.server, tab.routeSessionId))),
),
)
const report = (value: boolean) => {
void input.setActive(value).catch((error) => console.error("Failed to update desktop keep-awake activity", error))
}
createEffect(() => report(active()))
onCleanup(() => report(false))
}
export function KeepAwake() {
const platform = usePlatform()
if (!platform.setKeepAwakeActive) return null
const tabs = useTabs()
const global = useGlobal()
createOpenTabsKeepAwake({
tabs: () => tabs.store,
running: (server, sessionID) => {
const conn = global.servers.list().find((item) => ServerConnection.key(item) === server)
return !!conn && global.ensureServerCtx(conn).data.session.status(sessionID) === "running"
},
setActive: platform.setKeepAwakeActive,
})
return null
}
@@ -0,0 +1,90 @@
import { describe, expect, test } from "bun:test"
import { createRoot } from "solid-js"
import { createStore } from "solid-js/store"
import { ServerConnection } from "@/runtime/server/registry"
import type { Tab } from "@/shell/tabs/tabs"
import { createOpenTabsKeepAwake } from "../src/shell/keep-awake"
const local = ServerConnection.key({ type: "http", http: { url: "http://localhost:4096" } })
const remote = ServerConnection.key({ type: "http", http: { url: "https://example.com" } })
const session = (sessionId: string, server = local): Tab => ({ type: "session", server, sessionId })
function setup(tabs: Tab[] = []) {
const calls: boolean[] = []
const owned = createRoot((dispose) => {
const [store, setStore] = createStore({
tabs,
running: {} as Record<string, Record<string, boolean>>,
})
createOpenTabsKeepAwake({
tabs: () => store.tabs,
running: (server, sessionID) => store.running[server]?.[sessionID] ?? false,
setActive: async (active) => {
calls.push(active)
},
})
return { dispose, setStore }
})
return { ...owned, calls }
}
describe("open session tab keep-awake", () => {
test("ignores drafts and running sessions without an open tab", () => {
const state = setup([{ type: "draft", server: local, draftID: "draft", directory: "/project" }])
state.setStore("running", local, { draft: true, closed: true })
expect(state.calls).toEqual([false])
state.dispose()
})
test("counts every tab, not just the first or selected tab", () => {
const state = setup([session("idle"), session("background")])
state.setStore("running", local, { background: true })
expect(state.calls).toEqual([false, true])
state.setStore("tabs", [session("background"), session("idle")])
expect(state.calls).toEqual([false, true])
state.dispose()
expect(state.calls).toEqual([false, true, false])
})
test("closing the last running tab releases without stopping its execution", () => {
const state = setup([session("a"), session("b")])
state.setStore("running", local, { a: true, b: true })
state.setStore("tabs", [session("b")])
expect(state.calls).toEqual([false, true])
state.setStore("tabs", [])
expect(state.calls).toEqual([false, true, false])
state.setStore("tabs", [session("a")])
expect(state.calls).toEqual([false, true, false, true])
state.dispose()
})
test("releases when the last session finishes and follows refreshed activity", () => {
const state = setup([session("a"), session("b")])
state.setStore("running", local, { a: true, b: true })
state.setStore("running", local, "a", false)
expect(state.calls).toEqual([false, true])
state.setStore("running", local, "b", false)
expect(state.calls).toEqual([false, true, false])
state.setStore("running", local, "b", true)
expect(state.calls).toEqual([false, true, false, true])
state.dispose()
})
test("scopes session identity to its server", () => {
const state = setup([session("same", remote)])
state.setStore("running", local, { same: true })
expect(state.calls).toEqual([false])
state.setStore("running", remote, { same: true })
expect(state.calls).toEqual([false, true])
state.dispose()
})
test("counts the child session currently represented by a root tab", () => {
const state = setup([{ type: "session", server: local, sessionId: "root", routeSessionId: "child" }])
state.setStore("running", local, { child: true })
expect(state.calls).toEqual([false, true])
state.setStore("tabs", [session("root")])
expect(state.calls).toEqual([false, true, false])
state.dispose()
})
})
@@ -10,6 +10,7 @@ import { DesktopLogging, scoped } from "../native/logging"
import { createMenu, sendMenuCommand } from "../native/menu"
import { setNativeTranslations } from "../native/translations"
import { IpcPortHandoff } from "../ipc-transport"
import { KeepAwake } from "../keep-awake"
import { ApplicationLifecycle } from "../lifecycle"
import { finishFirstLaunchOnboarding, isFirstLaunchOnboardingPending } from "../lifecycle/onboarding"
import { BackgroundService } from "../service/background-service"
@@ -27,6 +28,7 @@ export const appHandlers = AppRpcs.toLayer(
const desktopCli = yield* DesktopCli.Service
const updater = yield* Updater.Service
const logging = yield* DesktopLogging.Service
const awake = yield* KeepAwake.Service
const runFork = Effect.runForkWith(yield* Effect.context())
return AppRpcs.of({
AppAwaitInitialization: () => background.connection,
@@ -34,6 +36,17 @@ export const appHandlers = AppRpcs.toLayer(
AppConsumeInitialDeepLinks: () => Effect.sync(lifecycle.consumeInitialDeepLinks),
AppGetDefaultServerUrl: () => Effect.sync(getDefaultServerUrl),
AppSetDefaultServerUrl: ({ url }) => Effect.sync(() => setDefaultServerUrl(url)),
AppGetKeepAwakeEnabled: () => Effect.sync(awake.getEnabled),
AppSetKeepAwakeEnabled: ({ enabled }) => Effect.sync(() => awake.setEnabled(enabled)),
AppSetKeepAwakeActive: ({ active }, context) =>
Effect.sync(() => {
const contents = sender(handoff, context)
const win = BrowserWindow.fromWebContents(contents)
if (!win || win.isDestroyed() || win.webContents !== contents) {
throw new Error("Invalid keep-awake sender")
}
awake.setActive(contents, active)
}),
AppIsFirstLaunchOnboardingPending: isFirstLaunchOnboardingPending,
AppFinishFirstLaunchOnboarding: ({ createDefaultProject }) =>
finishFirstLaunchOnboarding(createDefaultProject).pipe(Effect.orDie),
+2 -1
View File
@@ -15,6 +15,7 @@ import { updaterHandlers } from "./ipc-handlers/updater"
import { windowHandlers } from "./ipc-handlers/window"
import { wslHandlers } from "./ipc-handlers/wsl"
import { IpcPortHandoff, IpcServerProtocolLive } from "./ipc-transport"
import { KeepAwakeLive } from "./keep-awake/live"
import { ApplicationLifecycle } from "./lifecycle"
import { showCliInstaller } from "./native/install-cli"
import { createMenu, sendMenuCommand } from "./native/menu"
@@ -24,7 +25,7 @@ import { Updater } from "./updater"
import { getLastFocusedWindow } from "./windows"
import { Wsl } from "./wsl/start"
const services = Layer.mergeAll(DesktopFiles.layer, DesktopStorage.layer, Wsl.layer)
const services = Layer.mergeAll(DesktopFiles.layer, DesktopStorage.layer, Wsl.layer, KeepAwakeLive)
const handlers = Layer.mergeAll(
appHandlers,
storageHandlers,
@@ -0,0 +1,175 @@
import { afterEach, describe, expect, test } from "bun:test"
import { EventEmitter } from "node:events"
import { Layer, ManagedRuntime } from "effect"
import { KeepAwake } from "./index"
const disposers: Array<() => Promise<void>> = []
afterEach(async () => {
await Promise.all(disposers.splice(0).map((dispose) => dispose()))
})
async function setup(initial?: unknown) {
const starts: string[] = []
const stops: number[] = []
const writes: boolean[] = []
const runtime = ManagedRuntime.make(
Layer.effect(
KeepAwake.Service,
KeepAwake.make({
power: {
start: (type) => starts.push(type) - 1,
stop: (id) => {
stops.push(id)
return true
},
},
persistence: { read: () => initial, write: (value) => void writes.push(value) },
}),
),
)
const dispose = () => runtime.dispose()
disposers.push(dispose)
return { awake: await runtime.runPromise(KeepAwake.Service), starts, stops, writes, dispose }
}
class Sender extends EventEmitter {
destroyed = false
constructor(readonly id: number) {
super()
}
isDestroyed() {
return this.destroyed
}
}
describe("desktop keep awake", () => {
test.each([undefined, false, null, "true", 1])("defaults off without an explicit opt-in: %p", async (initial) => {
const { awake, starts, writes } = await setup(initial)
awake.setActive(new Sender(1), true)
expect(awake.getEnabled()).toBe(false)
expect(starts).toEqual([])
expect(writes).toEqual([])
})
test("applies a persisted opt-in only after an open tab reports activity", async () => {
const { awake, starts, writes } = await setup(true)
expect(awake.getEnabled()).toBe(true)
expect(starts).toEqual([])
awake.setActive(new Sender(1), true)
expect(starts).toEqual(["prevent-app-suspension"])
expect(writes).toEqual([])
})
test("enabling uses reports received while disabled, and disabling preserves them", async () => {
const { awake, starts, stops, writes } = await setup()
awake.setActive(new Sender(1), true)
awake.setEnabled(true)
expect(starts).toEqual(["prevent-app-suspension"])
awake.setEnabled(false)
expect(stops).toEqual([0])
awake.setEnabled(true)
expect(starts).toEqual(["prevent-app-suspension", "prevent-app-suspension"])
expect(writes).toEqual([true, false, true])
})
test("releases when the last running tab closes, without waiting for the session", async () => {
const { awake, starts, stops } = await setup(true)
const sender = new Sender(1)
awake.setActive(sender, true)
awake.setActive(sender, false)
expect(stops).toEqual([0])
expect(sender.eventNames()).toEqual([])
awake.setActive(sender, true)
expect(starts).toHaveLength(2)
})
test("ORs activity across windows, independent of focus or visibility", async () => {
const { awake, starts, stops } = await setup(true)
const first = new Sender(1)
const second = new Sender(2)
awake.setActive(first, true)
awake.setActive(second, false)
awake.setActive(second, true)
awake.setActive(first, false)
expect(starts).toHaveLength(1)
expect(stops).toEqual([])
second.emit("destroyed")
expect(stops).toEqual([0])
})
test("repeated reports and settings do not duplicate blockers or listeners", async () => {
const { awake, starts, stops, writes } = await setup()
const sender = new Sender(1)
for (let i = 0; i < 100; i++) {
awake.setEnabled(true)
awake.setActive(sender, true)
}
expect(starts).toHaveLength(1)
expect(writes).toEqual([true])
expect(sender.listenerCount("destroyed")).toBe(1)
expect(sender.listenerCount("render-process-gone")).toBe(1)
expect(sender.listenerCount("did-start-navigation")).toBe(1)
awake.setActive(sender, false)
awake.setActive(sender, false)
expect(stops).toEqual([0])
expect(sender.eventNames()).toEqual([])
})
test.each(["destroyed", "render-process-gone"])("clears activity on %s", async (event) => {
const { awake, stops } = await setup(true)
const sender = new Sender(1)
awake.setActive(sender, true)
sender.emit(event)
expect(stops).toEqual([0])
expect(sender.eventNames()).toEqual([])
})
test("clears on main-frame reload/navigation, not app routes or subframes", async () => {
const { awake, stops, starts } = await setup(true)
const sender = new Sender(1)
awake.setActive(sender, true)
sender.emit("did-start-navigation", {}, "app://index/session", true, true)
sender.emit("did-start-navigation", {}, "app://index/frame", false, false)
expect(stops).toEqual([])
sender.emit("did-start-navigation", {}, "app://index", false, true)
expect(stops).toEqual([0])
expect(sender.eventNames()).toEqual([])
awake.setActive(sender, true)
expect(starts).toHaveLength(2)
})
test("forgets disabled activity when its renderer closes", async () => {
const { awake, starts } = await setup()
const sender = new Sender(1)
awake.setActive(sender, true)
sender.emit("destroyed")
awake.setEnabled(true)
expect(starts).toEqual([])
})
test("ignores destroyed renderers", async () => {
const { awake, starts } = await setup(true)
const sender = new Sender(1)
sender.destroyed = true
awake.setActive(sender, true)
expect(starts).toEqual([])
expect(sender.eventNames()).toEqual([])
})
test("scope disposal releases the blocker and all listeners", async () => {
const { awake, starts, stops, dispose } = await setup(true)
const first = new Sender(1)
const second = new Sender(2)
awake.setActive(first, true)
awake.setActive(second, true)
await dispose()
awake.dispose()
awake.setActive(first, true)
awake.setEnabled(false)
awake.setEnabled(true)
expect(starts).toHaveLength(1)
expect(stops).toEqual([0])
expect(first.eventNames()).toEqual([])
expect(second.eventNames()).toEqual([])
})
})
@@ -0,0 +1,73 @@
export * as KeepAwake from "./index"
import type { PowerSaveBlocker, WebContents } from "electron"
import { Context, Effect } from "effect"
type Sender = Pick<WebContents, "id" | "isDestroyed" | "on" | "off">
type Dependencies = {
power: Pick<PowerSaveBlocker, "start" | "stop">
persistence: { read: () => unknown; write: (enabled: boolean) => void }
}
export class Service extends Context.Service<Service, Effect.Success<ReturnType<typeof make>>>()(
"opencode/desktop/KeepAwake",
) {}
export const make = Effect.fn("KeepAwake.make")(function* (dependencies: Dependencies) {
let enabled = dependencies.persistence.read() === true
let blocker: number | undefined
let disposed = false
const senders = new Map<number, () => void>()
const update = () => {
if (enabled && senders.size > 0) {
blocker ??= dependencies.power.start("prevent-app-suspension")
return
}
if (blocker === undefined) return
dependencies.power.stop(blocker)
blocker = undefined
}
const remove = (id: number) => {
senders.get(id)?.()
senders.delete(id)
update()
}
const dispose = () => {
disposed = true
senders.forEach((off) => off())
senders.clear()
update()
}
yield* Effect.addFinalizer(() => Effect.sync(dispose))
return {
getEnabled: () => enabled,
setEnabled(value: boolean) {
if (disposed || enabled === value) return
dependencies.persistence.write(value)
enabled = value
update()
},
setActive(sender: Sender, active: boolean) {
if (disposed) return
if (!active) return remove(sender.id)
if (sender.isDestroyed() || senders.has(sender.id)) return
const clear = () => remove(sender.id)
const navigate = (_event: Electron.Event, _url: string, inPlace: boolean, mainFrame: boolean) => {
// App route changes keep the same renderer and its open tabs.
if (mainFrame && !inPlace) clear()
}
sender.on("destroyed", clear)
sender.on("render-process-gone", clear)
sender.on("did-start-navigation", navigate)
senders.set(sender.id, () => {
sender.off("destroyed", clear)
sender.off("render-process-gone", clear)
sender.off("did-start-navigation", navigate)
})
// Retain reports while disabled so enabling applies to every window immediately.
update()
},
dispose,
}
})
@@ -0,0 +1,22 @@
import { app, powerSaveBlocker } from "electron"
import { Effect, Layer } from "effect"
import { KEEP_AWAKE_ENABLED_KEY } from "../storage/keys"
import { getStore } from "../storage/store"
import { KeepAwake } from "./index"
export const KeepAwakeLive = Layer.effect(
KeepAwake.Service,
Effect.gen(function* () {
const store = getStore()
const awake = yield* KeepAwake.make({
power: powerSaveBlocker,
persistence: {
read: () => store.get(KEEP_AWAKE_ENABLED_KEY),
write: (enabled) => store.set(KEEP_AWAKE_ENABLED_KEY, enabled),
},
})
app.on("will-quit", awake.dispose)
yield* Effect.addFinalizer(() => Effect.sync(() => app.off("will-quit", awake.dispose)))
return awake
}),
)
@@ -3,5 +3,6 @@ export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl"
export const FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY = "firstLaunchOnboardingComplete"
export const WSL_SERVERS_KEY = "wslServers"
export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled"
export const KEEP_AWAKE_ENABLED_KEY = "keepAwakeEnabled"
export const BACKGROUND_COLOR_KEY = "backgroundColor"
export const WINDOW_IDS_KEY = "windowIds"
@@ -28,6 +28,9 @@ export type ElectronAPI = {
consumeInitialDeepLinks(): Promise<string[]>
getDefaultServerUrl(): Promise<string | null>
setDefaultServerUrl(url: string | null): Promise<void>
getKeepAwakeEnabled(): Promise<boolean>
setKeepAwakeEnabled(enabled: boolean): Promise<void>
setKeepAwakeActive(active: boolean): Promise<void>
isFirstLaunchOnboardingPending(): Promise<boolean>
finishFirstLaunchOnboarding(createDefaultProject: boolean): Promise<string | null>
checkAppExists(appName: string): Promise<boolean>
+3
View File
@@ -70,6 +70,9 @@ export const api: ElectronAPI = {
consumeInitialDeepLinks: () => invoke("AppConsumeInitialDeepLinks").then(mutable),
getDefaultServerUrl: () => invoke("AppGetDefaultServerUrl"),
setDefaultServerUrl: (url) => invoke("AppSetDefaultServerUrl", { url }),
getKeepAwakeEnabled: () => invoke("AppGetKeepAwakeEnabled"),
setKeepAwakeEnabled: (enabled) => invoke("AppSetKeepAwakeEnabled", { enabled }),
setKeepAwakeActive: (active) => invoke("AppSetKeepAwakeActive", { active }),
isFirstLaunchOnboardingPending: () => invoke("AppIsFirstLaunchOnboardingPending"),
finishFirstLaunchOnboarding: (createDefaultProject) =>
invoke("AppFinishFirstLaunchOnboarding", { createDefaultProject }),
@@ -34,6 +34,9 @@ export function createDesktopPlatform(
updater,
exportDebugLogs: () => api.exportDebugLogs(),
setForceFocus: (enabled) => api.setForceFocus(enabled),
getKeepAwakeEnabled: () => api.getKeepAwakeEnabled(),
setKeepAwakeEnabled: (enabled) => api.setKeepAwakeEnabled(enabled),
setKeepAwakeActive: (active) => api.setKeepAwakeActive(active),
recordFatalRendererError: (error) => api.recordFatalRendererError(error),
restart: async () => api.relaunch(),
notify: createDesktopNotify(api),
@@ -17,6 +17,13 @@ export const AppGetDefaultServerUrl = Rpc.make("AppGetDefaultServerUrl", {
export const AppSetDefaultServerUrl = Rpc.make("AppSetDefaultServerUrl", {
payload: { url: Schema.NullOr(Schema.String) },
})
export const AppGetKeepAwakeEnabled = Rpc.make("AppGetKeepAwakeEnabled", { success: Schema.Boolean })
export const AppSetKeepAwakeEnabled = Rpc.make("AppSetKeepAwakeEnabled", {
payload: { enabled: Schema.Boolean },
})
export const AppSetKeepAwakeActive = Rpc.make("AppSetKeepAwakeActive", {
payload: { active: Schema.Boolean },
})
export const AppIsFirstLaunchOnboardingPending = Rpc.make("AppIsFirstLaunchOnboardingPending", {
success: Schema.Boolean,
})
@@ -60,6 +67,9 @@ export const AppRpcs = RpcGroup.make(
AppConsumeInitialDeepLinks,
AppGetDefaultServerUrl,
AppSetDefaultServerUrl,
AppGetKeepAwakeEnabled,
AppSetKeepAwakeEnabled,
AppSetKeepAwakeActive,
AppIsFirstLaunchOnboardingPending,
AppFinishFirstLaunchOnboarding,
AppCheckAppExists,