gentle-pi 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +8 -5
  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 +101 -151
  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 +61 -0
  16. package/docs/gentle-shell.md +77 -7
  17. package/docs/install-wizard.md +30 -6
  18. package/docs/permission-modes.md +7 -2
  19. package/docs/readme-reference.md +19 -15
  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 +173 -38
  25. package/extensions/gentle-ai.ts +214 -69
  26. package/extensions/gentle-design.ts +158 -0
  27. package/extensions/gentle-music.ts +289 -0
  28. package/extensions/gentle-shell.ts +332 -73
  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 +2 -0
  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 +123 -49
  52. package/lib/gentle-shell-launcher.ts +9 -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/nan-provider.ts +3 -0
  58. package/lib/native-review-cli.ts +11 -4
  59. package/lib/notification-audio-native.ts +149 -13
  60. package/lib/notification-audio.ts +4 -0
  61. package/lib/notification-pulse-worker.ts +82 -18
  62. package/lib/notification-service.ts +5 -1
  63. package/lib/orchestrator-presence.ts +78 -8
  64. package/lib/orchestrator-scope.ts +10 -0
  65. package/lib/orchestrator-state.ts +11 -3
  66. package/lib/permission-mode-policy.ts +2 -2
  67. package/lib/review-host-relay.ts +11 -2
  68. package/lib/review-reminder-receipt.ts +24 -1
  69. package/lib/session-profile-append-controller.ts +713 -0
  70. package/lib/session-profile-disk-reader.ts +182 -0
  71. package/lib/session-profile-persistence.ts +15 -7
  72. package/lib/session-scratch.ts +52 -3
  73. package/lib/session-worktree-registry.ts +7 -0
  74. package/lib/shell-bar.ts +4 -3
  75. package/lib/shell-changes.ts +81 -5
  76. package/lib/shell-prompt.ts +4 -4
  77. package/lib/shell-sidebar-layout.ts +26 -6
  78. package/lib/shell-todo.ts +1 -1
  79. package/lib/verifier-admission.ts +5 -4
  80. package/lib/vim-editor-adapter.ts +11 -8
  81. package/lib/voice-artifacts.ts +188 -0
  82. package/lib/voice-command.ts +576 -0
  83. package/lib/voice-install.ts +455 -0
  84. package/lib/voice-platform.ts +99 -0
  85. package/lib/voice-push-to-talk.ts +262 -0
  86. package/lib/voice-session.ts +460 -0
  87. package/lib/voice-settings.ts +50 -0
  88. package/lib/voice-ui.ts +158 -0
  89. package/lib/yolo-session-policy.ts +58 -22
  90. package/package.json +1 -1
  91. package/runtime/gentle-shell-bundled.mjs +122 -48
  92. package/runtime/gentle-shell-launcher.mjs +9 -4
  93. package/runtime/gentle-shell-startup-update.mjs +61 -9
  94. package/runtime/gentle-shell-uninstall.mjs +33 -1
  95. package/runtime/native-review-cli.mjs +11 -4
  96. package/scripts/bundled-engram-migration.mjs +0 -6
  97. package/scripts/gentle-ai-installer.mjs +10 -10
  98. package/scripts/installer-downloads.mjs +7 -7
  99. package/scripts/installer-preflight.mjs +5 -3
  100. package/scripts/installer-windows.mjs +37 -7
  101. package/scripts/main-channel.mjs +91 -17
  102. package/scripts/pi-update.mjs +34 -6
  103. package/scripts/verify-package-files.mjs +26 -2
  104. package/scripts/voice-worker.cjs +476 -0
  105. package/skills/design/SKILL.md +110 -0
  106. package/tests/agent-profiles.test.ts +24 -0
  107. package/tests/agents-config.test.ts +61 -0
  108. package/tests/agents-fake-child.ts +8 -1
  109. package/tests/agents-protocol.test.ts +32 -0
  110. package/tests/agents-runner.test.ts +226 -5
  111. package/tests/agents-session-transport.test.ts +53 -3
  112. package/tests/ask-user-choice.test.ts +30 -0
  113. package/tests/auto-mode.test.ts +17 -0
  114. package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
  115. package/tests/auto-permission-runtime.test.ts +85 -1
  116. package/tests/autonomous-guard.test.ts +20 -0
  117. package/tests/bundled-install.test.ts +3 -3
  118. package/tests/clipboard-image.test.ts +114 -0
  119. package/tests/command-palette.test.ts +1 -1
  120. package/tests/context-backstop.test.ts +70 -4
  121. package/tests/delegate-for-reason-contract.test.ts +2 -1
  122. package/tests/design-canvas-assets.test.ts +710 -0
  123. package/tests/design-canvas-server.test.ts +1096 -0
  124. package/tests/design-comments.test.ts +201 -0
  125. package/tests/design-handoffs.test.ts +282 -0
  126. package/tests/design-open-browser.test.ts +70 -0
  127. package/tests/design-sources.test.ts +132 -0
  128. package/tests/engram-safety.test.ts +132 -57
  129. package/tests/external-editor.test.ts +84 -0
  130. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +3 -3
  131. package/tests/gentle-agents.test.ts +365 -20
  132. package/tests/gentle-ai-binary.test.ts +1 -1
  133. package/tests/gentle-ai-installer.test.ts +51 -51
  134. package/tests/gentle-ai.test.ts +292 -13
  135. package/tests/gentle-design.test.ts +1313 -0
  136. package/tests/gentle-music.test.ts +335 -0
  137. package/tests/gentle-shell-bundled.test.ts +301 -248
  138. package/tests/gentle-shell-launcher.test.ts +28 -2
  139. package/tests/gentle-shell-startup-update.test.ts +77 -1
  140. package/tests/gentle-shell-uninstall.test.ts +59 -0
  141. package/tests/gentle-shell.test.ts +183 -8
  142. package/tests/gentle-todo.test.ts +11 -0
  143. package/tests/image-attachments.test.ts +119 -0
  144. package/tests/image-paste-chips.test.ts +236 -0
  145. package/tests/inprocess-reviewer.test.ts +55 -2
  146. package/tests/installer-bundles.test.ts +12 -6
  147. package/tests/installer-posix-bootstrap.test.ts +8 -8
  148. package/tests/installer-preflight.test.ts +16 -0
  149. package/tests/installer-server.test.ts +2 -2
  150. package/tests/installer-windows-bootstrap.test.ts +61 -3
  151. package/tests/installer-windows-diagnostics.test.ts +111 -0
  152. package/tests/main-channel-upgrade.test.ts +232 -2
  153. package/tests/main-channel.test.ts +140 -21
  154. package/tests/nan-provider.test.ts +36 -0
  155. package/tests/native-review-capability-contract.test.ts +11 -4
  156. package/tests/native-review-parity.test.ts +1 -1
  157. package/tests/notification-audio-native.test.ts +128 -9
  158. package/tests/notification-customize.test.ts +17 -5
  159. package/tests/odd-routing-canonical-ratchet.test.ts +2 -1
  160. package/tests/orchestrator-budget.test.ts +5 -2
  161. package/tests/orchestrator-presence.test.ts +100 -2
  162. package/tests/orchestrator-rdd-ownership.test.ts +2 -2
  163. package/tests/orchestrator-scope.test.ts +14 -0
  164. package/tests/orchestrator-state.test.ts +29 -0
  165. package/tests/package-files.test.ts +34 -0
  166. package/tests/package-manifest.test.ts +6 -6
  167. package/tests/permission-mode-policy.test.ts +10 -1
  168. package/tests/pi-update.test.ts +99 -4
  169. package/tests/rdd-aware-verification-contract.test.ts +1 -1
  170. package/tests/review-host-relay-routing.test.ts +88 -1
  171. package/tests/review-host-relay.test.ts +9 -2
  172. package/tests/review-json-arguments.test.ts +20 -3
  173. package/tests/review-loadout-rdd-off-sdk.test.ts +23 -2
  174. package/tests/review-relay-transport-agent.test.ts +26 -0
  175. package/tests/review-reminder-receipt.test.ts +31 -1
  176. package/tests/session-changes.test.ts +2 -1
  177. package/tests/session-profile-append-controller-fork.test.ts +458 -0
  178. package/tests/session-profile-append-controller-pi.test.ts +290 -0
  179. package/tests/session-profile-append-controller.test.ts +731 -0
  180. package/tests/session-profile-disk-reader-pi.test.ts +183 -0
  181. package/tests/session-profile-disk-reader.test.ts +311 -0
  182. package/tests/session-scratch.test.ts +68 -2
  183. package/tests/session-worktree-registry.test.ts +13 -1
  184. package/tests/shell-bar.test.ts +14 -2
  185. package/tests/shell-changes.test.ts +148 -1
  186. package/tests/shell-prompt.test.ts +3 -3
  187. package/tests/shell-render-cache.test.ts +134 -1
  188. package/tests/shell-sidebar-layout.test.ts +50 -0
  189. package/tests/short-check-routing-contract.test.ts +1 -1
  190. package/tests/skill-collision-prefixes.test.ts +3 -1
  191. package/tests/skill-registry-watcher.test.ts +17 -5
  192. package/tests/skill-registry.test.ts +47 -3
  193. package/tests/task-size-routing-contract.test.ts +1 -1
  194. package/tests/test-strength-contract.test.ts +1 -1
  195. package/tests/verifier-admission.test.ts +18 -1
  196. package/tests/voice-command.test.ts +773 -0
  197. package/tests/voice-install.test.ts +323 -0
  198. package/tests/voice-platform.test.ts +176 -0
  199. package/tests/voice-push-to-talk.test.ts +444 -0
  200. package/tests/voice-session.test.ts +434 -0
  201. package/tests/voice-settings.test.ts +49 -0
  202. package/tests/voice-ui.test.ts +97 -0
  203. package/tests/voice-worker.test.ts +445 -0
  204. package/tests/weak-model-economy-prompt.test.ts +32 -0
  205. package/tests/windows-hidden-processes.test.ts +25 -2
  206. package/tests/yolo-customize.test.ts +5 -1
  207. package/tests/yolo-mode.test.ts +1 -1
  208. 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";
@@ -20,13 +20,14 @@ import { JOB_GLYPH, JOB_NOTICE_TYPE, jobDetails, jobNoticeText, monitorEventsTex
20
20
  import { createMonitorController, mergeMonitorBatches, type MonitorNotice } from "../lib/background-monitor.ts";
21
21
  import { JobsView } from "../lib/jobs-view.ts";
22
22
  import { JOBS_SIDEBAR_EVENT, JOBS_STATUS_KEY } from "../lib/jobs-sidebar-state.ts";
23
- import { Text, type TUI } from "@earendil-works/pi-tui";
23
+ import { Text, type Component, type TUI } from "@earendil-works/pi-tui";
24
24
  import { invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
25
- import { VISUAL_SETTINGS_CHANGED } from "../lib/shell-sidebar.ts";
25
+ import { sidebarPart, VISUAL_SETTINGS_CHANGED } from "../lib/shell-sidebar.ts";
26
+ import { NativePointerRegion } from "../lib/native-pointer-region.ts";
26
27
  import { resolveVisualSettings } from "../lib/visual-customization-policy.ts";
27
28
  import { createCompletionQueue } from "../lib/agents-completion-delivery.ts";
28
29
  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";
30
+ import { AGENT_MODE, PROFILE_SOURCE, discoverAgents, formatModelRef, loadAgentsConfig, resolveAgentProfile, unmatchedProfileKeys, withPinnedModelProfiles, type AgentDefinition, type AgentMode, type ProfileSource } from "../lib/agents-config.ts";
30
31
  import { freezeSessionProfileAtStartup, inheritedProfileDriftNotice, resolveSessionProfile, sessionProfileRoutingAt, type InheritedProfileOptions, type SessionProfileResolution } from "../lib/session-profile-freeze.ts";
31
32
  import { resolveProfilePin, resolveUnversionedProjectProfile } from "../lib/agent-profile-pin.ts";
32
33
  import { resolveBackgroundSubagentsPolicy } from "../lib/background-subagents-policy.ts";
@@ -35,7 +36,7 @@ import { isFinished, MISSING_TOOLS_NOTE_PREFIX, TASK_EVENT, TASK_STATUS, TaskSto
35
36
  import { AgentRunner, piCommand, abortReasonText, REQUESTED_TOOLS_ENV, type AskAnswer, type RunnerDeps, type TaskRequest } from "../lib/agents-runner.ts";
36
37
  import { parseChildPackageInjection } from "../lib/child-package-injection.ts";
37
38
  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";
39
+ import { ActiveSessionClient, ActiveSessionListener, SessionPresenceError, SessionPresenceRegistry, type PresenceRecord, type ReceivedNotification, type SentNotification, type SessionPresenceCandidate } from "../lib/agents-session-transport.ts";
39
40
  import { WindowsActiveSessionClient, WindowsActiveSessionListener, WindowsSessionPresenceRegistry, type WindowsSessionRegistryPhaseObserver } from "../lib/windows-session-transport.ts";
40
41
  import { hasReviewSessionPermission, resolveCanonicalGitRepositoryIdentitySync, type ReviewSessionManager } from "../lib/review-session-standing-permission.ts";
41
42
  import { inheritedUnsafeGitEnvironmentKeys } from "../lib/review-repository.ts";
@@ -58,8 +59,8 @@ import { OrchestratorScopeCache, type RepositoryFact } from "../lib/orchestrator
58
59
  import { createRpcActivityPublisher, type RpcActivityPublisher } from "../lib/agents-rpc-publisher.ts";
59
60
  import { isInteractiveRpcHost } from "../lib/rpc-host.ts";
60
61
  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";
62
+ import { AGENTS_GLYPH, agentsCardTone, renderAgentsCard, widgetExpiryMs, widgetRows } from "../lib/agents-widget.ts";
63
+ import { CARD_TONE, panelHeaderRow, renderCard } from "../lib/shell-card.ts";
63
64
  import { openInExternalEditor } from "./gentle-shell.ts";
64
65
  import { gentlePiConfigHome } from "../lib/agent-home.ts";
65
66
  import { resolveAgentHomeDirectory } from "../lib/agent-model-resolution.ts";
@@ -561,7 +562,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
561
562
  };
562
563
  return PresencePublisher.start({ profile: agentHome, sessionId, label: labelSource(), labelSource, activity: [] });
563
564
  };
564
- const publishActivity = () => {
565
+ const publishActivityNow = (deferred = false) => {
565
566
  if (!sessions) return;
566
567
  try {
567
568
  if (!presence || presence.error) {
@@ -580,14 +581,59 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
580
581
  return data?.sessionId === activeSessionId() && typeof data.root === "string" && typeof data.evidence === "string" ? [data.root] : [];
581
582
  });
582
583
  presence?.updateDiscovery(transport.listener.record, { workspace: sessions.getCwd(), tasks, registered,
583
- scope: scopeCache.project(sessions.getCwd(), tasks, registered), state: stateCache.get(sessions) });
584
+ // A deferred (render-cadence trailing) publish must never resolve Git:
585
+ // its timer can land while a peer metadata query is being served, and
586
+ // that contract adds no caller Git probes. It reuses the snapshot the
587
+ // last event-time publication projected; the next event-time publish
588
+ // refreshes the facts.
589
+ scope: deferred ? scopeCache.cachedScope() : scopeCache.project(sessions.getCwd(), tasks, registered), state: stateCache.get(sessions) });
584
590
  }
585
591
  } catch { presence?.dispose(); presence = undefined; }
586
592
  };
593
+ // Coalesced like requestRender: the first call publishes immediately so an
594
+ // idle→working transition or a resumed session never waits, and every call
595
+ // inside the window collapses into one trailing publish, so a burst of child
596
+ // deltas costs one publication instead of one per event.
597
+ let activityPublishQueued = false;
598
+ let activityPublishTrailing = false;
599
+ let cancelActivityPublish: (() => void) | undefined;
600
+ const resetActivityPublish = () => {
601
+ cancelActivityPublish?.();
602
+ cancelActivityPublish = undefined;
603
+ activityPublishQueued = false;
604
+ activityPublishTrailing = false;
605
+ };
606
+ const flushDeferredPublish = () => {
607
+ cancelActivityPublish = undefined;
608
+ activityPublishQueued = false;
609
+ if (!activityPublishTrailing) return;
610
+ activityPublishTrailing = false;
611
+ publishActivityNow(true);
612
+ };
613
+ const publishActivity = () => {
614
+ if (activityPublishQueued) {
615
+ activityPublishTrailing = true;
616
+ return;
617
+ }
618
+ publishActivityNow();
619
+ activityPublishQueued = true;
620
+ cancelActivityPublish = deps.schedule(flushDeferredPublish, RENDER_COALESCE_MS);
621
+ };
622
+ // Discrete semantic events publish immediately so peers and readers see them
623
+ // promptly (a consult binds to the published snapshot); the coalescing window
624
+ // stays armed so a child-delta burst that follows still collapses into one
625
+ // trailing publish.
626
+ const publishActivityPrompt = () => {
627
+ publishActivityNow();
628
+ if (!activityPublishQueued) {
629
+ activityPublishQueued = true;
630
+ cancelActivityPublish = deps.schedule(flushDeferredPublish, RENDER_COALESCE_MS);
631
+ }
632
+ };
587
633
  const unsubscribeScope = pi.events.on(SESSION_WORKTREE_CHANGED, (event) => {
588
634
  if ((event as { sessionId?: string } | undefined)?.sessionId !== sessions?.getSessionId()) return;
589
635
  scopeCache.clear();
590
- publishActivity();
636
+ publishActivityPrompt();
591
637
  });
592
638
  // The todo list is the session's progress: publish it for peers so the model
593
639
  // spends no turn on orchestrator_session_id. Merged into the owner's curated
@@ -736,7 +782,11 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
736
782
  await closeStartupTransport();
737
783
  return;
738
784
  }
739
- publishActivity();
785
+ // Publish immediately, not on the coalescing trailing edge: this is the
786
+ // session's first discovery publication, and a peer metadata query binds
787
+ // to the activation only through it — a 400 ms lag here makes a prompt
788
+ // consult read publication-unknown-or-stale.
789
+ publishActivityPrompt();
740
790
  } catch {
741
791
  if (activeSessionTransport?.generation === generation) activeSessionTransport = undefined;
742
792
  await closeStartupTransport();
@@ -1565,7 +1615,11 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1565
1615
  ctx.ui.notify(`Could not read the task's session: ${error instanceof Error ? error.message : String(error)}`, "warning");
1566
1616
  return;
1567
1617
  }
1568
- if (!openInExternalEditor(overlayHost, transcriptPath)) ctx.ui.notify("No editor configured. Set $VISUAL or $EDITOR.", "warning");
1618
+ try {
1619
+ if (!openInExternalEditor(overlayHost, transcriptPath)) ctx.ui.notify("No editor configured. Set $VISUAL or $EDITOR.", "warning");
1620
+ } catch (error) {
1621
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
1622
+ }
1569
1623
  };
1570
1624
 
1571
1625
  const writeTranscript = async (task: TaskRecord): Promise<string> => {
@@ -1597,24 +1651,80 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1597
1651
  host?.requestRender();
1598
1652
  });
1599
1653
  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.
1654
+ // Agents follows Todos: the fullscreen rail shows it when the terminal is
1655
+ // wide enough, and sidebarPart suppresses the above-editor copy only
1656
+ // while the rail owns the host; narrower terminals keep the widget.
1603
1657
  ui?.setWidget(AGENTS_WIDGET_KEY, (tui, theme) => {
1604
1658
  host = tui;
1605
1659
  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
- };
1660
+ return sidebarPart(tui, "agents", agentsCard(tui, theme, false), agentsCard(tui, theme, true));
1661
+ });
1662
+ };
1663
+
1664
+ const toggleCollapsed = () => {
1665
+ collapsed = !collapsed;
1666
+ if (sidebarTui) invalidateSidebar(sidebarTui);
1667
+ host?.requestRender();
1668
+ };
1669
+
1670
+ // One surface of the Agents card. The rail is natively scrollable, so it
1671
+ // lists every task; the above-editor widget keeps its row budget. Only the
1672
+ // header row is the clickable collapse control, exactly like Todos.
1673
+ const agentsCard = (tui: TUI, theme: Parameters<typeof renderAgentsCard>[1], rail: boolean): Component & { dispose(): void; digest(): string } => {
1674
+ let hovered = false;
1675
+ let headerRow = 0;
1676
+ const card: Component = {
1677
+ render(width: number) {
1678
+ if (!agentsVisible) return [];
1679
+ const tasks = visibleTasks();
1680
+ const now = deps.now();
1681
+ headerRow = panelHeaderRow(theme, width, agentsCardTone(tasks, now));
1682
+ const lines = renderAgentsCard(tasks, theme, width, now, { collapsed, collapseKey, maxRows: rail ? undefined : widgetRows(tui.terminal?.rows), viewKey, collapseControl: true, hovered });
1683
+ return lines.length === 0 || rail ? lines : [...lines, ""];
1684
+ },
1685
+ invalidate() {
1686
+ hovered = false;
1687
+ },
1688
+ };
1689
+ const region = new NativePointerRegion(card, {
1690
+ onHover(event) {
1691
+ const next = event.y === headerRow;
1692
+ if (next === hovered) return { handled: true };
1693
+ hovered = next;
1694
+ return { handled: true, render: true };
1695
+ },
1696
+ onLeave() {
1697
+ if (!hovered) return;
1698
+ hovered = false;
1699
+ host?.requestRender();
1700
+ },
1701
+ onClick(event) {
1702
+ if (event.button !== "left" || event.y !== headerRow) return undefined;
1703
+ toggleCollapsed();
1704
+ return { handled: true, render: true };
1705
+ },
1614
1706
  });
1707
+ return {
1708
+ // The rail memo repaints a part only when its digest moves: task
1709
+ // state, the local controls, and the elapsed clock (to the second)
1710
+ // only while a task is still running, so an idle or finished card
1711
+ // never forces a rail rebuild every second.
1712
+ digest: () => {
1713
+ const tasks = visibleTasks();
1714
+ const clock = tasks.some((task) => !isFinished(task.status)) ? Math.floor(deps.now() / 1000) : 0;
1715
+ 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])]);
1716
+ },
1717
+ render: (width) => region.render(width),
1718
+ handleMouse: (event) => region.handleMouse(event),
1719
+ invalidate: () => region.invalidate(),
1720
+ dispose: () => region.dispose(),
1721
+ };
1615
1722
  };
1616
1723
 
1617
1724
  const roots = (ctx: ExtensionContext) => ({ cwd: ctx.sessionManager.getCwd(), home: deps.home, agentHome });
1725
+ // Issue #2400: parent sessions already told about model_profiles keys that
1726
+ // match no agent and suggest none, so the notice fires once per session.
1727
+ const unmatchedKeysNotifiedSessions = new Set<string>();
1618
1728
 
1619
1729
  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
1730
  if (retiredSddAgent(agent.name)) throw new Error("Retired SDD agents cannot be dispatched.");
@@ -1678,7 +1788,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1678
1788
  // same-clone registry. Never use a caller-injected resolver for this identity.
1679
1789
  const identity = resolveSessionWorktree(selectedRoot, parentCwd);
1680
1790
  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.");
1791
+ const spelling = nativeRootSpelling(selectedRoot);
1792
+ 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
1793
  await foreignGrants.authorize(ctx, identity, { continuation: resume !== undefined, signal });
1683
1794
  foreignIdentity = identity;
1684
1795
  foreignParent = canonicalParent;
@@ -1729,6 +1840,28 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1729
1840
  if (drift !== undefined) ctx.ui.notify(drift, "info");
1730
1841
  }
1731
1842
  const profile = resolveAgentProfile(agent, config);
1843
+ // Issue #2196 (PR #2233 maintainer decision): a model_profiles key that
1844
+ // names no discovered agent was silently ignored while the child inherited
1845
+ // the orchestrator model. When a configured key suggests this agent, name
1846
+ // it at launch; resolution stays exact-match, so an omitted role keeps
1847
+ // inheriting and a matched key never warns. Keys are checked against every
1848
+ // discovered agent, so a key naming another agent is never reported here.
1849
+ if (ctx.hasUI) {
1850
+ const unmatched = unmatchedProfileKeys(config.modelProfiles, discoverAgents(roots(ctx)).agents);
1851
+ const nearMisses = unmatched.filter((entry) => entry.suggestion === agent.name);
1852
+ if (nearMisses.length > 0) {
1853
+ ctx.ui.notify(nearMisses.map((entry) => `"${entry.key}" did not match any agent, did you mean ${agent.name}?`).join(" "), "warning");
1854
+ }
1855
+ // Issue #2400: a key with no suggestion never names the launched agent,
1856
+ // so the filter above kept it silent. The session's first launch lists
1857
+ // every such key once; later launches in the same session stay quiet.
1858
+ const unsuggested = unmatched.filter((entry) => entry.suggestion === undefined);
1859
+ const parentSession = ctx.sessionManager.getSessionId() ?? "";
1860
+ if (unsuggested.length > 0 && !unmatchedKeysNotifiedSessions.has(parentSession)) {
1861
+ unmatchedKeysNotifiedSessions.add(parentSession);
1862
+ ctx.ui.notify(unsuggested.map((entry) => `"${sanitizeTerminalText(entry.key)}" did not match any agent and was ignored.`).join(" "), "warning");
1863
+ }
1864
+ }
1732
1865
  // #2180: the winning layer is named from the resolution and routing this
1733
1866
  // request already performed, never by re-resolving them.
1734
1867
  const modelSource = modelSourceToken(profile.source.model, sessionProfile, routingTarget);
@@ -1839,7 +1972,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1839
1972
  if (launched && foreignRequest) foreignTasks.set(task.id, foreignRequest);
1840
1973
  ownedTaskIds.add(task.id);
1841
1974
  publishWork?.(task.id);
1842
- publishActivity(); // Admission's summary notification precedes runtime ownership.
1975
+ publishActivityPrompt(); // Admission's summary notification precedes runtime ownership.
1843
1976
  store.subscribe(task.id, () => { publishActivity(); requestRender(); });
1844
1977
  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
1978
  // A tool call aborted by the host (a human interrupting the turn, a timeout)
@@ -1912,7 +2045,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1912
2045
  pi.registerTool({
1913
2046
  name: "orchestrator_session_id",
1914
2047
  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.",
2048
+ 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
2049
  ...orchestratorToolRenderers("session", expandHint),
1917
2050
  parameters: { type: "object", additionalProperties: false, properties: {
1918
2051
  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 +2085,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1952
2085
  if (declared && !ctx.sessionManager.getSessionName?.()) pi.setSessionName(declared);
1953
2086
  const alias = sanitizeDisplayLabel(ctx.sessionManager.getSessionName?.() ?? "");
1954
2087
  const aliases = stateCache.get(ctx.sessionManager)?.aliases;
1955
- if (state !== undefined || declared) publishActivity();
2088
+ if (state !== undefined || declared) publishActivityPrompt();
1956
2089
  presence?.refreshLabel();
1957
2090
  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
2091
  { gentleAgents: { senderSessionId: transport.sessionId, alias, sessionName: alias, initialAlias: aliases?.initialAlias ?? null, currentAlias: aliases?.currentAlias ?? null } });
@@ -2073,8 +2206,11 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2073
2206
  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
2207
  });
2075
2208
  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" });
2209
+ } catch (error) {
2210
+ // SessionPresenceError codes are a fixed enum with no record content,
2211
+ // so surfacing the reason is safe and keeps diagnosis possible.
2212
+ const reason = error instanceof SessionPresenceError ? error.code : "unknown";
2213
+ return text(`Error: session discovery is unavailable (${reason}).`, { error: "unavailable", reason });
2078
2214
  }
2079
2215
  },
2080
2216
  });
@@ -2132,8 +2268,9 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2132
2268
  recipient = candidates[index].sessionId;
2133
2269
  activation = candidates[index];
2134
2270
  }
2135
- } catch {
2136
- return text("Error: session discovery is unavailable.", { error: "unavailable" });
2271
+ } catch (error) {
2272
+ const reason = error instanceof SessionPresenceError ? error.code : "unknown";
2273
+ return text(`Error: session discovery is unavailable (${reason}).`, { error: "unavailable", reason });
2137
2274
  }
2138
2275
  }
2139
2276
  if (recipient === undefined || !validTransportSessionId(recipient)) return text("Error: recipient session ID or message is invalid.", { error: "invalid input" });
@@ -2283,11 +2420,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2283
2420
  if (collapseKey) {
2284
2421
  pi.registerShortcut(collapseKey as Parameters<ExtensionAPI["registerShortcut"]>[0], {
2285
2422
  description: "Collapse or expand the agents card",
2286
- handler: async () => {
2287
- collapsed = !collapsed;
2288
- if (sidebarTui) invalidateSidebar(sidebarTui);
2289
- host?.requestRender();
2290
- },
2423
+ handler: async () => toggleCollapsed(),
2291
2424
  });
2292
2425
  }
2293
2426
 
@@ -2343,7 +2476,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2343
2476
  helperPermission.clear();
2344
2477
  if (sessions !== ctx.sessionManager) return;
2345
2478
  stateCache.load(ctx.sessionManager);
2346
- publishActivity();
2479
+ publishActivityPrompt();
2347
2480
  });
2348
2481
  pi.on("session_start", async (event, ctx) => {
2349
2482
  // gentle-shell#1064 slice 3b-ii: a parent session that has no explicit
@@ -2385,7 +2518,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2385
2518
  if (preexisting && sessionId) void restoreSessionHistory(ctx, sessionId);
2386
2519
  try {
2387
2520
  presence = startPresence(ctx.sessionManager);
2388
- publishActivity();
2521
+ resetActivityPublish();
2522
+ publishActivityPrompt();
2389
2523
  } catch { presence = undefined; }
2390
2524
  void startSessionTransport(ctx);
2391
2525
  // The desktop app's own pi process: publish live subagent state through
@@ -2428,6 +2562,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2428
2562
  rpcActivityPublisher?.stop();
2429
2563
  rpcActivityPublisher = undefined;
2430
2564
  cancelClock?.();
2565
+ resetActivityPublish();
2431
2566
  for (const view of overlays) { view.handleInput("q"); view.dispose(); }
2432
2567
  overlays.clear();
2433
2568
  sessions = undefined;