pi-ui-extend 0.1.1 → 0.1.3

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.
@@ -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
- ctx.ui.notify(lines.join("\n"), "info")
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: "emitted",
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
- annotateMessagesWithBranchEntryIds(event.messages, ctx)
266
- let prunedMessages = applyPruning(event.messages, state, config)
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(nudgeType, anchorResult.anchor, usage, toolCallsSinceLastUser)
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.1",
3
+ "version": "0.1.3",
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": ">=24 <25"
73
+ "node": ">=22.19.0 <25"
74
74
  },
75
75
  "overrides": {
76
76
  "get-uv-event-loop-napi-h": "1.0.6",