Skip to content

Commit 848f4e9

Browse files
committed
Add a setting to prevent sleep while the agent is working
1 parent 9c61e31 commit 848f4e9

9 files changed

Lines changed: 164 additions & 15 deletions

File tree

apps/twig/src/main/di/container.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { FsService } from "../services/fs/service.js";
1515
import { GitService } from "../services/git/service.js";
1616
import { OAuthService } from "../services/oauth/service.js";
1717
import { ShellService } from "../services/shell/service.js";
18+
import { SleepService } from "../services/sleep/service.js";
1819
import { TaskLinkService } from "../services/task-link/service.js";
1920
import { UIService } from "../services/ui/service.js";
2021
import { UpdatesService } from "../services/updates/service.js";
@@ -39,6 +40,7 @@ container.bind(MAIN_TOKENS.FoldersService).to(FoldersService);
3940
container.bind(MAIN_TOKENS.FsService).to(FsService);
4041
container.bind(MAIN_TOKENS.GitService).to(GitService);
4142
container.bind(MAIN_TOKENS.OAuthService).to(OAuthService);
43+
container.bind(MAIN_TOKENS.SleepService).to(SleepService);
4244
container.bind(MAIN_TOKENS.ShellService).to(ShellService);
4345
container.bind(MAIN_TOKENS.UIService).to(UIService);
4446
container.bind(MAIN_TOKENS.UpdatesService).to(UpdatesService);

apps/twig/src/main/di/tokens.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const MAIN_TOKENS = Object.freeze({
2020
GitService: Symbol.for("Main.GitService"),
2121
DeepLinkService: Symbol.for("Main.DeepLinkService"),
2222
OAuthService: Symbol.for("Main.OAuthService"),
23+
SleepService: Symbol.for("Main.SleepService"),
2324
ShellService: Symbol.for("Main.ShellService"),
2425
UIService: Symbol.for("Main.UIService"),
2526
UpdatesService: Symbol.for("Main.UpdatesService"),

apps/twig/src/main/services/agent/service.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ import { app } from "electron";
2222
import { injectable, preDestroy } from "inversify";
2323
import type { ExecutionMode } from "@/shared/types.js";
2424
import type { AcpMessage } from "../../../shared/types/session-events.js";
25+
import { container } from "../../di/container.js";
26+
import { MAIN_TOKENS } from "../../di/tokens.js";
2527
import { logger } from "../../lib/logger.js";
2628
import { TypedEventEmitter } from "../../lib/typed-event-emitter.js";
29+
import type { SleepService } from "../sleep/service.js";
2730
import {
2831
AgentServiceEvent,
2932
type AgentServiceEvents,
@@ -215,6 +218,10 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
215218
private currentToken: string | null = null;
216219
private pendingPermissions = new Map<string, PendingPermission>();
217220

221+
private get sleepService() {
222+
return container.get<SleepService>(MAIN_TOKENS.SleepService);
223+
}
224+
218225
public updateToken(newToken: string): void {
219226
this.currentToken = newToken;
220227

@@ -595,6 +602,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
595602

596603
session.lastActivityAt = Date.now();
597604
session.promptPending = true;
605+
this.sleepService.acquire(sessionId);
598606

599607
try {
600608
const result = await session.clientSideConnection.prompt({
@@ -621,6 +629,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
621629
throw err;
622630
} finally {
623631
session.promptPending = false;
632+
this.sleepService.release(sessionId);
624633
}
625634
}
626635

@@ -867,6 +876,7 @@ For git operations while detached:
867876
private async cleanupSession(taskRunId: string): Promise<void> {
868877
const session = this.sessions.get(taskRunId);
869878
if (session) {
879+
this.sleepService.release(taskRunId);
870880
try {
871881
await session.agent.cleanup();
872882
} catch {
@@ -936,21 +946,28 @@ For git operations while detached:
936946
// The claude.ts adapter only calls requestPermission when user input is needed.
937947
// (It handles auto-approve internally for acceptEdits/bypassPermissions modes)
938948
if (toolCallId) {
939-
return new Promise((resolve, reject) => {
940-
const key = `${taskRunId}:${toolCallId}`;
941-
service.pendingPermissions.set(key, {
942-
resolve,
943-
reject,
944-
sessionId: taskRunId,
945-
toolCallId,
946-
});
947-
948-
log.info("Emitting permission request to renderer", {
949-
sessionId: taskRunId,
950-
toolCallId,
951-
});
952-
service.emit(AgentServiceEvent.PermissionRequest, params);
953-
});
949+
service.sleepService.release(taskRunId);
950+
try {
951+
return await new Promise<RequestPermissionResponse>(
952+
(resolve, reject) => {
953+
const key = `${taskRunId}:${toolCallId}`;
954+
service.pendingPermissions.set(key, {
955+
resolve,
956+
reject,
957+
sessionId: taskRunId,
958+
toolCallId,
959+
});
960+
961+
log.info("Emitting permission request to renderer", {
962+
sessionId: taskRunId,
963+
toolCallId,
964+
});
965+
service.emit(AgentServiceEvent.PermissionRequest, params);
966+
},
967+
);
968+
} finally {
969+
service.sleepService.acquire(taskRunId);
970+
}
954971
}
955972

956973
// Fallback: no toolCallId means we can't track the response, auto-approve

apps/twig/src/main/services/settingsStore.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Store from "electron-store";
77

88
interface SettingsSchema {
99
worktreeLocation: string;
10+
preventSleepWhileRunning: boolean;
1011
}
1112

1213
function getDefaultWorktreeLocation(): string {
@@ -50,6 +51,10 @@ const schema = {
5051
type: "string" as const,
5152
default: getDefaultWorktreeLocation(),
5253
},
54+
preventSleepWhileRunning: {
55+
type: "boolean" as const,
56+
default: false,
57+
},
5358
};
5459

5560
export const settingsStore = new Store<SettingsSchema>({
@@ -58,6 +63,7 @@ export const settingsStore = new Store<SettingsSchema>({
5863
cwd: app.getPath("userData"),
5964
defaults: {
6065
worktreeLocation: getDefaultWorktreeLocation(),
66+
preventSleepWhileRunning: false,
6167
},
6268
});
6369

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { powerSaveBlocker } from "electron";
2+
import { injectable, preDestroy } from "inversify";
3+
import { logger } from "../../lib/logger.js";
4+
import { settingsStore } from "../settingsStore.js";
5+
6+
const log = logger.scope("sleep");
7+
8+
@injectable()
9+
export class SleepService {
10+
private enabled: boolean;
11+
private blockerId: number | null = null;
12+
private activeActivities = new Set<string>();
13+
14+
constructor() {
15+
this.enabled = settingsStore.get("preventSleepWhileRunning", false);
16+
}
17+
18+
setEnabled(enabled: boolean): void {
19+
log.info("setEnabled", { enabled });
20+
this.enabled = enabled;
21+
settingsStore.set("preventSleepWhileRunning", enabled);
22+
this.updateBlocker();
23+
}
24+
25+
getEnabled(): boolean {
26+
return this.enabled;
27+
}
28+
29+
acquire(activityId: string): void {
30+
this.activeActivities.add(activityId);
31+
this.updateBlocker();
32+
}
33+
34+
release(activityId: string): void {
35+
this.activeActivities.delete(activityId);
36+
this.updateBlocker();
37+
}
38+
39+
@preDestroy()
40+
cleanup(): void {
41+
this.stopBlocker();
42+
}
43+
44+
private updateBlocker(): void {
45+
if (this.enabled && this.activeActivities.size > 0) {
46+
this.startBlocker();
47+
} else {
48+
this.stopBlocker();
49+
}
50+
}
51+
52+
private startBlocker(): void {
53+
if (this.blockerId !== null) return;
54+
this.blockerId = powerSaveBlocker.start("prevent-app-suspension");
55+
log.info("Started power save blocker", { blockerId: this.blockerId });
56+
}
57+
58+
private stopBlocker(): void {
59+
if (this.blockerId === null) return;
60+
log.info("Stopping power save blocker", { blockerId: this.blockerId });
61+
powerSaveBlocker.stop(this.blockerId);
62+
this.blockerId = null;
63+
}
64+
}

apps/twig/src/main/trpc/router.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { oauthRouter } from "./routers/oauth.js";
1616
import { osRouter } from "./routers/os.js";
1717
import { secureStoreRouter } from "./routers/secure-store.js";
1818
import { shellRouter } from "./routers/shell.js";
19+
import { sleepRouter } from "./routers/sleep.js";
1920
import { uiRouter } from "./routers/ui.js";
2021
import { updatesRouter } from "./routers/updates.js";
2122
import { workspaceRouter } from "./routers/workspace.js";
@@ -37,6 +38,7 @@ export const trpcRouter = router({
3738
oauth: oauthRouter,
3839
logs: logsRouter,
3940
os: osRouter,
41+
sleep: sleepRouter,
4042
secureStore: secureStoreRouter,
4143
shell: shellRouter,
4244
ui: uiRouter,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { z } from "zod";
2+
import { container } from "../../di/container.js";
3+
import { MAIN_TOKENS } from "../../di/tokens.js";
4+
import type { SleepService } from "../../services/sleep/service.js";
5+
import { publicProcedure, router } from "../trpc.js";
6+
7+
const getService = () => container.get<SleepService>(MAIN_TOKENS.SleepService);
8+
9+
export const sleepRouter = router({
10+
getEnabled: publicProcedure
11+
.output(z.boolean())
12+
.query(() => getService().getEnabled()),
13+
14+
setEnabled: publicProcedure
15+
.input(z.object({ enabled: z.boolean() }))
16+
.mutation(({ input }) => {
17+
getService().setEnabled(input.enabled);
18+
}),
19+
});

apps/twig/src/renderer/features/settings/components/SettingsView.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,13 @@ export function SettingsView() {
8080
autoConvertLongText,
8181
sendMessagesWith,
8282
allowBypassPermissions,
83+
preventSleepWhileRunning,
8384
setCursorGlow,
8485
setDesktopNotifications,
8586
setAutoConvertLongText,
8687
setSendMessagesWith,
8788
setAllowBypassPermissions,
89+
setPreventSleepWhileRunning,
8890
} = useSettingsStore();
8991
const terminalFontFamily = useTerminalSettingsStore(
9092
(state) => state.terminalFontFamily,
@@ -115,6 +117,8 @@ export function SettingsView() {
115117

116118
const { data: appVersion } = trpcReact.os.getAppVersion.useQuery();
117119

120+
const preventSleepMutation = trpcReact.sleep.setEnabled.useMutation();
121+
118122
const [localWorktreeLocation, setLocalWorktreeLocation] =
119123
useState<string>("");
120124
const [checkingForUpdates, setCheckingForUpdates] = useState(false);
@@ -293,6 +297,19 @@ export function SettingsView() {
293297
[sendMessagesWith, setSendMessagesWith],
294298
);
295299

300+
const handlePreventSleepChange = useCallback(
301+
(checked: boolean) => {
302+
track(ANALYTICS_EVENTS.SETTING_CHANGED, {
303+
setting_name: "prevent_sleep_while_running",
304+
new_value: checked,
305+
old_value: !checked,
306+
});
307+
setPreventSleepWhileRunning(checked);
308+
preventSleepMutation.mutate({ enabled: checked });
309+
},
310+
[setPreventSleepWhileRunning, preventSleepMutation],
311+
);
312+
296313
const handleBypassPermissionsChange = useCallback(
297314
(checked: boolean) => {
298315
if (checked) {
@@ -598,6 +615,22 @@ export function SettingsView() {
598615
<Heading size="3">Task Execution</Heading>
599616
<Card>
600617
<Flex direction="column" gap="4">
618+
<Flex align="center" justify="between" gap="4">
619+
<Flex direction="column" gap="1">
620+
<Text size="1" weight="medium">
621+
Prevent sleep while running
622+
</Text>
623+
<Text size="1" color="gray">
624+
Keep your computer awake while the agent is running a
625+
task.
626+
</Text>
627+
</Flex>
628+
<Switch
629+
checked={preventSleepWhileRunning}
630+
onCheckedChange={handlePreventSleepChange}
631+
size="1"
632+
/>
633+
</Flex>
601634
<Flex align="start" justify="between" gap="4">
602635
<Flex direction="column" gap="1">
603636
<Flex align="center" gap="2">

apps/twig/src/renderer/features/settings/stores/settingsStore.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface SettingsStore {
1616
autoConvertLongText: boolean;
1717
sendMessagesWith: SendMessagesWith;
1818
allowBypassPermissions: boolean;
19+
preventSleepWhileRunning: boolean;
1920

2021
setDefaultRunMode: (mode: DefaultRunMode) => void;
2122
setLastUsedRunMode: (mode: "local" | "cloud") => void;
@@ -26,6 +27,7 @@ interface SettingsStore {
2627
setAutoConvertLongText: (enabled: boolean) => void;
2728
setSendMessagesWith: (mode: SendMessagesWith) => void;
2829
setAllowBypassPermissions: (enabled: boolean) => void;
30+
setPreventSleepWhileRunning: (enabled: boolean) => void;
2931
}
3032

3133
export const useSettingsStore = create<SettingsStore>()(
@@ -40,6 +42,7 @@ export const useSettingsStore = create<SettingsStore>()(
4042
autoConvertLongText: true,
4143
sendMessagesWith: "enter",
4244
allowBypassPermissions: false,
45+
preventSleepWhileRunning: false,
4346

4447
setDefaultRunMode: (mode) => set({ defaultRunMode: mode }),
4548
setLastUsedRunMode: (mode) => set({ lastUsedRunMode: mode }),
@@ -54,6 +57,8 @@ export const useSettingsStore = create<SettingsStore>()(
5457
setSendMessagesWith: (mode) => set({ sendMessagesWith: mode }),
5558
setAllowBypassPermissions: (enabled) =>
5659
set({ allowBypassPermissions: enabled }),
60+
setPreventSleepWhileRunning: (enabled) =>
61+
set({ preventSleepWhileRunning: enabled }),
5762
}),
5863
{
5964
name: "settings-storage",

0 commit comments

Comments
 (0)