@oh-my-pi/pi-coding-agent 15.12.4 → 15.13.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.
Files changed (291) hide show
  1. package/CHANGELOG.md +304 -6
  2. package/dist/cli.js +1015 -881
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/types.d.ts +1 -1
  8. package/dist/types/cli/args.d.ts +19 -1
  9. package/dist/types/cli/session-picker.d.ts +1 -1
  10. package/dist/types/cli/setup-cli.d.ts +1 -1
  11. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  12. package/dist/types/collab/protocol.d.ts +1 -1
  13. package/dist/types/commands/say.d.ts +24 -0
  14. package/dist/types/config/keybindings.d.ts +3 -3
  15. package/dist/types/config/model-registry.d.ts +10 -0
  16. package/dist/types/config/models-config-schema.d.ts +12 -0
  17. package/dist/types/config/models-config.d.ts +8 -2
  18. package/dist/types/config/settings-schema.d.ts +261 -58
  19. package/dist/types/export/html/index.d.ts +2 -1
  20. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  22. package/dist/types/extensibility/extensions/types.d.ts +47 -1
  23. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  25. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  26. package/dist/types/extensibility/shared-events.d.ts +1 -1
  27. package/dist/types/extensibility/skills.d.ts +10 -0
  28. package/dist/types/goals/guided-setup.d.ts +18 -0
  29. package/dist/types/goals/state.d.ts +1 -1
  30. package/dist/types/hindsight/transcript.d.ts +1 -1
  31. package/dist/types/index.d.ts +5 -0
  32. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  33. package/dist/types/main.d.ts +4 -3
  34. package/dist/types/mcp/startup-events.d.ts +11 -0
  35. package/dist/types/memories/index.d.ts +7 -0
  36. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  37. package/dist/types/mnemopi/config.d.ts +4 -4
  38. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  39. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  40. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  41. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  42. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  43. package/dist/types/modes/components/session-selector.d.ts +1 -1
  44. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  45. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  46. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  47. package/dist/types/modes/components/usage-row.d.ts +3 -0
  48. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  49. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  50. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  51. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  52. package/dist/types/modes/image-references.d.ts +6 -0
  53. package/dist/types/modes/interactive-mode.d.ts +27 -3
  54. package/dist/types/modes/magic-keywords.d.ts +13 -1
  55. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  56. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  57. package/dist/types/modes/runtime-init.d.ts +4 -0
  58. package/dist/types/modes/theme/theme.d.ts +13 -2
  59. package/dist/types/modes/types.d.ts +8 -2
  60. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  61. package/dist/types/registry/agent-registry.d.ts +17 -0
  62. package/dist/types/secrets/obfuscator.d.ts +1 -1
  63. package/dist/types/session/agent-session.d.ts +14 -2
  64. package/dist/types/session/indexed-session-storage.d.ts +3 -4
  65. package/dist/types/session/session-context.d.ts +39 -0
  66. package/dist/types/session/session-entries.d.ts +159 -0
  67. package/dist/types/session/session-listing.d.ts +69 -0
  68. package/dist/types/session/session-loader.d.ts +16 -0
  69. package/dist/types/session/session-manager.d.ts +82 -474
  70. package/dist/types/session/session-migrations.d.ts +12 -0
  71. package/dist/types/session/session-paths.d.ts +25 -0
  72. package/dist/types/session/session-persistence.d.ts +8 -0
  73. package/dist/types/session/session-storage.d.ts +11 -12
  74. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  75. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  76. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  77. package/dist/types/stt/asr-client.d.ts +90 -0
  78. package/dist/types/stt/asr-protocol.d.ts +97 -0
  79. package/dist/types/stt/asr-worker.d.ts +2 -0
  80. package/dist/types/stt/downloader.d.ts +38 -0
  81. package/dist/types/stt/endpointer.d.ts +59 -0
  82. package/dist/types/stt/index.d.ts +5 -1
  83. package/dist/types/stt/models.d.ts +120 -0
  84. package/dist/types/stt/recorder.d.ts +17 -0
  85. package/dist/types/stt/stt-controller.d.ts +6 -0
  86. package/dist/types/stt/transcriber.d.ts +5 -7
  87. package/dist/types/stt/wav.d.ts +29 -0
  88. package/dist/types/system-prompt.d.ts +4 -0
  89. package/dist/types/task/executor.d.ts +2 -0
  90. package/dist/types/task/index.d.ts +9 -1
  91. package/dist/types/task/types.d.ts +36 -0
  92. package/dist/types/tools/bash.d.ts +2 -2
  93. package/dist/types/tools/eval-render.d.ts +1 -1
  94. package/dist/types/tools/index.d.ts +11 -1
  95. package/dist/types/tools/irc.d.ts +1 -0
  96. package/dist/types/tools/learn.d.ts +51 -0
  97. package/dist/types/tools/manage-skill.d.ts +40 -0
  98. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  99. package/dist/types/tools/renderers.d.ts +7 -11
  100. package/dist/types/tools/ssh.d.ts +1 -1
  101. package/dist/types/tools/todo.d.ts +1 -1
  102. package/dist/types/tools/tts.d.ts +25 -0
  103. package/dist/types/tools/write.d.ts +1 -1
  104. package/dist/types/tts/downloader.d.ts +20 -0
  105. package/dist/types/tts/index.d.ts +8 -0
  106. package/dist/types/tts/models.d.ts +82 -0
  107. package/dist/types/tts/player.d.ts +32 -0
  108. package/dist/types/tts/runtime.d.ts +6 -0
  109. package/dist/types/tts/streaming-player.d.ts +41 -0
  110. package/dist/types/tts/tts-client.d.ts +93 -0
  111. package/dist/types/tts/tts-protocol.d.ts +95 -0
  112. package/dist/types/tts/tts-worker.d.ts +2 -0
  113. package/dist/types/tts/vocalizer.d.ts +41 -0
  114. package/dist/types/tts/wav.d.ts +8 -0
  115. package/dist/types/utils/tool-choice.d.ts +8 -0
  116. package/dist/types/utils/tools-manager.d.ts +2 -1
  117. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  118. package/dist/types/web/scrapers/github.d.ts +1 -1
  119. package/package.json +15 -14
  120. package/src/async/job-manager.ts +49 -0
  121. package/src/autolearn/controller.ts +139 -0
  122. package/src/autolearn/managed-skills.ts +257 -0
  123. package/src/autoresearch/state.ts +1 -1
  124. package/src/autoresearch/types.ts +1 -1
  125. package/src/cli/args.ts +56 -2
  126. package/src/cli/session-picker.ts +2 -1
  127. package/src/cli/setup-cli.ts +148 -47
  128. package/src/cli/setup-model-picker.ts +43 -0
  129. package/src/cli-commands.ts +1 -0
  130. package/src/cli.ts +45 -13
  131. package/src/collab/host.ts +1 -1
  132. package/src/collab/protocol.ts +1 -1
  133. package/src/commands/say.ts +102 -0
  134. package/src/commands/setup.ts +1 -1
  135. package/src/commit/agentic/tools/analyze-file.ts +3 -0
  136. package/src/config/keybindings.ts +2 -2
  137. package/src/config/model-discovery.ts +11 -5
  138. package/src/config/model-registry.ts +64 -9
  139. package/src/config/models-config-schema.ts +4 -1
  140. package/src/config/models-config.ts +2 -1
  141. package/src/config/settings-schema.ts +248 -32
  142. package/src/config/settings.ts +10 -0
  143. package/src/discovery/builtin.ts +23 -1
  144. package/src/discovery/claude-plugins.ts +44 -5
  145. package/src/discovery/helpers.ts +41 -1
  146. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  147. package/src/eval/js/shared/prelude.txt +69 -17
  148. package/src/export/html/index.ts +3 -6
  149. package/src/extensibility/extensions/model-api.ts +41 -0
  150. package/src/extensibility/extensions/runner.ts +4 -0
  151. package/src/extensibility/extensions/types.ts +52 -1
  152. package/src/extensibility/extensions/wrapper.ts +41 -5
  153. package/src/extensibility/hooks/index.ts +2 -1
  154. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  155. package/src/extensibility/plugins/loader.ts +30 -19
  156. package/src/extensibility/plugins/manager.ts +221 -90
  157. package/src/extensibility/shared-events.ts +1 -1
  158. package/src/extensibility/skills.ts +96 -15
  159. package/src/goals/guided-setup.ts +133 -0
  160. package/src/goals/state.ts +1 -1
  161. package/src/hindsight/transcript.ts +1 -1
  162. package/src/index.ts +5 -0
  163. package/src/internal-urls/docs-index.generated.ts +10 -10
  164. package/src/internal-urls/history-protocol.ts +1 -1
  165. package/src/internal-urls/local-protocol.ts +29 -7
  166. package/src/main.ts +27 -7
  167. package/src/mcp/startup-events.ts +21 -0
  168. package/src/mcp/transports/stdio.ts +2 -1
  169. package/src/memories/index.ts +146 -11
  170. package/src/memory-backend/local-backend.ts +11 -5
  171. package/src/mnemopi/backend.ts +1 -0
  172. package/src/mnemopi/config.ts +26 -10
  173. package/src/modes/acp/acp-agent.ts +3 -5
  174. package/src/modes/components/agent-hub.ts +49 -4
  175. package/src/modes/components/assistant-message.ts +4 -37
  176. package/src/modes/components/compaction-summary-message.ts +125 -26
  177. package/src/modes/components/custom-editor.test.ts +96 -0
  178. package/src/modes/components/custom-editor.ts +164 -8
  179. package/src/modes/components/session-selector.ts +1 -1
  180. package/src/modes/components/settings-defs.ts +7 -0
  181. package/src/modes/components/tool-execution.ts +82 -43
  182. package/src/modes/components/transcript-container.ts +70 -1
  183. package/src/modes/components/tree-selector.ts +1 -1
  184. package/src/modes/components/usage-row.ts +18 -0
  185. package/src/modes/components/user-message.ts +4 -2
  186. package/src/modes/controllers/command-controller.ts +14 -4
  187. package/src/modes/controllers/event-controller.ts +78 -11
  188. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  189. package/src/modes/controllers/input-controller.ts +258 -27
  190. package/src/modes/controllers/selector-controller.ts +12 -2
  191. package/src/modes/gradient-highlight.ts +21 -9
  192. package/src/modes/image-references.ts +20 -0
  193. package/src/modes/interactive-mode.ts +286 -40
  194. package/src/modes/magic-keywords.ts +27 -5
  195. package/src/modes/rpc/rpc-mode.ts +146 -14
  196. package/src/modes/rpc/rpc-subagents.ts +2 -2
  197. package/src/modes/rpc/rpc-types.ts +8 -2
  198. package/src/modes/runtime-init.ts +28 -3
  199. package/src/modes/theme/theme.ts +98 -50
  200. package/src/modes/types.ts +6 -2
  201. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  202. package/src/modes/utils/ui-helpers.ts +34 -6
  203. package/src/priority.json +5 -1
  204. package/src/prompts/agents/task.md +1 -0
  205. package/src/prompts/goals/guided-goal-interview.md +8 -0
  206. package/src/prompts/goals/guided-goal-system.md +12 -0
  207. package/src/prompts/memories/read-path.md +6 -0
  208. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  209. package/src/prompts/system/autolearn-guidance.md +7 -0
  210. package/src/prompts/system/autolearn-nudge.md +3 -0
  211. package/src/prompts/system/eager-task.md +7 -0
  212. package/src/prompts/system/eager-todo.md +11 -6
  213. package/src/prompts/system/subagent-system-prompt.md +4 -0
  214. package/src/prompts/system/system-prompt.md +10 -5
  215. package/src/prompts/system/title-marker-instruction.md +1 -0
  216. package/src/prompts/system/title-system-marker.md +16 -0
  217. package/src/prompts/tools/job.md +1 -0
  218. package/src/prompts/tools/learn.md +7 -0
  219. package/src/prompts/tools/manage-skill.md +9 -0
  220. package/src/prompts/tools/task.md +3 -0
  221. package/src/registry/agent-registry.ts +30 -0
  222. package/src/sdk.ts +88 -24
  223. package/src/secrets/obfuscator.ts +1 -1
  224. package/src/session/agent-session.ts +209 -87
  225. package/src/session/history-storage.ts +2 -2
  226. package/src/session/indexed-session-storage.ts +7 -17
  227. package/src/session/session-context.ts +352 -0
  228. package/src/session/session-entries.ts +194 -0
  229. package/src/session/session-listing.ts +588 -0
  230. package/src/session/session-loader.ts +106 -0
  231. package/src/session/session-manager.ts +933 -3145
  232. package/src/session/session-migrations.ts +78 -0
  233. package/src/session/session-paths.ts +193 -0
  234. package/src/session/session-persistence.ts +131 -0
  235. package/src/session/session-storage.ts +91 -50
  236. package/src/session/snapcompact-inline.ts +21 -1
  237. package/src/session/snapcompact-savings-journal.ts +113 -0
  238. package/src/session/tool-choice-queue.ts +23 -11
  239. package/src/slash-commands/builtin-registry.ts +25 -3
  240. package/src/stt/asr-client.ts +520 -0
  241. package/src/stt/asr-protocol.ts +65 -0
  242. package/src/stt/asr-worker.ts +790 -0
  243. package/src/stt/downloader.ts +107 -47
  244. package/src/stt/endpointer.ts +259 -0
  245. package/src/stt/index.ts +5 -1
  246. package/src/stt/models.ts +150 -0
  247. package/src/stt/recorder.ts +247 -60
  248. package/src/stt/stt-controller.ts +201 -22
  249. package/src/stt/transcriber.ts +37 -68
  250. package/src/stt/wav.ts +173 -0
  251. package/src/system-prompt.ts +8 -0
  252. package/src/task/agents.ts +1 -2
  253. package/src/task/executor.ts +49 -15
  254. package/src/task/index.ts +60 -6
  255. package/src/task/render.ts +83 -8
  256. package/src/task/types.ts +53 -0
  257. package/src/tools/ask.ts +8 -0
  258. package/src/tools/bash.ts +4 -3
  259. package/src/tools/eval-render.ts +4 -3
  260. package/src/tools/index.ts +40 -4
  261. package/src/tools/irc.ts +10 -2
  262. package/src/tools/job.ts +14 -2
  263. package/src/tools/learn.ts +144 -0
  264. package/src/tools/manage-skill.ts +104 -0
  265. package/src/tools/plan-mode-guard.ts +53 -19
  266. package/src/tools/renderers.ts +7 -11
  267. package/src/tools/ssh.ts +4 -3
  268. package/src/tools/todo.ts +1 -1
  269. package/src/tools/tts.ts +203 -92
  270. package/src/tools/write.ts +18 -2
  271. package/src/tts/downloader.ts +64 -0
  272. package/src/tts/index.ts +8 -0
  273. package/src/tts/models.ts +137 -0
  274. package/src/tts/player.ts +137 -0
  275. package/src/tts/runtime.ts +21 -0
  276. package/src/tts/streaming-player.ts +266 -0
  277. package/src/tts/tts-client.ts +647 -0
  278. package/src/tts/tts-protocol.ts +60 -0
  279. package/src/tts/tts-worker.ts +497 -0
  280. package/src/tts/vocalizer.ts +162 -0
  281. package/src/tts/wav.ts +58 -0
  282. package/src/utils/title-generator.ts +48 -5
  283. package/src/utils/tool-choice.ts +16 -0
  284. package/src/utils/tools-manager.test.ts +25 -0
  285. package/src/utils/tools-manager.ts +19 -1
  286. package/src/web/scrapers/github.ts +96 -0
  287. package/src/web/search/index.ts +13 -0
  288. package/src/web/search/providers/searxng.ts +13 -1
  289. package/dist/types/stt/setup.d.ts +0 -18
  290. package/src/stt/setup.ts +0 -52
  291. package/src/stt/transcribe.py +0 -70
@@ -1,7 +1,5 @@
1
- import type { AssistantMessage, ImageContent, Usage } from "@oh-my-pi/pi-ai";
1
+ import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
2
2
  import { Container, Image, type ImageBudget, ImageProtocol, Markdown, Spacer, TERMINAL, Text } from "@oh-my-pi/pi-tui";
3
- import { formatNumber } from "@oh-my-pi/pi-utils";
4
- import { settings } from "../../config/settings";
5
3
  import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
6
4
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
7
5
  import { resolveAbortLabel, shouldRenderAbortReason } from "../../session/messages";
@@ -24,7 +22,6 @@ export class AssistantMessageComponent extends Container {
24
22
  #contentContainer: Container;
25
23
  #lastMessage?: AssistantMessage;
26
24
  #toolImagesByCallId = new Map<string, ImageContent[]>();
27
- #usageInfo?: Usage;
28
25
  #convertedKittyImages = new Map<string, ImageContent>();
29
26
  #kittyConversionsInFlight = new Set<string>();
30
27
  #transcriptBlockFinalized: boolean;
@@ -40,11 +37,9 @@ export class AssistantMessageComponent extends Container {
40
37
  /**
41
38
  * Monotonic content version reported to the transcript container via
42
39
  * {@link getTranscriptBlockVersion}. Bumped by {@link updateContent} — the
43
- * choke point every mutator funnels through, including the post-finalize
44
- * ones: `setErrorPinned(false)` restoring the inline error at the next
45
- * turn's `agent_start`, late tool-result images, async Kitty conversions,
46
- * and `setUsageInfo`. Without it, the container's committed-scrollback
47
- * bypass would replay this block's pre-mutation bytes forever.
40
+ * choke point every mutator funnels through, including post-finalize changes
41
+ * such as `setErrorPinned(false)` restoring the inline error at the next
42
+ * turn's `agent_start`, late tool-result images, and async Kitty conversions.
48
43
  */
49
44
  #blockVersion = 0;
50
45
  /** Whether the last updateContent carried an in-flight streaming partial; such
@@ -185,13 +180,6 @@ export class AssistantMessageComponent extends Container {
185
180
  }
186
181
  }
187
182
 
188
- setUsageInfo(usage: Usage): void {
189
- this.#usageInfo = usage;
190
- if (this.#lastMessage) {
191
- this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
192
- }
193
- }
194
-
195
183
  #renderToolImages(): void {
196
184
  const imageEntries = Array.from(this.#toolImagesByCallId.entries()).flatMap(([toolCallId, images]) =>
197
185
  images.map((image, index) => ({ image, key: `${toolCallId}:${index}` })),
@@ -256,12 +244,6 @@ export class AssistantMessageComponent extends Container {
256
244
  parts.push(`O:${content.type}`);
257
245
  }
258
246
  }
259
- if (settings.get("display.showTokenUsage") && this.#usageInfo) {
260
- const u = this.#usageInfo;
261
- parts.push(`u:${u.input + u.cacheWrite}:${u.output}:${u.cacheRead}`);
262
- } else {
263
- parts.push("u:");
264
- }
265
247
  return parts.join("|");
266
248
  }
267
249
 
@@ -416,21 +398,6 @@ export class AssistantMessageComponent extends Container {
416
398
  ) {
417
399
  this.#appendErrorBlock(message.errorMessage);
418
400
  }
419
-
420
- // Token usage metadata
421
- if (settings.get("display.showTokenUsage") && this.#usageInfo) {
422
- const usage = this.#usageInfo;
423
- const totalInput = usage.input + usage.cacheWrite;
424
- const parts: string[] = [];
425
- parts.push(`${theme.icon.input} ${formatNumber(totalInput)}`);
426
- parts.push(`${theme.icon.output} ${formatNumber(usage.output)}`);
427
- if (usage.cacheRead > 0) {
428
- parts.push(`cache: ${formatNumber(usage.cacheRead)}`);
429
- }
430
- this.#contentContainer.addChild(new Spacer(1));
431
- this.#contentContainer.addChild(new Text(theme.fg("dim", parts.join(" ")), 1, 0));
432
- }
433
-
434
401
  // Store fast-path state for next call
435
402
  if (shouldCapture) {
436
403
  this.#fastPathItems = captureItems;
@@ -1,22 +1,18 @@
1
1
  import { Box, type Component, Markdown } from "@oh-my-pi/pi-tui";
2
2
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
3
- import type { CompactionSummaryMessage } from "../../session/messages";
3
+ import type { CompactionSummaryMessage, CustomMessage } from "../../session/messages";
4
4
 
5
- /**
6
- * Compaction point in the transcript, rendered as a slim horizontal divider:
7
- *
8
- * ──────── 📷 compacted · ctrl+o ────────
9
- *
10
- * The conversation above the divider stays visible (display transcript keeps
11
- * full history); only the LLM context was reset. Expanding (ctrl+o) reveals
12
- * the compaction summary below the divider.
13
- */
14
- export class CompactionSummaryMessageComponent implements Component {
5
+ interface SummaryDividerOptions {
6
+ label: () => string;
7
+ detailMarkdown: () => string;
8
+ }
9
+
10
+ class SummaryDividerComponent implements Component {
15
11
  #expanded = false;
16
12
  #cache?: { width: number; lines: string[] };
17
13
  #detail?: Box;
18
14
 
19
- constructor(private readonly message: CompactionSummaryMessage) {}
15
+ constructor(private readonly options: SummaryDividerOptions) {}
20
16
 
21
17
  setExpanded(expanded: boolean): void {
22
18
  if (this.#expanded === expanded) return;
@@ -44,7 +40,7 @@ export class CompactionSummaryMessageComponent implements Component {
44
40
 
45
41
  #divider(width: number): string {
46
42
  const rule = theme.tree.horizontal;
47
- const label = `${theme.icon.camera} compacted`;
43
+ const label = this.options.label();
48
44
  // sep.dot ships pre-padded (" · "); trim so the hint joins with single spaces.
49
45
  const hint = `${theme.sep.dot.trim()} ctrl+o`;
50
46
  const plainWidth = Bun.stringWidth(`${label} ${hint}`, { countAnsiEscapeCodes: false });
@@ -66,22 +62,125 @@ export class CompactionSummaryMessageComponent implements Component {
66
62
  #detailBox(): Box {
67
63
  if (this.#detail) return this.#detail;
68
64
  const box = new Box(1, 1, t => theme.bg("customMessageBg", t));
69
- const tokenStr = this.message.tokensBefore.toLocaleString();
70
- const frameCount = this.message.images?.length ?? 0;
71
- const frameNote =
72
- frameCount > 0 ? `\n\n_${frameCount} snapcompact frame${frameCount === 1 ? "" : "s"} attached_` : "";
73
65
  box.addChild(
74
- new Markdown(
75
- `**Compacted from ${tokenStr} tokens**\n\n${this.message.summary}${frameNote}`,
76
- 0,
77
- 0,
78
- getMarkdownTheme(),
79
- {
80
- color: (text: string) => theme.fg("customMessageText", text),
81
- },
82
- ),
66
+ new Markdown(this.options.detailMarkdown(), 0, 0, getMarkdownTheme(), {
67
+ color: (text: string) => theme.fg("customMessageText", text),
68
+ }),
83
69
  );
84
70
  this.#detail = box;
85
71
  return box;
86
72
  }
87
73
  }
74
+
75
+ /**
76
+ * Compaction point in the transcript, rendered as a slim horizontal divider:
77
+ *
78
+ * ──────── 📷 compacted · ctrl+o ────────
79
+ *
80
+ * The conversation above the divider stays visible (display transcript keeps
81
+ * full history); only the LLM context was reset. Expanding (ctrl+o) reveals
82
+ * the compaction summary below the divider.
83
+ */
84
+ export class CompactionSummaryMessageComponent implements Component {
85
+ #divider: SummaryDividerComponent;
86
+
87
+ constructor(private readonly message: CompactionSummaryMessage) {
88
+ this.#divider = new SummaryDividerComponent({
89
+ label: () => `${theme.icon.camera} compacted`,
90
+ detailMarkdown: () => this.#detailMarkdown(),
91
+ });
92
+ }
93
+
94
+ setExpanded(expanded: boolean): void {
95
+ this.#divider.setExpanded(expanded);
96
+ }
97
+
98
+ invalidate(): void {
99
+ this.#divider.invalidate();
100
+ }
101
+
102
+ render(width: number): readonly string[] {
103
+ return this.#divider.render(width);
104
+ }
105
+
106
+ #detailMarkdown(): string {
107
+ const tokenStr = this.message.tokensBefore.toLocaleString();
108
+ const frameCount = this.message.images?.length ?? 0;
109
+ const frameNote =
110
+ frameCount > 0 ? `\n\n_${frameCount} snapcompact frame${frameCount === 1 ? "" : "s"} attached_` : "";
111
+ return `**Compacted from ${tokenStr} tokens**\n\n${this.message.summary}${frameNote}`;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Handoff is a compaction strategy too, but it is persisted as a custom message
117
+ * so the LLM sees the handoff-specific developer context. Render it with the
118
+ * same divider affordance as `/compact` instead of the generic `[handoff]` box.
119
+ */
120
+ export class HandoffSummaryMessageComponent implements Component {
121
+ #divider: SummaryDividerComponent;
122
+
123
+ constructor(private readonly message: CustomMessage<unknown>) {
124
+ this.#divider = new SummaryDividerComponent({
125
+ label: () => `${theme.icon.context} handoff`,
126
+ detailMarkdown: () => this.#detailMarkdown(),
127
+ });
128
+ }
129
+
130
+ setExpanded(expanded: boolean): void {
131
+ this.#divider.setExpanded(expanded);
132
+ }
133
+
134
+ invalidate(): void {
135
+ this.#divider.invalidate();
136
+ }
137
+
138
+ render(width: number): readonly string[] {
139
+ return this.#divider.render(width);
140
+ }
141
+
142
+ #detailMarkdown(): string {
143
+ const document = extractHandoffDocument(getCustomMessageText(this.message));
144
+ return `**Handoff context**\n\n${document || "_No handoff content._"}`;
145
+ }
146
+ }
147
+
148
+ export function createHandoffSummaryMessageComponent(
149
+ message: CustomMessage<unknown>,
150
+ expanded: boolean,
151
+ ): HandoffSummaryMessageComponent | undefined {
152
+ if (message.customType !== "handoff" || !message.display) return undefined;
153
+ const component = new HandoffSummaryMessageComponent(message);
154
+ component.setExpanded(expanded);
155
+ return component;
156
+ }
157
+
158
+ function getCustomMessageText(message: CustomMessage<unknown>): string {
159
+ if (typeof message.content === "string") return message.content;
160
+ let firstText: string | undefined;
161
+ let parts: string[] | undefined;
162
+ for (const content of message.content) {
163
+ if (content.type !== "text") continue;
164
+ if (firstText === undefined) {
165
+ firstText = content.text;
166
+ continue;
167
+ }
168
+ if (parts === undefined) {
169
+ parts = [firstText];
170
+ }
171
+ parts.push(content.text);
172
+ }
173
+ return parts === undefined ? (firstText ?? "") : parts.join("\n");
174
+ }
175
+
176
+ function extractHandoffDocument(text: string): string {
177
+ const openTag = "<handoff-context>";
178
+ const closeTag = "</handoff-context>";
179
+ const openIndex = text.indexOf(openTag);
180
+ if (openIndex === -1) return text.trim();
181
+
182
+ const contentStart = openIndex + openTag.length;
183
+ const closeIndex = text.indexOf(closeTag, contentStart);
184
+ const document = closeIndex === -1 ? text.slice(contentStart) : text.slice(contentStart, closeIndex);
185
+ return document.trim();
186
+ }
@@ -0,0 +1,96 @@
1
+ import { afterEach, beforeAll, describe, expect, it, vi } from "bun:test";
2
+ import { $ } from "bun";
3
+ import { getEditorTheme, initTheme } from "../theme/theme";
4
+ import { CustomEditor, SPACE_HOLD_RELEASE_MS, SPACE_HOLD_THRESHOLD } from "./custom-editor";
5
+
6
+ function makeEditor() {
7
+ const editor = new CustomEditor(getEditorTheme());
8
+ const events: string[] = [];
9
+ editor.sttHoldEnabled = () => true;
10
+ editor.onSpaceHoldStart = () => events.push("start");
11
+ editor.onSpaceHoldEnd = () => events.push("end");
12
+ return { editor, events };
13
+ }
14
+
15
+ function holdSpace(editor: CustomEditor, count: number): void {
16
+ for (let i = 0; i < count; i++) editor.handleInput(" ");
17
+ }
18
+
19
+ async function decorateInFreshProcess(text: string): Promise<string> {
20
+ const customEditorUrl = new URL("./custom-editor.ts", import.meta.url).href;
21
+ const script = `
22
+ import { CustomEditor } from ${JSON.stringify(customEditorUrl)};
23
+ const editor = new CustomEditor({});
24
+ process.stdout.write(editor.decorateText(${JSON.stringify(text)}));
25
+ `;
26
+ const child = await $`bun -e ${script}`.quiet().nothrow();
27
+ const stdout = child.stdout.toString();
28
+ const stderr = child.stderr.toString();
29
+ if (child.exitCode !== 0) throw new Error(stderr || stdout || `decorate subprocess exited with ${child.exitCode}`);
30
+ return stdout;
31
+ }
32
+
33
+ describe("CustomEditor placeholder decoration", () => {
34
+ it("renders paste placeholders before theme initialization", async () => {
35
+ const output = await decorateInFreshProcess("[Paste #1, +30 lines]");
36
+ expect(output).toBe("[Paste #1, +30 lines]");
37
+ });
38
+
39
+ it("renders image placeholders before theme initialization", async () => {
40
+ const output = await decorateInFreshProcess("[Image #1]");
41
+ expect(output).toBe("[Image #1]");
42
+ });
43
+ });
44
+
45
+ describe("CustomEditor space-hold push-to-talk", () => {
46
+ beforeAll(async () => {
47
+ await initTheme();
48
+ });
49
+
50
+ afterEach(() => {
51
+ vi.useRealTimers();
52
+ });
53
+
54
+ it("inserts spaces normally below the hold threshold", () => {
55
+ const { editor, events } = makeEditor();
56
+ holdSpace(editor, SPACE_HOLD_THRESHOLD);
57
+ expect(editor.getText()).toBe(" ".repeat(SPACE_HOLD_THRESHOLD));
58
+ expect(events).toEqual([]);
59
+ });
60
+
61
+ it("tracks back the space burst and drives the hold lifecycle", () => {
62
+ vi.useFakeTimers();
63
+ const { editor, events } = makeEditor();
64
+ editor.handleInput("h");
65
+ editor.handleInput("i");
66
+ // Crossing the threshold deletes the optimistically-inserted spaces and starts recording,
67
+ // leaving only the pre-burst text behind.
68
+ holdSpace(editor, SPACE_HOLD_THRESHOLD + 1);
69
+ expect(editor.getText()).toBe("hi");
70
+ expect(events).toEqual(["start"]);
71
+ // Continued auto-repeat while the bar is held is swallowed: no spam, no re-trigger.
72
+ holdSpace(editor, 5);
73
+ expect(editor.getText()).toBe("hi");
74
+ expect(events).toEqual(["start"]);
75
+ // An idle gap with no further repeats means the bar was released -> stop + transcribe.
76
+ vi.advanceTimersByTime(SPACE_HOLD_RELEASE_MS + 1);
77
+ expect(events).toEqual(["start", "end"]);
78
+ });
79
+
80
+ it("does not trigger when a non-space breaks the run", () => {
81
+ const { editor, events } = makeEditor();
82
+ holdSpace(editor, SPACE_HOLD_THRESHOLD);
83
+ editor.handleInput("x");
84
+ holdSpace(editor, SPACE_HOLD_THRESHOLD);
85
+ expect(events).toEqual([]);
86
+ expect(editor.getText()).toBe(`${" ".repeat(SPACE_HOLD_THRESHOLD)}x${" ".repeat(SPACE_HOLD_THRESHOLD)}`);
87
+ });
88
+
89
+ it("leaves the space bar typing normally when the gesture is disabled", () => {
90
+ const { editor, events } = makeEditor();
91
+ editor.sttHoldEnabled = () => false;
92
+ holdSpace(editor, SPACE_HOLD_THRESHOLD + 5);
93
+ expect(editor.getText()).toBe(" ".repeat(SPACE_HOLD_THRESHOLD + 5));
94
+ expect(events).toEqual([]);
95
+ });
96
+ });
@@ -1,8 +1,9 @@
1
1
  import { addKeyAliases, canonicalKeyId, Editor, type KeyId, parseKey, parseKittySequence } from "@oh-my-pi/pi-tui";
2
2
  import type { AppKeybinding } from "../../config/keybindings";
3
+ import { isSettingsInitialized, settings } from "../../config/settings";
3
4
  import { imageReferenceHyperlink, PLACEHOLDER_REGEX, renderPlaceholders } from "../image-references";
4
- import { highlightMagicKeywords } from "../magic-keywords";
5
- import { theme } from "../theme/theme";
5
+ import { hasMagicKeyword, highlightMagicKeywords } from "../magic-keywords";
6
+ import { fgOrPlain } from "../theme/theme";
6
7
 
7
8
  type ConfigurableEditorAction = Extract<
8
9
  AppKeybinding,
@@ -61,6 +62,14 @@ const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i;
61
62
  const BRACKETED_IMAGE_PATH_BOUNDARY_REGEX = /\.(?:png|jpe?g|gif|webp)(?=$|["']?\s)/gi;
62
63
  const SHELL_ESCAPED_PATH_CHAR_REGEX = /\\([\\\s'"()[\]{}&;<>|?*!$`])/g;
63
64
 
65
+ /** Plain spaces from one auto-repeat run that trigger the space-hold push-to-talk STT gesture.
66
+ * Holding the space bar makes the terminal emit a burst of spaces; once more than this many land
67
+ * in the editor we treat it as "space held", track them back out, and start recording. */
68
+ export const SPACE_HOLD_THRESHOLD = 5;
69
+ /** Idle gap (ms) after the last repeated space that counts as the space bar being released, ending
70
+ * the push-to-talk recording. Must comfortably exceed the OS key-repeat interval. */
71
+ export const SPACE_HOLD_RELEASE_MS = 250;
72
+
64
73
  function isPastedPathSeparator(char: string | undefined): boolean {
65
74
  return char === undefined || char === " " || char === "\t" || char === "\r" || char === "\n";
66
75
  }
@@ -136,19 +145,85 @@ export class CustomEditor extends Editor {
136
145
  * instead of corrupting `[Paste #1, +30 lines]` into plain text. */
137
146
  override atomicTokenPattern = PLACEHOLDER_REGEX;
138
147
 
148
+ /** Magic-keyword shimmer cadence — drives one editor repaint every 70 ms while
149
+ * a keyword is on screen and the prompt is focused. ~14 frames/s is smooth
150
+ * without flooding the renderer. */
151
+ static readonly SHIMMER_FRAME_MS = 70;
152
+ /** Time for the gradient to sweep one full cycle across each keyword. */
153
+ static readonly SHIMMER_PERIOD_MS = 1800;
154
+
155
+ /** Per-render scratch flag: did any layout line in this render contain a magic
156
+ * keyword that should shimmer? Reset by {@link #scheduleShimmerIfNeeded} each
157
+ * time a frame is queued. */
158
+ #shimmerTimer: ReturnType<typeof setTimeout> | undefined;
159
+ /** Repaint hook the host wires once at construction. Called from the shimmer
160
+ * timer to request the next animation frame. Undefined when nobody is
161
+ * listening (tests, headless callers); the timer chain still self-cleans. */
162
+ #requestShimmerRepaint: (() => void) | undefined;
163
+
139
164
  /** Gradient-highlight the "ultrathink" / "orchestrate" / "workflowz" keywords as the user types
140
165
  * them, skipping any occurrence inside code spans, fenced blocks, or XML sections. Also make
141
- * pasted image placeholders visually distinct and hyperlink them once their blob file exists. */
142
- decorateText = (text: string): string =>
143
- renderPlaceholders(text, {
144
- renderText: value => highlightMagicKeywords(value),
166
+ * pasted image placeholders visually distinct and hyperlink them once their blob file exists.
167
+ * When the editor is focused, the buffer contains a magic keyword, and `magicKeywords.enabled`
168
+ * is on, the gradient shifts every frame to produce a Claude-Code-style shimmer; each render
169
+ * schedules the next frame, so losing focus, deleting the keyword, or flipping the setting
170
+ * stops the animation on its own. The static glow itself runs even when shimmering is gated
171
+ * off, matching existing behavior for the editor and sent bubbles. */
172
+ decorateText = (text: string): string => {
173
+ const animated = this.focused && this.#shimmerEnabled() && hasMagicKeyword(this.getText());
174
+ const phase = animated ? (Date.now() % CustomEditor.SHIMMER_PERIOD_MS) / CustomEditor.SHIMMER_PERIOD_MS : 0;
175
+ if (animated) this.#scheduleShimmerFrame();
176
+ return renderPlaceholders(text, {
177
+ renderText: value => highlightMagicKeywords(value, undefined, phase),
145
178
  renderReference: (value, kind, index) =>
146
179
  kind === "image"
147
180
  ? imageReferenceHyperlink(value, index, this.imageLinks, label =>
148
- theme.fg("accent", `\x1b[1m\x1b[4m${label}\x1b[24m\x1b[22m`),
181
+ fgOrPlain("accent", label, `\x1b[1m\x1b[4m${label}\x1b[24m\x1b[22m`),
149
182
  )
150
- : theme.fg("accent", `\x1b[1m${value}\x1b[22m`),
183
+ : fgOrPlain("accent", value, `\x1b[1m${value}\x1b[22m`),
151
184
  });
185
+ };
186
+
187
+ /** Optional test/host override for the magic-keyword shimmer gate. When
188
+ * defined, takes precedence over the global `magicKeywords.enabled` setting,
189
+ * letting tests assert the gating behaviour without mutating the
190
+ * process-wide Settings singleton (which races with parallel test files —
191
+ * see issue #2582). Production wires this through the host's Settings
192
+ * reader and updates it on the relevant setting change. */
193
+ magicKeywordsEnabledOverride: boolean | undefined;
194
+
195
+ /** Whether the shimmer should advance this frame. Defaults to "on" before
196
+ * settings have initialised (tests, early boot) so the animation does not
197
+ * silently disappear during a race; settings disabling the feature wins
198
+ * once they are loaded. An explicit `magicKeywordsEnabledOverride` overrides
199
+ * both paths. */
200
+ #shimmerEnabled(): boolean {
201
+ if (this.magicKeywordsEnabledOverride !== undefined) return this.magicKeywordsEnabledOverride;
202
+ return isSettingsInitialized() ? settings.get("magicKeywords.enabled") : true;
203
+ }
204
+
205
+ /** Bind the host's render request callback. Idempotent — the host wires this
206
+ * once after construction (and again after `setEditorComponent` swaps the
207
+ * editor). Passing `undefined` clears any pending frame. */
208
+ setShimmerRepaintHandler(handler: (() => void) | undefined): void {
209
+ this.#requestShimmerRepaint = handler;
210
+ if (!handler && this.#shimmerTimer) {
211
+ clearTimeout(this.#shimmerTimer);
212
+ this.#shimmerTimer = undefined;
213
+ }
214
+ }
215
+
216
+ /** Schedule one shimmer frame if none is already pending. The next render
217
+ * decides whether to schedule another, so the chain stops by itself when
218
+ * `focused` flips off or the keyword leaves the buffer. */
219
+ #scheduleShimmerFrame(): void {
220
+ if (this.#shimmerTimer || !this.#requestShimmerRepaint) return;
221
+ this.#shimmerTimer = setTimeout(() => {
222
+ this.#shimmerTimer = undefined;
223
+ this.#requestShimmerRepaint?.();
224
+ }, CustomEditor.SHIMMER_FRAME_MS);
225
+ this.#shimmerTimer.unref?.();
226
+ }
152
227
  onEscape?: () => void;
153
228
  onClear?: () => void;
154
229
  onExit?: () => void;
@@ -178,9 +253,25 @@ export class CustomEditor extends Editor {
178
253
  /** Called when left-arrow is pressed while the editor is empty (cursor necessarily at start). */
179
254
  onLeftAtStart?: () => void;
180
255
 
256
+ /** Fired when a sustained space-bar hold is recognized — the push-to-talk STT start. The
257
+ * optimistically-typed spaces have already been deleted by the time this runs. */
258
+ onSpaceHoldStart?: () => void;
259
+ /** Fired when the held space bar is released (detected as an idle gap with no further repeated
260
+ * spaces) — the push-to-talk STT stop. */
261
+ onSpaceHoldEnd?: () => void;
262
+ /** Gate for the space-hold gesture. Returns false to keep the space bar inserting spaces
263
+ * normally; wired to `stt.enabled` so disabling STT restores plain space behavior. */
264
+ sttHoldEnabled?: () => boolean;
265
+
181
266
  /** Custom key handlers from extensions and non-built-in app actions. */
182
267
  #customKeyHandlers = new Map<KeyId, () => void>();
183
268
  #customMatchKeys = new Map<string, () => void>();
269
+ /** Consecutive plain spaces inserted in the current run; any other key resets it. */
270
+ #spaceRunInserted = 0;
271
+ /** True while a recognized space-hold push-to-talk recording is in progress. */
272
+ #spaceHoldActive = false;
273
+ /** Idle timer that fires `onSpaceHoldEnd` once repeated spaces stop arriving. */
274
+ #spaceHoldTimer: NodeJS.Timeout | undefined;
184
275
  #actionKeys = new Map<ConfigurableEditorAction, KeyId[]>(
185
276
  Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [action as ConfigurableEditorAction, [...keys]]),
186
277
  );
@@ -238,6 +329,68 @@ export class CustomEditor extends Editor {
238
329
  this.#rebuildCustomMatchKeys();
239
330
  }
240
331
 
332
+ #spaceHoldGestureEnabled(): boolean {
333
+ return this.onSpaceHoldStart !== undefined && (this.sttHoldEnabled?.() ?? false) && !this.isShowingAutocomplete();
334
+ }
335
+
336
+ /** Drive the space-hold push-to-talk state machine. Returns true when the gesture consumed the
337
+ * input so it must not reach normal editing. Holding the space bar makes the terminal emit a
338
+ * burst of auto-repeat spaces; once more than {@link SPACE_HOLD_THRESHOLD} of them land we treat
339
+ * it as a hold, delete the spam, and start recording until the repeats stop. */
340
+ #handleSpaceHold(data: string, canonical: string | undefined): boolean {
341
+ const isSpace = canonical === "space";
342
+ if (this.#spaceHoldActive) {
343
+ if (isSpace) {
344
+ // Auto-repeat while held: swallow it and keep the release timer alive.
345
+ this.#armSpaceHoldReleaseTimer();
346
+ return true;
347
+ }
348
+ // Any non-space means the bar was released — stop recording, then let the key through.
349
+ this.#endSpaceHold();
350
+ return false;
351
+ }
352
+ if (!isSpace) {
353
+ this.#spaceRunInserted = 0;
354
+ return false;
355
+ }
356
+ if (!this.#spaceHoldGestureEnabled()) return false;
357
+ // A short tap should still type a normal space, so insert optimistically and count the run.
358
+ super.handleInput(data);
359
+ this.#spaceRunInserted++;
360
+ if (this.#spaceRunInserted > SPACE_HOLD_THRESHOLD) {
361
+ this.deleteBeforeCursor(this.#spaceRunInserted);
362
+ this.#spaceRunInserted = 0;
363
+ this.#beginSpaceHold();
364
+ }
365
+ return true;
366
+ }
367
+
368
+ #beginSpaceHold(): void {
369
+ this.#spaceHoldActive = true;
370
+ this.#armSpaceHoldReleaseTimer();
371
+ this.onSpaceHoldStart?.();
372
+ }
373
+
374
+ #armSpaceHoldReleaseTimer(): void {
375
+ if (this.#spaceHoldTimer) clearTimeout(this.#spaceHoldTimer);
376
+ this.#spaceHoldTimer = setTimeout(() => {
377
+ this.#spaceHoldTimer = undefined;
378
+ this.#endSpaceHold();
379
+ }, SPACE_HOLD_RELEASE_MS);
380
+ this.#spaceHoldTimer.unref?.();
381
+ }
382
+
383
+ #endSpaceHold(): void {
384
+ if (!this.#spaceHoldActive) return;
385
+ this.#spaceHoldActive = false;
386
+ this.#spaceRunInserted = 0;
387
+ if (this.#spaceHoldTimer) {
388
+ clearTimeout(this.#spaceHoldTimer);
389
+ this.#spaceHoldTimer = undefined;
390
+ }
391
+ this.onSpaceHoldEnd?.();
392
+ }
393
+
241
394
  handleInput(data: string): void {
242
395
  const kittyParsed = parseKittySequence(data);
243
396
  if (kittyParsed && (kittyParsed.modifier & 64) !== 0 && this.onCapsLock) {
@@ -267,6 +420,9 @@ export class CustomEditor extends Editor {
267
420
  return;
268
421
  }
269
422
 
423
+ // Space-hold push-to-talk: a sustained space bar starts/stops STT instead of typing spaces.
424
+ if (this.#handleSpaceHold(data, canonical)) return;
425
+
270
426
  if (canonical !== undefined) {
271
427
  // Intercept configured image paste (async - fires and handles result)
272
428
  if (this.#matchesAction(canonical, "app.clipboard.pasteImage") && this.onPasteImage) {
@@ -15,7 +15,7 @@ import {
15
15
  import { formatBytes } from "@oh-my-pi/pi-utils";
16
16
  import { theme } from "../../modes/theme/theme";
17
17
  import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
18
- import type { SessionInfo, SessionStatus } from "../../session/session-manager";
18
+ import type { SessionInfo, SessionStatus } from "../../session/session-listing";
19
19
  import { shortenPath } from "../../tools/render-utils";
20
20
  import { DynamicBorder } from "./dynamic-border";
21
21
  import { HookSelectorComponent } from "./hook-selector";
@@ -90,6 +90,13 @@ const CONDITIONS: Record<string, () => boolean> = {
90
90
  return false;
91
91
  }
92
92
  },
93
+ autolearnActive: () => {
94
+ try {
95
+ return Settings.instance.get("autolearn.enabled") === true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ },
93
100
  autoThinkingActive: () => {
94
101
  try {
95
102
  return Settings.instance.get("defaultThinkingLevel") === "auto";