pi-ui-extend 1.0.39 → 1.0.41

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 (113) hide show
  1. package/README.md +1 -1
  2. package/dist/app/commands/command-registry.js +2 -2
  3. package/dist/app/commands/command-session-actions.d.ts +0 -1
  4. package/dist/app/commands/command-session-actions.js +22 -13
  5. package/dist/app/icons.d.ts +14 -0
  6. package/dist/app/icons.js +33 -0
  7. package/dist/app/rendering/conversation-tool-renderer.js +2 -2
  8. package/dist/app/rendering/dcp-stats.d.ts +6 -1
  9. package/dist/app/rendering/dcp-stats.js +214 -46
  10. package/dist/app/rendering/editor-panels.js +8 -5
  11. package/dist/app/session/lazy-session-manager.js +12 -1
  12. package/dist/app/session/tabs-controller.d.ts +2 -5
  13. package/dist/app/session/tabs-controller.js +12 -21
  14. package/dist/app/subagents/subagents-model.d.ts +14 -1
  15. package/dist/app/subagents/subagents-model.js +34 -15
  16. package/dist/app/types.d.ts +2 -0
  17. package/dist/bundled-extensions/session-title/config.js +1 -1
  18. package/dist/markdown-format.js +27 -9
  19. package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
  20. package/dist/schemas/pi-tools-suite-schema.js +46 -31
  21. package/external/pi-tools-suite/README.md +392 -52
  22. package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
  23. package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
  24. package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
  25. package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
  26. package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
  27. package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
  28. package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
  29. package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
  30. package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
  31. package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
  32. package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
  33. package/external/pi-tools-suite/docs/evals.md +684 -0
  34. package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
  35. package/external/pi-tools-suite/package.json +10 -3
  36. package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
  37. package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
  38. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
  39. package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
  40. package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
  41. package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
  42. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
  44. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
  45. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
  46. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
  47. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
  48. package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
  49. package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
  50. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
  52. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
  53. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
  55. package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
  56. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
  57. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
  58. package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
  59. package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
  60. package/external/pi-tools-suite/src/config.ts +1 -22
  61. package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
  62. package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
  63. package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
  64. package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
  65. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
  66. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
  67. package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
  68. package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
  69. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
  70. package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
  71. package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
  72. package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
  73. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
  74. package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
  75. package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
  76. package/external/pi-tools-suite/src/dcp/config.ts +36 -61
  77. package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
  78. package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
  79. package/external/pi-tools-suite/src/dcp/index.ts +617 -203
  80. package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
  81. package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
  82. package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
  83. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
  84. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
  85. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
  86. package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
  87. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
  88. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
  89. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
  90. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
  91. package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
  92. package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
  93. package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
  94. package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
  95. package/external/pi-tools-suite/src/dcp/state.ts +158 -580
  96. package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
  98. package/external/pi-tools-suite/src/index.ts +9 -0
  99. package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
  100. package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
  101. package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
  102. package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
  103. package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
  104. package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
  105. package/package.json +6 -6
  106. package/schemas/pi-tools-suite.json +159 -78
  107. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
  108. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
  109. package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
  110. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
  111. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
  112. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
  113. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
@@ -0,0 +1,421 @@
1
+ import { createHash } from "node:crypto"
2
+ import {
3
+ hasExactCompressionMembership,
4
+ type CompressionMember,
5
+ type ConversationIndexEntry,
6
+ type DcpState,
7
+ } from "./state.js"
8
+ import { PASSTHROUGH_ROLES, stripStaleDcpMetadataLines } from "./pruner-metadata.js"
9
+
10
+ export interface ToolGroupSpan {
11
+ startIndex: number
12
+ endIndex: number
13
+ toolCallIds: string[]
14
+ complete: boolean
15
+ }
16
+
17
+ export interface ClosedRangeSelection {
18
+ requestedStartIndex: number
19
+ requestedEndIndex: number
20
+ startIndex: number
21
+ endIndex: number
22
+ startId?: string
23
+ endId?: string
24
+ expanded: boolean
25
+ incompleteToolGroup: boolean
26
+ }
27
+
28
+ export interface ExactRangeMembership {
29
+ /** Provider-projection entries that were actually given to the summarizer. */
30
+ sourceMembers: CompressionMember[]
31
+ /** Canonical raw entries that a future materialization may replace. */
32
+ mutationMembers: CompressionMember[]
33
+ /** Indices of selected non-control source entries in the supplied projection. */
34
+ sourceIndexes: number[]
35
+ }
36
+
37
+ const IDENTITY_ONLY_MESSAGE_KEYS = new Set([
38
+ "id",
39
+ "entryId",
40
+ "messageId",
41
+ "_dcpEntryId",
42
+ "_dcpStableId",
43
+ "_dcpOrigin",
44
+ "_dcpBlockId",
45
+ // Timestamp is transport/order metadata. A stable branch identity + content
46
+ // hash must survive hosts that normalize timestamps during reload/import.
47
+ "timestamp",
48
+ ])
49
+
50
+ function canonicalContentForMessageHash(message: any): unknown {
51
+ if (!Array.isArray(message?.content)) {
52
+ if (message?.role === "assistant") return message?.content ?? ""
53
+ return stripDcpCarrierText(typeof message?.content === "string" ? message.content : "")
54
+ }
55
+
56
+ if (message?.role === "assistant") return message.content
57
+
58
+ return message.content.flatMap((part: any) => {
59
+ if (!part || typeof part !== "object") return [part]
60
+ const next = { ...part }
61
+ if (typeof next.text === "string") {
62
+ const text = stripDcpCarrierText(next.text)
63
+ if (!text && next.type === "text") return []
64
+ next.text = text
65
+ }
66
+ if (typeof next.thinking === "string") {
67
+ const thinking = stripDcpCarrierText(next.thinking)
68
+ if (!thinking && next.type === "thinking") return []
69
+ next.thinking = thinking
70
+ }
71
+ return [next]
72
+ })
73
+ }
74
+
75
+ function stripDcpCarrierText(text: string): string {
76
+ return stripStaleDcpMetadataLines(text)
77
+ }
78
+
79
+ function canonicalHashValue(value: unknown, seen = new WeakSet<object>()): unknown {
80
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value
81
+ if (typeof value === "number") return Number.isFinite(value) ? value : String(value)
82
+ if (typeof value === "bigint") return `${value}n`
83
+ if (typeof value === "undefined") return "[undefined]"
84
+ if (typeof value === "function" || typeof value === "symbol") return String(value)
85
+ if (Array.isArray(value)) return value.map((item) => canonicalHashValue(item, seen))
86
+ if (typeof value === "object") {
87
+ if (seen.has(value as object)) return "[cycle]"
88
+ seen.add(value as object)
89
+ const output: Record<string, unknown> = {}
90
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
91
+ output[key] = canonicalHashValue((value as Record<string, unknown>)[key], seen)
92
+ }
93
+ seen.delete(value as object)
94
+ return output
95
+ }
96
+ return String(value)
97
+ }
98
+
99
+ /**
100
+ * Hash a message without mutating it. Assistant content is intentionally
101
+ * copied byte-for-byte into the canonical value: signatures/thinking blocks
102
+ * are never stripped or normalized by DCP membership accounting.
103
+ */
104
+ export function canonicalMessageHash(message: any): string {
105
+ const canonical: Record<string, unknown> = {}
106
+ if (message && typeof message === "object") {
107
+ for (const key of Object.keys(message).sort()) {
108
+ if (IDENTITY_ONLY_MESSAGE_KEYS.has(key)) continue
109
+ canonical[key] = key === "content"
110
+ ? canonicalContentForMessageHash(message)
111
+ : canonicalHashValue(message[key])
112
+ }
113
+ } else {
114
+ canonical.value = canonicalHashValue(message)
115
+ }
116
+ return createHash("sha256").update(JSON.stringify(canonicalHashValue(canonical))).digest("hex")
117
+ }
118
+
119
+ const CANONICAL_HASH_RE = /^[a-f0-9]{64}$/i
120
+ const rawMutationHashes = new WeakMap<object, string>()
121
+
122
+ /**
123
+ * Record the canonical hash of a raw message immediately before DCP rewrites
124
+ * its projection body (tool-output placeholder). The module-owned WeakMap is
125
+ * runtime-only and cannot leak into provider serialization or be spoofed by a
126
+ * message field. Provenance travels on the exact message occurrence, never
127
+ * keyed by a stable identity: fallback identities derived from projected
128
+ * content would not be stable across the rewrite.
129
+ *
130
+ * Without this, exact mutation membership binds to the pruned projection hash,
131
+ * the next raw context pass can never match the span, and the block silently
132
+ * fails to materialize while candidate selection keeps re-selecting the range.
133
+ */
134
+ export function preserveRawMutationHash(message: any): void {
135
+ if (!message || typeof message !== "object") return
136
+ if (!rawMutationHashes.has(message)) rawMutationHashes.set(message, canonicalMessageHash(message))
137
+ }
138
+
139
+ /** Canonical raw (pre-prune) hash preserved on a projected message, if any. */
140
+ export function rawMutationHashOf(message: any): string | undefined {
141
+ if (!message || typeof message !== "object") return undefined
142
+ const preserved = rawMutationHashes.get(message)
143
+ return typeof preserved === "string" && CANONICAL_HASH_RE.test(preserved) ? preserved : undefined
144
+ }
145
+
146
+ /** Transfer module-owned raw provenance when DCP clones a projection message. */
147
+ export function copyRawMutationHash(source: any, target: any): void {
148
+ if (!source || typeof source !== "object" || !target || typeof target !== "object") return
149
+ const preserved = rawMutationHashOf(source)
150
+ if (preserved) rawMutationHashes.set(target, preserved)
151
+ }
152
+
153
+ function hasAssistantSignature(message: any): boolean {
154
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) return false
155
+ return message.content.some((part: any) =>
156
+ part &&
157
+ typeof part === "object" &&
158
+ // These are the SDK's opaque replay fields. Presence is enough to freeze
159
+ // the whole assistant message: a malformed/empty signature is not a
160
+ // reason to rewrite the nested provider-owned content around it.
161
+ (
162
+ Object.prototype.hasOwnProperty.call(part, "textSignature") ||
163
+ Object.prototype.hasOwnProperty.call(part, "thinkingSignature")
164
+ ),
165
+ )
166
+ }
167
+
168
+ export function buildConversationIndex(
169
+ messages: any[],
170
+ stableKeys: string[],
171
+ state: DcpState,
172
+ ): ConversationIndexEntry[] {
173
+ const visibleByStableId = new Map<string, string>()
174
+ for (const [visibleId, meta] of state.messageMetaSnapshot) {
175
+ if (meta.stableId) visibleByStableId.set(meta.stableId, visibleId)
176
+ }
177
+
178
+ return messages.map((message, index) => {
179
+ const stableId = stableKeys[index]!
180
+ const visibleId = visibleByStableId.get(stableId)
181
+ const meta = visibleId ? state.messageMetaSnapshot.get(visibleId) : undefined
182
+ const contentHash = canonicalMessageHash(message)
183
+ const rawContentHash = rawMutationHashOf(message)
184
+ return {
185
+ index,
186
+ stableId,
187
+ contentHash,
188
+ ...(rawContentHash && rawContentHash !== contentHash ? { rawContentHash } : {}),
189
+ visibleId,
190
+ role: message?.role ?? "",
191
+ timestamp: Number.isFinite(message?.timestamp) ? message.timestamp : undefined,
192
+ blockId: meta?.blockId,
193
+ toolCallId: typeof message?.toolCallId === "string" ? message.toolCallId : meta?.toolCallId,
194
+ toolCallIds: message?.role === "assistant" && Array.isArray(message.content)
195
+ ? message.content.filter((part: any) => part?.type === "toolCall" && typeof part.id === "string").map((part: any) => part.id)
196
+ : undefined,
197
+ passthrough: PASSTHROUGH_ROLES.has(message?.role ?? ""),
198
+ origin: message?._dcpOrigin === "block"
199
+ ? "block"
200
+ : message?._dcpOrigin === "dcp-control"
201
+ ? "dcp-control"
202
+ : "raw",
203
+ signedAssistant: hasAssistantSignature(message),
204
+ }
205
+ })
206
+ }
207
+
208
+ function addressableAliases(entry: ConversationIndexEntry): string[] {
209
+ return [
210
+ ...(entry.visibleId ? [entry.visibleId] : []),
211
+ ...(entry.blockId !== undefined ? [`b${entry.blockId}`] : []),
212
+ ]
213
+ }
214
+
215
+ export function findConversationIndexEntry(
216
+ entries: ConversationIndexEntry[],
217
+ id: string,
218
+ ): ConversationIndexEntry | undefined {
219
+ const normalized = id.trim().toLowerCase()
220
+ return entries.find((entry) =>
221
+ addressableAliases(entry).some((alias) => alias.toLowerCase() === normalized),
222
+ )
223
+ }
224
+
225
+ /**
226
+ * Return canonical branch order for two stable identities when both are
227
+ * represented by the current projection. An active block's raw boundaries are
228
+ * deliberately resolved to its single projected placeholder, so roll-ups use
229
+ * projection order rather than timestamp or mNNN allocation order.
230
+ */
231
+ export function compareConversationStableIds(
232
+ entries: ConversationIndexEntry[],
233
+ leftStableId: string | undefined,
234
+ rightStableId: string | undefined,
235
+ state: DcpState,
236
+ ): number | undefined {
237
+ if (!leftStableId || !rightStableId) return undefined
238
+ if (leftStableId === rightStableId) return 0
239
+
240
+ const projectedIndexFor = (stableId: string): number | undefined => {
241
+ const direct = entries.find((entry) => entry.stableId === stableId)
242
+ if (direct) return direct.index
243
+
244
+ const block = state.compressionBlocks.find((candidate) =>
245
+ candidate.active && candidate.version === 2 && hasExactCompressionMembership(candidate) &&
246
+ (candidate.startMessageId === stableId || candidate.endMessageId === stableId),
247
+ )
248
+ if (!block) return undefined
249
+ return entries.find((entry) => entry.blockId === block.id)?.index
250
+ }
251
+
252
+ const left = projectedIndexFor(leftStableId)
253
+ const right = projectedIndexFor(rightStableId)
254
+ if (left === undefined || right === undefined) return undefined
255
+ return left - right
256
+ }
257
+
258
+ function appendUniqueMembers(
259
+ target: CompressionMember[],
260
+ members: CompressionMember[],
261
+ ): boolean {
262
+ const existing = new Map(target.map((member) => [member.stableId, member.hash]))
263
+ for (const member of members) {
264
+ const priorHash = existing.get(member.stableId)
265
+ if (priorHash !== undefined) {
266
+ if (priorHash !== member.hash) return false
267
+ continue
268
+ }
269
+ target.push({ stableId: member.stableId, hash: member.hash })
270
+ existing.set(member.stableId, member.hash)
271
+ }
272
+ return true
273
+ }
274
+
275
+ /**
276
+ * Convert a closed projection selection into both source and raw mutation
277
+ * membership. A synthetic block contributes its own visible source entry and
278
+ * expands only through its already-persisted raw mutation membership. This is
279
+ * what prevents a later materialization from rediscovering a timestamp span.
280
+ */
281
+ export function buildExactRangeMembership(
282
+ entries: ConversationIndexEntry[],
283
+ startId: string,
284
+ endId: string,
285
+ state: DcpState,
286
+ messageBody = false,
287
+ ): ExactRangeMembership | undefined {
288
+ const entry = messageBody ? findConversationIndexEntry(entries, startId) : undefined
289
+ const closure = messageBody
290
+ ? entry && { startIndex: entry.index, endIndex: entry.index, incompleteToolGroup: false }
291
+ : closeConversationRange(entries, startId, endId)
292
+ if (!closure || closure.incompleteToolGroup) return undefined
293
+
294
+ const sourceMembers: CompressionMember[] = []
295
+ const mutationMembers: CompressionMember[] = []
296
+ const sourceIndexes: number[] = []
297
+ for (let index = closure.startIndex; index <= closure.endIndex; index++) {
298
+ const entry = entries[index]!
299
+ // DCP control-plane entries are never source material and must not become
300
+ // accidental raw mutation members merely because they sit between IDs.
301
+ if (entry.origin === "dcp-control") continue
302
+ if (!entry.stableId || !entry.contentHash) return undefined
303
+ sourceMembers.push({ stableId: entry.stableId, hash: entry.contentHash })
304
+ sourceIndexes.push(entry.index)
305
+
306
+ if (entry.origin === "raw") {
307
+ // Mutation membership binds to the canonical RAW content (pre-prune):
308
+ // materialization always runs against the raw session history before
309
+ // DCP rewrites tool-result bodies. sourceMembers keeps the projected
310
+ // hash because that is exactly what the summarizer inspected.
311
+ const rawHash = entry.rawContentHash ?? entry.contentHash
312
+ if (!appendUniqueMembers(mutationMembers, [{ stableId: entry.stableId, hash: rawHash }])) {
313
+ return undefined
314
+ }
315
+ continue
316
+ }
317
+
318
+ if (entry.origin !== "block" || entry.blockId === undefined) return undefined
319
+ const block = state.compressionBlocks.find((candidate) => candidate.id === entry.blockId && candidate.active)
320
+ if (!block || !hasExactCompressionMembership(block)) return undefined
321
+ if (!appendUniqueMembers(mutationMembers, block.mutationMembers!)) return undefined
322
+ }
323
+
324
+ if (sourceMembers.length === 0 || mutationMembers.length === 0) return undefined
325
+ return { sourceMembers, mutationMembers, sourceIndexes }
326
+ }
327
+
328
+ export function detectToolGroupSpans(entries: ConversationIndexEntry[]): ToolGroupSpan[] {
329
+ const groups: ToolGroupSpan[] = []
330
+
331
+ for (let i = 0; i < entries.length; i++) {
332
+ const assistant = entries[i]!
333
+ if (assistant.role !== "assistant" || !assistant.toolCallIds?.length) continue
334
+
335
+ const pending = new Set(assistant.toolCallIds)
336
+ let endIndex = i
337
+ for (let j = i + 1; j < entries.length; j++) {
338
+ const entry = entries[j]!
339
+ if (entry.passthrough) {
340
+ endIndex = j
341
+ continue
342
+ }
343
+ if (
344
+ (entry.role === "toolResult" || entry.role === "bashExecution") &&
345
+ entry.toolCallId &&
346
+ pending.has(entry.toolCallId)
347
+ ) {
348
+ pending.delete(entry.toolCallId)
349
+ endIndex = j
350
+ if (pending.size === 0) break
351
+ continue
352
+ }
353
+ break
354
+ }
355
+
356
+ groups.push({
357
+ startIndex: i,
358
+ endIndex,
359
+ toolCallIds: [...assistant.toolCallIds],
360
+ complete: pending.size === 0,
361
+ })
362
+ }
363
+
364
+ return groups
365
+ }
366
+
367
+ function nearestAddressableId(
368
+ entries: ConversationIndexEntry[],
369
+ index: number,
370
+ direction: 1 | -1,
371
+ ): string | undefined {
372
+ for (let i = index; i >= 0 && i < entries.length; i += direction) {
373
+ const entry = entries[i]!
374
+ if (entry.blockId !== undefined) return `b${entry.blockId}`
375
+ if (entry.visibleId) return entry.visibleId
376
+ }
377
+ return undefined
378
+ }
379
+
380
+ export function closeConversationRange(
381
+ entries: ConversationIndexEntry[],
382
+ startId: string,
383
+ endId: string,
384
+ ): ClosedRangeSelection | undefined {
385
+ const startEntry = findConversationIndexEntry(entries, startId)
386
+ const endEntry = findConversationIndexEntry(entries, endId)
387
+ if (!startEntry || !endEntry) return undefined
388
+
389
+ const requestedStartIndex = Math.min(startEntry.index, endEntry.index)
390
+ const requestedEndIndex = Math.max(startEntry.index, endEntry.index)
391
+ let startIndex = requestedStartIndex
392
+ let endIndex = requestedEndIndex
393
+ let incompleteToolGroup = false
394
+ const groups = detectToolGroupSpans(entries)
395
+
396
+ let changed: boolean
397
+ do {
398
+ changed = false
399
+ for (const group of groups) {
400
+ const overlaps = group.startIndex <= endIndex && group.endIndex >= startIndex
401
+ if (!overlaps) continue
402
+ if (!group.complete) incompleteToolGroup = true
403
+ const nextStart = Math.min(startIndex, group.startIndex)
404
+ const nextEnd = Math.max(endIndex, group.endIndex)
405
+ if (nextStart !== startIndex || nextEnd !== endIndex) changed = true
406
+ startIndex = nextStart
407
+ endIndex = nextEnd
408
+ }
409
+ } while (changed)
410
+
411
+ return {
412
+ requestedStartIndex,
413
+ requestedEndIndex,
414
+ startIndex,
415
+ endIndex,
416
+ startId: nearestAddressableId(entries, startIndex, 1),
417
+ endId: nearestAddressableId(entries, endIndex, -1),
418
+ expanded: startIndex !== requestedStartIndex || endIndex !== requestedEndIndex,
419
+ incompleteToolGroup,
420
+ }
421
+ }
@@ -88,7 +88,11 @@ function sessionInfo(ctx: ExtensionContext | undefined): Record<string, unknown>
88
88
  return info
89
89
  }
90
90
 
91
- export function summarizeDcpState(state: DcpState): Record<string, unknown> {
91
+ export function summarizeDcpState(state: DcpState, config?: DcpConfig): Record<string, unknown> {
92
+ // Call sites often construct debug detail objects before writeDcpDebugLog can
93
+ // early-return. Passing config lets this potentially O(n) snapshot stay an
94
+ // O(1) no-op when debug logging is disabled.
95
+ if (config && !dcpDebugEnabled(config)) return {}
92
96
  const rawIds = [...new Set([
93
97
  ...state.messageIdSnapshot.keys(),
94
98
  ...state.messageMetaSnapshot.keys(),
@@ -130,12 +134,10 @@ export function summarizeDcpState(state: DcpState): Record<string, unknown> {
130
134
  persistentMessageIds: state.messageIdsByStableId.size,
131
135
  nextMessageId: state.nextMessageId,
132
136
  prunedTools: state.prunedToolIds.size,
133
- automaticPruneCheckpoint: {
134
- turn: state.lastAutomaticPruneTurn,
135
- blockId: state.lastAutomaticPruneBlockId,
136
- },
137
137
  providerSeenTools: state.providerSeenToolIds.size,
138
138
  consecutiveEmergencyPasses: state.consecutiveIgnoredStrongNudges,
139
+ consecutiveActionableOpportunities: state.consecutiveIgnoredNudges,
140
+ compressionProgress: state.compressionProgress,
139
141
  nudgeAnchors: state.nudgeAnchors.map((anchor) => ({
140
142
  id: anchor.id,
141
143
  type: anchor.type,