gentle-pi 4.3.0 → 4.5.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 (218) hide show
  1. package/README.md +10 -7
  2. package/assets/agents/gentle-ai-worker.md +1 -1
  3. package/assets/design-canvas/canvas.css +648 -0
  4. package/assets/design-canvas/canvas.js +1497 -0
  5. package/assets/design-canvas/index.html +88 -0
  6. package/assets/design-canvas/inspector.js +290 -0
  7. package/assets/orchestrator-delegation-reference.md +2 -2
  8. package/assets/orchestrator-verification.md +1 -1
  9. package/assets/orchestrator.md +2 -2
  10. package/bin/engram-guard +1 -1
  11. package/bin/gentle-shell.mjs +138 -155
  12. package/docs/bundled-install.md +47 -42
  13. package/docs/delegated-verification.md +6 -0
  14. package/docs/gentle-agents-activity.md +5 -1
  15. package/docs/gentle-music.md +82 -0
  16. package/docs/gentle-shell.md +80 -10
  17. package/docs/install-wizard.md +30 -6
  18. package/docs/permission-modes.md +7 -2
  19. package/docs/readme-reference.md +72 -55
  20. package/docs/session-profile-format.md +26 -0
  21. package/docs/voice.md +259 -0
  22. package/docs/yolo-mode.md +17 -2
  23. package/extensions/ask-user-choice.ts +4 -1
  24. package/extensions/gentle-agents.ts +187 -40
  25. package/extensions/gentle-ai.ts +1765 -1185
  26. package/extensions/gentle-design.ts +158 -0
  27. package/extensions/gentle-music.ts +354 -0
  28. package/extensions/gentle-shell.ts +418 -79
  29. package/extensions/gentle-voice.ts +11 -0
  30. package/extensions/session-scratch.ts +41 -22
  31. package/extensions/skill-registry.ts +63 -8
  32. package/lib/agent-profiles.ts +26 -0
  33. package/lib/agents-config.ts +35 -0
  34. package/lib/agents-protocol.ts +57 -1
  35. package/lib/agents-runner.ts +52 -3
  36. package/lib/agents-session-transport.ts +101 -6
  37. package/lib/agents-widget.ts +14 -1
  38. package/lib/auto-permission-memory.ts +48 -18
  39. package/lib/auto-permission-runtime.ts +21 -6
  40. package/lib/clipboard-image.ts +147 -0
  41. package/lib/command-palette-catalog.ts +3 -2
  42. package/lib/commit-slice-signal.ts +1 -1
  43. package/lib/context-backstop.ts +36 -0
  44. package/lib/design-canvas-server.ts +947 -0
  45. package/lib/design-comments.ts +437 -0
  46. package/lib/design-handoffs.ts +313 -0
  47. package/lib/design-open-browser.ts +58 -0
  48. package/lib/design-sources.ts +120 -0
  49. package/lib/external-editor.ts +31 -0
  50. package/lib/gentle-design.ts +513 -0
  51. package/lib/gentle-shell-bundled.ts +141 -57
  52. package/lib/gentle-shell-launcher.ts +11 -4
  53. package/lib/gentle-shell-startup-update.ts +61 -9
  54. package/lib/gentle-shell-uninstall.ts +33 -1
  55. package/lib/image-attachments.ts +150 -0
  56. package/lib/inprocess-reviewer.ts +33 -2
  57. package/lib/jobs-sidebar-state.ts +22 -3
  58. package/lib/jobs-widget.ts +112 -0
  59. package/lib/nan-provider.ts +3 -0
  60. package/lib/native-review-cli.ts +11 -4
  61. package/lib/notification-audio-native.ts +149 -13
  62. package/lib/notification-audio.ts +4 -0
  63. package/lib/notification-pulse-worker.ts +82 -18
  64. package/lib/notification-service.ts +5 -1
  65. package/lib/orchestrator-presence.ts +78 -8
  66. package/lib/orchestrator-scope.ts +10 -0
  67. package/lib/orchestrator-state.ts +11 -3
  68. package/lib/permission-mode-policy.ts +2 -2
  69. package/lib/review-host-relay.ts +11 -2
  70. package/lib/review-reminder-receipt.ts +24 -1
  71. package/lib/session-profile-append-controller.ts +713 -0
  72. package/lib/session-profile-disk-reader.ts +182 -0
  73. package/lib/session-profile-freeze.ts +2 -2
  74. package/lib/session-profile-persistence.ts +15 -7
  75. package/lib/session-scratch.ts +52 -3
  76. package/lib/session-worktree-registry.ts +7 -0
  77. package/lib/shell-bar.ts +6 -7
  78. package/lib/shell-changes.ts +81 -5
  79. package/lib/shell-prompt.ts +4 -4
  80. package/lib/shell-sidebar-layout.ts +27 -7
  81. package/lib/shell-todo.ts +1 -1
  82. package/lib/verifier-admission.ts +5 -4
  83. package/lib/vim-editor-adapter.ts +14 -10
  84. package/lib/voice-artifacts.ts +188 -0
  85. package/lib/voice-command.ts +576 -0
  86. package/lib/voice-install.ts +455 -0
  87. package/lib/voice-platform.ts +99 -0
  88. package/lib/voice-push-to-talk.ts +262 -0
  89. package/lib/voice-session.ts +460 -0
  90. package/lib/voice-settings.ts +50 -0
  91. package/lib/voice-ui.ts +158 -0
  92. package/lib/yolo-session-policy.ts +58 -22
  93. package/package.json +1 -1
  94. package/runtime/gentle-shell-bundled.mjs +140 -56
  95. package/runtime/gentle-shell-launcher.mjs +11 -4
  96. package/runtime/gentle-shell-startup-update.mjs +61 -9
  97. package/runtime/gentle-shell-uninstall.mjs +33 -1
  98. package/runtime/native-review-cli.mjs +11 -4
  99. package/scripts/bundled-engram-migration.mjs +0 -6
  100. package/scripts/bundled-install.mjs +209 -13
  101. package/scripts/gentle-ai-installer.mjs +10 -10
  102. package/scripts/installer-downloads.mjs +31 -7
  103. package/scripts/installer-preflight.mjs +6 -4
  104. package/scripts/installer-windows.mjs +37 -7
  105. package/scripts/main-channel.mjs +91 -17
  106. package/scripts/pi-update.mjs +37 -7
  107. package/scripts/verify-package-files.mjs +26 -2
  108. package/scripts/voice-worker.cjs +476 -0
  109. package/skills/design/SKILL.md +110 -0
  110. package/tests/agent-profiles.test.ts +24 -0
  111. package/tests/agents-config.test.ts +61 -0
  112. package/tests/agents-fake-child.ts +8 -1
  113. package/tests/agents-protocol.test.ts +32 -0
  114. package/tests/agents-runner.test.ts +226 -5
  115. package/tests/agents-session-transport.test.ts +53 -3
  116. package/tests/ask-user-choice.test.ts +30 -0
  117. package/tests/auto-mode.test.ts +17 -0
  118. package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
  119. package/tests/auto-permission-runtime.test.ts +85 -1
  120. package/tests/autonomous-guard.test.ts +20 -0
  121. package/tests/bundled-install.test.ts +276 -5
  122. package/tests/clipboard-image.test.ts +114 -0
  123. package/tests/command-palette.test.ts +4 -4
  124. package/tests/context-backstop.test.ts +70 -4
  125. package/tests/delegate-for-reason-contract.test.ts +2 -1
  126. package/tests/design-canvas-assets.test.ts +710 -0
  127. package/tests/design-canvas-server.test.ts +1096 -0
  128. package/tests/design-comments.test.ts +201 -0
  129. package/tests/design-handoffs.test.ts +282 -0
  130. package/tests/design-open-browser.test.ts +70 -0
  131. package/tests/design-sources.test.ts +132 -0
  132. package/tests/engram-safety.test.ts +132 -57
  133. package/tests/external-editor.test.ts +84 -0
  134. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +3 -3
  135. package/tests/gentle-agents.test.ts +369 -24
  136. package/tests/gentle-ai-binary.test.ts +1 -1
  137. package/tests/gentle-ai-installer.test.ts +51 -51
  138. package/tests/gentle-ai.test.ts +928 -448
  139. package/tests/gentle-design.test.ts +1313 -0
  140. package/tests/gentle-music.test.ts +452 -0
  141. package/tests/gentle-shell-bundled.test.ts +321 -248
  142. package/tests/gentle-shell-launcher.test.ts +29 -2
  143. package/tests/gentle-shell-startup-update.test.ts +77 -1
  144. package/tests/gentle-shell-uninstall.test.ts +59 -0
  145. package/tests/gentle-shell.test.ts +227 -27
  146. package/tests/gentle-todo.test.ts +11 -0
  147. package/tests/image-attachments.test.ts +119 -0
  148. package/tests/image-paste-chips.test.ts +236 -0
  149. package/tests/inprocess-reviewer.test.ts +55 -2
  150. package/tests/installer-bundles.test.ts +12 -6
  151. package/tests/installer-downloads.test.ts +32 -0
  152. package/tests/installer-posix-bootstrap.test.ts +8 -8
  153. package/tests/installer-preflight.test.ts +16 -0
  154. package/tests/installer-server.test.ts +2 -2
  155. package/tests/installer-windows-bootstrap.test.ts +61 -3
  156. package/tests/installer-windows-diagnostics.test.ts +111 -0
  157. package/tests/jobs-widget.test.ts +138 -0
  158. package/tests/main-channel-upgrade.test.ts +232 -2
  159. package/tests/main-channel.test.ts +140 -21
  160. package/tests/model-profiles-preservation.test.ts +2 -2
  161. package/tests/nan-provider.test.ts +36 -0
  162. package/tests/native-review-capability-contract.test.ts +11 -4
  163. package/tests/native-review-parity.test.ts +1 -1
  164. package/tests/notification-audio-native.test.ts +128 -9
  165. package/tests/notification-customize.test.ts +17 -5
  166. package/tests/odd-routing-canonical-ratchet.test.ts +2 -1
  167. package/tests/orchestrator-budget.test.ts +5 -2
  168. package/tests/orchestrator-presence.test.ts +100 -2
  169. package/tests/orchestrator-rdd-ownership.test.ts +2 -2
  170. package/tests/orchestrator-scope.test.ts +14 -0
  171. package/tests/orchestrator-state.test.ts +29 -0
  172. package/tests/package-files.test.ts +34 -0
  173. package/tests/package-manifest.test.ts +6 -6
  174. package/tests/permission-mode-policy.test.ts +10 -1
  175. package/tests/pi-update.test.ts +104 -4
  176. package/tests/rdd-aware-verification-contract.test.ts +1 -1
  177. package/tests/review-host-relay-routing.test.ts +88 -1
  178. package/tests/review-host-relay.test.ts +9 -2
  179. package/tests/review-json-arguments.test.ts +20 -3
  180. package/tests/review-loadout-rdd-off-sdk.test.ts +23 -2
  181. package/tests/review-relay-transport-agent.test.ts +26 -0
  182. package/tests/review-reminder-receipt.test.ts +31 -1
  183. package/tests/runtime-harness.mjs +188 -211
  184. package/tests/session-changes.test.ts +2 -1
  185. package/tests/session-profile-append-controller-fork.test.ts +458 -0
  186. package/tests/session-profile-append-controller-pi.test.ts +290 -0
  187. package/tests/session-profile-append-controller.test.ts +731 -0
  188. package/tests/session-profile-disk-reader-pi.test.ts +183 -0
  189. package/tests/session-profile-disk-reader.test.ts +311 -0
  190. package/tests/session-profile-freeze.test.ts +6 -4
  191. package/tests/session-scratch.test.ts +68 -2
  192. package/tests/session-worktree-registry.test.ts +13 -1
  193. package/tests/shell-bar.test.ts +18 -6
  194. package/tests/shell-changes.test.ts +148 -1
  195. package/tests/shell-prompt.test.ts +3 -3
  196. package/tests/shell-render-cache.test.ts +134 -1
  197. package/tests/shell-sidebar-layout.test.ts +67 -0
  198. package/tests/short-check-routing-contract.test.ts +1 -1
  199. package/tests/skill-collision-prefixes.test.ts +3 -1
  200. package/tests/skill-registry-watcher.test.ts +17 -5
  201. package/tests/skill-registry.test.ts +47 -3
  202. package/tests/task-size-routing-contract.test.ts +1 -1
  203. package/tests/test-strength-contract.test.ts +1 -1
  204. package/tests/verifier-admission.test.ts +18 -1
  205. package/tests/vim-editor-adapter.test.ts +2 -2
  206. package/tests/voice-command.test.ts +773 -0
  207. package/tests/voice-install.test.ts +323 -0
  208. package/tests/voice-platform.test.ts +176 -0
  209. package/tests/voice-push-to-talk.test.ts +444 -0
  210. package/tests/voice-session.test.ts +434 -0
  211. package/tests/voice-settings.test.ts +49 -0
  212. package/tests/voice-ui.test.ts +97 -0
  213. package/tests/voice-worker.test.ts +445 -0
  214. package/tests/weak-model-economy-prompt.test.ts +32 -0
  215. package/tests/windows-hidden-processes.test.ts +25 -2
  216. package/tests/yolo-customize.test.ts +5 -1
  217. package/tests/yolo-mode.test.ts +1 -1
  218. package/tests/yolo-session-policy-capture.test.ts +104 -0
@@ -4,7 +4,7 @@ import { Type } from "typebox";
4
4
  import { recordReviewMutation } from "../lib/review-reminder-receipt.ts";
5
5
  import { SESSION_CHANGE_RELAY } from "../lib/session-changes.ts";
6
6
  import { publishForeignSessionChange } from "../lib/session-change-capture.ts";
7
- import { SESSION_WORKTREE_ENTRY, SESSION_WORKTREE_CHANGED, SessionWorktreeRegistry, resolveSessionWorktree, type WorktreeResolver } from "../lib/session-worktree-registry.ts";
7
+ import { SESSION_WORKTREE_ENTRY, SESSION_WORKTREE_CHANGED, SessionWorktreeRegistry, nativeRootSpelling, resolveSessionWorktree, type WorktreeResolver } from "../lib/session-worktree-registry.ts";
8
8
  import { canonicalWriterRoot } from "../lib/writer-surfaces.ts";
9
9
  import { ForeignTargetGrants } from "../lib/foreign-target-grants.ts";
10
10
  import { MESSAGING_REASON_MAX_UTF8_BYTES, MESSAGING_REASON_MIN_CHARACTERS, normalizeMessagingReason, SessionMessagingGrants } from "../lib/session-messaging-grants.ts";
@@ -19,14 +19,16 @@ import { createJobRegistry, isStandaloneSleep, type JobExecOperations, type JobR
19
19
  import { JOB_GLYPH, JOB_NOTICE_TYPE, jobDetails, jobNoticeText, monitorEventsText, monitorStoppedText, registerBackgroundJobTools } from "../lib/background-jobs-tools.ts";
20
20
  import { createMonitorController, mergeMonitorBatches, type MonitorNotice } from "../lib/background-monitor.ts";
21
21
  import { JobsView } from "../lib/jobs-view.ts";
22
+ import { JOBS_FINISHED_TTL_MS } from "../lib/jobs-widget.ts";
22
23
  import { JOBS_SIDEBAR_EVENT, JOBS_STATUS_KEY } from "../lib/jobs-sidebar-state.ts";
23
- import { Text, type TUI } from "@earendil-works/pi-tui";
24
+ import { Text, type Component, type TUI } from "@earendil-works/pi-tui";
24
25
  import { invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
25
- import { VISUAL_SETTINGS_CHANGED } from "../lib/shell-sidebar.ts";
26
+ import { sidebarPart, VISUAL_SETTINGS_CHANGED } from "../lib/shell-sidebar.ts";
27
+ import { NativePointerRegion } from "../lib/native-pointer-region.ts";
26
28
  import { resolveVisualSettings } from "../lib/visual-customization-policy.ts";
27
29
  import { createCompletionQueue } from "../lib/agents-completion-delivery.ts";
28
30
  import { createAgentMessageQueue, type PendingAgentMessage } from "../lib/agents-message-delivery.ts";
29
- import { AGENT_MODE, PROFILE_SOURCE, discoverAgents, formatModelRef, loadAgentsConfig, resolveAgentProfile, withPinnedModelProfiles, type AgentDefinition, type AgentMode, type ProfileSource } from "../lib/agents-config.ts";
31
+ import { AGENT_MODE, PROFILE_SOURCE, discoverAgents, formatModelRef, loadAgentsConfig, resolveAgentProfile, unmatchedProfileKeys, withPinnedModelProfiles, type AgentDefinition, type AgentMode, type ProfileSource } from "../lib/agents-config.ts";
30
32
  import { freezeSessionProfileAtStartup, inheritedProfileDriftNotice, resolveSessionProfile, sessionProfileRoutingAt, type InheritedProfileOptions, type SessionProfileResolution } from "../lib/session-profile-freeze.ts";
31
33
  import { resolveProfilePin, resolveUnversionedProjectProfile } from "../lib/agent-profile-pin.ts";
32
34
  import { resolveBackgroundSubagentsPolicy } from "../lib/background-subagents-policy.ts";
@@ -35,7 +37,7 @@ import { isFinished, MISSING_TOOLS_NOTE_PREFIX, TASK_EVENT, TASK_STATUS, TaskSto
35
37
  import { AgentRunner, piCommand, abortReasonText, REQUESTED_TOOLS_ENV, type AskAnswer, type RunnerDeps, type TaskRequest } from "../lib/agents-runner.ts";
36
38
  import { parseChildPackageInjection } from "../lib/child-package-injection.ts";
37
39
  import { ChildMessenger, type IpcEndpoint } from "../lib/agents-messaging.ts";
38
- import { ActiveSessionClient, ActiveSessionListener, SessionPresenceRegistry, type PresenceRecord, type ReceivedNotification, type SentNotification, type SessionPresenceCandidate } from "../lib/agents-session-transport.ts";
40
+ import { ActiveSessionClient, ActiveSessionListener, SessionPresenceError, SessionPresenceRegistry, type PresenceRecord, type ReceivedNotification, type SentNotification, type SessionPresenceCandidate } from "../lib/agents-session-transport.ts";
39
41
  import { WindowsActiveSessionClient, WindowsActiveSessionListener, WindowsSessionPresenceRegistry, type WindowsSessionRegistryPhaseObserver } from "../lib/windows-session-transport.ts";
40
42
  import { hasReviewSessionPermission, resolveCanonicalGitRepositoryIdentitySync, type ReviewSessionManager } from "../lib/review-session-standing-permission.ts";
41
43
  import { inheritedUnsafeGitEnvironmentKeys } from "../lib/review-repository.ts";
@@ -58,8 +60,8 @@ import { OrchestratorScopeCache, type RepositoryFact } from "../lib/orchestrator
58
60
  import { createRpcActivityPublisher, type RpcActivityPublisher } from "../lib/agents-rpc-publisher.ts";
59
61
  import { isInteractiveRpcHost } from "../lib/rpc-host.ts";
60
62
  import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
61
- import { AGENTS_GLYPH, renderAgentsCard, widgetExpiryMs, widgetRows } from "../lib/agents-widget.ts";
62
- import { CARD_TONE, renderCard } from "../lib/shell-card.ts";
63
+ import { AGENTS_GLYPH, agentsCardTone, renderAgentsCard, widgetExpiryMs, widgetRows } from "../lib/agents-widget.ts";
64
+ import { CARD_TONE, panelHeaderRow, renderCard } from "../lib/shell-card.ts";
63
65
  import { openInExternalEditor } from "./gentle-shell.ts";
64
66
  import { gentlePiConfigHome } from "../lib/agent-home.ts";
65
67
  import { resolveAgentHomeDirectory } from "../lib/agent-model-resolution.ts";
@@ -561,7 +563,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
561
563
  };
562
564
  return PresencePublisher.start({ profile: agentHome, sessionId, label: labelSource(), labelSource, activity: [] });
563
565
  };
564
- const publishActivity = () => {
566
+ const publishActivityNow = (deferred = false) => {
565
567
  if (!sessions) return;
566
568
  try {
567
569
  if (!presence || presence.error) {
@@ -580,14 +582,59 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
580
582
  return data?.sessionId === activeSessionId() && typeof data.root === "string" && typeof data.evidence === "string" ? [data.root] : [];
581
583
  });
582
584
  presence?.updateDiscovery(transport.listener.record, { workspace: sessions.getCwd(), tasks, registered,
583
- scope: scopeCache.project(sessions.getCwd(), tasks, registered), state: stateCache.get(sessions) });
585
+ // A deferred (render-cadence trailing) publish must never resolve Git:
586
+ // its timer can land while a peer metadata query is being served, and
587
+ // that contract adds no caller Git probes. It reuses the snapshot the
588
+ // last event-time publication projected; the next event-time publish
589
+ // refreshes the facts.
590
+ scope: deferred ? scopeCache.cachedScope() : scopeCache.project(sessions.getCwd(), tasks, registered), state: stateCache.get(sessions) });
584
591
  }
585
592
  } catch { presence?.dispose(); presence = undefined; }
586
593
  };
594
+ // Coalesced like requestRender: the first call publishes immediately so an
595
+ // idle→working transition or a resumed session never waits, and every call
596
+ // inside the window collapses into one trailing publish, so a burst of child
597
+ // deltas costs one publication instead of one per event.
598
+ let activityPublishQueued = false;
599
+ let activityPublishTrailing = false;
600
+ let cancelActivityPublish: (() => void) | undefined;
601
+ const resetActivityPublish = () => {
602
+ cancelActivityPublish?.();
603
+ cancelActivityPublish = undefined;
604
+ activityPublishQueued = false;
605
+ activityPublishTrailing = false;
606
+ };
607
+ const flushDeferredPublish = () => {
608
+ cancelActivityPublish = undefined;
609
+ activityPublishQueued = false;
610
+ if (!activityPublishTrailing) return;
611
+ activityPublishTrailing = false;
612
+ publishActivityNow(true);
613
+ };
614
+ const publishActivity = () => {
615
+ if (activityPublishQueued) {
616
+ activityPublishTrailing = true;
617
+ return;
618
+ }
619
+ publishActivityNow();
620
+ activityPublishQueued = true;
621
+ cancelActivityPublish = deps.schedule(flushDeferredPublish, RENDER_COALESCE_MS);
622
+ };
623
+ // Discrete semantic events publish immediately so peers and readers see them
624
+ // promptly (a consult binds to the published snapshot); the coalescing window
625
+ // stays armed so a child-delta burst that follows still collapses into one
626
+ // trailing publish.
627
+ const publishActivityPrompt = () => {
628
+ publishActivityNow();
629
+ if (!activityPublishQueued) {
630
+ activityPublishQueued = true;
631
+ cancelActivityPublish = deps.schedule(flushDeferredPublish, RENDER_COALESCE_MS);
632
+ }
633
+ };
587
634
  const unsubscribeScope = pi.events.on(SESSION_WORKTREE_CHANGED, (event) => {
588
635
  if ((event as { sessionId?: string } | undefined)?.sessionId !== sessions?.getSessionId()) return;
589
636
  scopeCache.clear();
590
- publishActivity();
637
+ publishActivityPrompt();
591
638
  });
592
639
  // The todo list is the session's progress: publish it for peers so the model
593
640
  // spends no turn on orchestrator_session_id. Merged into the owner's curated
@@ -736,7 +783,11 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
736
783
  await closeStartupTransport();
737
784
  return;
738
785
  }
739
- publishActivity();
786
+ // Publish immediately, not on the coalescing trailing edge: this is the
787
+ // session's first discovery publication, and a peer metadata query binds
788
+ // to the activation only through it — a 400 ms lag here makes a prompt
789
+ // consult read publication-unknown-or-stale.
790
+ publishActivityPrompt();
740
791
  } catch {
741
792
  if (activeSessionTransport?.generation === generation) activeSessionTransport = undefined;
742
793
  await closeStartupTransport();
@@ -1181,11 +1232,22 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1181
1232
  // Structured descriptions feed Status; native hosts keep the compact count.
1182
1233
  const refreshJobStatus = () => {
1183
1234
  const sessionId = activeSessionId();
1184
- const running = jobs.list(sessionId ?? "").filter((job) => job.status === "running")
1235
+ const sessionJobs = jobs.list(sessionId ?? "");
1236
+ const running = sessionJobs.filter((job) => job.status === "running")
1185
1237
  .sort((a, b) => b.startedAt - a.startedAt);
1238
+ // Finished tails ride the same snapshot so the sidebar card can linger
1239
+ // them briefly, like the Agents card's results; failed jobs ride too,
1240
+ // painting the ✗ glyph like a nonzero exit.
1241
+ const finished = sessionJobs.filter((job) => (job.status === "exited" || job.status === "stopped" || job.status === "failed") && job.endedAt !== undefined && deps.now() - job.endedAt < JOBS_FINISHED_TTL_MS)
1242
+ .sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0)).slice(0, 3);
1186
1243
  try { parentCtx?.ui.setStatus(JOBS_STATUS_KEY, running.length === 0 ? undefined : `${JOB_GLYPH} ${running.length} job${running.length === 1 ? "" : "s"}`); } catch { /* Status is cosmetic. */ }
1187
1244
  if (sessionId) {
1188
- try { pi.events.emit(JOBS_SIDEBAR_EVENT, { sessionId, snapshot: { jobs: running.map(({ id, label, startedAt }) => ({ id, label, startedAt })) } }); } catch { /* Display only. */ }
1245
+ try {
1246
+ pi.events.emit(JOBS_SIDEBAR_EVENT, { sessionId, snapshot: { jobs: [
1247
+ ...running.map(({ id, label, startedAt }) => ({ id, label, startedAt })),
1248
+ ...finished.map(({ id, label, startedAt, status, endedAt, exitCode }) => ({ id, label, startedAt, status, endedAt, exitCode })),
1249
+ ] } });
1250
+ } catch { /* Display only. */ }
1189
1251
  }
1190
1252
  };
1191
1253
 
@@ -1565,7 +1627,11 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1565
1627
  ctx.ui.notify(`Could not read the task's session: ${error instanceof Error ? error.message : String(error)}`, "warning");
1566
1628
  return;
1567
1629
  }
1568
- if (!openInExternalEditor(overlayHost, transcriptPath)) ctx.ui.notify("No editor configured. Set $VISUAL or $EDITOR.", "warning");
1630
+ try {
1631
+ if (!openInExternalEditor(overlayHost, transcriptPath)) ctx.ui.notify("No editor configured. Set $VISUAL or $EDITOR.", "warning");
1632
+ } catch (error) {
1633
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
1634
+ }
1569
1635
  };
1570
1636
 
1571
1637
  const writeTranscript = async (task: TaskRecord): Promise<string> => {
@@ -1597,24 +1663,80 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1597
1663
  host?.requestRender();
1598
1664
  });
1599
1665
  tickClock();
1600
- // Agents is not a rail part: the fullscreen sidebar only suppresses
1601
- // bottom components registered through sidebarPart, and this widget is
1602
- // the only Agents surface in every mode, so it stays a plain component.
1666
+ // Agents follows Todos: the fullscreen rail shows it when the terminal is
1667
+ // wide enough, and sidebarPart suppresses the above-editor copy only
1668
+ // while the rail owns the host; narrower terminals keep the widget.
1603
1669
  ui?.setWidget(AGENTS_WIDGET_KEY, (tui, theme) => {
1604
1670
  host = tui;
1605
1671
  sidebarTui = tui;
1606
- return {
1607
- render(width: number) {
1608
- if (!agentsVisible) return [];
1609
- const lines = renderAgentsCard(visibleTasks(), theme, width, deps.now(), { collapsed, collapseKey, maxRows: widgetRows(tui.terminal?.rows), viewKey });
1610
- return lines.length === 0 ? [] : [...lines, ""];
1611
- },
1612
- invalidate() {},
1613
- };
1672
+ return sidebarPart(tui, "agents", agentsCard(tui, theme, false), agentsCard(tui, theme, true));
1614
1673
  });
1615
1674
  };
1616
1675
 
1676
+ const toggleCollapsed = () => {
1677
+ collapsed = !collapsed;
1678
+ if (sidebarTui) invalidateSidebar(sidebarTui);
1679
+ host?.requestRender();
1680
+ };
1681
+
1682
+ // One surface of the Agents card. The rail is natively scrollable, so it
1683
+ // lists every task; the above-editor widget keeps its row budget. Only the
1684
+ // header row is the clickable collapse control, exactly like Todos.
1685
+ const agentsCard = (tui: TUI, theme: Parameters<typeof renderAgentsCard>[1], rail: boolean): Component & { dispose(): void; digest(): string } => {
1686
+ let hovered = false;
1687
+ let headerRow = 0;
1688
+ const card: Component = {
1689
+ render(width: number) {
1690
+ if (!agentsVisible) return [];
1691
+ const tasks = visibleTasks();
1692
+ const now = deps.now();
1693
+ headerRow = panelHeaderRow(theme, width, agentsCardTone(tasks, now));
1694
+ const lines = renderAgentsCard(tasks, theme, width, now, { collapsed, collapseKey, maxRows: rail ? undefined : widgetRows(tui.terminal?.rows), viewKey, collapseControl: true, hovered });
1695
+ return lines.length === 0 || rail ? lines : [...lines, ""];
1696
+ },
1697
+ invalidate() {
1698
+ hovered = false;
1699
+ },
1700
+ };
1701
+ const region = new NativePointerRegion(card, {
1702
+ onHover(event) {
1703
+ const next = event.y === headerRow;
1704
+ if (next === hovered) return { handled: true };
1705
+ hovered = next;
1706
+ return { handled: true, render: true };
1707
+ },
1708
+ onLeave() {
1709
+ if (!hovered) return;
1710
+ hovered = false;
1711
+ host?.requestRender();
1712
+ },
1713
+ onClick(event) {
1714
+ if (event.button !== "left" || event.y !== headerRow) return undefined;
1715
+ toggleCollapsed();
1716
+ return { handled: true, render: true };
1717
+ },
1718
+ });
1719
+ return {
1720
+ // The rail memo repaints a part only when its digest moves: task
1721
+ // state, the local controls, and the elapsed clock (to the second)
1722
+ // only while a task is still running, so an idle or finished card
1723
+ // never forces a rail rebuild every second.
1724
+ digest: () => {
1725
+ const tasks = visibleTasks();
1726
+ const clock = tasks.some((task) => !isFinished(task.status)) ? Math.floor(deps.now() / 1000) : 0;
1727
+ return JSON.stringify([agentsVisible, collapsed, hovered, tui.terminal?.rows, clock, tasks.map((task) => [task.id, task.status, task.tokens, task.cost, task.label, task.lastStep, task.error])]);
1728
+ },
1729
+ render: (width) => region.render(width),
1730
+ handleMouse: (event) => region.handleMouse(event),
1731
+ invalidate: () => region.invalidate(),
1732
+ dispose: () => region.dispose(),
1733
+ };
1734
+ };
1735
+
1617
1736
  const roots = (ctx: ExtensionContext) => ({ cwd: ctx.sessionManager.getCwd(), home: deps.home, agentHome });
1737
+ // Issue #2400: parent sessions already told about model_profiles keys that
1738
+ // match no agent and suggest none, so the notice fires once per session.
1739
+ const unmatchedKeysNotifiedSessions = new Set<string>();
1618
1740
 
1619
1741
  const buildRequest = async (ctx: ExtensionContext, agent: AgentDefinition, prompt: string, label: string | undefined, context: string | undefined, mode: AgentMode, resume?: string, workspaceRoot?: string, signal?: AbortSignal, repositoryRoot?: string): Promise<TaskRequest> => {
1620
1742
  if (retiredSddAgent(agent.name)) throw new Error("Retired SDD agents cannot be dispatched.");
@@ -1678,7 +1800,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1678
1800
  // same-clone registry. Never use a caller-injected resolver for this identity.
1679
1801
  const identity = resolveSessionWorktree(selectedRoot, parentCwd);
1680
1802
  const canonicalParent = resolveSessionWorktree(parentCwd, parentCwd);
1681
- if (!identity || (canonicalParent && identity.commonDir === canonicalParent.commonDir) || selectedRoot !== identity.root || !isAbsolute(selectedRoot) || realpathSync(selectedRoot) !== selectedRoot) throw new Error("repository_root must select a canonical independent Git repository.");
1803
+ const spelling = nativeRootSpelling(selectedRoot);
1804
+ if (!identity || (canonicalParent && identity.commonDir === canonicalParent.commonDir) || spelling !== identity.root || !isAbsolute(spelling) || realpathSync(spelling) !== spelling) throw new Error("repository_root must select a canonical independent Git repository.");
1682
1805
  await foreignGrants.authorize(ctx, identity, { continuation: resume !== undefined, signal });
1683
1806
  foreignIdentity = identity;
1684
1807
  foreignParent = canonicalParent;
@@ -1729,6 +1852,28 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1729
1852
  if (drift !== undefined) ctx.ui.notify(drift, "info");
1730
1853
  }
1731
1854
  const profile = resolveAgentProfile(agent, config);
1855
+ // Issue #2196 (PR #2233 maintainer decision): a model_profiles key that
1856
+ // names no discovered agent was silently ignored while the child inherited
1857
+ // the orchestrator model. When a configured key suggests this agent, name
1858
+ // it at launch; resolution stays exact-match, so an omitted role keeps
1859
+ // inheriting and a matched key never warns. Keys are checked against every
1860
+ // discovered agent, so a key naming another agent is never reported here.
1861
+ if (ctx.hasUI) {
1862
+ const unmatched = unmatchedProfileKeys(config.modelProfiles, discoverAgents(roots(ctx)).agents);
1863
+ const nearMisses = unmatched.filter((entry) => entry.suggestion === agent.name);
1864
+ if (nearMisses.length > 0) {
1865
+ ctx.ui.notify(nearMisses.map((entry) => `"${entry.key}" did not match any agent, did you mean ${agent.name}?`).join(" "), "warning");
1866
+ }
1867
+ // Issue #2400: a key with no suggestion never names the launched agent,
1868
+ // so the filter above kept it silent. The session's first launch lists
1869
+ // every such key once; later launches in the same session stay quiet.
1870
+ const unsuggested = unmatched.filter((entry) => entry.suggestion === undefined);
1871
+ const parentSession = ctx.sessionManager.getSessionId() ?? "";
1872
+ if (unsuggested.length > 0 && !unmatchedKeysNotifiedSessions.has(parentSession)) {
1873
+ unmatchedKeysNotifiedSessions.add(parentSession);
1874
+ ctx.ui.notify(unsuggested.map((entry) => `"${sanitizeTerminalText(entry.key)}" did not match any agent and was ignored.`).join(" "), "warning");
1875
+ }
1876
+ }
1732
1877
  // #2180: the winning layer is named from the resolution and routing this
1733
1878
  // request already performed, never by re-resolving them.
1734
1879
  const modelSource = modelSourceToken(profile.source.model, sessionProfile, routingTarget);
@@ -1839,7 +1984,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1839
1984
  if (launched && foreignRequest) foreignTasks.set(task.id, foreignRequest);
1840
1985
  ownedTaskIds.add(task.id);
1841
1986
  publishWork?.(task.id);
1842
- publishActivity(); // Admission's summary notification precedes runtime ownership.
1987
+ publishActivityPrompt(); // Admission's summary notification precedes runtime ownership.
1843
1988
  store.subscribe(task.id, () => { publishActivity(); requestRender(); });
1844
1989
  if (request.mode === AGENT_MODE.BACKGROUND) return text(`Started ${task.agent} in the background as task ${task.id}. Retain that id; completion is pushed automatically. Never sleep or periodically poll subagent_status/subagent_result for completion or cache maintenance. Inspect status only at a real orchestration decision boundary; never relaunch equivalent queued/running work.`, taskDetails(task));
1845
1990
  // A tool call aborted by the host (a human interrupting the turn, a timeout)
@@ -1912,7 +2057,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1912
2057
  pi.registerTool({
1913
2058
  name: "orchestrator_session_id",
1914
2059
  label: "Orchestrator session ID",
1915
- description: "Return this host session's stable routing ID, Initial alias (FIRST TASK), Current alias (CURRENT TASK), and separate human Session name. Explicit subjects update Current even when named; Initial stays the first declared branch topic, unknown for legacy history. Before delegation or cross-session coordination, declare a short recognizable subject here. Do not require a subject declaration for small direct tasks; do not query all peers. Names never authenticate. Existing Pi names and human renames are preserved. Use a concise non-sensitive label, not a prompt. Progress is published automatically from the todo list; publish state here only for objective, decisions, blockers, or work, never just to report progress. Owner-curated state (2048 UTF-8 bytes total): null withdraws, omission leaves unchanged. Never include credentials, internal instructions, or raw prompts. Historical notes are not consent or an owner reply.",
2060
+ description: "Return this host session's stable routing ID, Initial alias (FIRST TASK), Current alias (CURRENT TASK), and separate human Session name. Explicit subjects update Current even when named; Initial stays the first declared branch topic, unknown for legacy history. Publishing state.work with an explicit topic and no subject also refreshes Current from that topic; keep work.topic current when the task changes. Before delegation or cross-session coordination, declare a short recognizable subject here. Do not require a subject declaration for small direct tasks; do not query all peers. Names never authenticate. Existing Pi names and human renames are preserved. Use a concise non-sensitive label, not a prompt. Progress is published automatically from the todo list; publish state here only for objective, decisions, blockers, or work, never just to report progress. Owner-curated state (2048 UTF-8 bytes total): null withdraws, omission leaves unchanged. Never include credentials, internal instructions, or raw prompts. Historical notes are not consent or an owner reply.",
1916
2061
  ...orchestratorToolRenderers("session", expandHint),
1917
2062
  parameters: { type: "object", additionalProperties: false, properties: {
1918
2063
  subject: { type: "string", maxLength: 120, description: "Optional short task subject; updates task aliases independently of human names. Also names an unnamed Pi session." },
@@ -1952,7 +2097,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1952
2097
  if (declared && !ctx.sessionManager.getSessionName?.()) pi.setSessionName(declared);
1953
2098
  const alias = sanitizeDisplayLabel(ctx.sessionManager.getSessionName?.() ?? "");
1954
2099
  const aliases = stateCache.get(ctx.sessionManager)?.aliases;
1955
- if (state !== undefined || declared) publishActivity();
2100
+ if (state !== undefined || declared) publishActivityPrompt();
1956
2101
  presence?.refreshLabel();
1957
2102
  return text(`Active session ID: ${transport.sessionId}\nCurrent alias (CURRENT TASK): ${aliases?.currentAlias ?? "unknown"}\nInitial alias (FIRST TASK): ${aliases?.initialAlias ?? "unknown"}\nSession name: ${alias || "unnamed"}`,
1958
2103
  { gentleAgents: { senderSessionId: transport.sessionId, alias, sessionName: alias, initialAlias: aliases?.initialAlias ?? null, currentAlias: aliases?.currentAlias ?? null } });
@@ -2073,8 +2218,11 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2073
2218
  return `- ${peer.sessionId}${context} · ${legacy ? "repository: not published" : repository(peer.scope?.host)} · metadata: ${legacy ? "legacy" : peer.freshness}${tasks}${registered}${gaps}${catalog}${note}${peer.omitted ? `\n (${peer.omitted} more tasks omitted)` : ""}`;
2074
2219
  });
2075
2220
  return peers.length === 0 ? text("No other sessions are currently advertised. Advertisements have unknown reachability and do not guarantee a live session.") : text(`Advertised sessions (reachability is unknown):\n${rows.join("\n")}`, { gentleAgents: { candidates: peers } });
2076
- } catch {
2077
- return text("Error: session discovery is unavailable.", { error: "unavailable" });
2221
+ } catch (error) {
2222
+ // SessionPresenceError codes are a fixed enum with no record content,
2223
+ // so surfacing the reason is safe and keeps diagnosis possible.
2224
+ const reason = error instanceof SessionPresenceError ? error.code : "unknown";
2225
+ return text(`Error: session discovery is unavailable (${reason}).`, { error: "unavailable", reason });
2078
2226
  }
2079
2227
  },
2080
2228
  });
@@ -2132,8 +2280,9 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2132
2280
  recipient = candidates[index].sessionId;
2133
2281
  activation = candidates[index];
2134
2282
  }
2135
- } catch {
2136
- return text("Error: session discovery is unavailable.", { error: "unavailable" });
2283
+ } catch (error) {
2284
+ const reason = error instanceof SessionPresenceError ? error.code : "unknown";
2285
+ return text(`Error: session discovery is unavailable (${reason}).`, { error: "unavailable", reason });
2137
2286
  }
2138
2287
  }
2139
2288
  if (recipient === undefined || !validTransportSessionId(recipient)) return text("Error: recipient session ID or message is invalid.", { error: "invalid input" });
@@ -2283,11 +2432,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2283
2432
  if (collapseKey) {
2284
2433
  pi.registerShortcut(collapseKey as Parameters<ExtensionAPI["registerShortcut"]>[0], {
2285
2434
  description: "Collapse or expand the agents card",
2286
- handler: async () => {
2287
- collapsed = !collapsed;
2288
- if (sidebarTui) invalidateSidebar(sidebarTui);
2289
- host?.requestRender();
2290
- },
2435
+ handler: async () => toggleCollapsed(),
2291
2436
  });
2292
2437
  }
2293
2438
 
@@ -2343,7 +2488,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2343
2488
  helperPermission.clear();
2344
2489
  if (sessions !== ctx.sessionManager) return;
2345
2490
  stateCache.load(ctx.sessionManager);
2346
- publishActivity();
2491
+ publishActivityPrompt();
2347
2492
  });
2348
2493
  pi.on("session_start", async (event, ctx) => {
2349
2494
  // gentle-shell#1064 slice 3b-ii: a parent session that has no explicit
@@ -2385,7 +2530,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2385
2530
  if (preexisting && sessionId) void restoreSessionHistory(ctx, sessionId);
2386
2531
  try {
2387
2532
  presence = startPresence(ctx.sessionManager);
2388
- publishActivity();
2533
+ resetActivityPublish();
2534
+ publishActivityPrompt();
2389
2535
  } catch { presence = undefined; }
2390
2536
  void startSessionTransport(ctx);
2391
2537
  // The desktop app's own pi process: publish live subagent state through
@@ -2428,6 +2574,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2428
2574
  rpcActivityPublisher?.stop();
2429
2575
  rpcActivityPublisher = undefined;
2430
2576
  cancelClock?.();
2577
+ resetActivityPublish();
2431
2578
  for (const view of overlays) { view.handleInput("q"); view.dispose(); }
2432
2579
  overlays.clear();
2433
2580
  sessions = undefined;