@herbertgao/pi-extensions 2026.9.9 → 2026.9.11

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.
Files changed (170) hide show
  1. package/README.md +1 -0
  2. package/THIRD_PARTY_NOTICES.md +26 -0
  3. package/node_modules/@herbertgao/pi-bark/package.json +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
  5. package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
  6. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -2
  7. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +12 -0
  8. package/node_modules/@herbertgao/pi-subagents/README.md +427 -120
  9. package/node_modules/@herbertgao/pi-subagents/docs/rpc.md +184 -0
  10. package/node_modules/@herbertgao/pi-subagents/docs/workflows.md +466 -0
  11. package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +6 -6
  12. package/node_modules/@herbertgao/pi-subagents/examples/workflows/compose.js +52 -0
  13. package/node_modules/@herbertgao/pi-subagents/examples/workflows/fan-out-audit.js +56 -0
  14. package/node_modules/@herbertgao/pi-subagents/examples/workflows/gated-fix.js +60 -0
  15. package/node_modules/@herbertgao/pi-subagents/examples/workflows/lib/count-child.js +30 -0
  16. package/node_modules/@herbertgao/pi-subagents/examples/workflows/review-panel.js +68 -0
  17. package/node_modules/@herbertgao/pi-subagents/examples/workflows/structured-findings.js +81 -0
  18. package/node_modules/@herbertgao/pi-subagents/package.json +12 -10
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +52 -12
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +837 -146
  21. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +213 -39
  22. package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +73 -14
  23. package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +101 -47
  24. package/node_modules/@herbertgao/pi-subagents/src/index.ts +2249 -914
  25. package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +13 -0
  26. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +215 -0
  27. package/node_modules/@herbertgao/pi-subagents/src/mention.ts +147 -0
  28. package/node_modules/@herbertgao/pi-subagents/src/model-resolver.ts +9 -1
  29. package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +40 -26
  30. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +18 -8
  31. package/node_modules/@herbertgao/pi-subagents/src/prompts.ts +46 -9
  32. package/node_modules/@herbertgao/pi-subagents/src/schedule.ts +21 -16
  33. package/node_modules/@herbertgao/pi-subagents/src/settings.ts +137 -7
  34. package/node_modules/@herbertgao/pi-subagents/src/structured-output.ts +136 -0
  35. package/node_modules/@herbertgao/pi-subagents/src/types.ts +126 -8
  36. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-mention.ts +274 -0
  37. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +20 -5
  38. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +10 -4
  39. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +167 -22
  40. package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-card.ts +555 -0
  41. package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-dialog.ts +1304 -0
  42. package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-menu.ts +226 -0
  43. package/node_modules/@herbertgao/pi-subagents/src/workflow/collisions.ts +122 -0
  44. package/node_modules/@herbertgao/pi-subagents/src/workflow/entry.ts +47 -0
  45. package/node_modules/@herbertgao/pi-subagents/src/workflow/host.ts +463 -0
  46. package/node_modules/@herbertgao/pi-subagents/src/workflow/journal.ts +164 -0
  47. package/node_modules/@herbertgao/pi-subagents/src/workflow/json-schema.ts +142 -0
  48. package/node_modules/@herbertgao/pi-subagents/src/workflow/meta.ts +401 -0
  49. package/node_modules/@herbertgao/pi-subagents/src/workflow/progress.ts +622 -0
  50. package/node_modules/@herbertgao/pi-subagents/src/workflow/runtime.ts +1399 -0
  51. package/node_modules/@herbertgao/pi-subagents/src/workflow/saved.ts +230 -0
  52. package/node_modules/@herbertgao/pi-subagents/src/workflow/task.ts +333 -0
  53. package/node_modules/@herbertgao/pi-subagents/src/workflow/tool-description.ts +200 -0
  54. package/node_modules/@herbertgao/pi-subagents/src/workflow/worker-source.ts +781 -0
  55. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +97 -95
  56. package/node_modules/@herbertgao/pi-subagents/src/xml.ts +13 -0
  57. package/node_modules/@herbertgao/resume-from/package.json +1 -1
  58. package/node_modules/@herbertgao/sol-pi/README.md +3 -3
  59. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
  60. package/node_modules/@herbertgao/sol-pi/agents-install.md +4 -4
  61. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +6 -6
  62. package/node_modules/@herbertgao/sol-pi/package.json +2 -2
  63. package/node_modules/@narumitw/pi-btw/dist/index.ts +39 -89
  64. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  65. package/node_modules/@narumitw/pi-btw/package.json +4 -4
  66. package/node_modules/@narumitw/pi-btw/src/btw.ts +28 -87
  67. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +8 -0
  68. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +40 -37
  69. package/node_modules/@narumitw/pi-caffeinate/README.md +21 -66
  70. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +10 -41
  71. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
  72. package/node_modules/@narumitw/pi-caffeinate/package.json +50 -51
  73. package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +637 -663
  74. package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +114 -120
  75. package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +29 -29
  76. package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +108 -126
  77. package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +124 -128
  78. package/node_modules/pi-multi-account/CHANGELOG.md +1209 -0
  79. package/node_modules/pi-multi-account/CONTRIBUTING.md +61 -0
  80. package/node_modules/pi-multi-account/LICENSE +21 -0
  81. package/node_modules/pi-multi-account/README.md +197 -0
  82. package/node_modules/pi-multi-account/SECURITY.md +27 -0
  83. package/node_modules/pi-multi-account/auth-file-transaction.ts +56 -0
  84. package/node_modules/pi-multi-account/child-usability.ts +233 -0
  85. package/node_modules/pi-multi-account/compaction-summary.ts +32 -0
  86. package/node_modules/pi-multi-account/completion-route-planner.ts +224 -0
  87. package/node_modules/pi-multi-account/context-guard.ts +420 -0
  88. package/node_modules/pi-multi-account/cursor/LICENSE +21 -0
  89. package/node_modules/pi-multi-account/cursor/NOTICE +2 -0
  90. package/node_modules/pi-multi-account/cursor/auth.ts +165 -0
  91. package/node_modules/pi-multi-account/cursor/bridge-handle.ts +155 -0
  92. package/node_modules/pi-multi-account/cursor/conversation-registry.ts +104 -0
  93. package/node_modules/pi-multi-account/cursor/cursor-models-raw.json +611 -0
  94. package/node_modules/pi-multi-account/cursor/cursor-shared.ts +192 -0
  95. package/node_modules/pi-multi-account/cursor/h2-bridge.mjs +175 -0
  96. package/node_modules/pi-multi-account/cursor/index.ts +572 -0
  97. package/node_modules/pi-multi-account/cursor/message-parsing.ts +323 -0
  98. package/node_modules/pi-multi-account/cursor/prompt-usage.ts +53 -0
  99. package/node_modules/pi-multi-account/cursor/proto/agent_pb.ts +15294 -0
  100. package/node_modules/pi-multi-account/cursor/proxy.ts +2510 -0
  101. package/node_modules/pi-multi-account/cursor/session-lifecycle.ts +40 -0
  102. package/node_modules/pi-multi-account/cursor/sse-keepalive.ts +24 -0
  103. package/node_modules/pi-multi-account/cursor/stream-lifecycle.ts +193 -0
  104. package/node_modules/pi-multi-account/cursor/upstream-watchdog.ts +88 -0
  105. package/node_modules/pi-multi-account/cursor-bridge.ts +240 -0
  106. package/node_modules/pi-multi-account/cursor-model-name.ts +12 -0
  107. package/node_modules/pi-multi-account/index.ts +11825 -0
  108. package/node_modules/pi-multi-account/model-catalog.ts +354 -0
  109. package/node_modules/pi-multi-account/package.json +101 -0
  110. package/node_modules/pi-multi-account/pi-contract.ts +281 -0
  111. package/node_modules/pi-multi-account/provider-payload-stream.ts +44 -0
  112. package/node_modules/pi-multi-account/provider-priority.ts +189 -0
  113. package/node_modules/pi-multi-account/slot-proxy-auth.ts +167 -0
  114. package/node_modules/pi-multi-account/slot-proxy.ts +344 -0
  115. package/node_modules/pi-multi-account/state-file-transaction.ts +67 -0
  116. package/node_modules/pi-multi-account/usage.ts +1099 -0
  117. package/node_modules/pi-typesafe/README.md +6 -2
  118. package/node_modules/pi-typesafe/dist/client.d.ts +11 -0
  119. package/node_modules/pi-typesafe/dist/client.js +45 -10
  120. package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
  121. package/node_modules/pi-typesafe/dist/index.js +1 -1
  122. package/node_modules/pi-typesafe/package.json +2 -2
  123. package/node_modules/pi-web-access/CHANGELOG.md +36 -0
  124. package/node_modules/pi-web-access/README.md +75 -18
  125. package/node_modules/pi-web-access/anysearch.ts +4 -15
  126. package/node_modules/pi-web-access/bocha.ts +3 -22
  127. package/node_modules/pi-web-access/brave.ts +3 -21
  128. package/node_modules/pi-web-access/brightdata.ts +5 -32
  129. package/node_modules/pi-web-access/content-find.ts +168 -53
  130. package/node_modules/pi-web-access/curator-page.ts +4 -1
  131. package/node_modules/pi-web-access/curator-run.ts +2 -1
  132. package/node_modules/pi-web-access/curator-server.ts +1 -0
  133. package/node_modules/pi-web-access/dist/index.js +24620 -0
  134. package/node_modules/pi-web-access/domain-filter-normalization.ts +14 -0
  135. package/node_modules/pi-web-access/duckduckgo.ts +3 -21
  136. package/node_modules/pi-web-access/extract.ts +3 -1
  137. package/node_modules/pi-web-access/firecrawl.ts +5 -29
  138. package/node_modules/pi-web-access/gemini-search.ts +81 -32
  139. package/node_modules/pi-web-access/index.ts +149 -148
  140. package/node_modules/pi-web-access/jina-search.ts +4 -15
  141. package/node_modules/pi-web-access/kagi.ts +4 -13
  142. package/node_modules/pi-web-access/kimi-search.ts +5 -30
  143. package/node_modules/pi-web-access/mistral-search.ts +1 -15
  144. package/node_modules/pi-web-access/ollama.ts +2 -7
  145. package/node_modules/pi-web-access/openai-search.ts +174 -36
  146. package/node_modules/pi-web-access/opencode-session-headers.ts +24 -0
  147. package/node_modules/pi-web-access/package.json +10 -4
  148. package/node_modules/pi-web-access/page-query.ts +10 -2
  149. package/node_modules/pi-web-access/parallel.ts +1 -15
  150. package/node_modules/pi-web-access/pdf-extract.ts +3 -0
  151. package/node_modules/pi-web-access/querit.ts +5 -29
  152. package/node_modules/pi-web-access/search-answer-formatting.ts +11 -0
  153. package/node_modules/pi-web-access/search-result-count-normalization.ts +4 -0
  154. package/node_modules/pi-web-access/search1api.ts +5 -29
  155. package/node_modules/pi-web-access/searchinfinity.ts +5 -29
  156. package/node_modules/pi-web-access/searxng.ts +3 -21
  157. package/node_modules/pi-web-access/serpapi.ts +5 -28
  158. package/node_modules/pi-web-access/serpbase.ts +3 -22
  159. package/node_modules/pi-web-access/serpdive.ts +3 -21
  160. package/node_modules/pi-web-access/serper.ts +5 -28
  161. package/node_modules/pi-web-access/serply.ts +197 -0
  162. package/node_modules/pi-web-access/source-check.ts +11 -47
  163. package/node_modules/pi-web-access/summary-review.ts +7 -3
  164. package/node_modules/pi-web-access/tavily.ts +3 -21
  165. package/node_modules/pi-web-access/tinyfish.ts +5 -29
  166. package/node_modules/pi-web-access/utils.ts +9 -1
  167. package/node_modules/pi-web-access/valyu.ts +5 -28
  168. package/node_modules/pi-web-access/xai-search.ts +1 -15
  169. package/node_modules/pi-web-access/xcrawl.ts +5 -32
  170. package/package.json +17 -11
@@ -1,811 +1,785 @@
1
1
  import type { ChildProcess } from "node:child_process";
2
2
  import process from "node:process";
3
- import type {
4
- ExtensionAPI,
5
- ExtensionCommandContext,
6
- ExtensionContext,
7
- } from "@earendil-works/pi-coding-agent";
3
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
4
  import {
9
- type DbusScreenSaverClient,
10
- type DbusScreenSaverFactory,
11
- defaultDbusScreenSaverFactory,
12
- INHIBIT_REASON,
13
- SCREENSAVER_BUS_NAME,
5
+ type DbusScreenSaverClient,
6
+ type DbusScreenSaverFactory,
7
+ defaultDbusScreenSaverFactory,
8
+ INHIBIT_REASON,
9
+ SCREENSAVER_BUS_NAME,
14
10
  } from "./dbus-inhibit.js";
15
11
  import { startInhibitorProcess, stopInhibitorProcess } from "./inhibitor-process.js";
16
12
  import { formatMode, getInhibitorCommand, type InhibitorCommand } from "./inhibitors.js";
17
13
  import {
18
- type CaffeinateMode,
19
- type CaffeinateSettings,
20
- loadSettings,
21
- saveSettings,
22
- settingsFilePath,
14
+ type CaffeinateMode,
15
+ type CaffeinateSettings,
16
+ loadSettings,
17
+ saveSettings,
18
+ settingsFilePath,
23
19
  } from "./settings.js";
24
20
 
25
21
  const STATUS_KEY = "caffeinate";
26
22
  const DISABLED_VALUES = new Set(["1", "true", "yes", "on"]);
27
23
  const DEFAULT_MODE = "display" satisfies CaffeinateMode;
28
24
  const COMMAND_COMPLETIONS = [
29
- { value: "display", label: "display", description: "Keep system and display awake" },
30
- { value: "sleep", label: "sleep", description: "Keep system awake; allow display sleep" },
31
- { value: "status", label: "status", description: "Show current status" },
32
- { value: "mode", label: "mode", description: "Choose keep-awake mode" },
33
- { value: "stop", label: "stop", description: "Release inhibitor for now" },
34
- { value: "help", label: "help", description: "Show command help" },
25
+ { value: "display", label: "display", description: "Keep system and display awake" },
26
+ { value: "sleep", label: "sleep", description: "Keep system awake; allow display sleep" },
27
+ { value: "status", label: "status", description: "Show current status" },
28
+ { value: "mode", label: "mode", description: "Choose keep-awake mode" },
29
+ { value: "stop", label: "stop", description: "Release inhibitor for now" },
30
+ { value: "help", label: "help", description: "Show command help" },
35
31
  ];
36
32
  const MENU_OPTIONS = {
37
- display: "Keep system and display awake",
38
- sleep: "Keep system awake; allow display sleep",
39
- status: "Show current status",
40
- stop: "Release inhibitor for now",
41
- help: "Show command help",
33
+ display: "Keep system and display awake",
34
+ sleep: "Keep system awake; allow display sleep",
35
+ status: "Show current status",
36
+ stop: "Release inhibitor for now",
37
+ help: "Show command help",
42
38
  } as const;
43
39
  const MODE_OPTIONS = {
44
- display: "Keep system and display awake",
45
- sleep: "Keep system awake; allow display sleep",
40
+ display: "Keep system and display awake",
41
+ sleep: "Keep system awake; allow display sleep",
46
42
  } as const;
47
43
 
48
44
  type CommandAction = "menu" | "help" | "status" | "mode" | "sleep" | "display" | "stop";
49
45
  type CommandContext = ExtensionCommandContext;
50
46
 
51
47
  interface CaffeinateOptions {
52
- dbusFactory?: DbusScreenSaverFactory;
48
+ dbusFactory?: DbusScreenSaverFactory;
53
49
  }
54
50
 
55
51
  interface PendingDbusStart {
56
- token: number;
57
- controller: AbortController;
58
- client?: DbusScreenSaverClient;
52
+ token: number;
53
+ controller: AbortController;
54
+ client?: DbusScreenSaverClient;
59
55
  }
60
56
 
61
57
  interface CaffeinateState {
62
- process?: ChildProcess;
63
- dbus?: DbusScreenSaverClient;
64
- currentSession?: { generation: number; ctx: ExtensionContext };
65
- dbusCleanup?: Promise<void>;
66
- pendingDbus?: PendingDbusStart;
67
- startedAt?: number;
68
- command?: InhibitorCommand;
69
- lastError?: string;
70
- inhibitWarning?: string;
71
- inhibitorStarting: boolean;
72
- activeTurns: number;
73
- available: boolean;
74
- disabled: boolean;
75
- mode: CaffeinateMode;
76
- quiet: boolean;
77
- settingsLoaded: boolean;
78
- settingsError?: string;
79
- settingsNotice?: string;
80
- iconWarningShown: boolean;
81
- sessionGeneration: number;
82
- menuController: AbortController;
58
+ process?: ChildProcess;
59
+ dbus?: DbusScreenSaverClient;
60
+ currentSession?: { generation: number; ctx: ExtensionContext };
61
+ dbusCleanup?: Promise<void>;
62
+ pendingDbus?: PendingDbusStart;
63
+ startedAt?: number;
64
+ command?: InhibitorCommand;
65
+ lastError?: string;
66
+ inhibitWarning?: string;
67
+ inhibitorStarting: boolean;
68
+ activeTurns: number;
69
+ available: boolean;
70
+ disabled: boolean;
71
+ mode: CaffeinateMode;
72
+ quiet: boolean;
73
+ settingsLoaded: boolean;
74
+ settingsError?: string;
75
+ settingsNotice?: string;
76
+ iconWarningShown: boolean;
77
+ sessionGeneration: number;
78
+ menuController: AbortController;
83
79
  }
84
80
 
85
81
  const state: CaffeinateState = {
86
- activeTurns: 0,
87
- available: true,
88
- inhibitorStarting: false,
89
- disabled: isDisabled(),
90
- mode: DEFAULT_MODE,
91
- quiet: false,
92
- settingsLoaded: false,
93
- iconWarningShown: false,
94
- sessionGeneration: 0,
95
- menuController: new AbortController(),
82
+ activeTurns: 0,
83
+ available: true,
84
+ inhibitorStarting: false,
85
+ disabled: isDisabled(),
86
+ mode: DEFAULT_MODE,
87
+ quiet: false,
88
+ settingsLoaded: false,
89
+ iconWarningShown: false,
90
+ sessionGeneration: 0,
91
+ menuController: new AbortController(),
96
92
  };
97
93
 
98
94
  let dbusFactory = defaultDbusScreenSaverFactory;
99
95
  let inhibitSequence = 0;
100
96
 
101
97
  export default function caffeinate(pi: ExtensionAPI, options: CaffeinateOptions = {}) {
102
- dbusFactory = options.dbusFactory ?? defaultDbusScreenSaverFactory;
103
- pi.on("session_start", async (_event, ctx) => {
104
- const generation = ++state.sessionGeneration;
105
- state.currentSession = { generation, ctx };
106
- replaceMenuController("Caffeinate session replaced");
107
- state.iconWarningShown = false;
108
- state.settingsNotice = undefined;
109
- warnDeprecatedIcon(ctx);
110
- await loadSettingsIntoState(ctx, generation);
111
- if (generation !== state.sessionGeneration) return;
112
- updateStatus(ctx);
113
- });
114
-
115
- pi.on("agent_start", async (_event, ctx) => {
116
- const generation = state.sessionGeneration;
117
- await ensureSettingsLoaded(ctx, generation);
118
- if (generation !== state.sessionGeneration) return;
119
- state.activeTurns += 1;
120
- await startInhibitor(ctx, generation, { notify: !state.quiet });
121
- });
122
-
123
- pi.on("agent_end", async (_event, ctx) => {
124
- const generation = state.sessionGeneration;
125
- state.activeTurns = Math.max(0, state.activeTurns - 1);
126
- if (state.activeTurns === 0) {
127
- await stopInhibitor(ctx, "agent finished", { notify: !state.quiet });
128
- }
129
- if (generation !== state.sessionGeneration) return;
130
- updateStatus(ctx);
131
- });
132
-
133
- pi.on("session_shutdown", async (_event, ctx) => {
134
- const generation = ++state.sessionGeneration;
135
- state.currentSession = undefined;
136
- state.menuController.abort(new DOMException("Caffeinate session shut down", "AbortError"));
137
- state.activeTurns = 0;
138
- await stopInhibitor(ctx, "session shutdown", { notify: false });
139
- await modeOperationQueue;
140
- if (generation !== state.sessionGeneration) return;
141
- ctx.ui.setStatus(STATUS_KEY, undefined);
142
- });
143
-
144
- pi.registerCommand("caffeinate", {
145
- description: "Open pi-caffeinate keep-awake controls",
146
- getArgumentCompletions: (prefix) => commandCompletions(prefix),
147
- handler: async (args, ctx) => {
148
- const generation = state.sessionGeneration;
149
- const command = parseCommand(args);
150
- if (command === "sleep" || command === "display") {
151
- await setModeAfterSettingsLoad(ctx, command, generation);
152
- return;
153
- }
154
- await ensureSettingsLoaded(ctx, generation);
155
- if (generation !== state.sessionGeneration) return;
156
- await handleCaffeinateCommand(args, ctx, generation);
157
- },
158
- });
98
+ dbusFactory = options.dbusFactory ?? defaultDbusScreenSaverFactory;
99
+ pi.on("session_start", async (_event, ctx) => {
100
+ const generation = ++state.sessionGeneration;
101
+ state.currentSession = { generation, ctx };
102
+ replaceMenuController("Caffeinate session replaced");
103
+ state.iconWarningShown = false;
104
+ state.settingsNotice = undefined;
105
+ warnDeprecatedIcon(ctx);
106
+ await loadSettingsIntoState(ctx, generation);
107
+ if (generation !== state.sessionGeneration) return;
108
+ updateStatus(ctx);
109
+ });
110
+
111
+ pi.on("agent_start", async (_event, ctx) => {
112
+ const generation = state.sessionGeneration;
113
+ await ensureSettingsLoaded(ctx, generation);
114
+ if (generation !== state.sessionGeneration) return;
115
+ state.activeTurns += 1;
116
+ await startInhibitor(ctx, generation, { notify: !state.quiet });
117
+ });
118
+
119
+ pi.on("agent_end", async (_event, ctx) => {
120
+ const generation = state.sessionGeneration;
121
+ state.activeTurns = Math.max(0, state.activeTurns - 1);
122
+ if (state.activeTurns === 0) {
123
+ await stopInhibitor(ctx, "agent finished", { notify: !state.quiet });
124
+ }
125
+ if (generation !== state.sessionGeneration) return;
126
+ updateStatus(ctx);
127
+ });
128
+
129
+ pi.on("session_shutdown", async (_event, ctx) => {
130
+ const generation = ++state.sessionGeneration;
131
+ state.currentSession = undefined;
132
+ state.menuController.abort(new DOMException("Caffeinate session shut down", "AbortError"));
133
+ state.activeTurns = 0;
134
+ await stopInhibitor(ctx, "session shutdown", { notify: false });
135
+ await modeOperationQueue;
136
+ if (generation !== state.sessionGeneration) return;
137
+ ctx.ui.setStatus(STATUS_KEY, undefined);
138
+ });
139
+
140
+ pi.registerCommand("caffeinate", {
141
+ description: "Open pi-caffeinate keep-awake controls",
142
+ getArgumentCompletions: (prefix) => commandCompletions(prefix),
143
+ handler: async (args, ctx) => {
144
+ const generation = state.sessionGeneration;
145
+ const command = parseCommand(args);
146
+ if (command === "sleep" || command === "display") {
147
+ await setModeAfterSettingsLoad(ctx, command, generation);
148
+ return;
149
+ }
150
+ await ensureSettingsLoaded(ctx, generation);
151
+ if (generation !== state.sessionGeneration) return;
152
+ await handleCaffeinateCommand(args, ctx, generation);
153
+ },
154
+ });
159
155
  }
160
156
 
161
157
  async function handleCaffeinateCommand(args: string, ctx: CommandContext, generation: number) {
162
- const command = parseCommand(args);
163
- switch (command) {
164
- case "menu":
165
- await showMenu(ctx, generation);
166
- return;
167
- case "help":
168
- ctx.ui.notify(buildCommandGuide(), "info");
169
- return;
170
- case "status":
171
- showStatus(ctx);
172
- return;
173
- case "mode":
174
- await showModeSelector(ctx, generation);
175
- return;
176
- case "sleep":
177
- await setMode(ctx, "sleep", generation);
178
- return;
179
- case "display":
180
- await setMode(ctx, "display", generation);
181
- return;
182
- case "stop":
183
- await stopCaffeinate(ctx, "manual stop", generation);
184
- return;
185
- }
186
-
187
- ctx.ui.notify(`Unknown /caffeinate command: ${args.trim()}\n\n${buildCommandGuide()}`, "warning");
158
+ const command = parseCommand(args);
159
+ switch (command) {
160
+ case "menu":
161
+ await showMenu(ctx, generation);
162
+ return;
163
+ case "help":
164
+ ctx.ui.notify(buildCommandGuide(), "info");
165
+ return;
166
+ case "status":
167
+ showStatus(ctx);
168
+ return;
169
+ case "mode":
170
+ await showModeSelector(ctx, generation);
171
+ return;
172
+ case "sleep":
173
+ await setMode(ctx, "sleep", generation);
174
+ return;
175
+ case "display":
176
+ await setMode(ctx, "display", generation);
177
+ return;
178
+ case "stop":
179
+ await stopCaffeinate(ctx, "manual stop", generation);
180
+ return;
181
+ }
182
+
183
+ ctx.ui.notify(`Unknown /caffeinate command: ${args.trim()}\n\n${buildCommandGuide()}`, "warning");
188
184
  }
189
185
 
190
186
  async function showMenu(ctx: CommandContext, generation: number) {
191
- await runCaffeinateMenu(ctx, generation, "main");
187
+ await runCaffeinateMenu(ctx, generation, "main");
192
188
  }
193
189
 
194
190
  async function showModeSelector(ctx: CommandContext, generation: number) {
195
- await runCaffeinateMenu(ctx, generation, "mode");
191
+ await runCaffeinateMenu(ctx, generation, "mode");
196
192
  }
197
193
 
198
194
  async function runCaffeinateMenu(ctx: CommandContext, generation: number, start: "main" | "mode") {
199
- if (!ctx.hasUI) {
200
- throw new Error(
201
- start === "mode"
202
- ? "Mode selection requires TUI or RPC mode. Use /caffeinate sleep or /caffeinate display."
203
- : `The pi-caffeinate menu requires TUI or RPC mode.\n\n${buildCommandGuide()}\n\n${describeState()}`,
204
- );
205
- }
206
- const menuSignal = state.menuController.signal;
207
- const isCurrent = () => generation === state.sessionGeneration && !menuSignal.aborted;
208
- const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
209
- if (!isCurrent()) return;
210
- type Screen = "main" | "mode";
211
- type Action = "display" | "sleep" | "status" | "stop" | "help";
212
- const menu = defineMenu<undefined, Screen, Action>({
213
- start,
214
- screens: {
215
- main: () => ({
216
- kind: "actions",
217
- title: "pi-caffeinate controls",
218
- lines: describeState().split("\n"),
219
- items: Object.entries(MENU_OPTIONS).map(([id, label]) => ({
220
- id,
221
- label,
222
- action: id as Action,
223
- })),
224
- hint: "close",
225
- }),
226
- mode: () => ({
227
- kind: "actions",
228
- title: `pi-caffeinate mode (current: ${formatMode(state.mode)})`,
229
- items: Object.entries(MODE_OPTIONS).map(([id, label]) => ({
230
- id,
231
- label,
232
- action: id as Action,
233
- })),
234
- hint: "close",
235
- }),
236
- },
237
- actions: {
238
- display: async () => {
239
- await setMode(ctx, "display", generation);
240
- return { kind: "close" };
241
- },
242
- sleep: async () => {
243
- await setMode(ctx, "sleep", generation);
244
- return { kind: "close" };
245
- },
246
- status: async () => {
247
- showStatus(ctx);
248
- return { kind: "close" };
249
- },
250
- stop: async () => {
251
- await stopCaffeinate(ctx, "manual stop", generation);
252
- return { kind: "close" };
253
- },
254
- help: async () => {
255
- ctx.ui.notify(buildCommandGuide(), "info");
256
- return { kind: "close" };
257
- },
258
- },
259
- });
260
- await runMenu(ctx, menu, {
261
- getState: () => undefined,
262
- signal: menuSignal,
263
- isCurrent,
264
- });
195
+ if (!ctx.hasUI) {
196
+ throw new Error(
197
+ start === "mode"
198
+ ? "Mode selection requires TUI or RPC mode. Use /caffeinate sleep or /caffeinate display."
199
+ : `The pi-caffeinate menu requires TUI or RPC mode.\n\n${buildCommandGuide()}\n\n${describeState()}`,
200
+ );
201
+ }
202
+ const menuSignal = state.menuController.signal;
203
+ const isCurrent = () => generation === state.sessionGeneration && !menuSignal.aborted;
204
+ const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
205
+ if (!isCurrent()) return;
206
+ type Screen = "main" | "mode";
207
+ type Action = "display" | "sleep" | "status" | "stop" | "help";
208
+ const menu = defineMenu<undefined, Screen, Action>({
209
+ start,
210
+ screens: {
211
+ main: () => ({
212
+ kind: "actions",
213
+ title: "pi-caffeinate controls",
214
+ lines: describeState().split("\n"),
215
+ items: Object.entries(MENU_OPTIONS).map(([id, label]) => ({
216
+ id,
217
+ label,
218
+ action: id as Action,
219
+ })),
220
+ hint: "close",
221
+ }),
222
+ mode: () => ({
223
+ kind: "actions",
224
+ title: `pi-caffeinate mode (current: ${formatMode(state.mode)})`,
225
+ items: Object.entries(MODE_OPTIONS).map(([id, label]) => ({
226
+ id,
227
+ label,
228
+ action: id as Action,
229
+ })),
230
+ hint: "close",
231
+ }),
232
+ },
233
+ actions: {
234
+ display: async () => {
235
+ await setMode(ctx, "display", generation);
236
+ return { kind: "close" };
237
+ },
238
+ sleep: async () => {
239
+ await setMode(ctx, "sleep", generation);
240
+ return { kind: "close" };
241
+ },
242
+ status: async () => {
243
+ showStatus(ctx);
244
+ return { kind: "close" };
245
+ },
246
+ stop: async () => {
247
+ await stopCaffeinate(ctx, "manual stop", generation);
248
+ return { kind: "close" };
249
+ },
250
+ help: async () => {
251
+ ctx.ui.notify(buildCommandGuide(), "info");
252
+ return { kind: "close" };
253
+ },
254
+ },
255
+ });
256
+ await runMenu(ctx, menu, {
257
+ getState: () => undefined,
258
+ signal: menuSignal,
259
+ isCurrent,
260
+ });
265
261
  }
266
262
 
267
263
  function replaceMenuController(reason: string) {
268
- state.menuController.abort(new DOMException(reason, "AbortError"));
269
- state.menuController = new AbortController();
264
+ state.menuController.abort(new DOMException(reason, "AbortError"));
265
+ state.menuController = new AbortController();
270
266
  }
271
267
 
272
268
  let modeOperationQueue = Promise.resolve();
273
269
 
274
- function setModeAfterSettingsLoad(
275
- ctx: ExtensionContext,
276
- mode: CaffeinateMode,
277
- generation: number,
278
- ): Promise<void> {
279
- return enqueueModeOperation(async () => {
280
- await ensureSettingsLoaded(ctx, generation);
281
- if (generation !== state.sessionGeneration) return;
282
- await setModeNow(ctx, mode, generation);
283
- });
270
+ function setModeAfterSettingsLoad(ctx: ExtensionContext, mode: CaffeinateMode, generation: number): Promise<void> {
271
+ return enqueueModeOperation(async () => {
272
+ await ensureSettingsLoaded(ctx, generation);
273
+ if (generation !== state.sessionGeneration) return;
274
+ await setModeNow(ctx, mode, generation);
275
+ });
284
276
  }
285
277
 
286
278
  function setMode(ctx: ExtensionContext, mode: CaffeinateMode, generation: number): Promise<void> {
287
- return enqueueModeOperation(() => setModeNow(ctx, mode, generation));
279
+ return enqueueModeOperation(() => setModeNow(ctx, mode, generation));
288
280
  }
289
281
 
290
282
  function enqueueModeOperation(operation: () => Promise<void>): Promise<void> {
291
- const queued = modeOperationQueue.then(operation);
292
- modeOperationQueue = queued.catch(() => undefined);
293
- return queued;
283
+ const queued = modeOperationQueue.then(operation);
284
+ modeOperationQueue = queued.catch(() => undefined);
285
+ return queued;
294
286
  }
295
287
 
296
288
  async function setModeNow(ctx: ExtensionContext, mode: CaffeinateMode, generation: number) {
297
- if (generation !== state.sessionGeneration) return;
298
- const previousMode = state.mode;
299
- const previousQuiet = state.quiet;
300
- const restartRequired = Boolean(
301
- hasActiveInhibitor() && previousMode !== mode && !state.command?.custom,
302
- );
303
- state.settingsError = undefined;
304
-
305
- if (restartRequired) {
306
- state.mode = mode;
307
- await stopInhibitor(ctx, "mode changed", { notify: false });
308
- if (generation !== state.sessionGeneration) return;
309
- await startInhibitor(ctx, generation, { notify: false });
310
- if (generation !== state.sessionGeneration) return;
311
- if (!hasActiveInhibitor()) {
312
- const applicationError = state.lastError ?? "the inhibitor could not be restarted";
313
- state.mode = previousMode;
314
- await startInhibitor(ctx, generation, { notify: false });
315
- if (generation !== state.sessionGeneration) return;
316
- const rollbackError = hasActiveInhibitor()
317
- ? undefined
318
- : (state.lastError ?? "the previous inhibitor could not be restored");
319
- state.settingsError = rollbackError
320
- ? `mode application failed (${applicationError}); runtime rollback failed: ${rollbackError}`
321
- : `mode application failed and was rolled back: ${applicationError}`;
322
- ctx.ui.notify(`pi-caffeinate ${state.settingsError}`, "warning");
323
- updateStatus(ctx);
324
- return;
325
- }
326
- }
327
-
328
- let savedSettings: CaffeinateSettings;
329
- try {
330
- savedSettings = await saveSettings({ mode, updatedAt: Date.now() });
331
- } catch (error) {
332
- if (generation !== state.sessionGeneration) return;
333
- let rollbackError: string | undefined;
334
- if (restartRequired) {
335
- state.mode = previousMode;
336
- state.quiet = previousQuiet;
337
- await stopInhibitor(ctx, "settings save failed", { notify: false });
338
- if (generation !== state.sessionGeneration) return;
339
- await startInhibitor(ctx, generation, { notify: false });
340
- if (generation !== state.sessionGeneration) return;
341
- if (!hasActiveInhibitor()) {
342
- rollbackError = state.lastError ?? "the prior inhibitor was not restored";
343
- }
344
- }
345
- state.settingsError = rollbackError
346
- ? `settings save failed (${formatError(error)}); runtime rollback failed: ${rollbackError}`
347
- : `settings save failed: ${formatError(error)}`;
348
- ctx.ui.notify(
349
- `pi-caffeinate mode remains ${formatMode(previousMode)}; settings were not saved: ${state.settingsError}`,
350
- "warning",
351
- );
352
- updateStatus(ctx);
353
- return;
354
- }
355
-
356
- if (generation !== state.sessionGeneration) return;
357
- state.mode = mode;
358
- state.quiet = savedSettings.quiet;
359
- ctx.ui.notify(`pi-caffeinate mode set to ${formatMode(mode)} and saved.`, "info");
360
- updateStatus(ctx);
289
+ if (generation !== state.sessionGeneration) return;
290
+ const previousMode = state.mode;
291
+ const previousQuiet = state.quiet;
292
+ const restartRequired = Boolean(hasActiveInhibitor() && previousMode !== mode && !state.command?.custom);
293
+ state.settingsError = undefined;
294
+
295
+ if (restartRequired) {
296
+ state.mode = mode;
297
+ await stopInhibitor(ctx, "mode changed", { notify: false });
298
+ if (generation !== state.sessionGeneration) return;
299
+ await startInhibitor(ctx, generation, { notify: false });
300
+ if (generation !== state.sessionGeneration) return;
301
+ if (!hasActiveInhibitor()) {
302
+ const applicationError = state.lastError ?? "the inhibitor could not be restarted";
303
+ state.mode = previousMode;
304
+ await startInhibitor(ctx, generation, { notify: false });
305
+ if (generation !== state.sessionGeneration) return;
306
+ const rollbackError = hasActiveInhibitor()
307
+ ? undefined
308
+ : (state.lastError ?? "the previous inhibitor could not be restored");
309
+ state.settingsError = rollbackError
310
+ ? `mode application failed (${applicationError}); runtime rollback failed: ${rollbackError}`
311
+ : `mode application failed and was rolled back: ${applicationError}`;
312
+ ctx.ui.notify(`pi-caffeinate ${state.settingsError}`, "warning");
313
+ updateStatus(ctx);
314
+ return;
315
+ }
316
+ }
317
+
318
+ let savedSettings: CaffeinateSettings;
319
+ try {
320
+ savedSettings = await saveSettings({ mode, updatedAt: Date.now() });
321
+ } catch (error) {
322
+ if (generation !== state.sessionGeneration) return;
323
+ let rollbackError: string | undefined;
324
+ if (restartRequired) {
325
+ state.mode = previousMode;
326
+ state.quiet = previousQuiet;
327
+ await stopInhibitor(ctx, "settings save failed", { notify: false });
328
+ if (generation !== state.sessionGeneration) return;
329
+ await startInhibitor(ctx, generation, { notify: false });
330
+ if (generation !== state.sessionGeneration) return;
331
+ if (!hasActiveInhibitor()) {
332
+ rollbackError = state.lastError ?? "the prior inhibitor was not restored";
333
+ }
334
+ }
335
+ state.settingsError = rollbackError
336
+ ? `settings save failed (${formatError(error)}); runtime rollback failed: ${rollbackError}`
337
+ : `settings save failed: ${formatError(error)}`;
338
+ ctx.ui.notify(
339
+ `pi-caffeinate mode remains ${formatMode(previousMode)}; settings were not saved: ${state.settingsError}`,
340
+ "warning",
341
+ );
342
+ updateStatus(ctx);
343
+ return;
344
+ }
345
+
346
+ if (generation !== state.sessionGeneration) return;
347
+ state.mode = mode;
348
+ state.quiet = savedSettings.quiet;
349
+ ctx.ui.notify(`pi-caffeinate mode set to ${formatMode(mode)} and saved.`, "info");
350
+ updateStatus(ctx);
361
351
  }
362
352
 
363
353
  function showStatus(ctx: ExtensionContext) {
364
- ctx.ui.notify(describeState(), statusLevel());
365
- updateStatus(ctx);
354
+ ctx.ui.notify(describeState(), statusLevel());
355
+ updateStatus(ctx);
366
356
  }
367
357
 
368
358
  async function stopCaffeinate(ctx: ExtensionContext, reason: string, sessionGeneration: number) {
369
- state.activeTurns = 0;
370
- await stopInhibitor(ctx, reason);
371
- if (sessionGeneration !== state.sessionGeneration) return;
372
- updateStatus(ctx);
359
+ state.activeTurns = 0;
360
+ await stopInhibitor(ctx, reason);
361
+ if (sessionGeneration !== state.sessionGeneration) return;
362
+ updateStatus(ctx);
373
363
  }
374
364
 
375
365
  export function parseCommand(args: string): CommandAction | "unknown" {
376
- const command = args.trim().toLowerCase();
377
- if (!command) return "menu";
378
- if (command === "help") return "help";
379
- if (command === "status") return "status";
380
- if (command === "mode" || command === "config" || command === "settings") return "mode";
381
- if (command === "sleep" || command === "system") return "sleep";
382
- if (command === "display" || command === "screen") return "display";
383
- if (command === "stop" || command === "off") return "stop";
384
- return "unknown";
366
+ const command = args.trim().toLowerCase();
367
+ if (!command) return "menu";
368
+ if (command === "help") return "help";
369
+ if (command === "status") return "status";
370
+ if (command === "mode" || command === "config" || command === "settings") return "mode";
371
+ if (command === "sleep" || command === "system") return "sleep";
372
+ if (command === "display" || command === "screen") return "display";
373
+ if (command === "stop" || command === "off") return "stop";
374
+ return "unknown";
385
375
  }
386
376
 
387
377
  export function commandCompletions(prefix: string) {
388
- const normalized = prefix.trimStart().toLowerCase();
389
- if (/\s/.test(normalized)) return null;
378
+ const normalized = prefix.trimStart().toLowerCase();
379
+ if (/\s/.test(normalized)) return null;
390
380
 
391
- const matches = COMMAND_COMPLETIONS.filter((completion) =>
392
- completion.value.startsWith(normalized),
393
- );
394
- return matches.length > 0 ? matches : null;
381
+ const matches = COMMAND_COMPLETIONS.filter((completion) => completion.value.startsWith(normalized));
382
+ return matches.length > 0 ? matches : null;
395
383
  }
396
384
 
397
385
  function buildCommandGuide() {
398
- return [
399
- "pi-caffeinate commands:",
400
- "/caffeinate — open keep-awake controls",
401
- "/caffeinate display — keep the system and display awake",
402
- "/caffeinate sleep — keep the system awake while allowing display sleep",
403
- "/caffeinate status — show current mode, settings, and inhibitor state",
404
- "/caffeinate mode — choose a keep-awake mode",
405
- "/caffeinate stop — release the active inhibitor until the next agent run",
406
- ].join("\n");
407
- }
408
-
409
- async function startInhibitor(
410
- ctx: ExtensionContext,
411
- sessionGeneration: number,
412
- options: { notify?: boolean } = {},
413
- ) {
414
- if (state.disabled || hasActiveInhibitor() || state.inhibitorStarting) {
415
- updateStatus(ctx);
416
- return;
417
- }
418
-
419
- const token = ++inhibitSequence;
420
- const command = getInhibitorCommand(state.mode);
421
- const wantsDbus =
422
- !command?.custom &&
423
- state.mode === "display" &&
424
- process.platform === "linux" &&
425
- (command === undefined || command.addDbusIdleInhibit === true);
426
- if (!command && !wantsDbus) {
427
- state.available = false;
428
- state.lastError = `No supported sleep inhibitor found for ${process.platform}.`;
429
- ctx.ui.notify(state.lastError, "warning");
430
- updateStatus(ctx);
431
- return;
432
- }
433
-
434
- state.inhibitorStarting = true;
435
- state.inhibitWarning = undefined;
436
- let child: ChildProcess | undefined;
437
- let childError =
438
- !command && wantsDbus
439
- ? "No supported system sleep inhibitor is available; direct system suspend may remain possible."
440
- : undefined;
441
- let assembling = true;
442
- const handleChildFailure = (message: string) => {
443
- if (!child || token !== inhibitSequence || state.process !== child) return;
444
- state.process = undefined;
445
- state.command = undefined;
446
- if (assembling) {
447
- childError = message;
448
- return;
449
- }
450
- applyChildFailure(message);
451
- };
452
-
453
- if (command) {
454
- try {
455
- child = startInhibitorProcess(
456
- command,
457
- (error) => handleChildFailure(`${command.description} failed: ${error.message}`),
458
- (exit) => handleChildFailure(`${command.description} exited unexpectedly (${exit}).`),
459
- );
460
- state.process = child;
461
- state.command = command;
462
- state.startedAt = Date.now();
463
- } catch (error) {
464
- childError = `${command.description}: ${formatError(error)}`;
465
- }
466
- }
467
-
468
- let dbus: DbusScreenSaverClient | undefined;
469
- let dbusError: string | undefined;
470
- if (wantsDbus) {
471
- const pending: PendingDbusStart = { token, controller: new AbortController() };
472
- state.pendingDbus = pending;
473
- try {
474
- const client = await dbusFactory();
475
- if (!pendingStartIsCurrent(pending, sessionGeneration)) {
476
- await client.close().catch(() => undefined);
477
- await cancelPendingStart(token, child, command);
478
- return;
479
- }
480
- pending.client = client;
481
- await client.inhibit(INHIBIT_REASON, pending.controller.signal);
482
- if (!pendingStartIsCurrent(pending, sessionGeneration)) {
483
- await cancelPendingStart(token, child, command);
484
- return;
485
- }
486
- pending.client = undefined;
487
- state.pendingDbus = undefined;
488
- dbus = client;
489
- } catch (error) {
490
- const stale =
491
- !pendingStartIsCurrent(pending, sessionGeneration) || pending.controller.signal.aborted;
492
- const pendingClient = takePendingDbusClient(pending);
493
- await pendingClient?.close().catch(() => undefined);
494
- if (stale) {
495
- await cancelPendingStart(token, child, command);
496
- return;
497
- }
498
- dbusError = `D-Bus idle inhibit (${SCREENSAVER_BUS_NAME}): ${formatError(error)}`;
499
- }
500
- }
501
-
502
- assembling = false;
503
- if (startIsStale(token, sessionGeneration)) {
504
- await cancelPendingStart(token, child, command);
505
- await dbus?.close().catch(() => undefined);
506
- return;
507
- }
508
-
509
- state.inhibitorStarting = false;
510
- state.dbus = dbus;
511
- if (dbus) {
512
- dbus.setFailureHandler((error) => applyDbusFailure(token, dbus, error));
513
- if (state.dbus !== dbus) return;
514
- }
515
- const failures = [childError, dbusError].filter((failure): failure is string => Boolean(failure));
516
- if (!hasActiveInhibitor()) {
517
- state.startedAt = undefined;
518
- state.command = undefined;
519
- state.available = false;
520
- state.lastError =
521
- failures.join("; ") || `No supported sleep inhibitor found for ${process.platform}.`;
522
- ctx.ui.notify(state.lastError, "warning");
523
- updateStatus(ctx);
524
- return;
525
- }
526
-
527
- state.startedAt ??= Date.now();
528
- state.available = true;
529
- state.lastError = undefined;
530
- state.inhibitWarning = failures.length > 0 ? failures.join("; ") : undefined;
531
- if (state.inhibitWarning) {
532
- ctx.ui.notify(`pi-caffeinate is partially active: ${state.inhibitWarning}`, "warning");
533
- } else if (options.notify !== false) {
534
- ctx.ui.notify(`Keeping computer awake (${statusModeLabel()}).`, "info");
535
- }
536
- updateStatus(ctx);
537
- }
538
-
539
- async function stopInhibitor(
540
- ctx: ExtensionContext,
541
- reason: string,
542
- options: { notify?: boolean } = {},
543
- ) {
544
- inhibitSequence += 1;
545
- const child = state.process;
546
- const dbus = state.dbus;
547
- const dbusCleanup = state.dbusCleanup;
548
- const pending = state.pendingDbus;
549
- const command = state.command;
550
- const wasStarting = state.inhibitorStarting;
551
- state.process = undefined;
552
- state.dbus = undefined;
553
- state.dbusCleanup = undefined;
554
- state.pendingDbus = undefined;
555
- state.command = undefined;
556
- state.startedAt = undefined;
557
- state.inhibitWarning = undefined;
558
- state.inhibitorStarting = false;
559
- dbus?.setFailureHandler(undefined);
560
- pending?.controller.abort(new DOMException(`Caffeinate stopped: ${reason}`, "AbortError"));
561
- const pendingClient = pending ? takePendingDbusClient(pending) : undefined;
562
- if (child) stopInhibitorProcess(child, command);
563
- if (!child && !dbus && !pendingClient && !wasStarting) {
564
- await dbusCleanup;
565
- return;
566
- }
567
- if (options.notify !== false) ctx.ui.notify(`Released pi-caffeinate (${reason}).`, "info");
568
- await Promise.all([dbusCleanup, pendingClient?.close().catch(() => undefined)]);
569
- if (!dbus) return;
570
- try {
571
- await dbus.uninhibit();
572
- } catch {
573
- // Closing the session-bus connection also releases its inhibition cookie.
574
- }
575
- await dbus.close().catch(() => undefined);
386
+ return [
387
+ "pi-caffeinate commands:",
388
+ "/caffeinate — open keep-awake controls",
389
+ "/caffeinate display — keep the system and display awake",
390
+ "/caffeinate sleep — keep the system awake while allowing display sleep",
391
+ "/caffeinate status — show current mode, settings, and inhibitor state",
392
+ "/caffeinate mode — choose a keep-awake mode",
393
+ "/caffeinate stop — release the active inhibitor until the next agent run",
394
+ ].join("\n");
395
+ }
396
+
397
+ async function startInhibitor(ctx: ExtensionContext, sessionGeneration: number, options: { notify?: boolean } = {}) {
398
+ if (state.disabled || hasActiveInhibitor() || state.inhibitorStarting) {
399
+ updateStatus(ctx);
400
+ return;
401
+ }
402
+
403
+ const token = ++inhibitSequence;
404
+ const command = getInhibitorCommand(state.mode);
405
+ const wantsDbus =
406
+ !command?.custom &&
407
+ state.mode === "display" &&
408
+ process.platform === "linux" &&
409
+ (command === undefined || command.addDbusIdleInhibit === true);
410
+ if (!command && !wantsDbus) {
411
+ state.available = false;
412
+ state.lastError = `No supported sleep inhibitor found for ${process.platform}.`;
413
+ ctx.ui.notify(state.lastError, "warning");
414
+ updateStatus(ctx);
415
+ return;
416
+ }
417
+
418
+ state.inhibitorStarting = true;
419
+ state.inhibitWarning = undefined;
420
+ let child: ChildProcess | undefined;
421
+ let childError =
422
+ !command && wantsDbus
423
+ ? "No supported system sleep inhibitor is available; direct system suspend may remain possible."
424
+ : undefined;
425
+ let assembling = true;
426
+ const handleChildFailure = (message: string) => {
427
+ if (!child || token !== inhibitSequence || state.process !== child) return;
428
+ state.process = undefined;
429
+ state.command = undefined;
430
+ if (assembling) {
431
+ childError = message;
432
+ return;
433
+ }
434
+ applyChildFailure(message);
435
+ };
436
+
437
+ if (command) {
438
+ try {
439
+ child = startInhibitorProcess(
440
+ command,
441
+ (error) => handleChildFailure(`${command.description} failed: ${error.message}`),
442
+ (exit) => handleChildFailure(`${command.description} exited unexpectedly (${exit}).`),
443
+ );
444
+ state.process = child;
445
+ state.command = command;
446
+ state.startedAt = Date.now();
447
+ } catch (error) {
448
+ childError = `${command.description}: ${formatError(error)}`;
449
+ }
450
+ }
451
+
452
+ let dbus: DbusScreenSaverClient | undefined;
453
+ let dbusError: string | undefined;
454
+ if (wantsDbus) {
455
+ const pending: PendingDbusStart = { token, controller: new AbortController() };
456
+ state.pendingDbus = pending;
457
+ try {
458
+ const client = await dbusFactory();
459
+ if (!pendingStartIsCurrent(pending, sessionGeneration)) {
460
+ await client.close().catch(() => undefined);
461
+ await cancelPendingStart(token, child, command);
462
+ return;
463
+ }
464
+ pending.client = client;
465
+ await client.inhibit(INHIBIT_REASON, pending.controller.signal);
466
+ if (!pendingStartIsCurrent(pending, sessionGeneration)) {
467
+ await cancelPendingStart(token, child, command);
468
+ return;
469
+ }
470
+ pending.client = undefined;
471
+ state.pendingDbus = undefined;
472
+ dbus = client;
473
+ } catch (error) {
474
+ const stale = !pendingStartIsCurrent(pending, sessionGeneration) || pending.controller.signal.aborted;
475
+ const pendingClient = takePendingDbusClient(pending);
476
+ await pendingClient?.close().catch(() => undefined);
477
+ if (stale) {
478
+ await cancelPendingStart(token, child, command);
479
+ return;
480
+ }
481
+ dbusError = `D-Bus idle inhibit (${SCREENSAVER_BUS_NAME}): ${formatError(error)}`;
482
+ }
483
+ }
484
+
485
+ assembling = false;
486
+ if (startIsStale(token, sessionGeneration)) {
487
+ await cancelPendingStart(token, child, command);
488
+ await dbus?.close().catch(() => undefined);
489
+ return;
490
+ }
491
+
492
+ state.inhibitorStarting = false;
493
+ state.dbus = dbus;
494
+ if (dbus) {
495
+ dbus.setFailureHandler((error) => applyDbusFailure(token, dbus, error));
496
+ if (state.dbus !== dbus) return;
497
+ }
498
+ const failures = [childError, dbusError].filter((failure): failure is string => Boolean(failure));
499
+ if (!hasActiveInhibitor()) {
500
+ state.startedAt = undefined;
501
+ state.command = undefined;
502
+ state.available = false;
503
+ state.lastError = failures.join("; ") || `No supported sleep inhibitor found for ${process.platform}.`;
504
+ ctx.ui.notify(state.lastError, "warning");
505
+ updateStatus(ctx);
506
+ return;
507
+ }
508
+
509
+ state.startedAt ??= Date.now();
510
+ state.available = true;
511
+ state.lastError = undefined;
512
+ state.inhibitWarning = failures.length > 0 ? failures.join("; ") : undefined;
513
+ if (state.inhibitWarning) {
514
+ ctx.ui.notify(`pi-caffeinate is partially active: ${state.inhibitWarning}`, "warning");
515
+ } else if (options.notify !== false) {
516
+ ctx.ui.notify(`Keeping computer awake (${statusModeLabel()}).`, "info");
517
+ }
518
+ updateStatus(ctx);
519
+ }
520
+
521
+ async function stopInhibitor(ctx: ExtensionContext, reason: string, options: { notify?: boolean } = {}) {
522
+ inhibitSequence += 1;
523
+ const child = state.process;
524
+ const dbus = state.dbus;
525
+ const dbusCleanup = state.dbusCleanup;
526
+ const pending = state.pendingDbus;
527
+ const command = state.command;
528
+ const wasStarting = state.inhibitorStarting;
529
+ state.process = undefined;
530
+ state.dbus = undefined;
531
+ state.dbusCleanup = undefined;
532
+ state.pendingDbus = undefined;
533
+ state.command = undefined;
534
+ state.startedAt = undefined;
535
+ state.inhibitWarning = undefined;
536
+ state.inhibitorStarting = false;
537
+ dbus?.setFailureHandler(undefined);
538
+ pending?.controller.abort(new DOMException(`Caffeinate stopped: ${reason}`, "AbortError"));
539
+ const pendingClient = pending ? takePendingDbusClient(pending) : undefined;
540
+ if (child) stopInhibitorProcess(child, command);
541
+ if (!child && !dbus && !pendingClient && !wasStarting) {
542
+ await dbusCleanup;
543
+ return;
544
+ }
545
+ if (options.notify !== false) ctx.ui.notify(`Released pi-caffeinate (${reason}).`, "info");
546
+ await Promise.all([dbusCleanup, pendingClient?.close().catch(() => undefined)]);
547
+ if (!dbus) return;
548
+ try {
549
+ await dbus.uninhibit();
550
+ } catch {
551
+ // Closing the session-bus connection also releases its inhibition cookie.
552
+ }
553
+ await dbus.close().catch(() => undefined);
576
554
  }
577
555
 
578
556
  function applyChildFailure(message: string) {
579
- if (state.dbus) {
580
- state.available = true;
581
- state.lastError = undefined;
582
- state.inhibitWarning = message;
583
- } else {
584
- state.startedAt = undefined;
585
- state.available = false;
586
- state.lastError = message;
587
- state.inhibitWarning = undefined;
588
- }
589
- const ctx = currentSessionContext();
590
- if (!ctx) return;
591
- ctx.ui.notify(message, "warning");
592
- updateStatus(ctx);
557
+ if (state.dbus) {
558
+ state.available = true;
559
+ state.lastError = undefined;
560
+ state.inhibitWarning = message;
561
+ } else {
562
+ state.startedAt = undefined;
563
+ state.available = false;
564
+ state.lastError = message;
565
+ state.inhibitWarning = undefined;
566
+ }
567
+ const ctx = currentSessionContext();
568
+ if (!ctx) return;
569
+ ctx.ui.notify(message, "warning");
570
+ updateStatus(ctx);
593
571
  }
594
572
 
595
573
  function applyDbusFailure(token: number, client: DbusScreenSaverClient, error: Error) {
596
- if (token !== inhibitSequence || state.dbus !== client) return;
597
- client.setFailureHandler(undefined);
598
- state.dbus = undefined;
599
- const message = `D-Bus idle inhibit (${SCREENSAVER_BUS_NAME}) failed: ${formatError(error)}`;
600
- const notification = state.process ? `pi-caffeinate is partially active: ${message}` : message;
601
- if (state.process) {
602
- state.available = true;
603
- state.lastError = undefined;
604
- state.inhibitWarning = message;
605
- } else {
606
- state.startedAt = undefined;
607
- state.command = undefined;
608
- state.available = false;
609
- state.lastError = [state.inhibitWarning, message].filter(Boolean).join("; ");
610
- state.inhibitWarning = undefined;
611
- }
612
- const cleanup = client.close().catch(() => undefined);
613
- state.dbusCleanup = cleanup;
614
- void cleanup.then(() => {
615
- if (state.dbusCleanup === cleanup) state.dbusCleanup = undefined;
616
- });
617
- const ctx = currentSessionContext();
618
- if (!ctx) return;
619
- ctx.ui.notify(notification, "warning");
620
- updateStatus(ctx);
574
+ if (token !== inhibitSequence || state.dbus !== client) return;
575
+ client.setFailureHandler(undefined);
576
+ state.dbus = undefined;
577
+ const message = `D-Bus idle inhibit (${SCREENSAVER_BUS_NAME}) failed: ${formatError(error)}`;
578
+ const notification = state.process ? `pi-caffeinate is partially active: ${message}` : message;
579
+ if (state.process) {
580
+ state.available = true;
581
+ state.lastError = undefined;
582
+ state.inhibitWarning = message;
583
+ } else {
584
+ state.startedAt = undefined;
585
+ state.command = undefined;
586
+ state.available = false;
587
+ state.lastError = [state.inhibitWarning, message].filter(Boolean).join("; ");
588
+ state.inhibitWarning = undefined;
589
+ }
590
+ const cleanup = client.close().catch(() => undefined);
591
+ state.dbusCleanup = cleanup;
592
+ void cleanup.then(() => {
593
+ if (state.dbusCleanup === cleanup) state.dbusCleanup = undefined;
594
+ });
595
+ const ctx = currentSessionContext();
596
+ if (!ctx) return;
597
+ ctx.ui.notify(notification, "warning");
598
+ updateStatus(ctx);
621
599
  }
622
600
 
623
601
  function currentSessionContext() {
624
- const session = state.currentSession;
625
- return session?.generation === state.sessionGeneration ? session.ctx : undefined;
602
+ const session = state.currentSession;
603
+ return session?.generation === state.sessionGeneration ? session.ctx : undefined;
626
604
  }
627
605
 
628
606
  function hasActiveInhibitor() {
629
- return Boolean(state.process || state.dbus);
607
+ return Boolean(state.process || state.dbus);
630
608
  }
631
609
 
632
610
  function startIsStale(token: number, sessionGeneration: number) {
633
- return token !== inhibitSequence || sessionGeneration !== state.sessionGeneration;
611
+ return token !== inhibitSequence || sessionGeneration !== state.sessionGeneration;
634
612
  }
635
613
 
636
614
  function pendingStartIsCurrent(pending: PendingDbusStart, sessionGeneration: number) {
637
- return (
638
- state.pendingDbus === pending &&
639
- !pending.controller.signal.aborted &&
640
- !startIsStale(pending.token, sessionGeneration)
641
- );
615
+ return (
616
+ state.pendingDbus === pending &&
617
+ !pending.controller.signal.aborted &&
618
+ !startIsStale(pending.token, sessionGeneration)
619
+ );
642
620
  }
643
621
 
644
622
  function takePendingDbusClient(pending: PendingDbusStart) {
645
- if (state.pendingDbus === pending) state.pendingDbus = undefined;
646
- const client = pending.client;
647
- pending.client = undefined;
648
- return client;
623
+ if (state.pendingDbus === pending) state.pendingDbus = undefined;
624
+ const client = pending.client;
625
+ pending.client = undefined;
626
+ return client;
649
627
  }
650
628
 
651
629
  async function cancelPendingStart(
652
- token: number,
653
- child: ChildProcess | undefined,
654
- command: InhibitorCommand | undefined,
630
+ token: number,
631
+ child: ChildProcess | undefined,
632
+ command: InhibitorCommand | undefined,
655
633
  ) {
656
- if (token !== inhibitSequence) return;
657
- inhibitSequence += 1;
658
- state.inhibitorStarting = false;
659
- const pending = state.pendingDbus?.token === token ? state.pendingDbus : undefined;
660
- state.pendingDbus = undefined;
661
- pending?.controller.abort(new DOMException("Caffeinate start cancelled", "AbortError"));
662
- const pendingClient = pending ? takePendingDbusClient(pending) : undefined;
663
- if (child && state.process === child) {
664
- state.process = undefined;
665
- state.command = undefined;
666
- state.startedAt = undefined;
667
- stopInhibitorProcess(child, command);
668
- }
669
- await pendingClient?.close().catch(() => undefined);
634
+ if (token !== inhibitSequence) return;
635
+ inhibitSequence += 1;
636
+ state.inhibitorStarting = false;
637
+ const pending = state.pendingDbus?.token === token ? state.pendingDbus : undefined;
638
+ state.pendingDbus = undefined;
639
+ pending?.controller.abort(new DOMException("Caffeinate start cancelled", "AbortError"));
640
+ const pendingClient = pending ? takePendingDbusClient(pending) : undefined;
641
+ if (child && state.process === child) {
642
+ state.process = undefined;
643
+ state.command = undefined;
644
+ state.startedAt = undefined;
645
+ stopInhibitorProcess(child, command);
646
+ }
647
+ await pendingClient?.close().catch(() => undefined);
670
648
  }
671
649
 
672
650
  function updateStatus(ctx: ExtensionContext) {
673
- if (state.disabled || state.quiet) {
674
- ctx.ui.setStatus(STATUS_KEY, undefined);
675
- return;
676
- }
651
+ if (state.disabled || state.quiet) {
652
+ ctx.ui.setStatus(STATUS_KEY, undefined);
653
+ return;
654
+ }
677
655
 
678
- if (hasActiveInhibitor()) {
679
- ctx.ui.setStatus(STATUS_KEY, withDeprecatedIcon(statusModeLabel()));
680
- return;
681
- }
656
+ if (hasActiveInhibitor()) {
657
+ ctx.ui.setStatus(STATUS_KEY, withDeprecatedIcon(statusModeLabel()));
658
+ return;
659
+ }
682
660
 
683
- if (!state.available) {
684
- ctx.ui.setStatus(STATUS_KEY, withDeprecatedIcon("unavailable"));
685
- return;
686
- }
661
+ if (!state.available) {
662
+ ctx.ui.setStatus(STATUS_KEY, withDeprecatedIcon("unavailable"));
663
+ return;
664
+ }
687
665
 
688
- ctx.ui.setStatus(STATUS_KEY, undefined);
666
+ ctx.ui.setStatus(STATUS_KEY, undefined);
689
667
  }
690
668
 
691
669
  function describeState() {
692
- const customCommand = hasCustomCommand();
693
- const lines = [
694
- `Mode: ${formatMode(state.mode)}${customCommand ? " (overridden by custom command)" : ""}`,
695
- `Quiet mode: ${state.quiet ? "enabled" : "disabled"}`,
696
- `Settings: ${settingsFilePath()}`,
697
- ];
698
-
699
- if (customCommand) lines.push("Custom command: PI_CAFFEINATE_COMMAND overrides the saved mode.");
700
- if (state.settingsNotice) lines.push(`Settings note: ${state.settingsNotice}`);
701
- if (state.settingsError) lines.push(`Settings warning: ${state.settingsError}`);
702
- if (state.disabled) {
703
- lines.unshift("pi-caffeinate is disabled by PI_CAFFEINATE_DISABLED.");
704
- return lines.join("\n");
705
- }
706
-
707
- if (hasActiveInhibitor()) {
708
- const seconds = state.startedAt ? Math.round((Date.now() - state.startedAt) / 1000) : 0;
709
- const activeParts: string[] = [];
710
- if (state.dbus) activeParts.push(`D-Bus idle inhibit (${SCREENSAVER_BUS_NAME})`);
711
- if (state.process && state.command) activeParts.push(state.command.description);
712
- lines.unshift(
713
- `pi-caffeinate is active using ${activeParts.join(" + ") || "an inhibitor"} for ${seconds}s.`,
714
- );
715
- if (state.inhibitWarning) lines.push(`Inhibitor warning: ${state.inhibitWarning}`);
716
- return lines.join("\n");
717
- }
718
-
719
- if (!state.available) {
720
- lines.unshift(`pi-caffeinate is unavailable: ${state.lastError ?? "unknown reason"}`);
721
- return lines.join("\n");
722
- }
723
-
724
- lines.unshift(
725
- "pi-caffeinate is idle and will keep the computer awake during the next agent run.",
726
- );
727
- return lines.join("\n");
670
+ const customCommand = hasCustomCommand();
671
+ const lines = [
672
+ `Mode: ${formatMode(state.mode)}${customCommand ? " (overridden by custom command)" : ""}`,
673
+ `Quiet mode: ${state.quiet ? "enabled" : "disabled"}`,
674
+ `Settings: ${settingsFilePath()}`,
675
+ ];
676
+
677
+ if (customCommand) lines.push("Custom command: PI_CAFFEINATE_COMMAND overrides the saved mode.");
678
+ if (state.settingsNotice) lines.push(`Settings note: ${state.settingsNotice}`);
679
+ if (state.settingsError) lines.push(`Settings warning: ${state.settingsError}`);
680
+ if (state.disabled) {
681
+ lines.unshift("pi-caffeinate is disabled by PI_CAFFEINATE_DISABLED.");
682
+ return lines.join("\n");
683
+ }
684
+
685
+ if (hasActiveInhibitor()) {
686
+ const seconds = state.startedAt ? Math.round((Date.now() - state.startedAt) / 1000) : 0;
687
+ const activeParts: string[] = [];
688
+ if (state.dbus) activeParts.push(`D-Bus idle inhibit (${SCREENSAVER_BUS_NAME})`);
689
+ if (state.process && state.command) activeParts.push(state.command.description);
690
+ lines.unshift(`pi-caffeinate is active using ${activeParts.join(" + ") || "an inhibitor"} for ${seconds}s.`);
691
+ if (state.inhibitWarning) lines.push(`Inhibitor warning: ${state.inhibitWarning}`);
692
+ return lines.join("\n");
693
+ }
694
+
695
+ if (!state.available) {
696
+ lines.unshift(`pi-caffeinate is unavailable: ${state.lastError ?? "unknown reason"}`);
697
+ return lines.join("\n");
698
+ }
699
+
700
+ lines.unshift("pi-caffeinate is idle and will keep the computer awake during the next agent run.");
701
+ return lines.join("\n");
728
702
  }
729
703
 
730
704
  function statusLevel() {
731
- return state.available && !state.settingsError && !state.inhibitWarning ? "info" : "warning";
705
+ return state.available && !state.settingsError && !state.inhibitWarning ? "info" : "warning";
732
706
  }
733
707
 
734
708
  function statusModeLabel() {
735
- if (state.command?.custom) return "custom";
736
- return formatMode(state.mode);
709
+ if (state.command?.custom) return "custom";
710
+ return formatMode(state.mode);
737
711
  }
738
712
 
739
713
  async function ensureSettingsLoaded(ctx: ExtensionContext, generation: number) {
740
- if (state.disabled || state.settingsLoaded) return;
741
- await loadSettingsIntoState(ctx, generation);
714
+ if (state.disabled || state.settingsLoaded) return;
715
+ await loadSettingsIntoState(ctx, generation);
742
716
  }
743
717
 
744
718
  async function loadSettingsIntoState(ctx: ExtensionContext, generation: number) {
745
- if (state.disabled) {
746
- state.settingsLoaded = true;
747
- state.settingsError = undefined;
748
- state.quiet = false;
749
- return;
750
- }
751
-
752
- const settings = await loadSettings();
753
- if (generation !== state.sessionGeneration) return;
754
- state.settingsLoaded = true;
755
- state.settingsError = undefined;
756
- if (settings.notice) {
757
- state.settingsNotice = settings.notice;
758
- ctx.ui.notify(settings.notice, "warning");
759
- }
760
-
761
- if (settings.kind === "loaded") {
762
- state.mode = settings.settings.mode;
763
- state.quiet = settings.settings.quiet;
764
- return;
765
- }
766
-
767
- state.mode = DEFAULT_MODE;
768
- state.quiet = false;
769
- if (settings.kind === "invalid") {
770
- state.settingsError = settings.reason;
771
- ctx.ui.notify(
772
- `pi-caffeinate settings ignored: ${settings.reason}; using ${formatMode(DEFAULT_MODE)} mode.`,
773
- "warning",
774
- );
775
- }
719
+ if (state.disabled) {
720
+ state.settingsLoaded = true;
721
+ state.settingsError = undefined;
722
+ state.quiet = false;
723
+ return;
724
+ }
725
+
726
+ const settings = await loadSettings();
727
+ if (generation !== state.sessionGeneration) return;
728
+ state.settingsLoaded = true;
729
+ state.settingsError = undefined;
730
+ if (settings.notice) {
731
+ state.settingsNotice = settings.notice;
732
+ ctx.ui.notify(settings.notice, "warning");
733
+ }
734
+
735
+ if (settings.kind === "loaded") {
736
+ state.mode = settings.settings.mode;
737
+ state.quiet = settings.settings.quiet;
738
+ return;
739
+ }
740
+
741
+ state.mode = DEFAULT_MODE;
742
+ state.quiet = false;
743
+ if (settings.kind === "invalid") {
744
+ state.settingsError = settings.reason;
745
+ ctx.ui.notify(
746
+ `pi-caffeinate settings ignored: ${settings.reason}; using ${formatMode(DEFAULT_MODE)} mode.`,
747
+ "warning",
748
+ );
749
+ }
776
750
  }
777
751
 
778
752
  function formatError(error: unknown) {
779
- return error instanceof Error ? error.message : String(error);
753
+ return error instanceof Error ? error.message : String(error);
780
754
  }
781
755
 
782
756
  function isDisabled() {
783
- const value = process.env.PI_CAFFEINATE_DISABLED?.trim().toLowerCase();
784
- return value ? DISABLED_VALUES.has(value) : false;
757
+ const value = process.env.PI_CAFFEINATE_DISABLED?.trim().toLowerCase();
758
+ return value ? DISABLED_VALUES.has(value) : false;
785
759
  }
786
760
 
787
761
  function hasCustomCommand() {
788
- return Boolean(process.env.PI_CAFFEINATE_COMMAND?.trim());
762
+ return Boolean(process.env.PI_CAFFEINATE_COMMAND?.trim());
789
763
  }
790
764
 
791
765
  function withDeprecatedIcon(text: string) {
792
- const icon = process.env.PI_CAFFEINATE_ICON?.trim();
793
- return icon ? `${icon} ${text}` : text;
766
+ const icon = process.env.PI_CAFFEINATE_ICON?.trim();
767
+ return icon ? `${icon} ${text}` : text;
794
768
  }
795
769
 
796
770
  function warnDeprecatedIcon(ctx: ExtensionContext) {
797
- if (state.iconWarningShown || !process.env.PI_CAFFEINATE_ICON?.trim()) return;
798
- state.iconWarningShown = true;
799
- ctx.ui.notify(
800
- "PI_CAFFEINATE_ICON is deprecated but still works for now. If you use @narumitw/pi-statusline, move it to pi-statusline.json (extensionStatusIcons.caffeinate).",
801
- "warning",
802
- );
771
+ if (state.iconWarningShown || !process.env.PI_CAFFEINATE_ICON?.trim()) return;
772
+ state.iconWarningShown = true;
773
+ ctx.ui.notify(
774
+ "PI_CAFFEINATE_ICON is deprecated but still works for now. If you use @narumitw/pi-statusline, move it to pi-statusline.json (extensionStatusIcons.caffeinate).",
775
+ "warning",
776
+ );
803
777
  }
804
778
 
805
779
  export {
806
- formatMode,
807
- getInhibitorCommand,
808
- splitCommand,
809
- windowsInhibitorScript,
780
+ formatMode,
781
+ getInhibitorCommand,
782
+ splitCommand,
783
+ windowsInhibitorScript,
810
784
  } from "./inhibitors.js";
811
785
  export { normalizeCaffeinateSettings } from "./settings.js";