pi-ui-extend 1.0.37 → 1.0.39

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 (44) hide show
  1. package/README.md +1 -0
  2. package/dist/app/extensions/extension-ui-controller.js +3 -0
  3. package/dist/app/rendering/extension-entry-renderer.js +2 -0
  4. package/dist/bundled-extensions/question/contract.d.ts +3 -0
  5. package/dist/bundled-extensions/question/contract.js +24 -0
  6. package/dist/bundled-extensions/question/desktop.d.ts +11 -0
  7. package/dist/bundled-extensions/question/desktop.js +143 -0
  8. package/dist/bundled-extensions/question/index.d.ts +1 -0
  9. package/dist/bundled-extensions/question/index.js +5 -1
  10. package/dist/bundled-extensions/question/render.js +6 -0
  11. package/dist/bundled-extensions/question/result.js +71 -9
  12. package/dist/bundled-extensions/question/tool-description.js +4 -3
  13. package/dist/bundled-extensions/question/tui.js +127 -12
  14. package/dist/bundled-extensions/question/types.d.ts +23 -2
  15. package/dist/tool-renderers/question.js +20 -1
  16. package/docs/desktop-markdown-media.md +77 -0
  17. package/docs/desktop-mvp.md +134 -0
  18. package/docs/desktop-task-manager.md +124 -0
  19. package/external/pi-tools-suite/package.json +3 -3
  20. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +9 -1
  21. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +41 -2
  22. package/external/pi-tools-suite/src/async-subagents/core/config.ts +13 -1
  23. package/external/pi-tools-suite/src/async-subagents/index.ts +6 -2
  24. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/SKILL.md +65 -22
  25. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +23 -8
  26. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/scripts/browser-qa-runner.mjs +180 -41
  27. package/external/pi-tools-suite/src/async-subagents/subagent-overlay.ts +1 -1
  28. package/external/pi-tools-suite/src/dcp/auto-compress.ts +1 -1
  29. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +1 -51
  30. package/external/pi-tools-suite/src/dcp/debug-log.ts +6 -0
  31. package/external/pi-tools-suite/src/dcp/index.ts +27 -125
  32. package/external/pi-tools-suite/src/dcp/prompts.ts +2 -2
  33. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +2 -1
  34. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +31 -10
  35. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +6 -7
  36. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +153 -111
  37. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +28 -17
  38. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +61 -27
  39. package/external/pi-tools-suite/src/dcp/pruner.ts +21 -13
  40. package/external/pi-tools-suite/src/dcp/state.ts +59 -0
  41. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +4 -0
  42. package/external/pi-tools-suite/src/lib/rpc-session-state.ts +34 -0
  43. package/external/pi-tools-suite/src/todo/todo.ts +8 -3
  44. package/package.json +10 -4
@@ -45,13 +45,7 @@ import {
45
45
  resolveContextThresholds,
46
46
  estimateTokens,
47
47
  } from "./pruner.js"
48
- import {
49
- stripStaleDcpMetadataFromAssistantMessage,
50
- stripStaleDcpMetadataFromMessage,
51
- } from "./pruner-metadata.js"
52
- import {
53
- buildMessageIdControlText,
54
- } from "./pruner-message-ids.js"
48
+ import { stripStaleDcpMetadataFromMessage } from "./pruner-metadata.js"
55
49
  import { summarizeDcpState, writeDcpDebugLog } from "./debug-log.js"
56
50
  import type { DcpNudgeType } from "./pruner-types.js"
57
51
  import { registerCompressTool } from "./compress-tool.js"
@@ -119,104 +113,6 @@ function isDcpControlPlaneMessage(message: any): boolean {
119
113
  return message?.role === "custom" && DCP_CONTROL_PLANE_CUSTOM_TYPES.has(message.customType)
120
114
  }
121
115
 
122
- const DCP_PROVIDER_CONTROL_HEADER = "DCP message ID control data (do not quote or output):"
123
-
124
- function appendTextToContent(content: unknown, text: string): unknown {
125
- if (typeof content === "string") return `${content}\n\n${text}`
126
- if (Array.isArray(content)) {
127
- const textType = content.some((part: any) => part?.type === "input_text") ? "input_text" : "text"
128
- return [...content, { type: textType, text }]
129
- }
130
- return text
131
- }
132
-
133
- function appendDcpControlToMessages(messages: unknown, text: string): unknown {
134
- if (!Array.isArray(messages)) return messages
135
- const block = `${DCP_PROVIDER_CONTROL_HEADER}\n${text}`
136
-
137
- // Keep DCP's volatile ID map at the tail of the provider context instead of
138
- // mutating the stable system/developer prefix. Prefix cache reuse is much
139
- // better when only the newest transcript item changes on each request.
140
- let targetIndex = -1
141
- for (let index = messages.length - 1; index >= 0; index--) {
142
- const message = messages[index] as any
143
- if (!message || typeof message !== "object") continue
144
- // Responses items such as reasoning/function_call_output do not accept a
145
- // `content` field. Keep a function output at the tail by appending to its
146
- // valid `output` string; otherwise scan back to an actual message item.
147
- if (typeof message.type === "string" && message.type !== "message" && message.role === undefined) {
148
- if (message.type === "function_call_output" && typeof message.output === "string") {
149
- return messages.map((candidate: any, candidateIndex) => candidateIndex === index
150
- ? { ...candidate, output: `${candidate.output}\n\n${block}` }
151
- : candidate)
152
- }
153
- continue
154
- }
155
- if (message.role === "system" || message.role === "developer") continue
156
- targetIndex = index
157
- break
158
- }
159
-
160
- if (targetIndex < 0) {
161
- targetIndex = messages.findIndex((message: any) =>
162
- message?.role === "system" || message?.role === "developer"
163
- )
164
- }
165
-
166
- if (targetIndex >= 0) {
167
- return messages.map((message: any, index) => index === targetIndex
168
- ? { ...message, content: appendTextToContent(message.content, block) }
169
- : message)
170
- }
171
- return [{ role: "system", content: block }, ...messages]
172
- }
173
-
174
- function appendDcpControlToAnthropicSystem(system: unknown, text: string): unknown {
175
- const block = `${DCP_PROVIDER_CONTROL_HEADER}\n${text}`
176
- if (typeof system === "string") return `${system}\n\n${block}`
177
- if (Array.isArray(system)) return [...system, { type: "text", text: block }]
178
- if (system === undefined || system === null) return [{ type: "text", text: block }]
179
- return system
180
- }
181
-
182
- function appendDcpControlToGoogleSystemInstruction(systemInstruction: unknown, text: string): unknown {
183
- const block = `${DCP_PROVIDER_CONTROL_HEADER}\n${text}`
184
- if (typeof systemInstruction === "string") return `${systemInstruction}\n\n${block}`
185
- if (systemInstruction === undefined || systemInstruction === null) return block
186
- return systemInstruction
187
- }
188
-
189
- function appendDcpControlToProviderPayload(payload: unknown, text: string): unknown {
190
- if (Array.isArray(payload)) return appendDcpControlToMessages(payload, text)
191
- if (!payload || typeof payload !== "object") return payload
192
- const record = payload as Record<string, unknown>
193
-
194
- if (Array.isArray(record.input)) {
195
- return { ...record, input: appendDcpControlToMessages(record.input, text) }
196
- }
197
-
198
- if (Array.isArray(record.messages)) {
199
- return { ...record, messages: appendDcpControlToMessages(record.messages, text) }
200
- }
201
-
202
- if ("system" in record) {
203
- return { ...record, system: appendDcpControlToAnthropicSystem(record.system, text) }
204
- }
205
-
206
- if (record.config && typeof record.config === "object") {
207
- const config = record.config as Record<string, unknown>
208
- return {
209
- ...record,
210
- config: {
211
- ...config,
212
- systemInstruction: appendDcpControlToGoogleSystemInstruction(config.systemInstruction, text),
213
- },
214
- }
215
- }
216
-
217
- return payload
218
- }
219
-
220
116
  // ---------------------------------------------------------------------------
221
117
  // Module export
222
118
  // ---------------------------------------------------------------------------
@@ -344,15 +240,6 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
344
240
  }
345
241
  })
346
242
 
347
- // ── 7b. message_end: never persist provider-echoed DCP control markers ─────
348
- pi.on("message_end", async (event, ctx) => {
349
- const effectiveConfig = configForContext(ctx)
350
- if (!effectiveConfig.enabled || event.message?.role !== "assistant") return undefined
351
-
352
- const sanitized = stripStaleDcpMetadataFromAssistantMessage(event.message)
353
- return { message: sanitized }
354
- })
355
-
356
243
  // ── 8. tool_call: record input args for dedup / purge fingerprinting ───────
357
244
  pi.on("tool_call", async (event, _ctx) => {
358
245
  if (!state.toolCalls.has(event.toolCallId)) {
@@ -418,7 +305,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
418
305
  inputMessages: event.messages.length,
419
306
  filteredMessages: contextMessages.length,
420
307
  outputMessages: messages.length,
421
- messageIdControl: "provider-payload",
308
+ messageIdControl: "distributed-carriers",
422
309
  state: summarizeDcpState(state),
423
310
  ...details,
424
311
  }, ctx)
@@ -454,7 +341,20 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
454
341
  await saveDcpState(ctx, state)
455
342
  }
456
343
  }
344
+ const prunedToolCountBeforeCheckpoint = state.prunedToolIds.size
457
345
  let prunedMessages = applyPruning(contextMessages, state, effectiveConfig)
346
+ const automaticPrunesCommitted = state.prunedToolIds.size - prunedToolCountBeforeCheckpoint
347
+ if (automaticPrunesCommitted > 0) {
348
+ const clearedAnchors = clearDcpNudgeAnchors(state)
349
+ await saveDcpState(ctx, state)
350
+ writeDcpDebugLog(effectiveConfig, "prune.tool_checkpoint", {
351
+ committed: automaticPrunesCommitted,
352
+ clearedAnchors,
353
+ turn: state.currentTurn,
354
+ blockId: state.lastAutomaticPruneBlockId,
355
+ state: summarizeDcpState(state),
356
+ }, ctx)
357
+ }
458
358
  let candidate = null as ReturnType<typeof detectCompressionCandidate>
459
359
  let messageCandidates = [] as ReturnType<typeof detectMessageCompressionCandidates>
460
360
  let emergencySelection = null as ReturnType<typeof analyzeEmergencyCurrentTurn> | null
@@ -746,6 +646,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
746
646
  )
747
647
  if (emergencyPruneResult.prunedToolCallIds.length > 0) {
748
648
  prunedMessages = applyPruning(contextMessages, state, effectiveConfig)
649
+ const clearedAnchors = clearDcpNudgeAnchors(state)
749
650
  state.consecutiveIgnoredStrongNudges = 0
750
651
  emergencySelection = analyzeEmergencyCurrentTurn(prunedMessages, state, effectiveConfig)
751
652
  messageCandidates = emergencyCurrentTurnMessageCandidates(emergencySelection, effectiveConfig)
@@ -758,6 +659,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
758
659
  targetContextPercent,
759
660
  targetRecoveryTokens,
760
661
  prunedOutputs: emergencyPruneResult.prunedToolCallIds.length,
662
+ clearedAnchors,
761
663
  estimatedTokensRecovered: emergencyPruneResult.estimatedTokensRecovered,
762
664
  estimatedContextPercentAfter: Math.max(
763
665
  0,
@@ -786,7 +688,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
786
688
  prunedMessages,
787
689
  state,
788
690
  nudgeType,
789
- { contextPercent },
691
+ { contextPercent, renderedReminder: nudgeText },
790
692
  )
791
693
  if (anchorResult.anchor) {
792
694
  if (anchorResult.updated) {
@@ -836,10 +738,10 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
836
738
  anchor.type === "context-strong" || anchor.type === "context-soft",
837
739
  )
838
740
  }
839
- applyAnchoredNudges(prunedMessages, state, (anchor) =>
741
+ const nudgeApplication = applyAnchoredNudges(prunedMessages, state, (anchor) =>
840
742
  appendConcreteNudgeGuidance(baseNudgeText(anchor.type), candidate, messageCandidates, state),
841
743
  )
842
- if (state.nudgeAnchors.length !== anchorsBeforeFinalization) {
744
+ if (state.nudgeAnchors.length !== anchorsBeforeFinalization || nudgeApplication.stateChanged) {
843
745
  await saveDcpState(ctx, state)
844
746
  }
845
747
 
@@ -851,7 +753,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
851
753
  })
852
754
  })
853
755
 
854
- // ── 10b. before_provider_request: inject DCP IDs outside transcript ────────
756
+ // ── 10b. before_provider_request: observe accepted tool-result payloads ────
855
757
  pi.on("before_provider_request", async (event, ctx) => {
856
758
  const effectiveConfig = configForContext(ctx)
857
759
  pendingProviderToolIds.clear()
@@ -868,16 +770,16 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
868
770
  }
869
771
  }
870
772
 
871
- const controlText = buildMessageIdControlText(state)
872
- if (!controlText) return undefined
873
-
874
- const payload = appendDcpControlToProviderPayload(event.payload, controlText)
875
773
  writeDcpDebugLog(effectiveConfig, "provider_payload.message_ids", {
876
- injected: payload !== event.payload,
774
+ injected: false,
775
+ delivery: "distributed-carriers",
877
776
  pendingToolResults: pendingProviderToolIds.size,
878
777
  state: summarizeDcpState(state),
879
778
  }, ctx)
880
- return payload === event.payload ? undefined : payload
779
+ // IDs are already attached to deterministic user/tool-result context
780
+ // carriers. Replacing the payload here would move metadata from the old
781
+ // tail to the new one and break strict append-only Responses continuation.
782
+ return undefined
881
783
  })
882
784
 
883
785
  // Promote pending IDs only after the provider accepted the exact payload.
@@ -118,9 +118,9 @@ You specify boundaries by ID using the injected metadata IDs present in the conv
118
118
  - \`mNNN\` IDs identify raw messages (3 digits, zero-padded, e.g. \`m001\`, \`m042\`)
119
119
  - \`bN\` IDs identify previously compressed blocks
120
120
 
121
- Current raw message IDs are provided in hidden DCP control metadata at the end of the model context.
121
+ Raw message IDs are provided in hidden DCP control metadata attached to stable user/tool-result carriers. IDs may be sparse; determine range order from where their carriers appear in context, not from the numeric suffix.
122
122
  Some message-compression candidate hints include a low/medium/high priority; prefer high-priority stale message IDs for message-mode compression when a full range would be too broad.
123
- The ID reference line appears at the end of the message it belongs to — it identifies the message above it, not the one below it.
123
+ Each carrier's metadata labels that carrier and any immediately preceding assistant message(s).
124
124
  Treat these reference lines as boundary metadata only, not as tool result content.
125
125
 
126
126
  Rules:
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { ToolRecord } from "./state.js";
3
+ import { stripStaleDcpMetadataLines } from "./pruner-metadata.js";
3
4
 
4
5
  export interface ProviderToolResultEvidence {
5
6
  ids: Set<string>;
@@ -19,7 +20,7 @@ function signature(toolName: string, outputText: string): string {
19
20
  return createHash("sha256")
20
21
  .update(toolName)
21
22
  .update("\u0000")
22
- .update(outputText)
23
+ .update(stripStaleDcpMetadataLines(outputText))
23
24
  .digest("hex");
24
25
  }
25
26
 
@@ -6,6 +6,7 @@ import {
6
6
  extractBlockId,
7
7
  messageText,
8
8
  } from "./pruner-metadata.js";
9
+ import { stableMessageKeys } from "./pruner-message-ids.js";
9
10
 
10
11
  interface CandidateBoundary {
11
12
  id: string;
@@ -24,13 +25,27 @@ function isActiveBlockId(blockId: number, state: DcpState): boolean {
24
25
  return state.compressionBlocks.some((block) => block.id === blockId && block.active);
25
26
  }
26
27
 
27
- function findCurrentMessageId(msg: any, state: DcpState): string | undefined {
28
+ function findCurrentMessageId(msg: any, stableKey: string, state: DcpState): string | undefined {
28
29
  const role = msg?.role ?? "";
29
30
  const timestamp = msg?.timestamp;
30
31
  if (!Number.isFinite(timestamp)) return undefined;
31
32
 
32
33
  for (const [id, meta] of state.messageMetaSnapshot) {
33
- if (meta.timestamp === timestamp && meta.role === role && meta.blockId === undefined) return id;
34
+ if (meta.stableId === stableKey) return id;
35
+ }
36
+
37
+ // Backward compatibility for snapshots created before stable IDs were
38
+ // recorded. Never mix timestamp fallback with a modern stable-ID snapshot.
39
+ const isLegacySnapshot = [...state.messageMetaSnapshot.values()]
40
+ .every((meta) => meta.stableId === undefined);
41
+ if (!isLegacySnapshot) return undefined;
42
+
43
+ for (const [id, meta] of state.messageMetaSnapshot) {
44
+ if (
45
+ meta.timestamp === timestamp &&
46
+ meta.role === role &&
47
+ meta.blockId === undefined
48
+ ) return id;
34
49
  }
35
50
 
36
51
  for (const [id, ts] of state.messageIdSnapshot) {
@@ -42,6 +57,7 @@ function findCurrentMessageId(msg: any, state: DcpState): string | undefined {
42
57
 
43
58
  function resolveAddressableBoundaryId(
44
59
  msg: any,
60
+ stableKey: string,
45
61
  state: DcpState,
46
62
  options: { allowBlocks: boolean },
47
63
  ): { id: string; blockId?: number; text: string } | null {
@@ -55,8 +71,9 @@ function resolveAddressableBoundaryId(
55
71
 
56
72
  // Inline [dcp-id] markers are no longer injected into message content; the
57
73
  // snapshot rebuilt by injectMessageIds() is the sole addressability source.
58
- // Resolve the message id by matching its (timestamp, role) in the snapshot.
59
- const currentId = findCurrentMessageId(msg, state);
74
+ // Resolve the message ID by its persistent stable identity. Timestamp/role
75
+ // matching is retained only for legacy snapshots without stable IDs.
76
+ const currentId = findCurrentMessageId(msg, stableKey, state);
60
77
  if (currentId) return { id: currentId, text };
61
78
 
62
79
  return null;
@@ -73,15 +90,17 @@ export function detectCompressionCandidate(
73
90
  if (contextPercent < settings.minContextPercent) return null;
74
91
 
75
92
  const boundaries: CandidateBoundary[] = [];
76
- for (const msg of messages) {
77
- const boundary = resolveAddressableBoundaryId(msg, _state, { allowBlocks: true });
93
+ const stableKeys = stableMessageKeys(messages);
94
+ for (let index = 0; index < messages.length; index++) {
95
+ const msg = messages[index];
96
+ const boundary = resolveAddressableBoundaryId(msg, stableKeys[index]!, _state, { allowBlocks: true });
78
97
  if (!boundary) continue;
79
98
  if (!Number.isFinite(msg.timestamp)) continue;
80
99
  boundaries.push({
81
100
  id: boundary.id,
82
101
  role: msg.role ?? "",
83
102
  timestamp: msg.timestamp,
84
- tokenEstimate: estimateMessageTokens(msg),
103
+ tokenEstimate: _state.messageMetaSnapshot.get(boundary.id)?.tokenEstimate ?? estimateMessageTokens(msg),
85
104
  blockId: boundary.blockId,
86
105
  isSystemReminder: boundary.text.includes("<dcp-system-reminder>"),
87
106
  });
@@ -153,15 +172,17 @@ export function detectMessageCompressionCandidates(
153
172
  if (contextPercent < settings.minContextPercent) return [];
154
173
 
155
174
  const boundaries: CandidateBoundary[] = [];
156
- for (const msg of messages) {
157
- const boundary = resolveAddressableBoundaryId(msg, state, { allowBlocks: false });
175
+ const stableKeys = stableMessageKeys(messages);
176
+ for (let index = 0; index < messages.length; index++) {
177
+ const msg = messages[index];
178
+ const boundary = resolveAddressableBoundaryId(msg, stableKeys[index]!, state, { allowBlocks: false });
158
179
  if (!boundary || boundary.blockId !== undefined) continue;
159
180
  if (!Number.isFinite(msg.timestamp)) continue;
160
181
  boundaries.push({
161
182
  id: boundary.id,
162
183
  role: msg.role ?? "",
163
184
  timestamp: msg.timestamp,
164
- tokenEstimate: estimateMessageTokens(msg),
185
+ tokenEstimate: state.messageMetaSnapshot.get(boundary.id)?.tokenEstimate ?? estimateMessageTokens(msg),
165
186
  isSystemReminder: boundary.text.includes("<dcp-system-reminder>"),
166
187
  });
167
188
  }
@@ -1,16 +1,15 @@
1
1
  import type { DcpConfig } from "./config.js";
2
2
  import type { DcpState } from "./state.js";
3
3
  import { PASSTHROUGH_ROLES, estimateMessageTokens } from "./pruner-metadata.js";
4
- import { stableMessageId } from "./pruner-message-ids.js";
4
+ import { stableMessageKeys } from "./pruner-message-ids.js";
5
5
  import { writeDcpDebugLog } from "./debug-log.js";
6
6
 
7
- function messageMatchesBoundary(msg: any, messageIndex: number, stableId: string | undefined, timestamp: number): boolean {
8
- if (stableId && stableMessageId(msg, messageIndex) === stableId) return true;
9
- return msg.timestamp === timestamp;
10
- }
11
-
12
7
  function findBoundaryIndex(messages: any[], stableId: string | undefined, timestamp: number): number {
13
- return messages.findIndex((m, index) => messageMatchesBoundary(m, index, stableId, timestamp));
8
+ const stableKeys = stableId ? stableMessageKeys(messages) : [];
9
+ return messages.findIndex((message, index) => {
10
+ if (stableId && stableKeys[index] === stableId) return true;
11
+ return message.timestamp === timestamp;
12
+ });
14
13
  }
15
14
 
16
15
  export interface ReconcileInheritedBlocksResult {