gentle-pi 2.5.0 → 2.6.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 (189) hide show
  1. package/README.md +139 -30
  2. package/assets/agents/gentle-ai-worker.md +4 -0
  3. package/assets/agents/jd-fix-agent.md +18 -0
  4. package/assets/agents/jd-judge-a.md +1 -1
  5. package/assets/agents/jd-judge-b.md +1 -1
  6. package/assets/agents/sdd-apply.md +7 -5
  7. package/assets/agents/sdd-archive.md +5 -3
  8. package/assets/agents/sdd-design.md +4 -0
  9. package/assets/agents/sdd-explore.md +4 -0
  10. package/assets/agents/sdd-init.md +4 -0
  11. package/assets/agents/sdd-onboard.md +4 -0
  12. package/assets/agents/sdd-proposal.md +4 -0
  13. package/assets/agents/sdd-remediate.md +37 -0
  14. package/assets/agents/sdd-research.md +26 -3
  15. package/assets/agents/sdd-spec.md +4 -0
  16. package/assets/agents/sdd-status.md +9 -75
  17. package/assets/agents/sdd-sync.md +4 -0
  18. package/assets/agents/sdd-tasks.md +4 -0
  19. package/assets/agents/sdd-verify.md +5 -3
  20. package/assets/chains/sdd-full.chain.md +4 -0
  21. package/assets/chains/sdd-plan.chain.md +4 -0
  22. package/assets/chains/sdd-verify.chain.md +4 -0
  23. package/assets/migrations/managed-assets-v2.5.0.json +7 -0
  24. package/assets/orchestrator-delegation.md +21 -3
  25. package/assets/sdd-orchestrator-workflow.md +54 -21
  26. package/assets/support/sdd-status-contract.md +34 -90
  27. package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
  28. package/docs/telemetry.md +57 -1
  29. package/docs/windows-startup-console-visibility.md +18 -0
  30. package/extensions/ask-user-choice.ts +143 -15
  31. package/extensions/codegraph-tools.ts +1 -0
  32. package/extensions/gentle-agents.ts +799 -50
  33. package/extensions/gentle-ai.ts +2033 -322
  34. package/extensions/gentle-shell.ts +145 -42
  35. package/extensions/gentle-todo.ts +47 -12
  36. package/extensions/quiet-tools.ts +1 -0
  37. package/extensions/runtime-metrics.ts +130 -0
  38. package/extensions/sdd-init.ts +2 -2
  39. package/extensions/startup-banner.ts +52 -75
  40. package/lib/agent-profiles.ts +550 -0
  41. package/lib/agents-completion-delivery.ts +72 -0
  42. package/lib/agents-config.ts +7 -10
  43. package/lib/agents-history.ts +9 -1
  44. package/lib/agents-messaging.ts +187 -0
  45. package/lib/agents-protocol.ts +77 -5
  46. package/lib/agents-runner.ts +548 -26
  47. package/lib/agents-thread-view.ts +57 -0
  48. package/lib/agents-view-layout.ts +40 -0
  49. package/lib/agents-view.ts +548 -191
  50. package/lib/agents-widget.ts +33 -14
  51. package/lib/gentle-ai-binary.ts +3 -1
  52. package/lib/gentle-ai-renderer.ts +8 -6
  53. package/lib/native-review-cli.ts +283 -1
  54. package/lib/orchestrator-presence.ts +337 -0
  55. package/lib/profiles-orchestrator.ts +203 -0
  56. package/lib/review-candidate-view-owner.ts +296 -46
  57. package/lib/review-candidate-view.ts +30 -20
  58. package/lib/review-consent-component.ts +247 -0
  59. package/lib/review-consent-ui.ts +53 -8
  60. package/lib/review-host-relay.ts +28 -0
  61. package/lib/review-integration-v2.ts +187 -5
  62. package/lib/review-last-event-controller.ts +7 -4
  63. package/lib/review-reminder-receipt.ts +74 -0
  64. package/lib/review-session-standing-permission.ts +27 -6
  65. package/lib/runtime-metrics-children.ts +199 -0
  66. package/lib/runtime-metrics-delivery.ts +68 -0
  67. package/lib/runtime-metrics-native.ts +166 -0
  68. package/lib/runtime-metrics-pi-identity.ts +113 -0
  69. package/lib/runtime-metrics-policy.ts +51 -0
  70. package/lib/runtime-metrics.ts +255 -0
  71. package/lib/sdd-preflight.ts +362 -81
  72. package/lib/sdd-research-capabilities.ts +228 -0
  73. package/lib/sdd-status.ts +29 -7
  74. package/lib/session-worktree-registry.ts +118 -0
  75. package/lib/shell-bar.ts +47 -1
  76. package/lib/shell-card.ts +1 -4
  77. package/lib/shell-changes-view.ts +362 -37
  78. package/lib/shell-changes.ts +81 -1
  79. package/lib/shell-prompt.ts +11 -15
  80. package/lib/shell-sidebar-banner.ts +11 -0
  81. package/lib/shell-sidebar-layout.ts +213 -0
  82. package/lib/shell-sidebar.ts +41 -0
  83. package/lib/shell-todo.ts +28 -11
  84. package/lib/telemetry-trigger.ts +2 -0
  85. package/package.json +6 -3
  86. package/runtime/gentle-ai-binary.mjs +3 -1
  87. package/runtime/native-review-cli.mjs +283 -1
  88. package/runtime/review-integration-v2.mjs +187 -5
  89. package/runtime/telemetry-trigger.mjs +2 -0
  90. package/scripts/build-runtime-modules.mjs +9 -1
  91. package/scripts/check-types.mjs +125 -0
  92. package/scripts/gentle-ai-installer.mjs +10 -10
  93. package/scripts/install-gentle-ai.mjs +12 -0
  94. package/scripts/install-tui-mode-setting.mjs +114 -0
  95. package/scripts/test-packed-runner.mjs +16 -2
  96. package/scripts/types-baseline.json +99 -0
  97. package/scripts/verify-package-files.mjs +4 -2
  98. package/skills/_shared/review-ledger-contract.md +17 -1
  99. package/skills/issue-creation/SKILL.md +3 -3
  100. package/skills/judgment-day/SKILL.md +17 -3
  101. package/skills/judgment-day/references/prompts-and-formats.md +14 -3
  102. package/tests/agent-profiles.test.ts +722 -0
  103. package/tests/agents-completion-delivery.test.ts +94 -0
  104. package/tests/agents-config.test.ts +62 -0
  105. package/tests/agents-fake-child.ts +15 -1
  106. package/tests/agents-grouping.test.ts +179 -0
  107. package/tests/agents-integration.test.ts +100 -0
  108. package/tests/agents-messaging.test.ts +94 -0
  109. package/tests/agents-protocol.test.ts +45 -0
  110. package/tests/agents-queries.test.ts +190 -0
  111. package/tests/agents-responsive.test.ts +43 -0
  112. package/tests/agents-runner.test.ts +571 -14
  113. package/tests/agents-thread-view.test.ts +45 -0
  114. package/tests/agents-view.test.ts +476 -65
  115. package/tests/agents-widget.test.ts +31 -1
  116. package/tests/artifact-language.test.ts +25 -2
  117. package/tests/ask-user-choice.test.ts +169 -3
  118. package/tests/asset-installation-runtime.test.ts +108 -0
  119. package/tests/autonomous-guard.test.ts +116 -1
  120. package/tests/codegraph-tools.test.ts +2 -1
  121. package/tests/delegated-key-learnings-contract.test.ts +1 -1
  122. package/tests/devbinary/native-review-parity.devtest.ts +2 -0
  123. package/tests/feature-request-form.test.ts +67 -0
  124. package/tests/fixtures/agents-messaging-child.mjs +5 -0
  125. package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
  126. package/tests/gentle-agents.test.ts +1460 -33
  127. package/tests/gentle-ai-binary.test.ts +7 -2
  128. package/tests/gentle-ai-installer.test.ts +47 -47
  129. package/tests/gentle-ai-renderer.test.ts +38 -0
  130. package/tests/gentle-ai.test.ts +944 -4
  131. package/tests/gentle-shell.test.ts +303 -12
  132. package/tests/gentle-todo.test.ts +54 -10
  133. package/tests/install-tui-mode-setting.test.ts +324 -0
  134. package/tests/issue-creation-skill.test.ts +22 -0
  135. package/tests/model-routing-authority.test.ts +12 -0
  136. package/tests/native-review-capability-contract.test.ts +12 -1
  137. package/tests/native-review-cli.test.ts +277 -3
  138. package/tests/native-review-parity.test.ts +14 -7
  139. package/tests/native-sdd-attempt-authority.test.ts +7 -2
  140. package/tests/orchestrator-presence.test.ts +389 -0
  141. package/tests/package-manifest.test.ts +232 -7
  142. package/tests/profiles-orchestrator.test.ts +208 -0
  143. package/tests/quiet-tool-rendering.test.ts +1 -0
  144. package/tests/rdd-aware-verification-contract.test.ts +10 -0
  145. package/tests/review-agent-end-preflight.test.ts +332 -24
  146. package/tests/review-candidate-view.test.ts +304 -6
  147. package/tests/review-consent-ui.test.ts +352 -0
  148. package/tests/review-contract-prompt.test.ts +14 -0
  149. package/tests/review-controller-native-routing.test.ts +563 -3
  150. package/tests/review-controller.test.ts +1 -1
  151. package/tests/review-host-relay-restart-parity.test.ts +142 -1
  152. package/tests/review-host-relay-routing.test.ts +364 -4
  153. package/tests/review-host-relay.test.ts +29 -0
  154. package/tests/review-integration-v2-forward.test.ts +44 -0
  155. package/tests/review-integration-v2.test.ts +164 -0
  156. package/tests/review-last-event-closure.test.ts +105 -1
  157. package/tests/review-ledger-contract.test.ts +61 -6
  158. package/tests/review-reminder-receipt.test.ts +62 -0
  159. package/tests/review-session-standing-permission-controller.test.ts +52 -4
  160. package/tests/review-session-standing-permission.test.ts +30 -0
  161. package/tests/runtime-harness.mjs +447 -39
  162. package/tests/runtime-metrics-children.test.ts +206 -0
  163. package/tests/runtime-metrics-delivery.test.ts +85 -0
  164. package/tests/runtime-metrics-extension.test.ts +187 -0
  165. package/tests/runtime-metrics-native.test.ts +209 -0
  166. package/tests/runtime-metrics-pi-identity.test.ts +113 -0
  167. package/tests/runtime-metrics-policy.test.ts +62 -0
  168. package/tests/runtime-metrics.test.ts +184 -0
  169. package/tests/sdd-agent-tools.test.ts +10 -1
  170. package/tests/sdd-execution-routing-contract.test.ts +28 -0
  171. package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
  172. package/tests/sdd-native-managed-uptake.test.ts +253 -0
  173. package/tests/sdd-planning-routing-contract.test.ts +45 -0
  174. package/tests/sdd-preflight.test.ts +252 -8
  175. package/tests/sdd-research-capabilities.test.ts +256 -0
  176. package/tests/sdd-research-live.test.ts +241 -0
  177. package/tests/sdd-selection-transport.test.ts +504 -0
  178. package/tests/sdd-status.test.ts +51 -0
  179. package/tests/session-worktree-registry.test.ts +135 -0
  180. package/tests/shell-card.test.ts +24 -3
  181. package/tests/shell-changes-view.test.ts +471 -8
  182. package/tests/shell-changes.test.ts +168 -0
  183. package/tests/shell-prompt.test.ts +28 -6
  184. package/tests/shell-sidebar-banner.test.ts +23 -0
  185. package/tests/shell-sidebar-layout.test.ts +387 -0
  186. package/tests/shell-sidebar.test.ts +50 -0
  187. package/tests/shell-todo.test.ts +100 -11
  188. package/tests/startup-banner.test.ts +126 -0
  189. package/tests/telemetry-trigger.test.ts +3 -1
@@ -0,0 +1,11 @@
1
+ import { visibleWidth } from "@earendil-works/pi-tui";
2
+ import type { ShellBarTheme } from "./shell-bar.ts";
3
+
4
+ export function renderSidebarBanner(theme: ShellBarTheme, width: number): string[] {
5
+ const label = "✿ Gentle-Pi ✿";
6
+ const space = width - visibleWidth(label);
7
+ if (space < 0) return [];
8
+ // Gentleman's accent is pink; card titles use a separate champagne role.
9
+ const title = theme.fg("accent", "✿") + " " + theme.fg("text", "Gentle-Pi") + " " + theme.fg("accent", "✿");
10
+ return [" ".repeat(Math.floor(space / 2)) + title + " ".repeat(Math.ceil(space / 2))];
11
+ }
@@ -0,0 +1,213 @@
1
+ import { ScrollView, visibleWidth, type Component, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
2
+ import { sidebarState, type SidebarRail } from "./shell-sidebar.ts";
3
+ import type { ShellBarTheme } from "./shell-bar.ts";
4
+ import { renderSidebarBanner } from "./shell-sidebar-banner.ts";
5
+
6
+ export const SIDEBAR_BREAKPOINT = 140;
7
+ const RAIL_WIDTH = 50;
8
+ const RAIL_PADDING = 1;
9
+ const GAP = 3;
10
+ // Experimental Pi 0.85.1 internals. Only the fullscreen layout tree is adapted;
11
+ // regular mode keeps native scrollback and the original bottom components.
12
+ const NODE = Symbol.for("@earendil-works/pi-tui/layout-node");
13
+ type LayoutNode = { type: string; entries?: unknown[]; gap?: number; align?: string };
14
+ type LayoutRoot = Component & { [NODE]?: () => LayoutNode };
15
+ type Host = TUI & { mode?: string; layoutRoot?: LayoutRoot };
16
+ type SidebarCache = { revision: number };
17
+ type RailHit = { key: string; component: Component; startY: number; height: number; width: number };
18
+ type SidebarPresentation = { scrollTop: number; output: LayoutNode };
19
+ type PreparedRail = {
20
+ revision: number;
21
+ width: number;
22
+ mode: string | undefined;
23
+ root: LayoutRoot;
24
+ theme: ShellBarTheme;
25
+ parts: Array<[string, SidebarRail]>;
26
+ digests: Array<string | undefined>;
27
+ contentWidth: number;
28
+ active: boolean;
29
+ lines: string[];
30
+ hits: RailHit[];
31
+ presentation?: SidebarPresentation;
32
+ };
33
+ const CACHE = Symbol.for("gentle-pi.experimental-sidebar.cache");
34
+
35
+ function sidebarCache(tui: TUI): SidebarCache {
36
+ const terminal = tui.terminal as unknown as Record<symbol, SidebarCache>;
37
+ return terminal[CACHE] ??= { revision: 0 };
38
+ }
39
+
40
+ /** Mark terminal-owned fullscreen sidebar output stale after a part state change. */
41
+ export function invalidateSidebar(tui: TUI): void {
42
+ if (tui.terminal) sidebarCache(tui).revision++;
43
+ }
44
+
45
+ // The memo keys on part identity and an explicit revision, neither of which can
46
+ // see live session state read inside a rail's render closure: a model switch, a
47
+ // new context percentage or an extension status change leaves the prepared lines
48
+ // intact. A rail that paints such state declares a digest of it, so the memo can
49
+ // notice by itself; a throwing digest degrades that rail to invalidation-only
50
+ // rather than taking the whole sidebar down with it.
51
+ function railDigest(rail: SidebarRail): string | undefined {
52
+ try {
53
+ return rail.digest?.();
54
+ } catch {
55
+ return undefined;
56
+ }
57
+ }
58
+
59
+ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
60
+ if (!tui.terminal) return () => {};
61
+ const host = tui as Host;
62
+ const state = sidebarState(tui);
63
+ const cache = sidebarCache(tui);
64
+ const cleanups: Array<() => void> = [];
65
+ const roots = new Set<LayoutRoot>();
66
+ let stopped = false;
67
+ let failed = false;
68
+ let railLines: string[] = [];
69
+ let prepared: PreparedRail | undefined;
70
+ state.active = false;
71
+ state.ownsHost = () => !stopped && host.mode === "fullscreen" && !!host.layoutRoot && roots.has(host.layoutRoot);
72
+ const rail: Component = {
73
+ render: () => railLines,
74
+ invalidate() {
75
+ invalidateSidebar(tui);
76
+ for (const part of state.parts.values()) part.invalidate();
77
+ },
78
+ };
79
+ const scroll = new ScrollView(rail, {
80
+ follow: "none",
81
+ primary: false,
82
+ overscroll: "contain",
83
+ scrollbar: "always",
84
+ scrollbarTrackStyle: (text) => theme.fg("border", text),
85
+ scrollbarThumbStyle: (text) => theme.fg("accent", text),
86
+ });
87
+ const nativeMouse = scroll.handleMouse.bind(scroll);
88
+ const dispatchPartMouse = (event: TuiMouseEvent) => {
89
+ const current = prepared;
90
+ if (!current || stopped || failed || !current.active || current.revision !== cache.revision ||
91
+ host.mode !== "fullscreen" || tui.terminal.columns !== current.width || host.layoutRoot !== current.root ||
92
+ scroll.getContentWidth(event.width) !== current.contentWidth || event.x < RAIL_PADDING ||
93
+ event.x >= current.contentWidth || event.y < 0 || event.y >= event.height) return undefined;
94
+ const contentY = scroll.scrollTop + event.y;
95
+ const hit = current.hits.find((candidate) => contentY >= candidate.startY && contentY < candidate.startY + candidate.height);
96
+ if (!hit || state.parts.get(hit.key) !== hit.component || event.x >= RAIL_PADDING + hit.width) return undefined;
97
+ return hit.component.handleMouse?.({
98
+ ...event,
99
+ x: event.x - RAIL_PADDING,
100
+ y: contentY - hit.startY,
101
+ width: hit.width,
102
+ height: hit.height,
103
+ });
104
+ };
105
+ scroll.handleMouse = (event) => {
106
+ if (event.type === "wheel") {
107
+ // Consume even at the boundary or over blank rail space: Pi 0.85.1
108
+ // can send unconsumed delta to the primary transcript despite containment.
109
+ scroll.scrollBy(event.wheelDelta ?? 0);
110
+ return {
111
+ handled: true,
112
+ render: true,
113
+ target: { component: scroll, originX: event.screenX - event.x, originY: event.screenY - event.y, width: event.width, height: event.height },
114
+ };
115
+ }
116
+ return dispatchPartMouse(event) ?? nativeMouse(event);
117
+ };
118
+ const prepare = (width: number, root: LayoutRoot): boolean => {
119
+ state.active = false;
120
+ if (stopped || failed || host.mode !== "fullscreen" || width < SIDEBAR_BREAKPOINT) {
121
+ prepared = undefined;
122
+ return false;
123
+ }
124
+ const parts = [...state.parts.entries()];
125
+ const digests = parts.map(([, rail]) => railDigest(rail));
126
+ const unchanged = prepared?.revision === cache.revision &&
127
+ prepared.width === width && prepared.mode === host.mode && prepared.root === root && prepared.theme === theme &&
128
+ prepared.parts.length === parts.length && prepared.parts.every(([key, part], index) => parts[index]?.[0] === key && parts[index]?.[1] === part) &&
129
+ prepared.digests.length === digests.length && prepared.digests.every((digest, index) => digest === digests[index]);
130
+ if (unchanged) {
131
+ railLines = prepared.lines;
132
+ state.active = prepared.active;
133
+ return prepared.active;
134
+ }
135
+ try {
136
+ const contentWidth = scroll.getContentWidth(RAIL_WIDTH);
137
+ const sections = ["footer", "changes", "agents", "todo"].map((key) => {
138
+ const component = state.parts.get(key);
139
+ const lines = [...(component?.render(contentWidth - RAIL_PADDING * 2) ?? [])];
140
+ while (lines.length && lines[lines.length - 1]?.trim() === "") lines.pop();
141
+ return { key, component, lines };
142
+ }).filter((section) => section.component !== undefined && section.lines.length > 0) as Array<{ key: string; component: Component; lines: string[] }>;
143
+ const branding = renderSidebarBanner(theme, contentWidth - RAIL_PADDING * 2);
144
+ const hits: RailHit[] = [];
145
+ railLines = [];
146
+ if (sections.length && branding.length) {
147
+ railLines.push(...branding.map((line) => " ".repeat(RAIL_PADDING) + line + " ".repeat(RAIL_PADDING)));
148
+ }
149
+ for (const section of sections) {
150
+ if (railLines.length > 0) railLines.push("");
151
+ const startY = railLines.length;
152
+ railLines.push(...section.lines.map((line) => " ".repeat(RAIL_PADDING) + line + " ".repeat(RAIL_PADDING)));
153
+ hits.push({ key: section.key, component: section.component, startY, height: section.lines.length, width: contentWidth - RAIL_PADDING * 2 });
154
+ }
155
+ // Height is owned by the native ScrollView, never by the transcript.
156
+ const active = railLines.length > 0 && railLines.every((line) => visibleWidth(line) <= contentWidth);
157
+ prepared = { revision: cache.revision, width, mode: host.mode, root, theme, parts, digests, contentWidth, active, lines: railLines, hits };
158
+ state.active = active;
159
+ return active;
160
+ } catch {
161
+ failed = true;
162
+ return false;
163
+ }
164
+ };
165
+ const attach = () => {
166
+ if (stopped || failed) return;
167
+ if (host.mode !== "fullscreen") { state.active = false; return; }
168
+ try {
169
+ const root = host.layoutRoot;
170
+ if (!root || typeof root[NODE] !== "function") { state.active = false; return; }
171
+ if (roots.has(root)) return;
172
+ const original = root[NODE]!;
173
+ const descriptor = Object.getOwnPropertyDescriptor(root, NODE);
174
+ const left = { render: (width: number) => root.render(width), invalidate() {}, [NODE]: () => original.call(root) };
175
+ const replacement = () => {
176
+ if (!prepare(tui.terminal.columns, root)) return original.call(root);
177
+ const current = prepared!;
178
+ if (current.presentation?.scrollTop === scroll.scrollTop) return current.presentation.output;
179
+ return (current.presentation = {
180
+ scrollTop: scroll.scrollTop,
181
+ output: { type: "hstack", gap: GAP, align: "stretch", entries: [
182
+ { component: left, basis: 0, grow: 1, shrink: 1, minSize: 1 },
183
+ { component: scroll, basis: RAIL_WIDTH, grow: 0, shrink: 0, minSize: RAIL_WIDTH },
184
+ ] },
185
+ }).output;
186
+ };
187
+ root[NODE] = replacement;
188
+ roots.add(root);
189
+ tui.requestRender();
190
+ cleanups.push(() => {
191
+ if (root[NODE] !== replacement) return;
192
+ if (descriptor) Object.defineProperty(root, NODE, descriptor);
193
+ else Reflect.deleteProperty(root, NODE);
194
+ });
195
+ } catch {
196
+ failed = true;
197
+ state.active = false;
198
+ }
199
+ };
200
+ attach();
201
+ // Pi replaces renderers without a session event. Rebind only that transition;
202
+ // resize and scroll remain owned by Pi's native layout/render loop.
203
+ const timer = setInterval(attach, 100);
204
+ timer.unref();
205
+ return () => {
206
+ stopped = true;
207
+ state.active = false;
208
+ clearInterval(timer);
209
+ scroll.hideTransientScrollbar();
210
+ for (const cleanup of cleanups.reverse()) cleanup();
211
+ tui.requestRender();
212
+ };
213
+ }
@@ -0,0 +1,41 @@
1
+ import type { Component, TUI } from "@earendil-works/pi-tui";
2
+
3
+ // Store on the terminal, not a module singleton: extension loaders may isolate
4
+ // modules, while Pi keeps this terminal across regular/fullscreen transitions.
5
+ const STATE = Symbol.for("gentle-pi.experimental-sidebar.state");
6
+ export interface SidebarState {
7
+ active: boolean;
8
+ ownsHost?: () => boolean;
9
+ parts: Map<string, SidebarRail>;
10
+ }
11
+
12
+ /** A rail slot in the fullscreen sidebar. */
13
+ export interface SidebarRail extends Component {
14
+ /**
15
+ * Cheap digest of the live state this rail paints. The fullscreen layout memo
16
+ * re-renders a part only when its digest changes, so a rail that reads session
17
+ * data (model, thinking level, context, cost, extension statuses) must declare
18
+ * one; explicit invalidateSidebar() stays for discrete state changes.
19
+ */
20
+ digest?(): string;
21
+ }
22
+ export function sidebarState(tui: TUI): SidebarState {
23
+ const terminal = tui.terminal as unknown as Record<symbol, SidebarState>;
24
+ return terminal[STATE] ??= { active: false, parts: new Map() };
25
+ }
26
+
27
+ /** Keep the original bottom component mounted, suppressing only its paint. */
28
+ export function sidebarPart<T extends Component & { dispose?(): void }>(tui: TUI, key: string, bottom: T, rail: SidebarRail = bottom): T {
29
+ // Minimal extension hosts cannot share terminal-owned layout state.
30
+ if (!tui.terminal) return bottom;
31
+ const state = sidebarState(tui);
32
+ state.parts.set(key, rail);
33
+ return {
34
+ ...bottom,
35
+ render: (width: number) => state.active && state.ownsHost?.() ? [] : bottom.render(width),
36
+ dispose() {
37
+ if (state.parts.get(key) === rail) state.parts.delete(key);
38
+ bottom.dispose?.();
39
+ },
40
+ };
41
+ }
package/lib/shell-todo.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { CARD_TONE, renderCard, type CardTheme } from "./shell-card.ts";
1
+ import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
+ import { CARD_TONE, cardInnerWidth, renderCard, type CardTheme } from "./shell-card.ts";
2
3
  import { sanitizeTerminalText } from "./terminal-theme.ts";
3
4
 
4
5
  // Gentle Todo: the task list the model keeps while it works. Everything here
@@ -71,6 +72,8 @@ export interface TodoTheme extends CardTheme {
71
72
  }
72
73
 
73
74
  export interface TodoRenderOptions {
75
+ /** A scrollable host supplies the height bound instead of folding tasks. */
76
+ scrollable?: boolean;
74
77
  collapsed: boolean;
75
78
  staleTurns: number;
76
79
  collapseKey?: string;
@@ -238,29 +241,38 @@ export function todoPromptBlock(state: TodoState, stale: number): string | undef
238
241
  ].join("\n") + staleLine;
239
242
  }
240
243
 
241
- function taskRow(task: TodoTask, theme: TodoTheme): string {
242
- const title = task.status === TODO_STATUS.DONE ? theme.strikethrough(task.title) : task.title;
244
+ function taskRow(task: TodoTask, theme: TodoTheme, width: number): string {
245
+ if (task.status === TODO_STATUS.DONE) {
246
+ // Wrap before applying SGR 9: every physical title closes its own style
247
+ // before renderCard adds padding and rails.
248
+ return wrapTextWithAnsi(task.title, Math.max(1, cardInnerWidth(width) - 2))
249
+ .map((line, index) => `${index === 0 ? theme.fg(GLYPH_ROLE[task.status], STATUS_GLYPH[task.status]) : " "} ${theme.fg(STATUS_ROLE[task.status], theme.strikethrough(line))}`)
250
+ .join("\n");
251
+ }
252
+ const title = task.title;
243
253
  const note = task.status === TODO_STATUS.IN_PROGRESS && task.note ? ` ${theme.fg(NOTE_ROLE, "·")} ${theme.fg(NOTE_ROLE, task.note)}` : "";
244
254
  return `${theme.fg(GLYPH_ROLE[task.status], STATUS_GLYPH[task.status])} ${theme.fg(STATUS_ROLE[task.status], title)}${note}`;
245
255
  }
246
256
 
247
257
  // Long lists keep the card short: done tasks fold into "✓ N done" and the
248
258
  // open ones fill the remaining rows, with a trailing count for the rest.
249
- function bodyRows(state: TodoState, theme: TodoTheme): string[] {
250
- if (state.tasks.length <= ROW_CAP) return state.tasks.map((task) => taskRow(task, theme));
259
+ function bodyRows(state: TodoState, theme: TodoTheme, width: number): string[] {
260
+ if (state.tasks.length <= ROW_CAP) return state.tasks.map((task) => taskRow(task, theme, width));
251
261
  const { done } = todoSummary(state);
252
262
  const open = state.tasks.filter((task) => task.status !== TODO_STATUS.DONE);
253
263
  const rows: string[] = [];
254
264
  if (done > 0) rows.push(`${theme.fg(GLYPH_ROLE[TODO_STATUS.DONE], STATUS_GLYPH[TODO_STATUS.DONE])} ${theme.fg(NOTE_ROLE, `${done} done`)}`);
255
265
  const room = ROW_CAP - rows.length - (open.length > ROW_CAP - rows.length ? 1 : 0);
256
- for (const task of open.slice(0, room)) rows.push(taskRow(task, theme));
266
+ for (const task of open.slice(0, room)) rows.push(taskRow(task, theme, width));
257
267
  if (open.length > room) rows.push(theme.fg(NOTE_ROLE, `… ${open.length - room} more`));
258
268
  return rows;
259
269
  }
260
270
 
261
- function collapsedRow(state: TodoState, theme: TodoTheme): string {
271
+ function collapsedRow(state: TodoState, theme: TodoTheme, width: number): string {
262
272
  const active = state.tasks.find((task) => task.status === TODO_STATUS.IN_PROGRESS);
263
- if (active) return taskRow(active, theme);
273
+ if (active) return taskRow(active, theme, width);
274
+ const pending = state.tasks.find((task) => task.status === TODO_STATUS.PENDING);
275
+ if (pending) return taskRow(pending, theme, width);
264
276
  const { open } = todoSummary(state);
265
277
  return `${theme.fg(GLYPH_ROLE[TODO_STATUS.PENDING], STATUS_GLYPH[TODO_STATUS.PENDING])} ${theme.fg(NOTE_ROLE, `${open} open`)}`;
266
278
  }
@@ -269,10 +281,15 @@ export function renderTodoCard(state: TodoState, theme: TodoTheme, width: number
269
281
  if (state.tasks.length === 0) return [];
270
282
  const { done, total } = todoSummary(state);
271
283
  const stale = options.staleTurns >= STALE_AFTER_TURNS;
272
- const hint = stale ? `stale · ${options.staleTurns} turns` : options.collapsed && options.collapseKey ? `${options.collapseKey} expand` : undefined;
273
- const body = options.collapsed ? [collapsedRow(state, theme)] : bodyRows(state, theme);
284
+ const action = options.collapsed ? "expand" : "collapse";
285
+ const actionLabel = action[0]!.toUpperCase() + action.slice(1);
286
+ const icon = options.collapsed ? "▸" : "▾";
287
+ const control = `${icon} ${width >= 28 ? actionLabel : ""}`.trimEnd();
288
+ const hint = options.collapseKey ? `${options.collapseKey} ${action}` : undefined;
289
+ const rows = options.collapsed ? [collapsedRow(state, theme, width)] : options.scrollable ? state.tasks.map((task) => taskRow(task, theme, width)) : bodyRows(state, theme, width);
290
+ const body = stale ? [theme.fg(NOTE_ROLE, `stale · ${options.staleTurns} turns`), ...rows] : rows;
274
291
  return renderCard(
275
- { title: "Todos", subtitle: `${done} of ${total}`, body, tone: stale ? CARD_TONE.WARNING : CARD_TONE.INFO, glyph: TODO_GLYPH },
292
+ { title: `Todos ${theme.fg("accent", control)}`, subtitle: `${done} of ${total}`, body, tone: stale ? CARD_TONE.WARNING : CARD_TONE.INFO, glyph: TODO_GLYPH },
276
293
  theme,
277
294
  width,
278
295
  { expanded: true, hint },
@@ -89,6 +89,7 @@ export interface TelemetryTriggerSpawnOptions {
89
89
  cwd: string;
90
90
  env: NodeJS.ProcessEnv;
91
91
  detached: boolean;
92
+ windowsHide: boolean;
92
93
  stdio: "ignore";
93
94
  }
94
95
 
@@ -129,6 +130,7 @@ export function spawnTelemetryTrigger(options: SpawnTelemetryTriggerOptions): Te
129
130
  cwd: options.cwd,
130
131
  env: options.env,
131
132
  detached: true,
133
+ windowsHide: true,
132
134
  stdio: "ignore",
133
135
  });
134
136
  child.on("error", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gentle-pi",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -48,7 +48,8 @@
48
48
  "check:runtime-modules": "node scripts/build-runtime-modules.mjs --check",
49
49
  "test:packed-package": "node scripts/test-packed-runner.mjs",
50
50
  "prepack": "pnpm test && pnpm run check:runtime-modules && node scripts/verify-package-files.mjs",
51
- "prepublishOnly": "pnpm test && pnpm run check:runtime-modules && node scripts/verify-package-files.mjs && pnpm run test:packed-package"
51
+ "prepublishOnly": "pnpm test && pnpm run check:runtime-modules && node scripts/verify-package-files.mjs && pnpm run test:packed-package",
52
+ "typecheck": "node scripts/check-types.mjs"
52
53
  },
53
54
  "pi": {
54
55
  "image": "https://cdn.jsdelivr.net/npm/gentle-pi/assets/gentle-logo-only.png",
@@ -79,7 +80,9 @@
79
80
  }
80
81
  },
81
82
  "devDependencies": {
82
- "@earendil-works/pi-coding-agent": "0.85.1"
83
+ "@earendil-works/pi-coding-agent": "0.85.1",
84
+ "@types/node": "^24.13.3",
85
+ "typescript": "^5.9.3"
83
86
  },
84
87
  "packageManager": "pnpm@11.1.1"
85
88
  }
@@ -17,6 +17,8 @@ import {
17
17
  import { fileURLToPath } from "node:url";
18
18
 
19
19
  export const GENTLE_AI_BINARY_MISSING_CODE = "package-local-binary-missing";
20
+ export const GENTLE_AI_INSTALL_RECOVERY_COMMAND = "node scripts/install-gentle-ai.mjs";
21
+ export const GENTLE_AI_INSTALL_RECOVERY_INSTRUCTIONS = `If GENTLE_PI_SKIP_GENTLE_AI_INSTALL is set, remove or unset it before changing to the installed gentle-pi package directory and running \`${GENTLE_AI_INSTALL_RECOVERY_COMMAND}\`.`;
20
22
  // Derived from the one authoritative pinned version in
21
23
  // scripts/gentle-ai-installer.mjs rather than repeating the literal here, so
22
24
  // the two can never independently drift apart the way they once did.
@@ -26,7 +28,7 @@ export class PackageLocalGentleAiBinaryMissingError extends Error {
26
28
  code = GENTLE_AI_BINARY_MISSING_CODE;
27
29
  constructor(path ) {
28
30
  super(
29
- `${GENTLE_AI_BINARY_MISSING_CODE}: Gentle AI v${GENTLE_AI_VERSION} is not installed at ${path}. Reinstall gentle-pi, or use GENTLE_PI_SKIP_GENTLE_AI_INSTALL=1 only for development/offline installs.`,
31
+ `${GENTLE_AI_BINARY_MISSING_CODE}: Gentle AI v${GENTLE_AI_VERSION} is not installed at ${path}. ${GENTLE_AI_INSTALL_RECOVERY_INSTRUCTIONS} A missing binary can result from skipped lifecycle scripts, but does not prove install lifecycle scripts were disabled.`,
30
32
  );
31
33
  this.name = "PackageLocalGentleAiBinaryMissingError";
32
34
  }