gentle-pi 2.4.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 +292 -25
- package/assets/agents/gentle-ai-worker.md +13 -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 +39 -11
- package/assets/orchestrator.md +5 -5
- 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/delegated-verification.md +25 -0
- package/docs/telemetry.md +94 -0
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +159 -25
- package/extensions/codegraph-tools.ts +95 -5
- package/extensions/gentle-agents.ts +1337 -0
- package/extensions/gentle-ai.ts +2916 -386
- package/extensions/gentle-shell.ts +650 -0
- package/extensions/gentle-todo.ts +234 -0
- package/extensions/quiet-tools.ts +2 -1
- 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 +315 -0
- package/lib/agents-history.ts +88 -0
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +501 -0
- package/lib/agents-runner.ts +1012 -0
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +914 -0
- package/lib/agents-widget.ts +241 -0
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +143 -25
- package/lib/native-choice-list.ts +194 -0
- package/lib/native-fullscreen-interaction.ts +47 -0
- package/lib/native-pointer-region.ts +164 -0
- package/lib/native-review-cli.ts +371 -13
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +427 -0
- package/lib/review-candidate-view.ts +150 -48
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +110 -0
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +243 -11
- package/lib/review-last-event-controller.ts +8 -4
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-repository.ts +2 -2
- package/lib/review-risk-assessment.ts +339 -0
- package/lib/review-session-standing-permission-ipc.ts +309 -0
- package/lib/review-session-standing-permission.ts +240 -0
- 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 +184 -0
- package/lib/shell-card.ts +133 -0
- package/lib/shell-changes-view.ts +530 -0
- package/lib/shell-changes.ts +290 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +115 -0
- 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 +297 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +153 -0
- package/package.json +8 -5
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +370 -12
- package/runtime/review-integration-v2.mjs +243 -11
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +154 -0
- package/scripts/build-runtime-modules.mjs +11 -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 +38 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +8 -2
- package/skills/_shared/review-ledger-contract.md +20 -2
- 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 +205 -0
- package/tests/agents-fake-child.ts +66 -0
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +198 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +959 -0
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +685 -0
- package/tests/agents-widget.test.ts +141 -0
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +325 -5
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +112 -2
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +110 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +2168 -0
- 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 +103 -0
- package/tests/gentle-ai.test.ts +971 -15
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +818 -0
- package/tests/gentle-todo.test.ts +226 -0
- 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-choice-list.test.ts +202 -0
- package/tests/native-fullscreen-interaction.test.ts +125 -0
- package/tests/native-pointer-region.test.ts +245 -0
- package/tests/native-review-capability-contract.test.ts +27 -1
- package/tests/native-review-cli.test.ts +317 -3
- package/tests/native-review-consent.test.ts +91 -0
- package/tests/native-review-parity-runtime.test.ts +8 -2
- package/tests/native-review-parity.test.ts +43 -29
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-budget.test.ts +69 -0
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/orchestrator-rdd-ownership.test.ts +9 -0
- package/tests/package-manifest.test.ts +243 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +97 -37
- package/tests/rdd-aware-verification-contract.test.ts +226 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +751 -7
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +17 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +884 -7
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +384 -8
- 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 +276 -0
- package/tests/review-last-event-closure.test.ts +112 -3
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-repository.test.ts +28 -1
- package/tests/review-risk-assessment.test.ts +626 -0
- package/tests/review-session-standing-permission-controller.test.ts +656 -0
- package/tests/review-session-standing-permission-ipc.test.ts +233 -0
- package/tests/review-session-standing-permission-runtime.test.ts +212 -0
- package/tests/review-session-standing-permission.test.ts +156 -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-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +139 -0
- package/tests/shell-changes-view.test.ts +609 -0
- package/tests/shell-changes.test.ts +350 -0
- package/tests/shell-prompt.test.ts +140 -0
- 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 +259 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +351 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import { NativePointerRegion } from "../lib/native-pointer-region.ts";
|
|
4
|
+
import { sidebarPart } from "../lib/shell-sidebar.ts";
|
|
5
|
+
import { invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
|
|
6
|
+
import {
|
|
7
|
+
applyTodo,
|
|
8
|
+
emptyTodo,
|
|
9
|
+
renderTodoCard,
|
|
10
|
+
replayTodo,
|
|
11
|
+
staleTurns,
|
|
12
|
+
TODO_DETAILS_KEY,
|
|
13
|
+
TODO_GLYPH,
|
|
14
|
+
TODO_TOOL_NAME,
|
|
15
|
+
todoPromptBlock,
|
|
16
|
+
todoSummary,
|
|
17
|
+
type TodoParams,
|
|
18
|
+
type TodoState,
|
|
19
|
+
} from "../lib/shell-todo.ts";
|
|
20
|
+
|
|
21
|
+
// Gentle Todo: the task list the model keeps while it works, drawn as a
|
|
22
|
+
// Gentle Shell card above the editor. Three things keep it current that a
|
|
23
|
+
// static tool description cannot: `write` replaces the whole list in one
|
|
24
|
+
// call, every turn's system prompt carries the open tasks and the rules, and
|
|
25
|
+
// a list that goes untouched while tasks stay open is marked stale for both
|
|
26
|
+
// the human and the model.
|
|
27
|
+
|
|
28
|
+
const WIDGET_KEY = "gentle-todo";
|
|
29
|
+
const COLLAPSE_KEY_DEFAULT = "ctrl+shift+t";
|
|
30
|
+
const TOOL_PARAMETERS = {
|
|
31
|
+
type: "object",
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
required: ["action"],
|
|
34
|
+
properties: {
|
|
35
|
+
action: { type: "string", enum: ["write", "add", "update", "clear", "list"], description: "write replaces the whole list; add appends one task; update changes one task by id; clear empties the list; list reports it." },
|
|
36
|
+
tasks: {
|
|
37
|
+
type: "array",
|
|
38
|
+
description: "For write: the complete ordered list. Keep the id of tasks that already exist so their history survives; omit it for new ones.",
|
|
39
|
+
items: {
|
|
40
|
+
type: "object",
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
required: ["title"],
|
|
43
|
+
properties: {
|
|
44
|
+
id: { type: "integer", description: "Existing task id to keep." },
|
|
45
|
+
title: { type: "string", description: "Short imperative title, e.g. 'Write the parser'." },
|
|
46
|
+
status: { type: "string", enum: ["pending", "in_progress", "done"], description: "Defaults to pending." },
|
|
47
|
+
note: { type: "string", description: "What is happening right now, shown while in_progress, e.g. 'writing tests'." },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
id: { type: "integer", description: "Task id for update." },
|
|
52
|
+
title: { type: "string", description: "Title for add, or a new title for update." },
|
|
53
|
+
status: { type: "string", enum: ["pending", "in_progress", "done"], description: "Status for add or update." },
|
|
54
|
+
note: { type: "string", description: "Note for add or update." },
|
|
55
|
+
},
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
export function todoEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
59
|
+
if (env.GENTLE_PI_AGENTS_CHILD === "1") return false;
|
|
60
|
+
const value = env.GENTLE_PI_TODO?.trim().toLowerCase();
|
|
61
|
+
return !(value === "0" || value === "false" || value === "off");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function todoCollapseKey(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
65
|
+
const value = env.GENTLE_PI_TODO_KEY?.trim();
|
|
66
|
+
if (value === undefined) return COLLAPSE_KEY_DEFAULT;
|
|
67
|
+
return value === "" || value.toLowerCase() === "off" ? undefined : value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface TodoSession {
|
|
71
|
+
state: TodoState;
|
|
72
|
+
turn: number;
|
|
73
|
+
collapsed: boolean;
|
|
74
|
+
/** A finished list stays on screen for the turn it finished in, then clears. */
|
|
75
|
+
clearOnNextTurn: boolean;
|
|
76
|
+
ui: ExtensionContext["ui"] | undefined;
|
|
77
|
+
host: { requestRender(): void } | undefined;
|
|
78
|
+
tui: TUI | undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sessionKey(ctx: ExtensionContext): string {
|
|
82
|
+
return ctx.sessionManager.getSessionId() ?? "";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = process.env): void {
|
|
86
|
+
if (!todoEnabled(env)) return;
|
|
87
|
+
const sessions = new Map<string, TodoSession>();
|
|
88
|
+
const collapseKey = todoCollapseKey(env);
|
|
89
|
+
|
|
90
|
+
const session = (ctx: ExtensionContext): TodoSession => {
|
|
91
|
+
const key = sessionKey(ctx);
|
|
92
|
+
let current = sessions.get(key);
|
|
93
|
+
if (!current) {
|
|
94
|
+
current = { state: emptyTodo(), turn: 0, collapsed: false, clearOnNextTurn: false, ui: undefined, host: undefined, tui: undefined };
|
|
95
|
+
sessions.set(key, current);
|
|
96
|
+
}
|
|
97
|
+
return current;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const toggle = (current: TodoSession) => {
|
|
101
|
+
current.collapsed = !current.collapsed;
|
|
102
|
+
if (current.tui) invalidateSidebar(current.tui);
|
|
103
|
+
current.host?.requestRender();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const todoCard = (current: TodoSession, theme: Parameters<typeof renderTodoCard>[1], scrollable: boolean, spacer: boolean): Component & { dispose(): void } => {
|
|
107
|
+
const card: Component = {
|
|
108
|
+
render(width: number) {
|
|
109
|
+
const lines = renderTodoCard(current.state, theme, width, {
|
|
110
|
+
collapsed: current.collapsed,
|
|
111
|
+
staleTurns: staleTurns(current.state, current.turn),
|
|
112
|
+
collapseKey,
|
|
113
|
+
...(scrollable ? { scrollable: true } : {}),
|
|
114
|
+
});
|
|
115
|
+
return spacer && lines.length > 0 ? [...lines, ""] : lines;
|
|
116
|
+
},
|
|
117
|
+
invalidate() {},
|
|
118
|
+
};
|
|
119
|
+
const region = new NativePointerRegion(card, {
|
|
120
|
+
onClick(event) {
|
|
121
|
+
if (event.button !== "left" || event.y !== 0) return undefined;
|
|
122
|
+
toggle(current);
|
|
123
|
+
return { handled: true, render: true };
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
render: (width) => region.render(width),
|
|
128
|
+
handleMouse: (event) => region.handleMouse(event),
|
|
129
|
+
invalidate: () => region.invalidate(),
|
|
130
|
+
dispose: () => region.dispose(),
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const show = (current: TodoSession) => {
|
|
135
|
+
if (current.tui) invalidateSidebar(current.tui);
|
|
136
|
+
if (!current.ui) return;
|
|
137
|
+
if (current.state.tasks.length === 0) {
|
|
138
|
+
current.ui.setWidget(WIDGET_KEY, undefined);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const snapshot = current;
|
|
142
|
+
current.ui.setWidget(WIDGET_KEY, (tui, theme) => {
|
|
143
|
+
snapshot.host = tui;
|
|
144
|
+
snapshot.tui = tui;
|
|
145
|
+
return sidebarPart(tui, "todo", todoCard(snapshot, theme, false, true), todoCard(snapshot, theme, true, false));
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
pi.registerTool({
|
|
150
|
+
name: TODO_TOOL_NAME,
|
|
151
|
+
renderShell: "self",
|
|
152
|
+
label: "Todo",
|
|
153
|
+
description: "Plan and track multi-step work. Use write to set the whole list, update to move one task, add for a new one, clear to reset, list to read it back.",
|
|
154
|
+
promptSnippet: "Track multi-step work; rewrite the whole list as the plan changes",
|
|
155
|
+
promptGuidelines: [
|
|
156
|
+
"Use todo for work with three or more steps or when the user hands you a list. Skip it for single trivial requests.",
|
|
157
|
+
"Mark a task in_progress before starting it and done right after finishing it; keep exactly one task in_progress.",
|
|
158
|
+
"Prefer write with the complete list whenever the plan changes; keep ids of tasks that already exist.",
|
|
159
|
+
"Never mark a task done while tests fail or the work is partial; add a task for the blocker instead.",
|
|
160
|
+
],
|
|
161
|
+
parameters: TOOL_PARAMETERS,
|
|
162
|
+
executionMode: "sequential",
|
|
163
|
+
renderCall(args, theme) {
|
|
164
|
+
const params = args as TodoParams;
|
|
165
|
+
return new Text(theme.fg("toolTitle", `${TODO_GLYPH} todo · ${params.action}`), 0, 0);
|
|
166
|
+
},
|
|
167
|
+
renderResult(result, options, theme) {
|
|
168
|
+
const text = result.content.map((part) => (part.type === "text" ? part.text : "")).join("\n");
|
|
169
|
+
return new Text(options.expanded ? text : theme.fg("muted", text.split("\n")[0] ?? ""), 0, 0);
|
|
170
|
+
},
|
|
171
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
172
|
+
const current = session(ctx);
|
|
173
|
+
const result = applyTodo(current.state, params as TodoParams, current.turn);
|
|
174
|
+
if (!result.error) {
|
|
175
|
+
current.state = result.state;
|
|
176
|
+
current.clearOnNextTurn = false;
|
|
177
|
+
if (current.tui) invalidateSidebar(current.tui);
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
content: [{ type: "text", text: result.text }],
|
|
181
|
+
details: { [TODO_DETAILS_KEY]: result.state, ...(result.error ? { error: result.error } : {}) },
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (collapseKey) {
|
|
187
|
+
pi.registerShortcut(collapseKey as Parameters<ExtensionAPI["registerShortcut"]>[0], {
|
|
188
|
+
description: "Collapse or expand the todo list",
|
|
189
|
+
handler: async (ctx) => {
|
|
190
|
+
toggle(session(ctx));
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
pi.on("session_start", (_event, ctx) => {
|
|
196
|
+
const current = session(ctx);
|
|
197
|
+
// A list that was already finished when the session was left is history,
|
|
198
|
+
// not work: it would otherwise sit on screen until two more turns pass.
|
|
199
|
+
const replayed = replayTodo(ctx.sessionManager.getBranch());
|
|
200
|
+
current.state = replayed.tasks.length > 0 && todoSummary(replayed).open === 0 ? { ...replayed, tasks: [] } : replayed;
|
|
201
|
+
current.turn = ctx.sessionManager.getBranch().filter((entry) => (entry as { type?: string }).type === "message" && (entry as { message?: { role?: string } }).message?.role === "user").length;
|
|
202
|
+
current.ui = ctx.hasUI ? ctx.ui : undefined;
|
|
203
|
+
show(current);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
207
|
+
sessions.delete(sessionKey(ctx));
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
211
|
+
const current = session(ctx);
|
|
212
|
+
current.turn += 1;
|
|
213
|
+
if (current.tui) invalidateSidebar(current.tui);
|
|
214
|
+
if (current.clearOnNextTurn) {
|
|
215
|
+
current.state = { ...current.state, tasks: [] };
|
|
216
|
+
current.clearOnNextTurn = false;
|
|
217
|
+
show(current);
|
|
218
|
+
}
|
|
219
|
+
const block = todoPromptBlock(current.state, staleTurns(current.state, current.turn));
|
|
220
|
+
if (!block) return undefined;
|
|
221
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${block}` };
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
225
|
+
if (event.toolName !== TODO_TOOL_NAME) return;
|
|
226
|
+
show(session(ctx));
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
pi.on("agent_end", (_event, ctx) => {
|
|
230
|
+
const current = session(ctx);
|
|
231
|
+
if (current.state.tasks.length > 0 && todoSummary(current.state).open === 0) current.clearOnNextTurn = true;
|
|
232
|
+
show(current);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
@@ -625,6 +625,7 @@ function registerQuietTool(pi: ExtensionAPI, toolName: QuietToolName, commandArg
|
|
|
625
625
|
|
|
626
626
|
pi.registerTool({
|
|
627
627
|
...registrationTool,
|
|
628
|
+
renderShell: "self",
|
|
628
629
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
629
630
|
const runtimeTool = getBuiltInTools(ctx.cwd)[toolName];
|
|
630
631
|
return runtimeTool.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
@@ -653,7 +654,7 @@ function registerQuietTool(pi: ExtensionAPI, toolName: QuietToolName, commandArg
|
|
|
653
654
|
{ result: true, isPartial: options.isPartial },
|
|
654
655
|
).directResult;
|
|
655
656
|
if (directResult) {
|
|
656
|
-
return renderGentleAiResult(safeResult, { expanded: options.expanded });
|
|
657
|
+
return renderGentleAiResult(safeResult, { expanded: options.expanded, isPartial: options.isPartial, isError }, theme, renderContext as GentleAiRenderContext | undefined);
|
|
657
658
|
}
|
|
658
659
|
if (options.isPartial) {
|
|
659
660
|
if (options.expanded) return new Text(`${theme.fg("warning", partialLabel(toolName, text))}\n${theme.fg("muted", text)}`, 0, 0);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { EFFORTS, ORCHESTRATOR_AGENT_CLASS, RuntimeMetrics, UNKNOWN_AGENT_CLASS, type FinalResponse, type TokenMeasurement } from "../lib/runtime-metrics.ts";
|
|
3
|
+
import { classifyPiCatalogName, lookupPiCatalogName } from "../lib/runtime-metrics-pi-identity.ts";
|
|
4
|
+
import { CHILD_METRICS_EVENT, CHILD_METRICS_REVOKED, snapshotChildEvent, type ChildLaunchBucket } from "../lib/runtime-metrics-children.ts";
|
|
5
|
+
import { RuntimeMetricsAttempt } from "../lib/runtime-metrics-delivery.ts";
|
|
6
|
+
import { sendNativeRuntimeEvent, type NativeRuntimeTransportDeps } from "../lib/runtime-metrics-native.ts";
|
|
7
|
+
import { runtimeMetricsEnvAllows } from "../lib/runtime-metrics-policy.ts";
|
|
8
|
+
|
|
9
|
+
/** Available final usage -> one deferred attempt -> discard. No history reads,
|
|
10
|
+
* cumulative session accounting, policy leases, delivery queue or retry. Print
|
|
11
|
+
* mode alone joins its accepted attempt for at most 1.5 seconds at shutdown.
|
|
12
|
+
* Pi hooks lack request correlation: latency and SDK-zero presence are unknown.
|
|
13
|
+
*/
|
|
14
|
+
export default function runtimeMetrics(pi: ExtensionAPI, env = process.env,
|
|
15
|
+
{ lookup = lookupPiCatalogName, classify = classifyPiCatalogName, native, send = sendNativeRuntimeEvent,
|
|
16
|
+
now = () => performance.now(), shutdownWaitMs = 1500 }:
|
|
17
|
+
{ lookup?: typeof lookupPiCatalogName; classify?: typeof classifyPiCatalogName; native?: NativeRuntimeTransportDeps;
|
|
18
|
+
send?: typeof sendNativeRuntimeEvent; now?: () => number; shutdownWaitMs?: number } = {}): void {
|
|
19
|
+
const allows = () => env.GENTLE_PI_AGENTS_CHILD !== "1" && runtimeMetricsEnvAllows(env);
|
|
20
|
+
if (!allows()) return;
|
|
21
|
+
type Selection = Pick<FinalResponse, "selectedModelId" | "selectedProvider" | "effort">;
|
|
22
|
+
let selection: Selection | undefined;
|
|
23
|
+
let active = false;
|
|
24
|
+
let requestSeen = false;
|
|
25
|
+
let ambiguous = false;
|
|
26
|
+
let live: { id: string; started: number; ctx: ExtensionContext; attempt: RuntimeMetricsAttempt;
|
|
27
|
+
seen: WeakSet<object>; children: Set<string>; catalog: boolean } | undefined;
|
|
28
|
+
const missing = { state: "unavailable" } as const;
|
|
29
|
+
function invalidate() { selection = undefined; ambiguous = true; }
|
|
30
|
+
function dispose() { live?.attempt.dispose(); live = undefined; active = false; invalidate(); }
|
|
31
|
+
function current(ctx: ExtensionContext) {
|
|
32
|
+
if (!allows() || live?.id !== ctx.sessionManager.getSessionId()) dispose();
|
|
33
|
+
return live;
|
|
34
|
+
}
|
|
35
|
+
function refreshCatalog(owner: NonNullable<typeof live>, input: { provider: unknown; modelId: unknown }) {
|
|
36
|
+
void lookup(input).then(result => {
|
|
37
|
+
if (live === owner && result.classification === "catalog_public") owner.catalog = true;
|
|
38
|
+
}, () => { /* A later classification callback may retry the bounded load. */ });
|
|
39
|
+
}
|
|
40
|
+
function submit(owner: NonNullable<typeof live>, responses: FinalResponse[], launches?: ChildLaunchBucket[]) {
|
|
41
|
+
if (!responses.length || live !== owner || !current(owner.ctx)) return;
|
|
42
|
+
// Ephemeral event-local accounting only; source IDs never enter these rows.
|
|
43
|
+
const metrics = new RuntimeMetrics({ classifyModel: classify });
|
|
44
|
+
for (const [index, row] of responses.entries()) metrics.record({ ...row, responseId: String(index) });
|
|
45
|
+
const rows = metrics.snapshot();
|
|
46
|
+
if (!rows.length) return;
|
|
47
|
+
const cwd = owner.ctx.cwd;
|
|
48
|
+
owner.attempt.offer(async signal => {
|
|
49
|
+
if (live !== owner || !current(owner.ctx) || signal.aborted) return;
|
|
50
|
+
await send(rows, cwd, { ...native, launches, env, signal,
|
|
51
|
+
current: () => live === owner && current(owner.ctx) === owner });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const offChild = pi.events.on(CHILD_METRICS_EVENT, value => {
|
|
55
|
+
try {
|
|
56
|
+
const owner = live && current(live.ctx);
|
|
57
|
+
const event = snapshotChildEvent(value);
|
|
58
|
+
if (!owner || !event || event.parentSessionId !== owner.id || event.launchedAt < owner.started
|
|
59
|
+
|| owner.children.has(event.taskId) || owner.children.size >= 256) return;
|
|
60
|
+
owner.children.add(event.taskId); // Busy/failed completions stay consumed.
|
|
61
|
+
submit(owner, event.responses, [{ ...event.launch, evidence: "launch_configuration", launches: 1 }]);
|
|
62
|
+
} catch { /* No telemetry error enters the shared event bus. */ }
|
|
63
|
+
});
|
|
64
|
+
const offRevoke = pi.events.on(CHILD_METRICS_REVOKED, id => {
|
|
65
|
+
if (live?.id === id) {
|
|
66
|
+
live.attempt.dispose();
|
|
67
|
+
live.attempt = new RuntimeMetricsAttempt();
|
|
68
|
+
invalidate();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
pi.on("session_start", (_event, ctx) => {
|
|
72
|
+
dispose();
|
|
73
|
+
if (!allows()) return;
|
|
74
|
+
const owner = { id: ctx.sessionManager.getSessionId(), started: now(), ctx,
|
|
75
|
+
attempt: new RuntimeMetricsAttempt(), seen: new WeakSet<object>(), children: new Set<string>(), catalog: false };
|
|
76
|
+
live = owner;
|
|
77
|
+
refreshCatalog(owner, { provider: "openai", modelId: "gpt-4o" });
|
|
78
|
+
});
|
|
79
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
80
|
+
const teardown = () => { dispose(); offChild(); offRevoke(); };
|
|
81
|
+
const owner = live;
|
|
82
|
+
if (ctx.mode !== "print" || !owner) { teardown(); return; }
|
|
83
|
+
return owner.attempt.waitForSettled(shutdownWaitMs).finally(teardown);
|
|
84
|
+
});
|
|
85
|
+
pi.on("turn_start", () => { ambiguous = active; active = true; requestSeen = false; selection = undefined; });
|
|
86
|
+
pi.on("turn_end", () => { active = false; invalidate(); });
|
|
87
|
+
pi.on("agent_end", () => { active = false; invalidate(); });
|
|
88
|
+
pi.on("session_before_compact", invalidate);
|
|
89
|
+
pi.on("session_before_tree", invalidate);
|
|
90
|
+
pi.on("before_provider_request", (_event, ctx) => {
|
|
91
|
+
const owner = current(ctx);
|
|
92
|
+
if (!owner) return;
|
|
93
|
+
if (requestSeen) invalidate();
|
|
94
|
+
requestSeen = true;
|
|
95
|
+
if (!active || ambiguous) return;
|
|
96
|
+
let effort: FinalResponse["effort"] = "unavailable";
|
|
97
|
+
try { const value = pi.getThinkingLevel(); if (EFFORTS.includes(value)) effort = value; } catch { /* No evidence. */ }
|
|
98
|
+
selection = { selectedProvider: owner.catalog ? ctx.model?.provider as FinalResponse["provider"] ?? "unknown" : "unknown",
|
|
99
|
+
selectedModelId: owner.catalog && typeof ctx.model?.id === "string" && ctx.model.id.length <= 128 ? ctx.model.id : undefined, effort };
|
|
100
|
+
});
|
|
101
|
+
function token(value: unknown): TokenMeasurement {
|
|
102
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= 1_000_000_000
|
|
103
|
+
? { state: "reported", value } : missing;
|
|
104
|
+
}
|
|
105
|
+
pi.on("message_end", (event, ctx) => {
|
|
106
|
+
try {
|
|
107
|
+
const owner = current(ctx);
|
|
108
|
+
const message = event.message;
|
|
109
|
+
if (!owner || message.role !== "assistant" || message.stopReason === "pending" || message.stopReason === "deferred"
|
|
110
|
+
|| owner.seen.has(message)) return;
|
|
111
|
+
owner.seen.add(message);
|
|
112
|
+
refreshCatalog(owner, { provider: "openai", modelId: "gpt-4o" });
|
|
113
|
+
const selected = active && !ambiguous ? selection : undefined;
|
|
114
|
+
submit(owner, [{ kind: "final_assistant_response", responseId: "0",
|
|
115
|
+
selectedProvider: selected?.selectedProvider ?? "unknown", selectedModelId: selected?.selectedModelId,
|
|
116
|
+
effort: selected?.effort ?? "unavailable",
|
|
117
|
+
executor: env.GENTLE_PI_AGENTS_CHILD === undefined ? "orchestrator" : "unknown",
|
|
118
|
+
agentClass: env.GENTLE_PI_AGENTS_CHILD === undefined ? ORCHESTRATOR_AGENT_CLASS : UNKNOWN_AGENT_CLASS,
|
|
119
|
+
observedModelId: message.model, responseModelId: message.responseModel,
|
|
120
|
+
providerThinkingLevel: EFFORTS.includes(message.providerThinkingLevel as FinalResponse["effort"])
|
|
121
|
+
? message.providerThinkingLevel as FinalResponse["effort"] : "unavailable",
|
|
122
|
+
provider: message.provider as FinalResponse["provider"], modelFamily: "unknown",
|
|
123
|
+
error: message.stopReason === "aborted" ? "aborted" : message.stopReason === "error" ? "unknown" : "none",
|
|
124
|
+
tokens: { input: token(message.usage?.input), output: token(message.usage?.output), cacheRead: token(message.usage?.cacheRead),
|
|
125
|
+
cacheWrite: token(message.usage?.cacheWrite), reasoning: token(message.usage?.reasoning), totalTokens: token(message.usage?.totalTokens) },
|
|
126
|
+
responseHeadersMs: missing, fullResponseMs: missing }]);
|
|
127
|
+
invalidate();
|
|
128
|
+
} catch { /* Provider hooks never expose telemetry failures. */ }
|
|
129
|
+
});
|
|
130
|
+
}
|
package/extensions/sdd-init.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "node:fs";
|
|
8
8
|
import { basename, dirname, join, relative } from "node:path";
|
|
9
9
|
import { applySavedModelConfig } from "./gentle-ai.ts";
|
|
10
|
-
import { ensureSddPreflight,
|
|
10
|
+
import { ensureSddPreflight, installPackageAssets } from "../lib/sdd-preflight.ts";
|
|
11
11
|
type ExtensionAPI = any;
|
|
12
12
|
|
|
13
13
|
const CONFIG_REL_PATH = "openspec/config.yaml";
|
|
@@ -775,7 +775,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
775
775
|
description:
|
|
776
776
|
"Auto-detect project stack and bootstrap openspec/config.yaml for SDD.",
|
|
777
777
|
handler: async (_args: unknown, ctx: any) => {
|
|
778
|
-
const prefs = await ensureSddPreflight(ctx, { pi, installAssets: (cwd) =>
|
|
778
|
+
const prefs = await ensureSddPreflight(ctx, { pi, installAssets: (cwd) => installPackageAssets(cwd, true, ["sdd"]), applyModelConfig: () => applySavedModelConfig(ctx) }, { promptFields: [] });
|
|
779
779
|
|
|
780
780
|
const detection = detectProject(ctx.cwd);
|
|
781
781
|
const testSummary = detection.testCommand
|
|
@@ -2,12 +2,10 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
4
4
|
import * as os from "node:os";
|
|
5
|
-
import {
|
|
6
|
-
import { promisify } from "node:util";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
7
6
|
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
8
7
|
import { join } from "node:path";
|
|
9
8
|
|
|
10
|
-
const execAsync = promisify(exec);
|
|
11
9
|
const PI_AGENT_DIR = join(os.homedir(), ".pi", "agent");
|
|
12
10
|
const PI_NPM_DIR = join(PI_AGENT_DIR, "npm", "node_modules");
|
|
13
11
|
|
|
@@ -527,7 +525,26 @@ async function countPackageExtensions(packages: unknown[]): Promise<number> {
|
|
|
527
525
|
return count;
|
|
528
526
|
}
|
|
529
527
|
|
|
528
|
+
export function readGitBranch(cwd: string, run: typeof execFile = execFile): Promise<string> {
|
|
529
|
+
return new Promise((resolve) => {
|
|
530
|
+
run("git", ["-C", cwd, "branch", "--show-current"], {
|
|
531
|
+
encoding: "utf8",
|
|
532
|
+
shell: false,
|
|
533
|
+
windowsHide: true,
|
|
534
|
+
}, (error, stdout) => {
|
|
535
|
+
if (error) {
|
|
536
|
+
resolve("Not a git repo");
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const branch = String(stdout).trim();
|
|
540
|
+
resolve(branch ? `On branch ${branch}` : "Detached HEAD");
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
530
545
|
export default function (pi: ExtensionAPI) {
|
|
546
|
+
let disposeHeader = () => {};
|
|
547
|
+
pi.on("session_shutdown", () => disposeHeader());
|
|
531
548
|
const notifyBannerConfig = (ctx: any, config: BannerConfig) => {
|
|
532
549
|
ctx.ui.notify(
|
|
533
550
|
[
|
|
@@ -597,6 +614,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
597
614
|
registerColorCommand("gentle:banner-color");
|
|
598
615
|
|
|
599
616
|
pi.on("session_start", async (_event, ctx) => {
|
|
617
|
+
disposeHeader();
|
|
600
618
|
if (!ctx.hasUI) return;
|
|
601
619
|
|
|
602
620
|
// Si se está ejecutando un comando de CLI como "pi update" o "pi install", no mostramos la intro animada.
|
|
@@ -607,16 +625,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
607
625
|
|
|
608
626
|
if (currentIntroMode() === "skip") return;
|
|
609
627
|
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
void warmupLetterStrokes();
|
|
613
|
-
|
|
614
|
-
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
615
|
-
|
|
628
|
+
// Pi has already started its renderer. Let setHeader schedule the paint;
|
|
629
|
+
// clearing stdout here would leave its previous-frame cache out of sync.
|
|
616
630
|
const bannerConfig = await readBannerConfig();
|
|
617
631
|
const palette = BANNER_PALETTES[bannerConfig.color];
|
|
618
632
|
const roseBase = padLines(normalizeAscii(ROSE_LARGE_RAW));
|
|
619
633
|
const logoBase = padLines(TEXT_LOGO);
|
|
634
|
+
void warmupLetterStrokes();
|
|
620
635
|
|
|
621
636
|
let gitBranch = "Not a git repo";
|
|
622
637
|
let mcpServersCount = 0;
|
|
@@ -632,12 +647,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
632
647
|
);
|
|
633
648
|
|
|
634
649
|
setTimeout(() => {
|
|
635
|
-
|
|
636
|
-
.then(({
|
|
637
|
-
|
|
638
|
-
gitBranch = b ? `On branch ${b}` : "Detached HEAD";
|
|
639
|
-
})
|
|
640
|
-
.catch(() => {});
|
|
650
|
+
readGitBranch(ctx.cwd)
|
|
651
|
+
.then((branch) => { gitBranch = branch; })
|
|
652
|
+
.finally(() => refreshStats());
|
|
641
653
|
}, 100);
|
|
642
654
|
|
|
643
655
|
setTimeout(() => {
|
|
@@ -652,6 +664,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
652
664
|
} catch {
|
|
653
665
|
mcpServersCount = 0;
|
|
654
666
|
}
|
|
667
|
+
refreshStats();
|
|
655
668
|
})();
|
|
656
669
|
}, 150);
|
|
657
670
|
|
|
@@ -671,10 +684,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
671
684
|
extensionsCount = 0;
|
|
672
685
|
packagesCount = 0;
|
|
673
686
|
}
|
|
687
|
+
refreshStats();
|
|
674
688
|
})();
|
|
675
689
|
}, 200);
|
|
676
690
|
|
|
677
691
|
let tick = 0;
|
|
692
|
+
let refreshStats = () => {};
|
|
678
693
|
const state = {
|
|
679
694
|
timer: null as NodeJS.Timeout | null,
|
|
680
695
|
mode: currentIntroMode() as IntroMode,
|
|
@@ -683,6 +698,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
683
698
|
};
|
|
684
699
|
|
|
685
700
|
const cleanup = () => {
|
|
701
|
+
refreshStats = () => {};
|
|
686
702
|
if (state.timer) {
|
|
687
703
|
clearInterval(state.timer);
|
|
688
704
|
state.timer = null;
|
|
@@ -697,27 +713,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
697
713
|
}
|
|
698
714
|
};
|
|
699
715
|
|
|
716
|
+
disposeHeader = cleanup;
|
|
700
717
|
setTimeout(() => {
|
|
701
718
|
ctx.ui.setHeader((tui, theme) => {
|
|
702
719
|
if (state.timer) clearInterval(state.timer);
|
|
703
720
|
|
|
721
|
+
refreshStats = () => tui.requestRender();
|
|
704
722
|
const animStart = Date.now();
|
|
705
|
-
const HARD_TIMEOUT_MS = 5000;
|
|
706
|
-
|
|
707
723
|
state.timer = setInterval(() => {
|
|
708
724
|
tick++;
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
cleanup();
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
try {
|
|
717
|
-
tui.requestRender();
|
|
718
|
-
} catch {
|
|
719
|
-
cleanup();
|
|
725
|
+
const finished = allStrokesReady() && tick > WRITING_END_TICK + 22;
|
|
726
|
+
if (finished || Date.now() - animStart > 5000) {
|
|
727
|
+
clearInterval(state.timer!);
|
|
728
|
+
state.timer = null;
|
|
720
729
|
}
|
|
730
|
+
try { tui.requestRender(); } catch { cleanup(); }
|
|
721
731
|
}, 25);
|
|
722
732
|
|
|
723
733
|
// Grace period: pi-tui emite resizes transitorios mientras compone su layout inicial.
|
|
@@ -730,11 +740,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
730
740
|
const next = currentIntroMode();
|
|
731
741
|
if (next === state.mode) return;
|
|
732
742
|
state.mode = next;
|
|
733
|
-
|
|
734
|
-
cleanup();
|
|
735
|
-
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
743
|
+
// Pi owns removal in skip mode; keep listening for later expansion.
|
|
738
744
|
try {
|
|
739
745
|
tui.requestRender();
|
|
740
746
|
} catch {
|
|
@@ -758,9 +764,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
758
764
|
const frame = Math.floor(tick / 2);
|
|
759
765
|
|
|
760
766
|
const sideBySideMinWidth = roseBase.width + 3 + logoBase.width + 4;
|
|
761
|
-
const wideStatsMinWidth = 122;
|
|
762
767
|
const horizontal =
|
|
763
768
|
state.mode === "full" && bannerConfig.showRose && bannerConfig.showTextLogo && width >= sideBySideMinWidth;
|
|
769
|
+
const wideStatsMinWidth = 122;
|
|
764
770
|
const wideStats = width >= wideStatsMinWidth;
|
|
765
771
|
|
|
766
772
|
const b = new LayoutBuilder();
|
|
@@ -772,52 +778,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
772
778
|
const logoLine = logoBase.lines[logoI];
|
|
773
779
|
b.addRow();
|
|
774
780
|
b.lines[b.lines.length - 1].push(
|
|
775
|
-
...buildPenLogoLine(
|
|
776
|
-
logoLine,
|
|
777
|
-
logoI,
|
|
778
|
-
logoBase.lines.length,
|
|
779
|
-
tick,
|
|
780
|
-
),
|
|
781
|
+
...buildPenLogoLine(logoLine, logoI, logoBase.lines.length, tick),
|
|
781
782
|
);
|
|
782
783
|
b.center(width);
|
|
783
784
|
}
|
|
784
785
|
} else if (horizontal) {
|
|
785
|
-
const rowCount = Math.max(
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
);
|
|
789
|
-
const roseOffset = Math.max(
|
|
790
|
-
0,
|
|
791
|
-
Math.floor((rowCount - roseBase.lines.length) / 2),
|
|
792
|
-
);
|
|
793
|
-
const logoOffset = Math.max(
|
|
794
|
-
0,
|
|
795
|
-
Math.floor((rowCount - logoBase.lines.length) / 2),
|
|
796
|
-
);
|
|
797
|
-
|
|
786
|
+
const rowCount = Math.max(roseBase.lines.length, logoBase.lines.length);
|
|
787
|
+
const roseOffset = Math.max(0, Math.floor((rowCount - roseBase.lines.length) / 2));
|
|
788
|
+
const logoOffset = Math.max(0, Math.floor((rowCount - logoBase.lines.length) / 2));
|
|
798
789
|
for (let i = 0; i < rowCount; i++) {
|
|
799
790
|
const roseI = i - roseOffset;
|
|
800
791
|
const logoI = i - logoOffset;
|
|
801
|
-
const roseLine =
|
|
802
|
-
roseI
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
const logoLine =
|
|
806
|
-
logoI >= 0 && logoI < logoBase.lines.length
|
|
807
|
-
? logoBase.lines[logoI]
|
|
808
|
-
: " ".repeat(logoBase.width);
|
|
809
|
-
|
|
792
|
+
const roseLine = roseI >= 0 && roseI < roseBase.lines.length
|
|
793
|
+
? roseBase.lines[roseI] : " ".repeat(roseBase.width);
|
|
794
|
+
const logoLine = logoI >= 0 && logoI < logoBase.lines.length
|
|
795
|
+
? logoBase.lines[logoI] : " ".repeat(logoBase.width);
|
|
810
796
|
b.addRow();
|
|
811
797
|
b.add("rose", roseLine);
|
|
812
798
|
b.add("none", " ");
|
|
813
799
|
if (logoI >= 0 && logoI < logoBase.lines.length) {
|
|
814
800
|
b.lines[b.lines.length - 1].push(
|
|
815
|
-
...buildPenLogoLine(
|
|
816
|
-
logoLine,
|
|
817
|
-
logoI,
|
|
818
|
-
logoBase.lines.length,
|
|
819
|
-
tick,
|
|
820
|
-
),
|
|
801
|
+
...buildPenLogoLine(logoLine, logoI, logoBase.lines.length, tick),
|
|
821
802
|
);
|
|
822
803
|
} else {
|
|
823
804
|
b.add("none", " ".repeat(logoBase.width));
|
|
@@ -832,12 +813,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
832
813
|
const logoLine = logoBase.lines[logoI];
|
|
833
814
|
b.addRow();
|
|
834
815
|
b.lines[b.lines.length - 1].push(
|
|
835
|
-
...buildPenLogoLine(
|
|
836
|
-
logoLine,
|
|
837
|
-
logoI,
|
|
838
|
-
logoBase.lines.length,
|
|
839
|
-
tick,
|
|
840
|
-
),
|
|
816
|
+
...buildPenLogoLine(logoLine, logoI, logoBase.lines.length, tick),
|
|
841
817
|
);
|
|
842
818
|
b.center(width);
|
|
843
819
|
}
|
|
@@ -1075,7 +1051,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1075
1051
|
|
|
1076
1052
|
return out;
|
|
1077
1053
|
},
|
|
1078
|
-
invalidate() {
|
|
1054
|
+
invalidate() {},
|
|
1055
|
+
dispose() {
|
|
1079
1056
|
cleanup();
|
|
1080
1057
|
},
|
|
1081
1058
|
};
|