pi-ui-extend 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/app/app.d.ts +0 -1
  2. package/dist/app/app.js +1 -4
  3. package/dist/app/conversation-entry-renderer.d.ts +0 -1
  4. package/dist/app/conversation-entry-renderer.js +2 -6
  5. package/dist/app/conversation-tool-renderer.js +2 -3
  6. package/dist/app/conversation-viewport.d.ts +0 -1
  7. package/dist/app/conversation-viewport.js +0 -1
  8. package/dist/config.d.ts +0 -3
  9. package/dist/config.js +0 -79
  10. package/dist/default-pix-config.js +2 -2
  11. package/dist/markdown-format.js +18 -1
  12. package/external/pi-tools-suite/README.md +4 -4
  13. package/external/pi-tools-suite/licenses/opencode-dynamic-context-pruning-AGPL-3.0.txt +619 -0
  14. package/external/pi-tools-suite/package.json +1 -1
  15. package/external/pi-tools-suite/src/config.ts +5 -1
  16. package/external/pi-tools-suite/src/{compress → dcp}/config.ts +10 -70
  17. package/external/pi-tools-suite/src/{compress → dcp}/index.ts +16 -66
  18. package/external/pi-tools-suite/src/dcp/ui.ts +45 -0
  19. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -2
  20. package/external/pi-tools-suite/src/index.ts +1 -1
  21. package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
  22. package/package.json +1 -1
  23. package/external/pi-tools-suite/src/compress/dcp-tui-filter.ts +0 -498
  24. package/external/pi-tools-suite/src/compress/ui.ts +0 -308
  25. /package/external/pi-tools-suite/src/{compress → dcp}/commands.ts +0 -0
  26. /package/external/pi-tools-suite/src/{compress → dcp}/compress-tool.ts +0 -0
  27. /package/external/pi-tools-suite/src/{compress → dcp}/compression-blocks.ts +0 -0
  28. /package/external/pi-tools-suite/src/{compress → dcp}/prompts.ts +0 -0
  29. /package/external/pi-tools-suite/src/{compress → dcp}/pruner-candidates.ts +0 -0
  30. /package/external/pi-tools-suite/src/{compress → dcp}/pruner-compression-blocks.ts +0 -0
  31. /package/external/pi-tools-suite/src/{compress → dcp}/pruner-message-ids.ts +0 -0
  32. /package/external/pi-tools-suite/src/{compress → dcp}/pruner-metadata.ts +0 -0
  33. /package/external/pi-tools-suite/src/{compress → dcp}/pruner-nudge.ts +0 -0
  34. /package/external/pi-tools-suite/src/{compress → dcp}/pruner-tools.ts +0 -0
  35. /package/external/pi-tools-suite/src/{compress → dcp}/pruner-types.ts +0 -0
  36. /package/external/pi-tools-suite/src/{compress → dcp}/pruner.ts +0 -0
  37. /package/external/pi-tools-suite/src/{compress → dcp}/state.ts +0 -0
@@ -220,48 +220,6 @@ function readDcpFromSuiteConfig(filePath: string): Record<string, unknown> {
220
220
  return dcp as Record<string, unknown>
221
221
  }
222
222
 
223
- /**
224
- * Walk up from `startDir` looking for `.pi/dcp.jsonc`.
225
- * Returns the path if found, otherwise null.
226
- */
227
- function findProjectConfig(startDir: string): string | null {
228
- let dir = path.resolve(startDir)
229
- const root = path.parse(dir).root
230
-
231
- while (true) {
232
- const candidate = path.join(dir, ".pi", "dcp.jsonc")
233
- if (fs.existsSync(candidate)) return candidate
234
- if (dir === root) return null
235
- const parent = path.dirname(dir)
236
- if (parent === dir) return null
237
- dir = parent
238
- }
239
- }
240
-
241
- /**
242
- * Walk up from `startDir` looking for `.pi/pi-tools-suite.jsonc`.
243
- * Returns the path if found, otherwise null.
244
- */
245
- function findProjectSuiteConfig(startDir: string): string | null {
246
- let dir = path.resolve(startDir)
247
- const root = path.parse(dir).root
248
-
249
- while (true) {
250
- const candidate = path.join(dir, ".pi", "pi-tools-suite.jsonc")
251
- if (fs.existsSync(candidate)) return candidate
252
- if (dir === root) return null
253
- const parent = path.dirname(dir)
254
- if (parent === dir) return null
255
- dir = parent
256
- }
257
- }
258
-
259
- function mergeConfigFile(config: DcpConfig, filePath: string): DcpConfig {
260
- const raw = readJsoncFile(filePath)
261
- if (Object.keys(raw).length === 0) return config
262
- return deepMerge(config, raw as Partial<DcpConfig>)
263
- }
264
-
265
223
  function mergeSuiteDcpConfig(config: DcpConfig, filePath: string): DcpConfig {
266
224
  const raw = readDcpFromSuiteConfig(filePath)
267
225
  if (Object.keys(raw).length === 0) return config
@@ -272,39 +230,21 @@ function mergeSuiteDcpConfig(config: DcpConfig, filePath: string): DcpConfig {
272
230
  // Public API
273
231
  // ---------------------------------------------------------------------------
274
232
 
233
+ export interface LoadConfigOptions {
234
+ homeDir?: string
235
+ }
236
+
275
237
  /**
276
- * Load the DCP configuration by merging (in order):
277
- * 1. Built-in defaults
278
- * 2. legacy ~/.config/pi/dcp.jsonc, then dcp in ~/.config/pi/pi-tools-suite.jsonc
279
- * 3. legacy $PI_CONFIG_DIR/dcp.jsonc, then dcp in $PI_CONFIG_DIR/pi-tools-suite.jsonc
280
- * 4. legacy <project>/.pi/dcp.jsonc, then dcp in nearest <project>/.pi/pi-tools-suite.jsonc
281
- *
282
- * The shared pi-tools-suite config wins over legacy dcp.jsonc at each layer so
283
- * DCP settings can live beside the rest of the suite config while preserving
284
- * backwards compatibility with existing standalone files.
238
+ * Load the DCP configuration by merging built-in defaults with the `dcp` section
239
+ * from the user-level shared pi-tools-suite config only:
240
+ * `~/.config/pi/pi-tools-suite.jsonc`.
285
241
  */
286
- export function loadConfig(projectDir: string): DcpConfig {
242
+ export function loadConfig(options: LoadConfigOptions = {}): DcpConfig {
287
243
  // Layer 1: defaults (deep clone so we never mutate the constant)
288
244
  let config: DcpConfig = deepMerge(DEFAULT_CONFIG, {})
289
245
 
290
- // Layer 2: global config
291
- const globalConfigPath = path.join(os.homedir(), ".config", "pi", "dcp.jsonc")
292
- config = mergeConfigFile(config, globalConfigPath)
293
- config = mergeSuiteDcpConfig(config, path.join(os.homedir(), ".config", "pi", "pi-tools-suite.jsonc"))
294
-
295
- // Layer 3: $PI_CONFIG_DIR/dcp.jsonc
296
- const piConfigDir = process.env["PI_CONFIG_DIR"]
297
- if (piConfigDir) {
298
- config = mergeConfigFile(config, path.join(piConfigDir, "dcp.jsonc"))
299
- config = mergeSuiteDcpConfig(config, path.join(piConfigDir, "pi-tools-suite.jsonc"))
300
- }
301
-
302
- // Layer 4: project-local config (walk up from projectDir)
303
- const projectConfigPath = findProjectConfig(projectDir)
304
- if (projectConfigPath) config = mergeConfigFile(config, projectConfigPath)
305
-
306
- const projectSuiteConfigPath = findProjectSuiteConfig(projectDir)
307
- if (projectSuiteConfigPath) config = mergeSuiteDcpConfig(config, projectSuiteConfigPath)
246
+ const homeDir = options.homeDir ?? os.homedir()
247
+ config = mergeSuiteDcpConfig(config, path.join(homeDir, ".config", "pi", "pi-tools-suite.jsonc"))
308
248
 
309
249
  return config
310
250
  }
@@ -37,11 +37,8 @@ import {
37
37
  import type { DcpNudgeType } from "./pruner-types.js"
38
38
  import { registerCompressTool } from "./compress-tool.js"
39
39
  import { DCP_STATS_MESSAGE_TYPE, registerCommands } from "./commands.js"
40
- import { DcpUiController, normalizeDcpContextUsage } from "./ui.js"
41
- import { registerTuiFilter } from "./dcp-tui-filter.js"
42
- import { ignoreStaleExtensionContextError, safeGetContextUsage } from "../context-usage.js"
43
-
44
- const DCP_CONTEXT_USAGE_EVENT = "pi-tools-suite:dcp-context-usage"
40
+ import { normalizeDcpContextUsage } from "./ui.js"
41
+ import { safeGetContextUsage } from "../context-usage.js"
45
42
 
46
43
  // ---------------------------------------------------------------------------
47
44
  // Helpers
@@ -101,31 +98,12 @@ function isUserVisibleOnlyMessage(message: any): boolean {
101
98
 
102
99
  export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
103
100
  // ── 1. Load config ────────────────────────────────────────────────────────
104
- const config = loadConfig(process.cwd())
101
+ const config = loadConfig()
105
102
 
106
103
  if (!config.enabled) return
107
104
 
108
105
  // ── 2. Create state ───────────────────────────────────────────────────────
109
106
  const state = createState()
110
- const ui = new DcpUiController(state)
111
- const updateUi = (ctx: ExtensionContext): void => {
112
- try {
113
- if (!ctx?.hasUI) return
114
- ui.setUICtx(ctx.ui)
115
- ui.update(ctx)
116
- } catch (error) {
117
- ignoreStaleExtensionContextError(error)
118
- }
119
- }
120
- const emitContextUsage = (ctx: ExtensionContext): void => {
121
- const usage = safeGetContextUsage(ctx)
122
- if (!usage) return
123
- try {
124
- ;(pi as { events?: { emit?: (name: string, data: unknown) => void } }).events?.emit?.(DCP_CONTEXT_USAGE_EVENT, usage)
125
- } catch (error) {
126
- ignoreStaleExtensionContextError(error)
127
- }
128
- }
129
107
  const appendNudgeTelemetry = (
130
108
  event: "emitted" | "upgraded",
131
109
  type: DcpNudgeType,
@@ -158,20 +136,13 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
158
136
  state.manualMode = true
159
137
  }
160
138
 
161
- // ── 3. Register TUI filter (strip DCP tags from displayed messages) ───────
162
- registerTuiFilter(pi)
163
-
164
- // ── 4. Register compress tool ─────────────────────────────────────────────
139
+ // ── 3. Register compress tool ─────────────────────────────────────────────
165
140
  registerCompressTool(pi, state, config)
166
141
 
167
- // ── 5. Register /dcp commands ─────────────────────────────────────────────
168
- registerCommands(pi, state, config, {
169
- onStateChanged(ctx) {
170
- updateUi(ctx)
171
- },
172
- })
142
+ // ── 4. Register /dcp commands ─────────────────────────────────────────────
143
+ registerCommands(pi, state, config)
173
144
 
174
- // ── 6. session_start: restore state from session entries ──────────────────
145
+ // ── 5. session_start: restore state from session entries ──────────────────
175
146
  pi.on("session_start", async (_event, ctx) => {
176
147
  // Reset to a clean slate first.
177
148
  resetState(state)
@@ -191,17 +162,15 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
191
162
 
192
163
  restoreState(state, latestDcpState)
193
164
 
194
- // Show a rich status indicator + floating cleaned-context widget in the pi TUI.
195
- updateUi(ctx)
165
+ // Headless by design: no extension status/footer/widgets are rendered.
196
166
  })
197
167
 
198
- // ── 7. session_shutdown: save state ───────────────────────────────────────
168
+ // ── 6. session_shutdown: save state ───────────────────────────────────────
199
169
  pi.on("session_shutdown", async (_event, _ctx) => {
200
170
  saveState(pi, state)
201
- ui.dispose()
202
171
  })
203
172
 
204
- // ── 8. before_agent_start: inject system prompt ───────────────────────────
173
+ // ── 7. before_agent_start: inject system prompt ───────────────────────────
205
174
  pi.on("before_agent_start", async (event, _ctx) => {
206
175
  const promptAddition = state.manualMode
207
176
  ? MANUAL_MODE_SYSTEM_PROMPT
@@ -212,7 +181,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
212
181
  }
213
182
  })
214
183
 
215
- // ── 9. tool_call: record input args for dedup / purge fingerprinting ───────
184
+ // ── 8. tool_call: record input args for dedup / purge fingerprinting ───────
216
185
  pi.on("tool_call", async (event, _ctx) => {
217
186
  if (!state.toolCalls.has(event.toolCallId)) {
218
187
  state.toolCalls.set(event.toolCallId, {
@@ -231,8 +200,8 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
231
200
  }
232
201
  })
233
202
 
234
- // ── 10. tool_result: finalise tool record with result info ─────────────────
235
- pi.on("tool_result", async (event, ctx) => {
203
+ // ── 9. tool_result: finalise tool record with result info ─────────────────
204
+ pi.on("tool_result", async (event, _ctx) => {
236
205
  const record = state.toolCalls.get(event.toolCallId)
237
206
 
238
207
  const outputText = event.content
@@ -261,13 +230,9 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
261
230
  })
262
231
  }
263
232
 
264
- if (event.toolName === "compress" && ctx.hasUI) {
265
- updateUi(ctx)
266
- emitContextUsage(ctx)
267
- }
268
233
  })
269
234
 
270
- // ── 11. context: apply pruning and inject nudges ──────────────────────────
235
+ // ── 10. context: apply pruning and inject nudges ──────────────────────────
271
236
  pi.on("context", async (event, ctx) => {
272
237
  const contextMessages = event.messages.filter((message: any) => !isUserVisibleOnlyMessage(message))
273
238
  annotateMessagesWithBranchEntryIds(contextMessages, ctx)
@@ -295,8 +260,6 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
295
260
  applyAnchoredNudges(prunedMessages, state, (anchor) =>
296
261
  appendConcreteNudgeGuidance(baseNudgeText(anchor.type), candidate, messageCandidates, state),
297
262
  )
298
- updateUi(ctx)
299
- emitContextUsage(ctx)
300
263
  return { messages: prunedMessages }
301
264
  }
302
265
 
@@ -388,24 +351,11 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
388
351
  appendConcreteNudgeGuidance(baseNudgeText(anchor.type), candidate, messageCandidates, state),
389
352
  )
390
353
 
391
-
392
- // Update footer status and floating widget after each context event.
393
- updateUi(ctx)
394
- emitContextUsage(ctx)
395
-
396
354
  return { messages: prunedMessages }
397
355
  })
398
356
 
399
- // ── 12. turn_end: refresh DCP status from Pi's final context percentage ───
400
- pi.on("turn_end", async (_event, ctx) => {
401
- updateUi(ctx)
402
- emitContextUsage(ctx)
403
- })
404
-
405
- // ── 13. agent_end: persist state after each agent run ────────────────────
406
- pi.on("agent_end", async (_event, ctx) => {
407
- updateUi(ctx)
408
- emitContextUsage(ctx)
357
+ // ── 11. agent_end: persist state after each agent run ────────────────────
358
+ pi.on("agent_end", async (_event, _ctx) => {
409
359
  saveState(pi, state)
410
360
  })
411
361
  }
@@ -0,0 +1,45 @@
1
+ export interface DcpCompressionVisualDetails {
2
+ topic: string
3
+ blockIds: number[]
4
+ ranges: number
5
+ messages: number
6
+ itemCount: number
7
+ totalSummaryTokens: number
8
+ activeBlocks: number
9
+ totalBlocks: number
10
+ prunedTools: number
11
+ tokensSaved: number
12
+ contextTokens?: number | null
13
+ contextWindow?: number
14
+ contextPercent?: number | null
15
+ skippedMessages?: number
16
+ skippedMessageIssues?: string[]
17
+ }
18
+
19
+ export interface DcpContextUsage {
20
+ tokens: number | null
21
+ contextWindow: number
22
+ percent?: number | null
23
+ }
24
+
25
+ type RawDcpContextUsage = {
26
+ tokens?: number | null
27
+ contextWindow?: number
28
+ percent?: number | null
29
+ } | undefined
30
+
31
+ export function normalizeDcpContextUsage(usage: RawDcpContextUsage): DcpContextUsage | undefined {
32
+ if (!usage || typeof usage.contextWindow !== "number" || usage.contextWindow <= 0) return undefined
33
+ const contextWindow = usage.contextWindow
34
+ const percent = typeof usage.percent === "number" && Number.isFinite(usage.percent)
35
+ ? Math.max(0, usage.percent)
36
+ : usage.percent === null
37
+ ? null
38
+ : undefined
39
+ const tokens = typeof percent === "number"
40
+ ? Math.round((contextWindow * percent) / 100)
41
+ : typeof usage.tokens === "number"
42
+ ? usage.tokens
43
+ : null
44
+ return { tokens, contextWindow, percent }
45
+ }
@@ -13,10 +13,11 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = `{
13
13
  // "model-tools",
14
14
  // "usage",
15
15
  // "web-search",
16
- // "compress"
16
+ // "dcp"
17
17
  ],
18
18
 
19
- // Dynamic Context Pruning (DCP) / compress module config.
19
+ // Dynamic Context Pruning (DCP) module config.
20
+ // The DCP module owns the compress tool and /dcp commands.
20
21
  "dcp": {
21
22
  "enabled": true,
22
23
  "manualMode": {
@@ -20,7 +20,7 @@ const MODULES: Array<{ name: string; load: () => Promise<ExtensionModule> }> = [
20
20
  { name: "model-tools", load: () => import("./model-tools/index") },
21
21
  { name: "usage", load: () => import("./usage/index") },
22
22
  { name: "web-search", load: () => import("./web-search/index") },
23
- { name: "compress", load: () => import("./compress/index") },
23
+ { name: "dcp", load: () => import("./dcp/index") },
24
24
  { name: "prompt-commands", load: () => import("./prompt-commands/index") },
25
25
  ];
26
26
 
@@ -1,4 +1,4 @@
1
- import { COMPRESS_RANGE_DESCRIPTION } from "./compress/prompts.js";
1
+ import { COMPRESS_RANGE_DESCRIPTION } from "./dcp/prompts.js";
2
2
 
3
3
  export type ToolDescription = {
4
4
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ui-extend",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {