gentle-pi 4.2.0 → 4.4.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 (334) hide show
  1. package/README.md +340 -138
  2. package/assets/agents/gentle-ai-explore.md +1 -0
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -1
  5. package/assets/design-canvas/canvas.css +648 -0
  6. package/assets/design-canvas/canvas.js +1497 -0
  7. package/assets/design-canvas/index.html +88 -0
  8. package/assets/design-canvas/inspector.js +290 -0
  9. package/assets/orchestrator-delegation-reference.md +163 -0
  10. package/assets/orchestrator-delegation.md +25 -176
  11. package/assets/orchestrator-tracking.md +1 -1
  12. package/assets/orchestrator-verification.md +14 -8
  13. package/assets/orchestrator-writer.md +5 -1
  14. package/assets/orchestrator.md +11 -11
  15. package/bin/engram-guard +1 -1
  16. package/bin/gentle-shell.mjs +237 -139
  17. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  18. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  19. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  20. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  21. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  22. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  23. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  24. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  25. package/docs/assets/diagrams/measured-routing.svg +49 -0
  26. package/docs/assets/diagrams/native-review.svg +52 -12
  27. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  28. package/docs/assets/diagrams/permission-modes.svg +51 -0
  29. package/docs/assets/features/audio-notifications.png +0 -0
  30. package/docs/assets/features/command-palette.png +0 -0
  31. package/docs/bundled-install.md +65 -30
  32. package/docs/delegated-verification.md +16 -8
  33. package/docs/gentle-agents-activity.md +16 -6
  34. package/docs/gentle-music.md +61 -0
  35. package/docs/gentle-shell-uninstall.md +8 -1
  36. package/docs/gentle-shell.md +80 -9
  37. package/docs/install-wizard.md +37 -6
  38. package/docs/permission-modes.md +8 -3
  39. package/docs/readme-reference.md +120 -28
  40. package/docs/session-profile-format.md +26 -0
  41. package/docs/voice.md +259 -0
  42. package/docs/yolo-mode.md +38 -10
  43. package/extensions/ask-user-choice.ts +4 -1
  44. package/extensions/codegraph-tools.ts +68 -6
  45. package/extensions/gentle-agents.ts +270 -48
  46. package/extensions/gentle-ai.ts +490 -113
  47. package/extensions/gentle-design.ts +158 -0
  48. package/extensions/gentle-herdr-activity.ts +2 -3
  49. package/extensions/gentle-music.ts +289 -0
  50. package/extensions/gentle-shell.ts +359 -85
  51. package/extensions/gentle-todo.ts +43 -9
  52. package/extensions/gentle-voice.ts +11 -0
  53. package/extensions/quiet-tools.ts +1 -1
  54. package/extensions/session-scratch.ts +384 -0
  55. package/extensions/skill-registry.ts +63 -8
  56. package/extensions/startup-banner.ts +2 -3
  57. package/lib/agent-home.ts +8 -3
  58. package/lib/agent-profiles.ts +26 -0
  59. package/lib/agents-config.ts +35 -0
  60. package/lib/agents-protocol.ts +60 -1
  61. package/lib/agents-runner.ts +107 -9
  62. package/lib/agents-session-transport.ts +101 -6
  63. package/lib/agents-view.ts +3 -1
  64. package/lib/agents-widget.ts +14 -1
  65. package/lib/auto-permission-memory.ts +48 -18
  66. package/lib/auto-permission-runtime.ts +21 -6
  67. package/lib/bounded-writer-admission.ts +51 -0
  68. package/lib/clipboard-image.ts +147 -0
  69. package/lib/command-palette-catalog.ts +2 -4
  70. package/lib/commit-slice-signal.ts +100 -0
  71. package/lib/context-backstop.ts +120 -0
  72. package/lib/delivered-test-lint.ts +80 -0
  73. package/lib/design-canvas-server.ts +947 -0
  74. package/lib/design-comments.ts +437 -0
  75. package/lib/design-handoffs.ts +313 -0
  76. package/lib/design-open-browser.ts +58 -0
  77. package/lib/design-sources.ts +120 -0
  78. package/lib/destructive-command-guard.ts +120 -9
  79. package/lib/external-editor.ts +31 -0
  80. package/lib/gauge-theme.ts +146 -0
  81. package/lib/gentle-ai-binary.ts +28 -7
  82. package/lib/gentle-design.ts +513 -0
  83. package/lib/gentle-shell-bundled.ts +146 -36
  84. package/lib/gentle-shell-import-pi.ts +233 -0
  85. package/lib/gentle-shell-launcher.ts +154 -15
  86. package/lib/gentle-shell-startup-update.ts +61 -9
  87. package/lib/gentle-shell-uninstall.ts +51 -2
  88. package/lib/herdr-activity.ts +24 -0
  89. package/lib/image-attachments.ts +150 -0
  90. package/lib/inprocess-reviewer.ts +33 -2
  91. package/lib/nan-provider.ts +3 -0
  92. package/lib/native-review-cli.ts +20 -5
  93. package/lib/notification-audio-native.ts +149 -13
  94. package/lib/notification-audio.ts +4 -0
  95. package/lib/notification-pulse-worker.ts +82 -18
  96. package/lib/notification-service.ts +5 -1
  97. package/lib/orchestrator-discovery.ts +5 -1
  98. package/lib/orchestrator-presence.ts +87 -8
  99. package/lib/orchestrator-scope.ts +31 -3
  100. package/lib/orchestrator-state.ts +11 -3
  101. package/lib/permission-mode-policy.ts +2 -2
  102. package/lib/provider-contract-bundle.ts +99 -53
  103. package/lib/review-candidate-view-owner.ts +29 -19
  104. package/lib/review-candidate-view.ts +36 -2
  105. package/lib/review-host-relay.ts +11 -2
  106. package/lib/review-reminder-receipt.ts +24 -1
  107. package/lib/review-risk-assessment.ts +43 -38
  108. package/lib/review-session-standing-permission.ts +3 -1
  109. package/lib/session-profile-append-controller.ts +713 -0
  110. package/lib/session-profile-disk-reader.ts +182 -0
  111. package/lib/session-profile-freeze.ts +30 -1
  112. package/lib/session-profile-persistence.ts +15 -7
  113. package/lib/session-scratch.ts +523 -0
  114. package/lib/session-worktree-registry.ts +7 -0
  115. package/lib/shell-bar.ts +29 -28
  116. package/lib/shell-changes-view.ts +80 -13
  117. package/lib/shell-changes.ts +81 -5
  118. package/lib/shell-gauge.ts +49 -6
  119. package/lib/shell-prompt.ts +4 -4
  120. package/lib/shell-sidebar-layout.ts +26 -6
  121. package/lib/shell-todo.ts +24 -5
  122. package/lib/shell-usage-view.ts +5 -1
  123. package/lib/shell-usage.ts +7 -7
  124. package/lib/verifier-admission.ts +30 -0
  125. package/lib/vim-editor-adapter.ts +11 -8
  126. package/lib/voice-artifacts.ts +188 -0
  127. package/lib/voice-command.ts +576 -0
  128. package/lib/voice-install.ts +455 -0
  129. package/lib/voice-platform.ts +99 -0
  130. package/lib/voice-push-to-talk.ts +262 -0
  131. package/lib/voice-session.ts +460 -0
  132. package/lib/voice-settings.ts +50 -0
  133. package/lib/voice-ui.ts +158 -0
  134. package/lib/yolo-session-policy.ts +58 -22
  135. package/lib/yolo-temp-scratch.ts +69 -0
  136. package/package.json +1 -2
  137. package/runtime/gentle-ai-binary.mjs +28 -7
  138. package/runtime/gentle-shell-bundled.mjs +145 -35
  139. package/runtime/gentle-shell-import-pi.mjs +234 -0
  140. package/runtime/gentle-shell-launcher.mjs +152 -13
  141. package/runtime/gentle-shell-startup-update.mjs +61 -9
  142. package/runtime/gentle-shell-uninstall.mjs +51 -2
  143. package/runtime/native-review-cli.mjs +19 -4
  144. package/runtime/review-risk-assessment.mjs +43 -38
  145. package/scripts/build-runtime-modules.mjs +1 -0
  146. package/scripts/bundled-engram-migration.mjs +119 -0
  147. package/scripts/gentle-ai-installer.mjs +10 -10
  148. package/scripts/installer-downloads.mjs +7 -7
  149. package/scripts/installer-preflight.mjs +5 -3
  150. package/scripts/installer-server.mjs +14 -4
  151. package/scripts/installer-windows.mjs +37 -7
  152. package/scripts/main-channel.mjs +94 -19
  153. package/scripts/pi-update.mjs +34 -6
  154. package/scripts/run-test-suite.mjs +5 -0
  155. package/scripts/verify-package-files.mjs +43 -13
  156. package/scripts/voice-worker.cjs +476 -0
  157. package/skills/design/SKILL.md +110 -0
  158. package/skills/gentle-ai/SKILL.md +3 -3
  159. package/skills/work-unit-commits/SKILL.md +5 -3
  160. package/tests/agent-profiles.test.ts +24 -0
  161. package/tests/agent-risk-escalation.test.ts +37 -10
  162. package/tests/agents-config.test.ts +61 -0
  163. package/tests/agents-fake-child.ts +8 -1
  164. package/tests/agents-protocol.test.ts +32 -0
  165. package/tests/agents-runner.test.ts +485 -7
  166. package/tests/agents-session-transport.test.ts +53 -3
  167. package/tests/ask-user-choice.test.ts +30 -0
  168. package/tests/auto-mode.test.ts +17 -0
  169. package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
  170. package/tests/auto-permission-runtime.test.ts +85 -1
  171. package/tests/autonomous-guard.test.ts +142 -0
  172. package/tests/bounded-writer-admission.test.ts +11 -3
  173. package/tests/bundled-install.test.ts +3 -3
  174. package/tests/card-content-policy.test.ts +19 -9
  175. package/tests/child-safety.test.ts +39 -0
  176. package/tests/clipboard-image.test.ts +114 -0
  177. package/tests/codegraph-tools.test.ts +123 -2
  178. package/tests/command-palette.test.ts +15 -5
  179. package/tests/commit-slice-signal.test.ts +162 -0
  180. package/tests/context-backstop.test.ts +225 -0
  181. package/tests/delegate-for-reason-contract.test.ts +92 -24
  182. package/tests/delivered-test-lint.test.ts +105 -0
  183. package/tests/design-canvas-assets.test.ts +710 -0
  184. package/tests/design-canvas-server.test.ts +1096 -0
  185. package/tests/design-comments.test.ts +201 -0
  186. package/tests/design-handoffs.test.ts +282 -0
  187. package/tests/design-open-browser.test.ts +70 -0
  188. package/tests/design-sources.test.ts +132 -0
  189. package/tests/destructive-command-guard.test.ts +85 -1
  190. package/tests/engram-safety.test.ts +132 -57
  191. package/tests/external-editor.test.ts +84 -0
  192. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  193. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  194. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  195. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  196. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  197. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  198. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  199. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  200. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  201. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  202. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  203. package/tests/gauge-theme.test.ts +120 -0
  204. package/tests/gentle-agents.test.ts +645 -21
  205. package/tests/gentle-ai-binary.test.ts +1 -1
  206. package/tests/gentle-ai-child-guards.test.ts +61 -0
  207. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  208. package/tests/gentle-ai-installer.test.ts +51 -51
  209. package/tests/gentle-ai.test.ts +292 -13
  210. package/tests/gentle-design.test.ts +1313 -0
  211. package/tests/gentle-music.test.ts +335 -0
  212. package/tests/gentle-shell-bin.test.ts +163 -0
  213. package/tests/gentle-shell-bundled.test.ts +392 -176
  214. package/tests/gentle-shell-import-pi.test.ts +262 -0
  215. package/tests/gentle-shell-launcher.test.ts +107 -2
  216. package/tests/gentle-shell-startup-update.test.ts +77 -1
  217. package/tests/gentle-shell-uninstall.test.ts +89 -0
  218. package/tests/gentle-shell.test.ts +225 -17
  219. package/tests/gentle-todo.test.ts +190 -0
  220. package/tests/headless-delivery-question.test.ts +58 -0
  221. package/tests/herdr-activity.test.ts +34 -1
  222. package/tests/image-attachments.test.ts +119 -0
  223. package/tests/image-paste-chips.test.ts +236 -0
  224. package/tests/inprocess-reviewer.test.ts +55 -2
  225. package/tests/installer-bundles.test.ts +12 -6
  226. package/tests/installer-posix-bootstrap.test.ts +8 -8
  227. package/tests/installer-preflight.test.ts +16 -0
  228. package/tests/installer-server.test.ts +10 -2
  229. package/tests/installer-windows-bootstrap.test.ts +94 -7
  230. package/tests/installer-windows-diagnostics.test.ts +111 -0
  231. package/tests/main-channel-upgrade.test.ts +232 -2
  232. package/tests/main-channel.test.ts +140 -21
  233. package/tests/nan-provider.test.ts +36 -0
  234. package/tests/native-review-capability-contract.test.ts +17 -4
  235. package/tests/native-review-parity.test.ts +23 -2
  236. package/tests/no-self-pause-contract.test.ts +5 -2
  237. package/tests/notification-audio-native.test.ts +128 -9
  238. package/tests/notification-customize.test.ts +17 -5
  239. package/tests/odd-integration.test.ts +5 -1
  240. package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
  241. package/tests/odd-routing-contract.test.ts +115 -30
  242. package/tests/orchestrator-budget.test.ts +22 -8
  243. package/tests/orchestrator-discovery.test.ts +29 -0
  244. package/tests/orchestrator-presence.test.ts +100 -2
  245. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  246. package/tests/orchestrator-scope.test.ts +80 -0
  247. package/tests/orchestrator-state.test.ts +29 -0
  248. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  249. package/tests/package-files.test.ts +34 -0
  250. package/tests/package-manifest.test.ts +24 -20
  251. package/tests/permission-mode-policy.test.ts +10 -1
  252. package/tests/pi-update-launcher.test.ts +21 -2
  253. package/tests/pi-update.test.ts +99 -4
  254. package/tests/primary-session-prompt.test.ts +101 -0
  255. package/tests/provider-contract-bundle.test.ts +118 -0
  256. package/tests/provider-contract-mirror.test.ts +24 -0
  257. package/tests/quiet-tool-rendering.test.ts +23 -0
  258. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  259. package/tests/rdd-status-line.test.ts +79 -0
  260. package/tests/review-agent-end-preflight.test.ts +6 -0
  261. package/tests/review-candidate-view.test.ts +49 -0
  262. package/tests/review-contract-prompt.test.ts +5 -2
  263. package/tests/review-controller-native-routing.test.ts +14 -1
  264. package/tests/review-controller.test.ts +1 -1
  265. package/tests/review-host-relay-routing.test.ts +147 -3
  266. package/tests/review-host-relay.test.ts +9 -2
  267. package/tests/review-json-arguments.test.ts +20 -3
  268. package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
  269. package/tests/review-owner-identity.test.ts +132 -0
  270. package/tests/review-relay-transport-agent.test.ts +26 -0
  271. package/tests/review-reminder-receipt.test.ts +31 -1
  272. package/tests/review-risk-assessment.test.ts +163 -76
  273. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  274. package/tests/review-session-standing-permission.test.ts +27 -1
  275. package/tests/run-test-suite.test.ts +15 -0
  276. package/tests/runtime-harness.mjs +16 -6
  277. package/tests/session-changes-shell.test.ts +1 -1
  278. package/tests/session-changes.test.ts +2 -1
  279. package/tests/session-profile-append-controller-fork.test.ts +458 -0
  280. package/tests/session-profile-append-controller-pi.test.ts +290 -0
  281. package/tests/session-profile-append-controller.test.ts +731 -0
  282. package/tests/session-profile-disk-reader-pi.test.ts +183 -0
  283. package/tests/session-profile-disk-reader.test.ts +311 -0
  284. package/tests/session-profile-freeze.test.ts +23 -0
  285. package/tests/session-scratch.test.ts +1011 -0
  286. package/tests/session-subject-routing-contract.test.ts +2 -1
  287. package/tests/session-worktree-registry.test.ts +13 -1
  288. package/tests/shell-bar.test.ts +21 -2
  289. package/tests/shell-changes-view.test.ts +137 -1
  290. package/tests/shell-changes.test.ts +148 -1
  291. package/tests/shell-gauge.test.ts +42 -0
  292. package/tests/shell-prompt.test.ts +3 -3
  293. package/tests/shell-render-cache.test.ts +134 -1
  294. package/tests/shell-sidebar-layout.test.ts +50 -0
  295. package/tests/short-check-routing-contract.test.ts +1 -1
  296. package/tests/skill-collision-prefixes.test.ts +3 -1
  297. package/tests/skill-registry-watcher.test.ts +17 -5
  298. package/tests/skill-registry.test.ts +47 -3
  299. package/tests/small-task-inline-contract.test.ts +37 -0
  300. package/tests/standalone-config-home.test.ts +324 -0
  301. package/tests/startup-banner.test.ts +5 -4
  302. package/tests/startup-home-repo.test.ts +34 -0
  303. package/tests/support/orchestrator-modules.ts +1 -0
  304. package/tests/task-size-routing-contract.test.ts +70 -10
  305. package/tests/test-scratch-contract.test.ts +102 -0
  306. package/tests/test-strength-contract.test.ts +36 -1
  307. package/tests/tool-batching-contract.test.ts +64 -0
  308. package/tests/verification-proportionality.test.ts +46 -0
  309. package/tests/verifier-admission.test.ts +154 -0
  310. package/tests/verify-agent-contract.test.ts +8 -8
  311. package/tests/voice-command.test.ts +773 -0
  312. package/tests/voice-install.test.ts +323 -0
  313. package/tests/voice-platform.test.ts +176 -0
  314. package/tests/voice-push-to-talk.test.ts +444 -0
  315. package/tests/voice-session.test.ts +434 -0
  316. package/tests/voice-settings.test.ts +49 -0
  317. package/tests/voice-ui.test.ts +97 -0
  318. package/tests/voice-worker.test.ts +445 -0
  319. package/tests/weak-model-economy-prompt.test.ts +32 -0
  320. package/tests/windows-hidden-processes.test.ts +30 -2
  321. package/tests/writer-edit-surface-scope.test.ts +3 -0
  322. package/tests/writer-feature-document-admission.test.ts +156 -0
  323. package/tests/yolo-customize.test.ts +5 -1
  324. package/tests/yolo-mode.test.ts +19 -1
  325. package/tests/yolo-session-policy-capture.test.ts +104 -0
  326. package/tests/yolo-temp-scratch.test.ts +71 -0
  327. package/scripts/mirror-odd-routing.mjs +0 -242
  328. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  329. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  330. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  331. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  332. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  333. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  334. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -1,7 +1,10 @@
1
1
  import { CustomEditor, keyHint, type ExtensionAPI, type ExtensionContext, type KeybindingsManager } from "@earendil-works/pi-coding-agent";
2
2
  import { isHomeOrBroadUserDirectory } from "../lib/bounded-writer-admission.ts";
3
- import { Editor, decodeKittyPrintable, isKeyRelease, matchesKey, parseKey, truncateToWidth, visibleWidth, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
3
+ import { Editor, Image, decodeKittyPrintable, getCapabilities, isKeyRelease, matchesKey, parseKey, truncateToWidth, visibleWidth, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
4
+ import { readClipboardImage, type ClipboardImage } from "../lib/clipboard-image.ts";
5
+ import { describeAttachment, ImageAttachments } from "../lib/image-attachments.ts";
4
6
  import { execFile, spawnSync } from "node:child_process";
7
+ import { openInExternalEditor, type ExternalEditorHost } from "../lib/external-editor.ts";
5
8
  import { existsSync, realpathSync, statSync } from "node:fs";
6
9
  import { lstat, readFile } from "node:fs/promises";
7
10
  import { fileURLToPath } from "node:url";
@@ -10,8 +13,9 @@ import { resolveSessionProfile, sessionProfileLabel, sessionProfileModelProfiles
10
13
  import * as os from "node:os";
11
14
  import { dirname, join, resolve } from "node:path";
12
15
  import { buildShellHeaderModel, renderShellBar, renderShellBelowInputFloat, renderShellBottomOnlyBar, renderShellHeaderChrome, renderShellSidebarBar, keepNativeWorkingRow, shellEnabled, shellHeaderUsageHit, shellJobsCount, type ShellBarModel, type ShellBarTheme } from "../lib/shell-bar.ts";
13
- import { CHANGE_STATUS, DIFF_MAX_BYTES, DIFF_MAX_LINES, DIFF_TOO_LARGE, WorktreeChangesTracker, renderChangesWidget, type ChangedFile, type ChangesModel, type FileRevision, type GitRunner, type LineCounter, type WorktreeChanges } from "../lib/shell-changes.ts";
16
+ import { CHANGE_STATUS, DIFF_MAX_BYTES, DIFF_MAX_LINES, DIFF_TOO_LARGE, WorktreeChangesTracker, changesDigest, renderChangesWidget, type ChangedFile, type ChangesModel, type FileRevision, type GitRunner, type LineCounter, type WorktreeChanges } from "../lib/shell-changes.ts";
14
17
  import { WorktreeChangesView } from "../lib/shell-changes-view.ts";
18
+ import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
15
19
  import { SessionWorktreeRegistry, resolveSessionWorktree, worktreeGitEnvironment, type WorktreeResolver, type WorktreeIdentity } from "../lib/session-worktree-registry.ts";
16
20
  import { CARD_STYLE, CARD_TONE, cardStyle, renderCard, setCardStyle, type Card, type CardTheme } from "../lib/shell-card.ts";
17
21
  import { resolveCardStyle, writeCardStyle } from "../lib/card-style-policy.ts";
@@ -21,17 +25,18 @@ import { buildCommandPaletteGroups } from "../lib/command-palette-catalog.ts";
21
25
  import { discoverYoloUiAdapter, YOLO_DISPLAY, type YoloDisplay, type YoloUiAdapter } from "../lib/yolo-session-policy.ts";
22
26
  import { VisualCustomizeView, type CustomizeCategory, type CustomizeRow, type ProfileActions } from "../lib/visual-customize-view.ts";
23
27
  import { deleteVisualProfile, getVisualProfile, listVisualProfiles, resetVisualProfiles, saveVisualProfile } from "../lib/visual-profiles.ts";
28
+ import { gaugePaintFor } from "../lib/gauge-theme.ts";
29
+ import type { GaugePaint } from "../lib/shell-gauge.ts";
24
30
  import { sourcePalettePreview } from "../lib/theme-customization.ts";
25
31
  import { DEFAULT_VISUAL_SETTINGS, DENSITY, HEADER_PLACEMENT, STATUS_PLACEMENT, VISUAL_SECTION_KEYS, resolveVisualSettings, writeVisualSettings } from "../lib/visual-customization-policy.ts";
26
32
  import { BANNER_COLORS, DEFAULT_BANNER_CONFIG, readBannerConfig, readBannerConfigForEdit, writeBannerConfig } from "./startup-banner.ts";
27
33
  import { agentsViewKey } from "../lib/agents-keys.ts";
28
34
  import { GentleAiDevBinaryOverrideError, resolveGentleAiDevBinaryOverride } from "../lib/gentle-ai-binary.ts";
29
- import { DOUBLE_ESC_CANCEL_HINT, floatPromptRow, framePromptLines, resolvePromptLayout, IDLE_ESC_CLEAR_HINT, PROMPT_HINT, PROMPT_STATE, SHELL_PULSE_MS, withPromptHint, type PromptState } from "../lib/shell-prompt.ts";
35
+ import { DOUBLE_ESC_CANCEL_HINT, floatPromptRow, framePromptLines, resolvePromptLayout, IDLE_ESC_CLEAR_HINT, PROMPT_HINT, PROMPT_STATE, SHELL_PULSE_WATCHDOG_MS, withPromptHint, type PromptState } from "../lib/shell-prompt.ts";
30
36
  import { oddPhaseRegistry } from "../lib/odd-phase.ts";
31
37
  import { inferOddPhase } from "../lib/odd-phase-inference.ts";
32
38
  import { isInteractiveMode } from "../lib/rpc-host.ts";
33
39
  import { gentlePiConfigHome } from "../lib/agent-home.ts";
34
- import { bundledGentlePiConfigHome } from "../lib/gentle-shell-bundled.ts";
35
40
  import { isNanAdaptiveReasoningModel } from "../lib/nan-provider.ts";
36
41
  import { resolveAnimationPolicy, writeAnimationPolicy, type AnimationPolicy } from "../lib/animation-policy.ts";
37
42
  import { resolveVimPolicy, writeVimPolicy, type VimPolicy } from "../lib/vim-policy.ts";
@@ -169,6 +174,8 @@ export interface ShellDeps {
169
174
  /** A root Changes may scan: removed worktrees and leftover directories are skipped. */
170
175
  rootExists(root: string): boolean;
171
176
  vimRuntimeVersion?(): string | undefined;
177
+ /** Clipboard image source for the prompt's attachment chips; tests inject a fake. */
178
+ readClipboardImage?(): Promise<ClipboardImage | null>;
172
179
  }
173
180
 
174
181
  export type ActiveProfileReader = (() => string | undefined) & {
@@ -180,7 +187,7 @@ export type ActiveProfileReader = (() => string | undefined) & {
180
187
  // Unbound reads retain the global file-identity cache, including atomic replacements.
181
188
  // Bound reads are snapshots: no filesystem or Git work is done during a frame.
182
189
  export function createActiveProfileReader(env: NodeJS.ProcessEnv = process.env): ActiveProfileReader {
183
- const path = profilesFilePath(bundledGentlePiConfigHome(env) ?? env.GENTLE_PI_CONFIG_HOME ?? join(os.homedir(), ".pi", "gentle-ai"));
190
+ const path = profilesFilePath(gentlePiConfigHome(env));
184
191
  let fingerprint: string | undefined;
185
192
  let name: string | undefined;
186
193
  let bound = false;
@@ -215,7 +222,7 @@ export function createActiveProfileReader(env: NodeJS.ProcessEnv = process.env):
215
222
  const next = cwd === undefined ? global() : sessionProfileLabel(resolveSessionProfile({
216
223
  sessionId,
217
224
  cwd,
218
- configHome: bundledGentlePiConfigHome(env) ?? env.GENTLE_PI_CONFIG_HOME ?? join(os.homedir(), ".pi", "gentle-ai"),
225
+ configHome: gentlePiConfigHome(env),
219
226
  resolveWorktree: () => identity,
220
227
  env,
221
228
  }));
@@ -272,33 +279,38 @@ function sessionCost(ctx: ExtensionContext): number {
272
279
  }
273
280
 
274
281
  type ShellSessionStats = { costTotal: number; usage: ReturnType<ExtensionContext["getContextUsage"]> };
275
- const shellSessionStats = new WeakMap<object, {
276
- sessionId: string | undefined;
277
- leafId: string | null;
278
- entryCount: number | undefined;
279
- model: ExtensionContext["model"];
280
- contextWindow: number | undefined;
281
- stats: ShellSessionStats;
282
- }>();
283
-
284
- function sessionStats(ctx: ExtensionContext): ShellSessionStats {
282
+ // The exact identity sessionStats' memo keys on: when any element differs the
283
+ // stats are recomputed. Undefined means the host lacks the leaf identity and
284
+ // the stats must be computed fresh on every call. Callers that digest live
285
+ // state per frame (issue #1681) reuse this key so their own memos can never
286
+ // disagree with sessionStats about what is still current.
287
+ type SessionStatsKey = readonly [
288
+ sessionId: string | undefined,
289
+ leafId: string | null,
290
+ entryCount: number | undefined,
291
+ model: ExtensionContext["model"],
292
+ contextWindow: number | undefined,
293
+ ];
294
+ const shellSessionStats = new WeakMap<object, { key: SessionStatsKey; stats: ShellSessionStats }>();
295
+
296
+ function sessionStatsKey(ctx: ExtensionContext): SessionStatsKey | undefined {
285
297
  const manager = ctx.sessionManager as ExtensionContext["sessionManager"] & { getEntryCount?(): number };
286
- const compute = (): ShellSessionStats => ({ costTotal: sessionCost(ctx), usage: ctx.getContextUsage() });
287
298
  // Append-only session entries move the leaf, including compaction and branch
288
299
  // switches. A virtual model's routed physical model comes from the latest
289
300
  // response, which is appended as an entry, so the leaf covers it as well.
290
301
  // Older/test hosts without that identity must stay uncached.
291
- if (typeof manager.getLeafId !== "function") return compute();
292
- const sessionId = manager.getSessionId();
293
- const leafId = manager.getLeafId();
294
- const entryCount = manager.getEntryCount?.();
295
- const model = ctx.model;
296
- const contextWindow = model?.contextWindow;
297
- const cached = shellSessionStats.get(manager);
298
- if (cached && cached.sessionId === sessionId && cached.leafId === leafId &&
299
- cached.entryCount === entryCount && cached.model === model && cached.contextWindow === contextWindow) return cached.stats;
302
+ if (typeof manager.getLeafId !== "function") return undefined;
303
+ return [manager.getSessionId(), manager.getLeafId(), manager.getEntryCount?.(), ctx.model, ctx.model?.contextWindow] as const;
304
+ }
305
+
306
+ function sessionStats(ctx: ExtensionContext): ShellSessionStats {
307
+ const compute = (): ShellSessionStats => ({ costTotal: sessionCost(ctx), usage: ctx.getContextUsage() });
308
+ const key = sessionStatsKey(ctx);
309
+ if (!key) return compute();
310
+ const cached = shellSessionStats.get(ctx.sessionManager);
311
+ if (cached && cached.key.every((value, index) => value === key[index])) return cached.stats;
300
312
  const stats = compute();
301
- shellSessionStats.set(manager, { sessionId, leafId, entryCount, model, contextWindow, stats });
313
+ shellSessionStats.set(ctx.sessionManager, { key, stats });
302
314
  return stats;
303
315
  }
304
316
 
@@ -334,6 +346,85 @@ export function buildShellBarModel(
334
346
  };
335
347
  }
336
348
 
349
+ export interface FooterStatusMemoDeps {
350
+ pi: ExtensionAPI;
351
+ ctx: ExtensionContext;
352
+ footerData: ShellFooterData;
353
+ usage: () => ProviderUsage | undefined;
354
+ profile: () => string | undefined;
355
+ jobs: () => RunningJobDisplay[] | undefined;
356
+ review: () => ReviewSidebarSnapshot | undefined;
357
+ tracker: () => Pick<WorktreeChangesTracker, "model" | "unbornRoots">;
358
+ visualSettings: () => unknown;
359
+ cardStyle: () => unknown;
360
+ }
361
+
362
+ // Issue #1681: railDigest evaluates every rail's digest before the fullscreen
363
+ // layout's prepared cache can answer, so both Status rails digest per
364
+ // keystroke and a cache hit still paid for building and serializing the whole
365
+ // bar model. The memo keys on the cheap live inputs the model reads instead:
366
+ // the digest serializes only this key, and the model is rebuilt just when one
367
+ // of those inputs moved. On hosts without leaf identity the session stats
368
+ // stay uncached and ride the key by value, so those hosts keep exactly the
369
+ // pre-memo digest honesty.
370
+ export function createFooterStatusMemo(deps: FooterStatusMemoDeps): { key(): string; model(): ShellBarModel } {
371
+ const { pi, ctx, footerData } = deps;
372
+ let cache: { key: string; model: ShellBarModel } | undefined;
373
+ const key = (): string => {
374
+ const statsKey = sessionStatsKey(ctx);
375
+ const model = ctx.model;
376
+ return JSON.stringify([
377
+ // Session statistics identity by value: without leaf identity the
378
+ // computed stats ride the key, so those hosts keep exactly the pre-memo
379
+ // digest honesty. The model rides its value pieces (id, contextWindow,
380
+ // effort lookup, OAuth state) because that is all buildShellBarModel
381
+ // reads — serializing the model object itself would bloat every digest.
382
+ statsKey ? [statsKey[0], statsKey[1], statsKey[2]] : sessionStats(ctx),
383
+ model?.id ?? null,
384
+ model?.contextWindow ?? null,
385
+ model?.reasoning ? (isNanAdaptiveReasoningModel(model) ? "auto" : pi.getThinkingLevel()) : undefined,
386
+ model ? ctx.modelRegistry.isUsingOAuth(model) : false,
387
+ footerData.getGitBranch(),
388
+ deps.profile(),
389
+ ctx.sessionManager.getSessionName(),
390
+ ctx.sessionManager.getCwd(),
391
+ [...footerData.getExtensionStatuses()].sort(([a], [b]) => a.localeCompare(b)),
392
+ deps.usage(),
393
+ deps.jobs(),
394
+ deps.review(),
395
+ (() => {
396
+ const { model: changes, unbornRoots } = deps.tracker();
397
+ return { fingerprint: changesFingerprint(changes), files: changes.files.length, added: changes.added, deleted: changes.deleted, unbornRoots: unbornRoots.length };
398
+ })(),
399
+ deps.visualSettings(),
400
+ deps.cardStyle(),
401
+ ]);
402
+ };
403
+ const model = (): ShellBarModel => {
404
+ const k = key();
405
+ if (cache?.key === k) return cache.model;
406
+ const tracker = deps.tracker();
407
+ const built: ShellBarModel = {
408
+ ...buildShellBarModel(pi, ctx, footerData, { dirty: tracker.model.files.length, usage: deps.usage(), profile: deps.profile(), jobs: deps.jobs() }),
409
+ changes: { files: tracker.model.files.length, added: tracker.model.added, deleted: tracker.model.deleted },
410
+ review: deps.review(),
411
+ };
412
+ cache = { key: k, model: built };
413
+ return built;
414
+ };
415
+ return { key, model };
416
+ }
417
+
418
+ // Gauge-specific theme roles (issue #2222): resolved from the installed theme
419
+ // source and memoized by gauge-theme.ts, so the render hot path only stats the
420
+ // theme file. Undefined — and therefore today's shared roles — whenever the theme
421
+ // source is unavailable.
422
+ function gaugePaint(ctx: ExtensionContext): GaugePaint | undefined {
423
+ const themeName = ctx.ui.theme.name;
424
+ const source = ctx.ui.theme.sourcePath ?? ctx.ui.getAllThemes?.().find((item) => item?.name === themeName)?.path;
425
+ return gaugePaintFor(typeof source === "string" ? source : undefined);
426
+ }
427
+
337
428
  export function createShellBarComponent(
338
429
  pi: ExtensionAPI,
339
430
  ctx: ExtensionContext,
@@ -350,7 +441,7 @@ export function createShellBarComponent(
350
441
  });
351
442
  return {
352
443
  render(width: number) {
353
- return renderShellBar(buildShellBarModel(pi, ctx, footerData, { dirty: dirty(), usage: usage() }), theme, width, presentation?.());
444
+ return renderShellBar(buildShellBarModel(pi, ctx, footerData, { dirty: dirty(), usage: usage() }), theme, width, presentation?.(), gaugePaint(ctx));
354
445
  },
355
446
  invalidate() {},
356
447
  dispose() {
@@ -380,6 +471,11 @@ interface PromptEditorDeps {
380
471
  * generic "working…" label. Never applied to IDLE or QUEUED.
381
472
  */
382
473
  workingLabel?: () => string | undefined;
474
+ /** Pasted images become atomic `[Image#N]` chips; absent, Pi's paste handler runs unchanged. */
475
+ attachments?: ImageAttachments;
476
+ readClipboardImage?: () => Promise<ClipboardImage | null>;
477
+ /** Injectable silence-watchdog window; production and tests default to SHELL_PULSE_WATCHDOG_MS. */
478
+ watchdogMs?: number;
383
479
  }
384
480
 
385
481
  const PROMPT_FRAME_ROLE = "border";
@@ -434,7 +530,10 @@ export class GentlePromptEditor extends CustomEditor {
434
530
  private visualReplacePending = false;
435
531
  private visualObjectPending: "i" | "a" | undefined;
436
532
  private visualAnchor: { line: number; col: number } | undefined;
437
- private pulse: NodeJS.Timeout | undefined;
533
+ private watchdog: NodeJS.Timeout | undefined;
534
+ // Last time a frame painted (or the watchdog forced one) while working; the
535
+ // quality watchdog repaints only when now() drifts past its window.
536
+ private lastRenderAt = 0;
438
537
  private readonly deps: PromptEditorDeps;
439
538
  private floatMouseLayout?: ReturnType<typeof resolvePromptLayout>;
440
539
  // Native selection engine (shift+home/end, alt+a, replace-on-key): ported
@@ -452,19 +551,97 @@ export class GentlePromptEditor extends CustomEditor {
452
551
  // the text is still exactly this, so an edit in between never gets
453
552
  // silently discarded (issue #1218 review).
454
553
  private pendingIdleClearText: string | undefined;
554
+ // Pi's own paste handler (files, text, unsupported formats). Pi assigns
555
+ // `onPasteImage` after construction; the accessor below keeps it as the
556
+ // fallback and routes every paste key, Vim NORMAL included, to pasteImage.
557
+ private piPasteImage: (() => void) | undefined;
558
+ private pastingImage = false;
559
+ private disposed = false;
560
+ // Counts submitted drafts, so a clipboard read that resolves after its
561
+ // draft was sent never lands in (and rides along with) the next message.
562
+ private submittedDrafts = 0;
455
563
 
456
564
  constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, deps: PromptEditorDeps) {
457
565
  super(tui, theme, keybindings);
458
566
  this.selectionEngine = new SelectionEngine(this);
459
567
  this.deps = deps;
460
568
  this.keybindingsManager = keybindings;
569
+ const attachments = deps.attachments;
570
+ if (!attachments || !deps.readClipboardImage) return;
571
+ // SAFETY: Pi's Editor routes cursor movement, wrapping and deletion through
572
+ // its private segment(). Wrapping it only merges registered chips into one
573
+ // segment; if a future Pi drops the method, chips degrade to plain text.
574
+ const host = this as unknown as {
575
+ segment?: (text: string, mode?: string) => Iterable<{ segment: string; index: number; input: string }>;
576
+ submitValue?: () => void;
577
+ };
578
+ const segment = host.segment;
579
+ if (typeof segment === "function") host.segment = (text, mode) => attachments.segment(text, segment.call(this, text, mode));
580
+ // SAFETY: Pi empties the draft in its private submitValue() before onSubmit.
581
+ // Counting here only marks the draft boundary; if a future Pi drops the
582
+ // method, a late read falls back to landing in the next draft.
583
+ const submitValue = host.submitValue;
584
+ if (typeof submitValue === "function") host.submitValue = () => { this.submittedDrafts += 1; submitValue.call(this); };
585
+ // Undefined until Pi installs its handler, so Pi's `if (!onPasteImage)`
586
+ // wiring still runs; afterwards every read returns the chip handler.
587
+ Object.defineProperty(this, "onPasteImage", {
588
+ configurable: true,
589
+ enumerable: true,
590
+ get: () => this.piPasteImage ? () => this.pasteImage() : undefined,
591
+ set: (handler: (() => void) | undefined) => { this.piPasteImage = handler; },
592
+ });
593
+ }
594
+
595
+ /** Registered image chips on `line`; the Vim adapter keeps NORMAL motions out of them. */
596
+ atomicSpans(line: string): Array<{ start: number; end: number }> {
597
+ return this.deps.attachments?.spans(line) ?? [];
598
+ }
599
+
600
+ /** Insert the clipboard image as a chip, or hand the paste to Pi. */
601
+ private pasteImage(): void {
602
+ const attachments = this.deps.attachments;
603
+ const read = this.deps.readClipboardImage;
604
+ if (!attachments || !read) { this.piPasteImage?.(); return; }
605
+ // The Windows clipboard read on WSL takes about a second; a repeated key
606
+ // press during it must not insert a second chip or a stray path.
607
+ if (this.pastingImage) return;
608
+ this.pastingImage = true;
609
+ void this.insertClipboardImage(attachments, read).finally(() => { this.pastingImage = false; });
610
+ }
611
+
612
+ private async insertClipboardImage(attachments: ImageAttachments, read: () => Promise<ClipboardImage | null>): Promise<void> {
613
+ const draft = this.submittedDrafts;
614
+ let image: ClipboardImage | null;
615
+ try { image = await read(); } catch { image = null; }
616
+ // A read that outlives this editor (session switch, quit) must not
617
+ // register bytes in the next session or write into a disposed prompt.
618
+ if (this.disposed) return;
619
+ // The draft this paste belonged to was already sent: neither attach the
620
+ // image to the next message nor run Pi's fallback into the new draft.
621
+ if (this.submittedDrafts !== draft) {
622
+ if (image) this.deps.notifyCompatibility?.("Image paste discarded: the message was sent before the clipboard finished reading. Paste again to attach it.");
623
+ return;
624
+ }
625
+ try {
626
+ if (!image) { this.piPasteImage?.(); return; }
627
+ const { token } = attachments.add(image);
628
+ const cursor = this.getCursor();
629
+ const line = this.getLines()[cursor.line] ?? "";
630
+ const before = cursor.col > 0 && !/\s/.test(line[cursor.col - 1] ?? "") ? " " : "";
631
+ const after = cursor.col < line.length && !/\s/.test(line[cursor.col] ?? "") ? " " : "";
632
+ this.insertTextAtCursor(`${before}${token}${after}`);
633
+ this.deps.requestRender();
634
+ } catch (error) {
635
+ // Never let a paste reject unhandled: Node 24 ends the process on it.
636
+ this.deps.notifyCompatibility?.(`Image paste failed: ${error instanceof Error ? error.message : String(error)}`);
637
+ }
461
638
  }
462
639
 
463
640
  setAnimationPolicy(policy: AnimationPolicy): void {
464
641
  if (this.animationPolicy === policy) return;
465
642
  this.animationPolicy = policy;
466
- this.stopPulse();
467
- if (this.promptState === PROMPT_STATE.WORKING) this.startPulse();
643
+ this.stopWatchdog();
644
+ if (this.promptState === PROMPT_STATE.WORKING) this.startWatchdog();
468
645
  this.deps.requestRender();
469
646
  }
470
647
 
@@ -549,24 +726,32 @@ export class GentlePromptEditor extends CustomEditor {
549
726
  this.deps.requestRender();
550
727
  }
551
728
 
552
- private startPulse(): void {
729
+ private startWatchdog(): void {
553
730
  if (this.animationPolicy === "potato") return;
554
- this.pulse = setInterval(() => {
731
+ const windowMs = this.deps.watchdogMs ?? SHELL_PULSE_WATCHDOG_MS;
732
+ this.lastRenderAt = this.deps.now();
733
+ this.watchdog = setInterval(() => {
734
+ // Render-driven petal (gentle-shell#994): streaming frames already
735
+ // advance the animation in render(), so under quality the watchdog
736
+ // stays silent unless no frame painted within its window. performance
737
+ // keeps its coarse cadence regardless of render traffic.
738
+ if (this.animationPolicy === "quality" && this.deps.now() - this.lastRenderAt < windowMs) return;
555
739
  this.tick += 1;
740
+ this.lastRenderAt = this.deps.now();
556
741
  this.deps.requestRender();
557
- }, this.animationPolicy === "performance" ? 1000 : SHELL_PULSE_MS);
558
- this.pulse.unref();
742
+ }, windowMs);
743
+ this.watchdog.unref();
559
744
  }
560
745
 
561
746
  setWorking(working: boolean): void {
562
747
  this.promptState = working ? PROMPT_STATE.WORKING : PROMPT_STATE.IDLE;
563
- this.stopPulse();
748
+ this.stopWatchdog();
564
749
  if (!working) {
565
750
  this.pendingEscapeCancelDeadline = undefined;
566
751
  } else {
567
752
  this.pendingIdleClearDeadline = undefined;
568
753
  this.pendingIdleClearText = undefined;
569
- this.startPulse();
754
+ this.startWatchdog();
570
755
  }
571
756
  this.deps.requestRender();
572
757
  }
@@ -1053,6 +1238,13 @@ export class GentlePromptEditor extends CustomEditor {
1053
1238
  }
1054
1239
 
1055
1240
  render(width: number): string[] {
1241
+ // Render-driven petal (gentle-shell#994): every frame the host already
1242
+ // paints while working advances the animation for free, so no fast
1243
+ // timer forces renders during streaming.
1244
+ if (this.promptState === PROMPT_STATE.WORKING && this.animationPolicy === "quality") {
1245
+ this.tick += 1;
1246
+ this.lastRenderAt = this.deps.now();
1247
+ }
1056
1248
  const layout = resolvePromptLayout(width, cardStyle(), this.deps.bg);
1057
1249
  this.floatMouseLayout = layout.background ? layout : undefined;
1058
1250
  const inner = layout.nativeWidth;
@@ -1116,7 +1308,8 @@ export class GentlePromptEditor extends CustomEditor {
1116
1308
  }
1117
1309
 
1118
1310
  dispose(): void {
1119
- this.stopPulse();
1311
+ this.disposed = true;
1312
+ this.stopWatchdog();
1120
1313
  }
1121
1314
 
1122
1315
  private isPendingEscapeCancel(): boolean {
@@ -1131,13 +1324,62 @@ export class GentlePromptEditor extends CustomEditor {
1131
1324
  );
1132
1325
  }
1133
1326
 
1134
- private stopPulse(): void {
1135
- if (this.pulse) clearInterval(this.pulse);
1136
- this.pulse = undefined;
1327
+ private stopWatchdog(): void {
1328
+ if (this.watchdog) clearInterval(this.watchdog);
1329
+ this.watchdog = undefined;
1137
1330
  this.tick = 0;
1138
1331
  }
1139
1332
  }
1140
1333
 
1334
+ const IMAGE_ATTACHMENTS_WIDGET_KEY = "gentle-shell-image-attachments";
1335
+ const IMAGE_PREVIEW_MAX_ROWS = 6;
1336
+ const IMAGE_PREVIEW_MAX_COLUMNS = 40;
1337
+
1338
+ /** Unknown capabilities count as image-capable; Pi decides for its own models. */
1339
+ export function modelAcceptsImages(model: { input?: readonly string[] } | undefined): boolean {
1340
+ return !model?.input || model.input.includes("image");
1341
+ }
1342
+
1343
+ /**
1344
+ * Lists the chips still in the draft above the prompt. Terminals with an image
1345
+ * protocol (Kitty, iTerm2) get a thumbnail under each label; every other
1346
+ * terminal, or a multiplexer that hides the protocol, gets the label alone.
1347
+ */
1348
+ export function imageAttachmentsWidget(
1349
+ attachments: ImageAttachments,
1350
+ draft: () => string,
1351
+ theme: { fg(color: string, text: string): string },
1352
+ acceptsImages: () => boolean,
1353
+ canDrawImages: () => boolean = () => getCapabilities().images !== null,
1354
+ ) {
1355
+ const previews = new Map<number, Image>();
1356
+ return {
1357
+ render(width: number): string[] {
1358
+ const shown = attachments.referenced(draft());
1359
+ for (const id of previews.keys()) if (!shown.some((attachment) => attachment.id === id)) previews.delete(id);
1360
+ if (shown.length === 0) return [];
1361
+ const draw = canDrawImages();
1362
+ const warning = acceptsImages() ? "" : theme.fg("warning", " · not sent: the current model does not accept images");
1363
+ const lines: string[] = [];
1364
+ for (const attachment of shown) {
1365
+ lines.push(truncateToWidth(`${theme.fg("accent", attachment.token)} ${theme.fg("dim", describeAttachment(attachment))}${warning}`, width));
1366
+ if (!draw) continue;
1367
+ let preview = previews.get(attachment.id);
1368
+ if (!preview) {
1369
+ preview = new Image(attachment.base64, attachment.mimeType, { fallbackColor: (text) => theme.fg("dim", text) },
1370
+ { maxWidthCells: IMAGE_PREVIEW_MAX_COLUMNS, maxHeightCells: IMAGE_PREVIEW_MAX_ROWS }, attachment.dimensions);
1371
+ previews.set(attachment.id, preview);
1372
+ }
1373
+ lines.push(...preview.render(width));
1374
+ }
1375
+ return lines;
1376
+ },
1377
+ invalidate(): void {
1378
+ for (const preview of previews.values()) preview.invalidate();
1379
+ },
1380
+ };
1381
+ }
1382
+
1141
1383
  // Stable across extension module reloads; never infer ownership from a name.
1142
1384
  const PROMPT_OWNER = Symbol.for("gentle-pi.prompt-owner");
1143
1385
  type PromptFactory = NonNullable<ReturnType<ExtensionContext["ui"]["getEditorComponent"]>> & { [PROMPT_OWNER]?: boolean };
@@ -1145,7 +1387,7 @@ type PromptFactory = NonNullable<ReturnType<ExtensionContext["ui"]["getEditorCom
1145
1387
  function installPrompt(
1146
1388
  ctx: ExtensionContext,
1147
1389
  onCreated: (prompt: GentlePromptEditor) => void,
1148
- promptDeps: { now: () => number; doubleEscCancelEnabled: () => boolean; dispatchQueuedText: (text: string) => void; vimRuntimeVersion?: () => string | undefined },
1390
+ promptDeps: { now: () => number; doubleEscCancelEnabled: () => boolean; dispatchQueuedText: (text: string) => void; vimRuntimeVersion?: () => string | undefined; attachments?: ImageAttachments; readClipboardImage?: () => Promise<ClipboardImage | null> },
1149
1391
  ): boolean {
1150
1392
  const previous = ctx.ui.getEditorComponent() as PromptFactory | undefined;
1151
1393
  if (previous && !previous[PROMPT_OWNER]) return false;
@@ -1162,10 +1404,12 @@ function installPrompt(
1162
1404
  notifyCompatibility: (message) => ctx.ui.notify(message, "warning"),
1163
1405
  tuiVersion: promptDeps.vimRuntimeVersion,
1164
1406
  workingLabel: () => oddPhaseRegistry.label(ctx.sessionManager.getSessionId()),
1407
+ attachments: promptDeps.attachments,
1408
+ readClipboardImage: promptDeps.readClipboardImage,
1165
1409
  });
1166
1410
  // Pi's own docs require an explicit requestRender() after a state change
1167
1411
  // (docs/tui.md: "Call tui.requestRender() after state changes"); no host
1168
- // re-render is implicitly guaranteed, and the WORKING pulse loop does not
1412
+ // re-render is implicitly guaranteed, and the WORKING watchdog does not
1169
1413
  // run at all under the "potato" animation policy. Register this session's
1170
1414
  // redraw so a phase reported by the tool is visible immediately.
1171
1415
  oddPhaseRegistry.setRenderRequest(ctx.sessionManager.getSessionId(), () => tui.requestRender());
@@ -1223,8 +1467,6 @@ const CHANGES_SHORTCUT_DEFAULT = "alt+g";
1223
1467
  const CHANGES_POLL_DEFAULT_MS = 2000;
1224
1468
  const GIT_TIMEOUT_MS = 5000;
1225
1469
  const COMMANDS_COMMAND_NAME = "gentle:commands";
1226
- const OVERLAY_HEIGHT_RATIO = 0.8;
1227
- const OVERLAY_MIN_ROWS = 8;
1228
1470
 
1229
1471
  export function shellGitRunner(cwd: string, env: NodeJS.ProcessEnv = process.env, run: typeof execFile = execFile, maxBuffer = Infinity): GitRunner {
1230
1472
  // Pi exec cannot replace the inherited environment. Use argv directly and
@@ -1269,26 +1511,7 @@ const fileRevision = (root: string): FileRevision => async (path) => {
1269
1511
  return `${info.size}:${info.mtimeMs}`;
1270
1512
  };
1271
1513
 
1272
- export interface ExternalEditorHost {
1273
- stop(): void;
1274
- start(): void;
1275
- requestRender(force?: boolean): void;
1276
- }
1277
-
1278
- export function openInExternalEditor(host: ExternalEditorHost, path: string, env: NodeJS.ProcessEnv = process.env, spawn: typeof spawnSync = spawnSync, cwd?: string): boolean {
1279
- const command = env.VISUAL || env.EDITOR;
1280
- if (!command) return false;
1281
- const [editor, ...editorArgs] = command.split(" ");
1282
- host.stop();
1283
- try {
1284
- spawn(editor, [...editorArgs, path], { cwd, stdio: "inherit", shell: process.platform === "win32" });
1285
- } finally {
1286
- host.start();
1287
- host.requestRender(true);
1288
- }
1289
- return true;
1290
- }
1291
-
1514
+ export { openInExternalEditor, type ExternalEditorHost } from "../lib/external-editor.ts";
1292
1515
  export function changesShortcut(env: NodeJS.ProcessEnv = process.env): string | undefined {
1293
1516
  const value = env.GENTLE_PI_SHELL_CHANGES_KEY?.trim();
1294
1517
  if (value === undefined) return CHANGES_SHORTCUT_DEFAULT;
@@ -1316,8 +1539,11 @@ function changesPollMs(env: NodeJS.ProcessEnv): number {
1316
1539
  const USAGE_FETCH_TIMEOUT_DEFAULT_MS = 10_000;
1317
1540
  const usageFetchTimeoutMs = (env: NodeJS.ProcessEnv): number => positiveMs(env.GENTLE_PI_SHELL_USAGE_TIMEOUT_MS, USAGE_FETCH_TIMEOUT_DEFAULT_MS);
1318
1541
 
1542
+ // The footer memo reads this every keystroke: tracker models store the
1543
+ // digest once per scan (lib/shell-changes.ts), so this is O(1); hand-built
1544
+ // models without a stored digest fall back to computing it here.
1319
1545
  function changesFingerprint(model: ChangesModel): string {
1320
- return model.files.map((file) => `${file.path}:${file.status}:${file.added}:${file.deleted}:${file.diffRevision ?? ""}:${file.countsUnavailable ?? ""}`).join("|");
1546
+ return model.digest ?? changesDigest(model);
1321
1547
  }
1322
1548
 
1323
1549
  interface OverlayDeps {
@@ -1329,6 +1555,8 @@ interface OverlayDeps {
1329
1555
  }
1330
1556
 
1331
1557
  // The overlay polls each root's Git state while it is open; diffs load lazily.
1558
+ // Like Gentle Agents it takes the whole terminal, and the native fullscreen
1559
+ // wrapper keeps mouse dispatch on the view while it owns the keyboard.
1332
1560
  async function showChangesOverlay(ctx: ExtensionContext, deps: OverlayDeps): Promise<void> {
1333
1561
  let host: ExternalEditorHost | undefined;
1334
1562
  let view: WorktreeChangesView | undefined;
@@ -1346,19 +1574,25 @@ async function showChangesOverlay(ctx: ExtensionContext, deps: OverlayDeps): Pro
1346
1574
  host = tui;
1347
1575
  view = new WorktreeChangesView(deps.worktrees(), {
1348
1576
  theme,
1349
- rows: () => Math.max(OVERLAY_MIN_ROWS, Math.floor(tui.terminal.rows * OVERLAY_HEIGHT_RATIO)),
1577
+ rows: () => Math.max(0, tui.terminal.rows),
1350
1578
  loadDiff: deps.loadDiff,
1351
1579
  onOpen: (root, file) => close({ root, file }),
1352
1580
  onRefresh: () => void refresh().catch(() => {}),
1353
1581
  onClose: () => close(null),
1354
1582
  requestRender: () => tui.requestRender(),
1355
1583
  });
1356
- return view;
1584
+ const interaction = createNativeFullscreenInteraction({ keyboardTarget: view, requestRender: () => tui.requestRender() });
1585
+ interaction.addChild(view);
1586
+ return interaction;
1357
1587
  },
1358
- { overlay: true, overlayOptions: { width: "92%", anchor: "center" } },
1588
+ { overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 0, anchor: "center" } },
1359
1589
  );
1360
1590
  if (!chosen || !host) return;
1361
- if (!openInExternalEditor(host, chosen.file.path, process.env, spawnSync, chosen.root)) ctx.ui.notify("No editor configured. Set $VISUAL or $EDITOR.", "warning");
1591
+ try {
1592
+ if (!openInExternalEditor(host, chosen.file.path, process.env, spawnSync, chosen.root)) ctx.ui.notify("No editor configured. Set $VISUAL or $EDITOR.", "warning");
1593
+ } catch (error) {
1594
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
1595
+ }
1362
1596
  } finally {
1363
1597
  clearInterval(poll);
1364
1598
  view?.dispose();
@@ -1738,6 +1972,7 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1738
1972
  onClose: () => withOverlayRepaint(tui, done)(null),
1739
1973
  requestRender: () => tui.requestRender(),
1740
1974
  scope: () => ({ providers: usageScope, failed: new Set(usageFailures) }),
1975
+ gauge: () => gaugePaint(ctx),
1741
1976
  });
1742
1977
  // Open promptly: the panel draws whatever the store already holds
1743
1978
  // while the forced refresh runs underneath it, and repaints when
@@ -1759,6 +1994,8 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1759
1994
  });
1760
1995
  }
1761
1996
  let prompt: GentlePromptEditor | undefined;
1997
+ // Images pasted into the current draft; emptied on every submit.
1998
+ const imageAttachments = new ImageAttachments();
1762
1999
  // Set by abortAndDispatchQueued via dispatchQueuedText when an Esc aborts
1763
2000
  // a turn with a non-empty queue; sent exactly once, from agent_settled,
1764
2001
  // once the aborted run has fully settled (issue #1218). Several aborts
@@ -1911,20 +2148,28 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1911
2148
  const bottom = createShellBarComponent(pi, ctx, renderHost, theme, footerData, () => tracker.model.files.length, () => usage.get(ctx.model?.provider ?? ""), () => visualSettings);
1912
2149
  // The Status card paints live session state that no event re-registers a
1913
2150
  // part for: model, effort, context, cost, session name and extension
1914
- // statuses. The digest is what keeps the fullscreen memo honest, and it
1915
- // rebuilds the model exactly as the narrow bottom bar does every frame.
1916
- const footerModel = (): ShellBarModel => ({
1917
- ...buildShellBarModel(pi, ctx, footerData, { dirty: tracker.model.files.length, usage: usage.get(ctx.model?.provider ?? ""), profile: deps.activeProfile(), jobs: runningJobs }),
1918
- changes: { files: tracker.model.files.length, added: tracker.model.added, deleted: tracker.model.deleted },
1919
- review,
2151
+ // statuses. The digest is what keeps the fullscreen memo honest. Issue
2152
+ // #1681: railDigest evaluates every rail's digest per keystroke, so both
2153
+ // Status rails digest a cheap key and reuse the memoized model
2154
+ // (createFooterStatusMemo).
2155
+ const footerStatus = createFooterStatusMemo({
2156
+ pi, ctx, footerData,
2157
+ usage: () => usage.get(ctx.model?.provider ?? ""),
2158
+ profile: () => deps.activeProfile(),
2159
+ jobs: () => runningJobs,
2160
+ review: () => review,
2161
+ tracker: () => tracker,
2162
+ visualSettings: () => visualSettings,
2163
+ cardStyle: () => cardStyle(),
1920
2164
  });
2165
+ const footerModel = footerStatus.model;
1921
2166
  // At narrow fullscreen widths only one status row paints: a top header
1922
2167
  // suppresses the bottom bar in the layout, and otherwise the bottom bar
1923
2168
  // takes over the header's data while the below-input header steps aside.
1924
2169
  const statusOwner = () => narrowStatusOwner({ mode: (tui as TUI & { mode?: string }).mode, columns: tui.terminal?.columns ?? 0, statusPlacement: visualSettings.statusPlacement, headerPlacement: visualSettings.headerPlacement });
1925
2170
  const statusCardVisible = () => Boolean(tui.terminal && tui.terminal.columns >= SIDEBAR_BREAKPOINT && sidebarState(tui).ownsHost?.() && visualSettings.statusPlacement !== "hidden");
1926
2171
  const belowFloat = (width: number, statuses: boolean) => (tui as TUI & { mode?: string }).mode === "fullscreen"
1927
- ? renderShellBelowInputFloat({ ...footerModel(), ...(statuses ? {} : { statuses: [] }), ...(statusCardVisible() ? { jobs: undefined } : {}) }, theme, width, usageShortcutKey, visualSettings, tracker.model)
2172
+ ? renderShellBelowInputFloat({ ...footerModel(), ...(statuses ? {} : { statuses: [] }), ...(statusCardVisible() ? { jobs: undefined } : {}) }, theme, width, usageShortcutKey, visualSettings, tracker.model, gaugePaint(ctx))
1928
2173
  : undefined;
1929
2174
  // Match sidebarPart's live paint ownership, including an unavailable rail.
1930
2175
  const footerSuppressed = () => {
@@ -1935,7 +2180,7 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1935
2180
  groupedChangesOwner = (width) => belowFloat(width, !footerSuppressed()) !== undefined;
1936
2181
  const bottomBar = {
1937
2182
  ...bottom,
1938
- render: (width: number) => belowFloat(width, true)?.rows ?? (statusOwner() === STATUS_OWNER.BOTTOM ? renderShellBottomOnlyBar(footerModel(), theme, width, usageShortcutKey, visualSettings) : bottom.render(width)),
2183
+ render: (width: number) => belowFloat(width, true)?.rows ?? (statusOwner() === STATUS_OWNER.BOTTOM ? renderShellBottomOnlyBar(footerModel(), theme, width, usageShortcutKey, visualSettings, undefined, gaugePaint(ctx)) : bottom.render(width)),
1939
2184
  handleMouse(event: Parameters<NonNullable<SidebarRail["handleMouse"]>>[0]) {
1940
2185
  if (footerSuppressed() || event.type !== "click" || event.button !== "left") return undefined;
1941
2186
  const chrome = belowFloat(event.width, true);
@@ -1945,16 +2190,16 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1945
2190
  },
1946
2191
  };
1947
2192
  const part = sidebarPart(tui, "footer", bottomBar, {
1948
- digest: () => JSON.stringify([footerModel(), tracker.model, visualSettings, cardStyle()]),
2193
+ digest: () => footerStatus.key(),
1949
2194
  render: (width) => renderShellSidebarBar(footerModel(), theme, width, visualSettings),
1950
2195
  invalidate() {},
1951
2196
  });
1952
2197
  // The header row carries everything that ticks every frame (model,
1953
2198
  // effort, context, cost, usage) plus session identity and the jobs count
1954
2199
  // when Status is absent. Opaque statuses and working state stay excluded.
1955
- const headerBar = (width: number) => renderShellHeaderChrome(buildShellHeaderModel(footerModel(), !statusCardVisible() && (statusOwner() === STATUS_OWNER.HEADER || visualSettings.statusPlacement === "hidden")), theme, width, usageShortcutKey, visualSettings);
2200
+ const headerBar = (width: number) => renderShellHeaderChrome(buildShellHeaderModel(footerModel(), !statusCardVisible() && (statusOwner() === STATUS_OWNER.HEADER || visualSettings.statusPlacement === "hidden")), theme, width, usageShortcutKey, visualSettings, gaugePaint(ctx));
1956
2201
  const disposeHeader = sidebarHeader(tui, {
1957
- digest: () => JSON.stringify([footerModel(), tracker.model, visualSettings, cardStyle()]),
2202
+ digest: () => footerStatus.key(),
1958
2203
  render: (width) => headerBar(width).rows,
1959
2204
  invalidate() {},
1960
2205
  handleMouse(event) {
@@ -2003,8 +2248,15 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
2003
2248
  prompt.setAnimationPolicy(animationPolicy);
2004
2249
  prompt.setVimPolicy(vimPolicy);
2005
2250
  },
2006
- { now: () => deps.now(), doubleEscCancelEnabled: () => doubleEscCancelPolicy === "on", dispatchQueuedText: (text) => { pendingQueuedText = pendingQueuedText === undefined ? text : `${pendingQueuedText}\n\n${text}`; }, vimRuntimeVersion: deps.vimRuntimeVersion },
2251
+ { now: () => deps.now(), doubleEscCancelEnabled: () => doubleEscCancelPolicy === "on", dispatchQueuedText: (text) => { pendingQueuedText = pendingQueuedText === undefined ? text : `${pendingQueuedText}\n\n${text}`; }, vimRuntimeVersion: deps.vimRuntimeVersion, attachments: imageAttachments, readClipboardImage: () => (deps.readClipboardImage ?? readClipboardImage)() },
2007
2252
  );
2253
+ imageAttachments.clear();
2254
+ ctx.ui.setWidget(IMAGE_ATTACHMENTS_WIDGET_KEY, ownsPrompt
2255
+ // Kitty re-sends a full image block whenever content above it moves, so
2256
+ // thumbnails show only while idle; while the agent streams, labels only.
2257
+ ? (_tui, theme) => imageAttachmentsWidget(imageAttachments, () => prompt?.getText() ?? "", theme, () => modelAcceptsImages(ctx.model),
2258
+ () => getCapabilities().images !== null && ctx.isIdle())
2259
+ : undefined);
2008
2260
  // Hide native feedback only when our petal replaces it. Native transcript
2009
2261
  // thinking blocks remain Pi-owned; this changes only the supported loader UI.
2010
2262
  // Inside Herdr the row stays: its native Pi detection reads it.
@@ -2020,6 +2272,20 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
2020
2272
  shown = "";
2021
2273
  applyChanges(ctx, tracker.model);
2022
2274
  });
2275
+ // Chips still in the submitted draft travel as real image attachments, so
2276
+ // the model sees the image without calling `read` on a temp-file path.
2277
+ pi.on("input", (event, ctx) => {
2278
+ if (event.source !== "interactive" || imageAttachments.size === 0) return { action: "continue" };
2279
+ // The editor may already hold the next draft (a prompt Pi queued during
2280
+ // compaction): its chips stay registered for their own submit.
2281
+ const submitted = imageAttachments.submit(event.text, modelAcceptsImages(ctx.model), prompt?.getText() ?? "");
2282
+ if (!submitted) return { action: "continue" };
2283
+ if (submitted.omitted > 0 && ctx.hasUI) {
2284
+ const count = submitted.omitted === 1 ? "1 pasted image was" : `${submitted.omitted} pasted images were`;
2285
+ ctx.ui.notify(`${count} not sent: the current model does not accept images. Switch to a vision model and paste again.`, "warning");
2286
+ }
2287
+ return { action: "transform", text: submitted.text, images: [...(event.images ?? []), ...submitted.images] };
2288
+ });
2023
2289
  pi.on("session_shutdown", (_event, ctx) => {
2024
2290
  closeCustomize?.();
2025
2291
  if (review) {
@@ -2035,6 +2301,8 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
2035
2301
  oddPhaseRegistry.clear(ctx.sessionManager.getSessionId());
2036
2302
  oddPhaseRegistry.clearRenderRequest(ctx.sessionManager.getSessionId());
2037
2303
  pendingQueuedText = undefined;
2304
+ imageAttachments.clear();
2305
+ ctx.ui.setWidget(IMAGE_ATTACHMENTS_WIDGET_KEY, undefined);
2038
2306
  prompt?.dispose();
2039
2307
  prompt = undefined;
2040
2308
  if ((ctx.ui.getEditorComponent() as PromptFactory | undefined)?.[PROMPT_OWNER]) {
@@ -2054,10 +2322,14 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
2054
2322
  applyChanges(ctx, model);
2055
2323
  // A failed root scan is unknown, never clean: say so whenever one failed.
2056
2324
  const failed = tracker.failedRoots.length;
2325
+ const unborn = tracker.unbornRoots.length;
2057
2326
  const roots = tracker.scannedRootCount;
2058
2327
  if (model.files.length === 0) {
2059
2328
  if (failed > 0 && failed === roots) ctx.ui.notify(`Could not read Git status for ${failed} ${failed === 1 ? "root" : "roots"}; uncommitted changes are unknown.`, "error");
2060
2329
  else if (failed > 0) ctx.ui.notify(`No uncommitted changes in the roots Git could read; Git status failed for ${failed} of ${roots} roots.`, "warning");
2330
+ else if (unborn > 0) ctx.ui.notify(unborn === roots
2331
+ ? `No commits yet in ${unborn === 1 ? "this session's repository" : "any root"} — make a commit to start tracking changes.`
2332
+ : `No uncommitted changes; ${unborn} of ${roots} roots have no commits yet, so they are not tracked.`, "info");
2061
2333
  else ctx.ui.notify("No uncommitted changes in this session's repository or its subagents' worktrees.", "info");
2062
2334
  return;
2063
2335
  }
@@ -2254,19 +2526,21 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
2254
2526
  await refreshYolo();
2255
2527
  },
2256
2528
  });
2257
- for (const mode of ["normal", "auto", "yolo"] as const) rows.push({
2529
+ for (const mode of ["normal", "auto", "yolo", "super-yolo"] as const) rows.push({
2258
2530
  category,
2259
2531
  label: () => `Session permissions: ${mode.toUpperCase()}${mode === "auto" ? " (beta)" : ""}${permissionDisplay === mode ? " (current)" : ""}`,
2260
2532
  preview: () => ({ title: "Permissions · current session", sample: mode === "auto"
2261
2533
  ? "independent session-model action checks · extra inference cost · hard safety and native consent remain"
2262
- : mode === "yolo" ? "ordinary scoped delivery permission · destructive confirmations remain" : "normal checks · overrides an elevated global default" }),
2534
+ : mode === "yolo" ? "ordinary scoped delivery permission · destructive confirmations remain"
2535
+ : mode === "super-yolo" ? "no permission prompts · only private-path protection (.env, keys, secrets) remains"
2536
+ : "normal checks · overrides an elevated global default" }),
2263
2537
  action: async () => {
2264
2538
  if (closed) return;
2265
2539
  if (!adapter?.setMode || !await adapter.setMode(mode)) ctx.ui.notify("Session mode unchanged — live primary UI unavailable or changed.", "warning");
2266
2540
  await refreshYolo();
2267
2541
  },
2268
2542
  });
2269
- for (const mode of ["normal", "auto", "yolo"] as const) rows.push({
2543
+ for (const mode of ["normal", "auto", "yolo", "super-yolo"] as const) rows.push({
2270
2544
  category,
2271
2545
  label: () => `Default permissions: ${mode.toUpperCase()}${mode === "auto" ? " (beta)" : ""}${permissionDefault === mode ? " (saved)" : ""}`,
2272
2546
  preview: () => ({ title: "Permissions · user-global default", sample: `New primary sessions start in ${mode.toUpperCase()}; current session stays unchanged. ${mode === "auto" ? "Additional classifier inference uses the session model; usage is separate." : "Independent safety and consent remain."}` }),