pi-ui-extend 0.1.1 → 0.1.2
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/bin/pix.mjs
CHANGED
|
@@ -4,8 +4,8 @@ import { existsSync, readdirSync, statSync } from "node:fs";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
const
|
|
7
|
+
const minimumNodeVersion = [22, 19, 0];
|
|
8
|
+
const minimumNodeVersionLabel = "22.19.0";
|
|
9
9
|
const mainPath = fileURLToPath(new URL("../dist/main.js", import.meta.url));
|
|
10
10
|
const updatePath = fileURLToPath(new URL("../dist/app/update.js", import.meta.url));
|
|
11
11
|
const distPath = dirname(mainPath);
|
|
@@ -13,8 +13,10 @@ const rawArgs = process.argv.slice(2);
|
|
|
13
13
|
const childArgs = [];
|
|
14
14
|
let reloadOnBuild = truthyEnv(process.env.PIX_RELOAD_ON_BUILD);
|
|
15
15
|
|
|
16
|
-
if (
|
|
17
|
-
|
|
16
|
+
if (!isCurrentNodeSupported()) {
|
|
17
|
+
console.error(`[pix] Node ${minimumNodeVersionLabel}+ is required; current Node is ${process.versions.node}.`);
|
|
18
|
+
console.error("[pix] Install/use a newer Node, for example `mise install node@22.19.0` or `nvm install 22`.");
|
|
19
|
+
process.exit(1);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
for (const arg of rawArgs) {
|
|
@@ -66,52 +68,15 @@ function truthyEnv(value) {
|
|
|
66
68
|
return !["0", "false", "no", "off"].includes(value.toLowerCase());
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
for (const candidate of candidates) {
|
|
78
|
-
const result = await runNode24Candidate(candidate.command, candidate.args);
|
|
79
|
-
if (!result.launched) continue;
|
|
80
|
-
if (result.signal) process.exitCode = result.signal === "SIGINT" ? 130 : result.signal === "SIGTERM" ? 143 : 1;
|
|
81
|
-
else process.exitCode = result.code ?? 1;
|
|
82
|
-
process.exit();
|
|
71
|
+
function isCurrentNodeSupported() {
|
|
72
|
+
const parts = process.versions.node.split(".").map((part) => Number.parseInt(part, 10));
|
|
73
|
+
for (let index = 0; index < minimumNodeVersion.length; index += 1) {
|
|
74
|
+
const current = parts[index] ?? 0;
|
|
75
|
+
const minimum = minimumNodeVersion[index];
|
|
76
|
+
if (current > minimum) return true;
|
|
77
|
+
if (current < minimum) return false;
|
|
83
78
|
}
|
|
84
|
-
|
|
85
|
-
console.error("[pix] Node 24 is required. Install/use it with `mise install node@24.16.0` or set PIX_NODE24=/path/to/node24.");
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function node24Candidates(args) {
|
|
90
|
-
const envNode = process.env.PIX_NODE24;
|
|
91
|
-
return [
|
|
92
|
-
...(envNode ? [{ command: envNode, args: [launcherPath, ...args] }] : []),
|
|
93
|
-
{ command: "mise", args: ["exec", "node@24.16.0", "--", "node", launcherPath, ...args] },
|
|
94
|
-
{ command: "node24", args: [launcherPath, ...args] },
|
|
95
|
-
{ command: "node-24", args: [launcherPath, ...args] },
|
|
96
|
-
];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function runNode24Candidate(command, args) {
|
|
100
|
-
return await new Promise((resolve) => {
|
|
101
|
-
const child = spawn(command, args, {
|
|
102
|
-
stdio: "inherit",
|
|
103
|
-
env: { ...process.env, PIX_NODE24_REEXEC: "1" },
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
child.once("error", (error) => {
|
|
107
|
-
if (error && error.code === "ENOENT") resolve({ launched: false });
|
|
108
|
-
else {
|
|
109
|
-
console.error(error?.message ?? String(error));
|
|
110
|
-
resolve({ launched: true, code: 1 });
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
child.once("exit", (code, signal) => resolve({ launched: true, code, signal }));
|
|
114
|
-
});
|
|
79
|
+
return true;
|
|
115
80
|
}
|
|
116
81
|
|
|
117
82
|
function startChild() {
|
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-cod
|
|
|
2
2
|
import type { AutocompleteItem } from "@mariozechner/pi-tui"
|
|
3
3
|
import type { DcpState } from "./state.js"
|
|
4
4
|
import type { DcpConfig } from "./config.js"
|
|
5
|
+
import type { DcpNudgeType } from "./pruner-types.js"
|
|
5
6
|
import { isToolRecordProtected, markToolPruned } from "./pruner.js"
|
|
6
7
|
import { safeGetContextUsage } from "../context-usage.js"
|
|
7
8
|
|
|
@@ -11,6 +12,8 @@ import { safeGetContextUsage } from "../context-usage.js"
|
|
|
11
12
|
|
|
12
13
|
/** Tools whose outputs are always protected from sweep regardless of config. */
|
|
13
14
|
const ALWAYS_PROTECTED_TOOLS = ["compress", "write", "edit"] as const
|
|
15
|
+
export const DCP_STATS_MESSAGE_TYPE = "pix-system"
|
|
16
|
+
const DCP_STATS_DETAILS_KIND = "dcp-stats"
|
|
14
17
|
|
|
15
18
|
export interface DcpCommandHooks {
|
|
16
19
|
onStateChanged?: (ctx: ExtensionCommandContext) => void
|
|
@@ -24,6 +27,130 @@ function fmt(n: number): string {
|
|
|
24
27
|
return n.toLocaleString()
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
const NUDGE_TYPES: DcpNudgeType[] = ["turn", "iteration", "context-soft", "context-strong"]
|
|
31
|
+
|
|
32
|
+
function pct(numerator: number, denominator: number): string {
|
|
33
|
+
if (denominator <= 0) return "n/a"
|
|
34
|
+
return `${((numerator / denominator) * 100).toFixed(1)}%`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function nudgeLabel(type: DcpNudgeType): string {
|
|
38
|
+
switch (type) {
|
|
39
|
+
case "context-strong": return "context-strong"
|
|
40
|
+
case "context-soft": return "context-soft"
|
|
41
|
+
case "iteration": return "iteration"
|
|
42
|
+
case "turn": return "turn"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isNudgeType(value: unknown): value is DcpNudgeType {
|
|
47
|
+
return typeof value === "string" && (NUDGE_TYPES as string[]).includes(value)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function customEntryData(entry: unknown, customType: string): Record<string, unknown> | undefined {
|
|
51
|
+
const record = entry as { type?: unknown; customType?: unknown; data?: unknown }
|
|
52
|
+
if (record?.type !== "custom" || record.customType !== customType) return undefined
|
|
53
|
+
if (!record.data || typeof record.data !== "object" || Array.isArray(record.data)) return undefined
|
|
54
|
+
return record.data as Record<string, unknown>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function branchEntries(ctx: ExtensionCommandContext): unknown[] {
|
|
58
|
+
try {
|
|
59
|
+
const branch = ctx.sessionManager?.getBranch?.()
|
|
60
|
+
return Array.isArray(branch) ? branch : []
|
|
61
|
+
} catch {
|
|
62
|
+
return []
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface DcpNudgeStats {
|
|
67
|
+
emitted: number
|
|
68
|
+
upgraded: number
|
|
69
|
+
clearedEvents: number
|
|
70
|
+
clearedAnchors: number
|
|
71
|
+
byType: Record<DcpNudgeType, number>
|
|
72
|
+
activeByType: Record<DcpNudgeType, number>
|
|
73
|
+
last?: {
|
|
74
|
+
type: DcpNudgeType
|
|
75
|
+
event: "emitted" | "upgraded"
|
|
76
|
+
createdAt?: number
|
|
77
|
+
contextPercent?: number | null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function collectNudgeStats(ctx: ExtensionCommandContext, state: DcpState): DcpNudgeStats {
|
|
82
|
+
const stats: DcpNudgeStats = {
|
|
83
|
+
emitted: 0,
|
|
84
|
+
upgraded: 0,
|
|
85
|
+
clearedEvents: 0,
|
|
86
|
+
clearedAnchors: 0,
|
|
87
|
+
byType: { "turn": 0, "iteration": 0, "context-soft": 0, "context-strong": 0 },
|
|
88
|
+
activeByType: { "turn": 0, "iteration": 0, "context-soft": 0, "context-strong": 0 },
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const anchor of state.nudgeAnchors) {
|
|
92
|
+
if (isNudgeType(anchor.type)) stats.activeByType[anchor.type]++
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const entry of branchEntries(ctx)) {
|
|
96
|
+
const data = customEntryData(entry, "dcp-nudge")
|
|
97
|
+
if (!data) continue
|
|
98
|
+
const event = data.event
|
|
99
|
+
if ((event === "emitted" || event === "upgraded") && isNudgeType(data.type)) {
|
|
100
|
+
if (event === "emitted") stats.emitted++
|
|
101
|
+
else stats.upgraded++
|
|
102
|
+
stats.byType[data.type]++
|
|
103
|
+
const createdAt = typeof data.createdAt === "number" ? data.createdAt : undefined
|
|
104
|
+
const contextPercent = typeof data.contextPercent === "number" || data.contextPercent === null
|
|
105
|
+
? data.contextPercent
|
|
106
|
+
: undefined
|
|
107
|
+
if (!stats.last || (createdAt ?? 0) >= (stats.last.createdAt ?? 0)) {
|
|
108
|
+
stats.last = { type: data.type, event, createdAt, contextPercent }
|
|
109
|
+
}
|
|
110
|
+
} else if (event === "cleared") {
|
|
111
|
+
stats.clearedEvents++
|
|
112
|
+
stats.clearedAnchors += typeof data.clearedAnchors === "number" ? Math.max(0, data.clearedAnchors) : 0
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!stats.last && state.lastNudge && isNudgeType(state.lastNudge.type)) {
|
|
117
|
+
stats.last = {
|
|
118
|
+
type: state.lastNudge.type,
|
|
119
|
+
event: "emitted",
|
|
120
|
+
createdAt: state.lastNudge.createdAt,
|
|
121
|
+
contextPercent: typeof state.lastNudge.contextPercent === "number"
|
|
122
|
+
? state.lastNudge.contextPercent * 100
|
|
123
|
+
: undefined,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return stats
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function formatDate(ts: number | undefined): string {
|
|
131
|
+
if (typeof ts !== "number" || !Number.isFinite(ts) || ts <= 0) return "unknown time"
|
|
132
|
+
return new Date(ts).toLocaleString()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatContextPercent(value: number | null | undefined): string {
|
|
136
|
+
if (value === null) return "unknown context"
|
|
137
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "unknown context"
|
|
138
|
+
return `${value.toFixed(1)}% context`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function sendChatSystemMessage(pi: ExtensionAPI, customType: string, content: string, details?: Record<string, unknown>): void {
|
|
142
|
+
pi.sendMessage({
|
|
143
|
+
customType,
|
|
144
|
+
content,
|
|
145
|
+
display: true,
|
|
146
|
+
details: {
|
|
147
|
+
kind: DCP_STATS_DETAILS_KIND,
|
|
148
|
+
userVisibleOnly: true,
|
|
149
|
+
...(details ?? {}),
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
27
154
|
// ---------------------------------------------------------------------------
|
|
28
155
|
// Help
|
|
29
156
|
// ---------------------------------------------------------------------------
|
|
@@ -83,17 +210,44 @@ function handleContext(ctx: ExtensionCommandContext, state: DcpState): void {
|
|
|
83
210
|
// Stats
|
|
84
211
|
// ---------------------------------------------------------------------------
|
|
85
212
|
|
|
86
|
-
function handleStats(ctx: ExtensionCommandContext, state: DcpState): void {
|
|
213
|
+
function handleStats(pi: ExtensionAPI, ctx: ExtensionCommandContext, state: DcpState): void {
|
|
87
214
|
const activeBlocks = state.compressionBlocks.filter((b) => b.active).length
|
|
88
215
|
const totalBlocks = state.compressionBlocks.length
|
|
216
|
+
const nudgeStats = collectNudgeStats(ctx, state)
|
|
217
|
+
const totalNudgeEvents = nudgeStats.emitted + nudgeStats.upgraded
|
|
218
|
+
const activeAnchors = state.nudgeAnchors.length
|
|
89
219
|
const lines: string[] = []
|
|
90
220
|
lines.push("DCP Session Statistics:")
|
|
91
221
|
lines.push(` Tokens saved (estimated): ${fmt(state.tokensSaved)}`)
|
|
92
222
|
lines.push(` Total pruning operations: ${fmt(state.totalPruneCount)}`)
|
|
93
223
|
lines.push(` Compression blocks active: ${activeBlocks} / ${totalBlocks} total`)
|
|
94
224
|
lines.push(` Manual mode: ${state.manualMode ? "on" : "off"}`)
|
|
225
|
+
lines.push("")
|
|
226
|
+
lines.push("Nudge telemetry:")
|
|
227
|
+
lines.push(` Sent: ${fmt(nudgeStats.emitted)} emitted, ${fmt(nudgeStats.upgraded)} upgraded`)
|
|
228
|
+
lines.push(
|
|
229
|
+
` By type: ${NUDGE_TYPES.map((type) => `${nudgeLabel(type)}=${fmt(nudgeStats.byType[type])}`).join(", ")}`,
|
|
230
|
+
)
|
|
231
|
+
lines.push(
|
|
232
|
+
` Active anchors: ${fmt(activeAnchors)}${activeAnchors > 0
|
|
233
|
+
? ` (${NUDGE_TYPES.map((type) => `${nudgeLabel(type)}=${fmt(nudgeStats.activeByType[type])}`).join(", ")})`
|
|
234
|
+
: ""}`,
|
|
235
|
+
)
|
|
236
|
+
lines.push(` Cleared after compress: ${fmt(nudgeStats.clearedEvents)} time${nudgeStats.clearedEvents === 1 ? "" : "s"} (${fmt(nudgeStats.clearedAnchors)} anchor${nudgeStats.clearedAnchors === 1 ? "" : "s"})`)
|
|
237
|
+
lines.push(` Compliance proxy: ${fmt(nudgeStats.clearedEvents)} compress-after-nudge / ${fmt(totalNudgeEvents)} nudge event${totalNudgeEvents === 1 ? "" : "s"} (${pct(nudgeStats.clearedEvents, totalNudgeEvents)})`)
|
|
238
|
+
if (nudgeStats.last) {
|
|
239
|
+
lines.push(
|
|
240
|
+
` Last nudge: ${nudgeLabel(nudgeStats.last.type)} ${nudgeStats.last.event} at ${formatDate(nudgeStats.last.createdAt)} (${formatContextPercent(nudgeStats.last.contextPercent)})`,
|
|
241
|
+
)
|
|
242
|
+
} else {
|
|
243
|
+
lines.push(" Last nudge: none recorded")
|
|
244
|
+
}
|
|
95
245
|
|
|
96
|
-
|
|
246
|
+
sendChatSystemMessage(pi, DCP_STATS_MESSAGE_TYPE, lines.join("\n"), {
|
|
247
|
+
generatedAt: new Date().toISOString(),
|
|
248
|
+
activeAnchors,
|
|
249
|
+
nudgeEvents: totalNudgeEvents,
|
|
250
|
+
})
|
|
97
251
|
}
|
|
98
252
|
|
|
99
253
|
// ---------------------------------------------------------------------------
|
|
@@ -399,7 +553,7 @@ export function registerCommands(
|
|
|
399
553
|
break
|
|
400
554
|
|
|
401
555
|
case "stats":
|
|
402
|
-
handleStats(ctx, state)
|
|
556
|
+
handleStats(pi, ctx, state)
|
|
403
557
|
break
|
|
404
558
|
|
|
405
559
|
case "sweep": {
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
} from "./pruner.js"
|
|
37
37
|
import type { DcpNudgeType } from "./pruner-types.js"
|
|
38
38
|
import { registerCompressTool } from "./compress-tool.js"
|
|
39
|
-
import { registerCommands } from "./commands.js"
|
|
39
|
+
import { DCP_STATS_MESSAGE_TYPE, registerCommands } from "./commands.js"
|
|
40
40
|
import { DcpUiController, normalizeDcpContextUsage } from "./ui.js"
|
|
41
41
|
import { registerTuiFilter } from "./dcp-tui-filter.js"
|
|
42
42
|
import { ignoreStaleExtensionContextError, safeGetContextUsage } from "../context-usage.js"
|
|
@@ -89,6 +89,12 @@ function baseNudgeText(type: DcpNudgeType): string {
|
|
|
89
89
|
return TURN_NUDGE
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
function isUserVisibleOnlyMessage(message: any): boolean {
|
|
93
|
+
if (message?.role !== "custom") return false
|
|
94
|
+
if (message.customType !== DCP_STATS_MESSAGE_TYPE) return false
|
|
95
|
+
return message.details?.userVisibleOnly === true
|
|
96
|
+
}
|
|
97
|
+
|
|
92
98
|
// ---------------------------------------------------------------------------
|
|
93
99
|
// Module export
|
|
94
100
|
// ---------------------------------------------------------------------------
|
|
@@ -121,6 +127,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
121
127
|
}
|
|
122
128
|
}
|
|
123
129
|
const appendNudgeTelemetry = (
|
|
130
|
+
event: "emitted" | "upgraded",
|
|
124
131
|
type: DcpNudgeType,
|
|
125
132
|
anchor: { id: number; anchorTimestamp: number; anchorStableId?: string; anchorRole: string },
|
|
126
133
|
usage: ReturnType<typeof normalizeDcpContextUsage>,
|
|
@@ -128,7 +135,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
128
135
|
): void => {
|
|
129
136
|
try {
|
|
130
137
|
pi.appendEntry("dcp-nudge", {
|
|
131
|
-
event
|
|
138
|
+
event,
|
|
132
139
|
type,
|
|
133
140
|
label: nudgeTypeLabel(type),
|
|
134
141
|
anchorId: anchor.id,
|
|
@@ -262,8 +269,9 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
262
269
|
|
|
263
270
|
// ── 11. context: apply pruning and inject nudges ──────────────────────────
|
|
264
271
|
pi.on("context", async (event, ctx) => {
|
|
265
|
-
|
|
266
|
-
|
|
272
|
+
const contextMessages = event.messages.filter((message: any) => !isUserVisibleOnlyMessage(message))
|
|
273
|
+
annotateMessagesWithBranchEntryIds(contextMessages, ctx)
|
|
274
|
+
let prunedMessages = applyPruning(contextMessages, state, config)
|
|
267
275
|
let candidate = null as ReturnType<typeof detectCompressionCandidate>
|
|
268
276
|
let messageCandidates = [] as ReturnType<typeof detectMessageCompressionCandidates>
|
|
269
277
|
|
|
@@ -350,7 +358,13 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
350
358
|
)
|
|
351
359
|
if (anchorResult.anchor) {
|
|
352
360
|
if (anchorResult.updated) {
|
|
353
|
-
appendNudgeTelemetry(
|
|
361
|
+
appendNudgeTelemetry(
|
|
362
|
+
anchorResult.created ? "emitted" : "upgraded",
|
|
363
|
+
nudgeType,
|
|
364
|
+
anchorResult.anchor,
|
|
365
|
+
usage,
|
|
366
|
+
toolCallsSinceLastUser,
|
|
367
|
+
)
|
|
354
368
|
saveState(pi, state)
|
|
355
369
|
}
|
|
356
370
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ui-extend",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"typescript": "5.9.3"
|
|
71
71
|
},
|
|
72
72
|
"engines": {
|
|
73
|
-
"node": ">=
|
|
73
|
+
"node": ">=22.19.0 <25"
|
|
74
74
|
},
|
|
75
75
|
"overrides": {
|
|
76
76
|
"get-uv-event-loop-napi-h": "1.0.6",
|