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.
- package/README.md +340 -138
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -1
- package/assets/design-canvas/canvas.css +648 -0
- package/assets/design-canvas/canvas.js +1497 -0
- package/assets/design-canvas/index.html +88 -0
- package/assets/design-canvas/inspector.js +290 -0
- package/assets/orchestrator-delegation-reference.md +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +11 -11
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +237 -139
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/bundled-install.md +65 -30
- package/docs/delegated-verification.md +16 -8
- package/docs/gentle-agents-activity.md +16 -6
- package/docs/gentle-music.md +61 -0
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +80 -9
- package/docs/install-wizard.md +37 -6
- package/docs/permission-modes.md +8 -3
- package/docs/readme-reference.md +120 -28
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +38 -10
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +270 -48
- package/extensions/gentle-ai.ts +490 -113
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +359 -85
- package/extensions/gentle-todo.ts +43 -9
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +384 -0
- package/extensions/skill-registry.ts +63 -8
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +60 -1
- package/lib/agents-runner.ts +107 -9
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-view.ts +3 -1
- package/lib/agents-widget.ts +14 -1
- package/lib/auto-permission-memory.ts +48 -18
- package/lib/auto-permission-runtime.ts +21 -6
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +2 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +120 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/design-canvas-server.ts +947 -0
- package/lib/design-comments.ts +437 -0
- package/lib/design-handoffs.ts +313 -0
- package/lib/design-open-browser.ts +58 -0
- package/lib/design-sources.ts +120 -0
- package/lib/destructive-command-guard.ts +120 -9
- package/lib/external-editor.ts +31 -0
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +146 -36
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +154 -15
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +51 -2
- package/lib/herdr-activity.ts +24 -0
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +20 -5
- package/lib/notification-audio-native.ts +149 -13
- package/lib/notification-audio.ts +4 -0
- package/lib/notification-pulse-worker.ts +82 -18
- package/lib/notification-service.ts +5 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +87 -8
- package/lib/orchestrator-scope.ts +31 -3
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-freeze.ts +30 -1
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +523 -0
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +29 -28
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +24 -5
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +30 -0
- package/lib/vim-editor-adapter.ts +11 -8
- package/lib/voice-artifacts.ts +188 -0
- package/lib/voice-command.ts +576 -0
- package/lib/voice-install.ts +455 -0
- package/lib/voice-platform.ts +99 -0
- package/lib/voice-push-to-talk.ts +262 -0
- package/lib/voice-session.ts +460 -0
- package/lib/voice-settings.ts +50 -0
- package/lib/voice-ui.ts +158 -0
- package/lib/yolo-session-policy.ts +58 -22
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +1 -2
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +145 -35
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +152 -13
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +51 -2
- package/runtime/native-review-cli.mjs +19 -4
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +119 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +5 -3
- package/scripts/installer-server.mjs +14 -4
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +94 -19
- package/scripts/pi-update.mjs +34 -6
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +43 -13
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-config.test.ts +61 -0
- package/tests/agents-fake-child.ts +8 -1
- package/tests/agents-protocol.test.ts +32 -0
- package/tests/agents-runner.test.ts +485 -7
- package/tests/agents-session-transport.test.ts +53 -3
- package/tests/ask-user-choice.test.ts +30 -0
- package/tests/auto-mode.test.ts +17 -0
- package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
- package/tests/auto-permission-runtime.test.ts +85 -1
- package/tests/autonomous-guard.test.ts +142 -0
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +15 -5
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +225 -0
- package/tests/delegate-for-reason-contract.test.ts +92 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/design-canvas-assets.test.ts +710 -0
- package/tests/design-canvas-server.test.ts +1096 -0
- package/tests/design-comments.test.ts +201 -0
- package/tests/design-handoffs.test.ts +282 -0
- package/tests/design-open-browser.test.ts +70 -0
- package/tests/design-sources.test.ts +132 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +132 -57
- package/tests/external-editor.test.ts +84 -0
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +645 -21
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-ai.test.ts +292 -13
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +335 -0
- package/tests/gentle-shell-bin.test.ts +163 -0
- package/tests/gentle-shell-bundled.test.ts +392 -176
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +107 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +89 -0
- package/tests/gentle-shell.test.ts +225 -17
- package/tests/gentle-todo.test.ts +190 -0
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/image-attachments.test.ts +119 -0
- package/tests/image-paste-chips.test.ts +236 -0
- package/tests/inprocess-reviewer.test.ts +55 -2
- package/tests/installer-bundles.test.ts +12 -6
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +10 -2
- package/tests/installer-windows-bootstrap.test.ts +94 -7
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +17 -4
- package/tests/native-review-parity.test.ts +23 -2
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/orchestrator-budget.test.ts +22 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +80 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +24 -20
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update-launcher.test.ts +21 -2
- package/tests/pi-update.test.ts +99 -4
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +79 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +147 -3
- package/tests/review-host-relay.test.ts +9 -2
- package/tests/review-json-arguments.test.ts +20 -3
- package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-changes.test.ts +2 -1
- package/tests/session-profile-append-controller-fork.test.ts +458 -0
- package/tests/session-profile-append-controller-pi.test.ts +290 -0
- package/tests/session-profile-append-controller.test.ts +731 -0
- package/tests/session-profile-disk-reader-pi.test.ts +183 -0
- package/tests/session-profile-disk-reader.test.ts +311 -0
- package/tests/session-profile-freeze.test.ts +23 -0
- package/tests/session-scratch.test.ts +1011 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +21 -2
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +50 -0
- package/tests/short-check-routing-contract.test.ts +1 -1
- package/tests/skill-collision-prefixes.test.ts +3 -1
- package/tests/skill-registry-watcher.test.ts +17 -5
- package/tests/skill-registry.test.ts +47 -3
- package/tests/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +70 -10
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +154 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/voice-command.test.ts +773 -0
- package/tests/voice-install.test.ts +323 -0
- package/tests/voice-platform.test.ts +176 -0
- package/tests/voice-push-to-talk.test.ts +444 -0
- package/tests/voice-session.test.ts +434 -0
- package/tests/voice-settings.test.ts +49 -0
- package/tests/voice-ui.test.ts +97 -0
- package/tests/voice-worker.test.ts +445 -0
- package/tests/weak-model-economy-prompt.test.ts +32 -0
- package/tests/windows-hidden-processes.test.ts +30 -2
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +19 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /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,
|
|
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(
|
|
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:
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
if (
|
|
299
|
-
|
|
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(
|
|
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
|
|
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.
|
|
467
|
-
if (this.promptState === PROMPT_STATE.WORKING) this.
|
|
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
|
|
729
|
+
private startWatchdog(): void {
|
|
553
730
|
if (this.animationPolicy === "potato") return;
|
|
554
|
-
|
|
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
|
-
},
|
|
558
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
1135
|
-
if (this.
|
|
1136
|
-
this.
|
|
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
|
|
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
|
|
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.
|
|
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(
|
|
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
|
-
|
|
1584
|
+
const interaction = createNativeFullscreenInteraction({ keyboardTarget: view, requestRender: () => tui.requestRender() });
|
|
1585
|
+
interaction.addChild(view);
|
|
1586
|
+
return interaction;
|
|
1357
1587
|
},
|
|
1358
|
-
{ overlay: true, overlayOptions: { width: "
|
|
1588
|
+
{ overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 0, anchor: "center" } },
|
|
1359
1589
|
);
|
|
1360
1590
|
if (!chosen || !host) return;
|
|
1361
|
-
|
|
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
|
|
1915
|
-
//
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
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: () =>
|
|
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: () =>
|
|
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"
|
|
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."}` }),
|