@narumitw/pi-caffeinate 0.35.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,7 +59,8 @@ If no supported inhibitor is available, the extension stays loaded and reports t
59
59
  /caffeinate
60
60
  ```
61
61
 
62
- Opens keep-awake controls. In non-interactive sessions, it prints command usage and status.
62
+ Opens standard keep-awake controls in TUI or RPC mode. Print and JSON modes reject the interactive
63
+ menu observably; use the direct `status`, `sleep`, `display`, `stop`, or `help` routes instead.
63
64
 
64
65
  ```text
65
66
  /caffeinate display
@@ -83,7 +84,7 @@ Shows whether an inhibitor is active, unavailable, disabled, or idle. The status
83
84
  /caffeinate mode
84
85
  ```
85
86
 
86
- Opens an interactive selector for the keep-awake mode.
87
+ Opens the standard keep-awake mode selector in TUI or RPC mode. Escape closes the selector.
87
88
 
88
89
  ```text
89
90
  /caffeinate stop
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-caffeinate",
3
- "version": "0.35.0",
3
+ "version": "0.39.0",
4
4
  "description": "Pi extension that keeps the computer awake while the agent is running.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,14 +29,17 @@
29
29
  "typecheck": "tsc --noEmit"
30
30
  },
31
31
  "devDependencies": {
32
- "@biomejs/biome": "2.5.5",
33
- "@earendil-works/pi-coding-agent": "0.82.1",
34
- "@types/node": "26.1.1",
32
+ "@biomejs/biome": "2.5.6",
33
+ "@earendil-works/pi-coding-agent": "0.83.0",
34
+ "@types/node": "26.1.2",
35
35
  "typescript": "7.0.2"
36
36
  },
37
37
  "repository": {
38
38
  "type": "git",
39
39
  "url": "https://github.com/narumiruna/pi-extensions",
40
40
  "directory": "extensions/pi-caffeinate"
41
+ },
42
+ "dependencies": {
43
+ "@narumitw/pi-tui-kit": "<1"
41
44
  }
42
45
  }
package/src/caffeinate.ts CHANGED
@@ -5,6 +5,7 @@ import type {
5
5
  ExtensionCommandContext,
6
6
  ExtensionContext,
7
7
  } from "@earendil-works/pi-coding-agent";
8
+ import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
8
9
  import { startInhibitorProcess, stopInhibitorProcess } from "./inhibitor-process.js";
9
10
  import { formatMode, getInhibitorCommand, type InhibitorCommand } from "./inhibitors.js";
10
11
  import {
@@ -56,6 +57,7 @@ interface CaffeinateState {
56
57
  settingsNotice?: string;
57
58
  iconWarningShown: boolean;
58
59
  sessionGeneration: number;
60
+ menuController: AbortController;
59
61
  }
60
62
 
61
63
  const state: CaffeinateState = {
@@ -67,11 +69,13 @@ const state: CaffeinateState = {
67
69
  settingsLoaded: false,
68
70
  iconWarningShown: false,
69
71
  sessionGeneration: 0,
72
+ menuController: new AbortController(),
70
73
  };
71
74
 
72
75
  export default function caffeinate(pi: ExtensionAPI) {
73
76
  pi.on("session_start", async (_event, ctx) => {
74
77
  const generation = ++state.sessionGeneration;
78
+ replaceMenuController("Caffeinate session replaced");
75
79
  state.iconWarningShown = false;
76
80
  state.settingsNotice = undefined;
77
81
  warnDeprecatedIcon(ctx);
@@ -98,6 +102,7 @@ export default function caffeinate(pi: ExtensionAPI) {
98
102
 
99
103
  pi.on("session_shutdown", async (_event, ctx) => {
100
104
  state.sessionGeneration += 1;
105
+ state.menuController.abort(new DOMException("Caffeinate session shut down", "AbortError"));
101
106
  state.activeTurns = 0;
102
107
  stopInhibitor(ctx, "session shutdown", { notify: false });
103
108
  ctx.ui.setStatus(STATUS_KEY, undefined);
@@ -151,55 +156,81 @@ async function handleCaffeinateCommand(args: string, ctx: CommandContext, genera
151
156
  }
152
157
 
153
158
  async function showMenu(ctx: CommandContext, generation: number) {
154
- if (!ctx.hasUI) {
155
- ctx.ui.notify(`${buildCommandGuide()}\n\n${describeState()}`, statusLevel());
156
- updateStatus(ctx);
157
- return;
158
- }
159
-
160
- const choice = await ctx.ui.select("pi-caffeinate controls", Object.values(MENU_OPTIONS));
161
- if (generation !== state.sessionGeneration) return;
162
- switch (choice) {
163
- case MENU_OPTIONS.status:
164
- showStatus(ctx);
165
- return;
166
- case MENU_OPTIONS.sleep:
167
- await setMode(ctx, "sleep", generation);
168
- return;
169
- case MENU_OPTIONS.display:
170
- await setMode(ctx, "display", generation);
171
- return;
172
- case MENU_OPTIONS.stop:
173
- stopCaffeinate(ctx, "manual stop");
174
- return;
175
- case MENU_OPTIONS.help:
176
- ctx.ui.notify(buildCommandGuide(), "info");
177
- return;
178
- }
159
+ await runCaffeinateMenu(ctx, generation, "main");
179
160
  }
180
161
 
181
162
  async function showModeSelector(ctx: CommandContext, generation: number) {
163
+ await runCaffeinateMenu(ctx, generation, "mode");
164
+ }
165
+
166
+ async function runCaffeinateMenu(ctx: CommandContext, generation: number, start: "main" | "mode") {
182
167
  if (!ctx.hasUI) {
183
- ctx.ui.notify(
184
- `Mode selection needs an interactive UI. Run /caffeinate sleep or /caffeinate display.\n\n${describeState()}`,
185
- statusLevel(),
168
+ throw new Error(
169
+ start === "mode"
170
+ ? "Mode selection requires TUI or RPC mode. Use /caffeinate sleep or /caffeinate display."
171
+ : `The pi-caffeinate menu requires TUI or RPC mode.\n\n${buildCommandGuide()}\n\n${describeState()}`,
186
172
  );
187
- updateStatus(ctx);
188
- return;
189
173
  }
174
+ type Screen = "main" | "mode";
175
+ type Action = "display" | "sleep" | "status" | "stop" | "help";
176
+ const menu = defineMenu<undefined, Screen, Action>({
177
+ start,
178
+ screens: {
179
+ main: () => ({
180
+ kind: "actions",
181
+ title: "pi-caffeinate controls",
182
+ lines: describeState().split("\n"),
183
+ items: Object.entries(MENU_OPTIONS).map(([id, label]) => ({
184
+ id,
185
+ label,
186
+ action: id as Action,
187
+ })),
188
+ hint: "close",
189
+ }),
190
+ mode: () => ({
191
+ kind: "actions",
192
+ title: `pi-caffeinate mode (current: ${formatMode(state.mode)})`,
193
+ items: Object.entries(MODE_OPTIONS).map(([id, label]) => ({
194
+ id,
195
+ label,
196
+ action: id as Action,
197
+ })),
198
+ hint: "close",
199
+ }),
200
+ },
201
+ actions: {
202
+ display: async () => {
203
+ await setMode(ctx, "display", generation);
204
+ return { kind: "close" };
205
+ },
206
+ sleep: async () => {
207
+ await setMode(ctx, "sleep", generation);
208
+ return { kind: "close" };
209
+ },
210
+ status: async () => {
211
+ showStatus(ctx);
212
+ return { kind: "close" };
213
+ },
214
+ stop: async () => {
215
+ stopCaffeinate(ctx, "manual stop");
216
+ return { kind: "close" };
217
+ },
218
+ help: async () => {
219
+ ctx.ui.notify(buildCommandGuide(), "info");
220
+ return { kind: "close" };
221
+ },
222
+ },
223
+ });
224
+ await runMenu(ctx, menu, {
225
+ getState: () => undefined,
226
+ signal: state.menuController.signal,
227
+ isCurrent: () => generation === state.sessionGeneration,
228
+ });
229
+ }
190
230
 
191
- const choice = await ctx.ui.select(
192
- `pi-caffeinate mode (current: ${formatMode(state.mode)})`,
193
- Object.values(MODE_OPTIONS),
194
- );
195
- if (generation !== state.sessionGeneration) return;
196
- if (choice === MODE_OPTIONS.sleep) {
197
- await setMode(ctx, "sleep", generation);
198
- return;
199
- }
200
- if (choice === MODE_OPTIONS.display) {
201
- await setMode(ctx, "display", generation);
202
- }
231
+ function replaceMenuController(reason: string) {
232
+ state.menuController.abort(new DOMException(reason, "AbortError"));
233
+ state.menuController = new AbortController();
203
234
  }
204
235
 
205
236
  let modeOperationQueue = Promise.resolve();