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.
- package/README.md +139 -30
- package/assets/agents/gentle-ai-worker.md +4 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +21 -3
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
- package/docs/telemetry.md +57 -1
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +143 -15
- package/extensions/codegraph-tools.ts +1 -0
- package/extensions/gentle-agents.ts +799 -50
- package/extensions/gentle-ai.ts +2033 -322
- package/extensions/gentle-shell.ts +145 -42
- package/extensions/gentle-todo.ts +47 -12
- package/extensions/quiet-tools.ts +1 -0
- package/extensions/runtime-metrics.ts +130 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +7 -10
- package/lib/agents-history.ts +9 -1
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +77 -5
- package/lib/agents-runner.ts +548 -26
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +548 -191
- package/lib/agents-widget.ts +33 -14
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +8 -6
- package/lib/native-review-cli.ts +283 -1
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +296 -46
- package/lib/review-candidate-view.ts +30 -20
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +53 -8
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +187 -5
- package/lib/review-last-event-controller.ts +7 -4
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-session-standing-permission.ts +27 -6
- package/lib/runtime-metrics-children.ts +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +47 -1
- package/lib/shell-card.ts +1 -4
- package/lib/shell-changes-view.ts +362 -37
- package/lib/shell-changes.ts +81 -1
- package/lib/shell-prompt.ts +11 -15
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +28 -11
- package/lib/telemetry-trigger.ts +2 -0
- package/package.json +6 -3
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +283 -1
- package/runtime/review-integration-v2.mjs +187 -5
- package/runtime/telemetry-trigger.mjs +2 -0
- package/scripts/build-runtime-modules.mjs +9 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +4 -2
- package/skills/_shared/review-ledger-contract.md +17 -1
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +62 -0
- package/tests/agents-fake-child.ts +15 -1
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +45 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner.test.ts +571 -14
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-view.test.ts +476 -65
- package/tests/agents-widget.test.ts +31 -1
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +169 -3
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +2 -1
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +2 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +1460 -33
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +38 -0
- package/tests/gentle-ai.test.ts +944 -4
- package/tests/gentle-shell.test.ts +303 -12
- package/tests/gentle-todo.test.ts +54 -10
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-review-capability-contract.test.ts +12 -1
- package/tests/native-review-cli.test.ts +277 -3
- package/tests/native-review-parity.test.ts +14 -7
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/package-manifest.test.ts +232 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +1 -0
- package/tests/rdd-aware-verification-contract.test.ts +10 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +304 -6
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +14 -0
- package/tests/review-controller-native-routing.test.ts +563 -3
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +364 -4
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +164 -0
- package/tests/review-last-event-closure.test.ts +105 -1
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-session-standing-permission-controller.test.ts +52 -4
- package/tests/review-session-standing-permission.test.ts +30 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-card.test.ts +24 -3
- package/tests/shell-changes-view.test.ts +471 -8
- package/tests/shell-changes.test.ts +168 -0
- package/tests/shell-prompt.test.ts +28 -6
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +100 -11
- package/tests/startup-banner.test.ts +126 -0
- 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 {
|
|
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
|
-
|
|
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
|
|
273
|
-
const
|
|
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:
|
|
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 },
|
package/lib/telemetry-trigger.ts
CHANGED
|
@@ -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.
|
|
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}.
|
|
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
|
}
|